Terraform+AnsibleでGoogle Compute Engineインスタンスを作る

Posted on Jul 12, 2021

Google Cloud Buildを使ってGoogle Compute Engine上に開発環境を作ろうと思ったんで諸々メモ。

TerraformでGoogle Secret Managerを扱う

Sethが書いたブログ記事に簡潔にまとまっている。さすが元Hashicorp、さすがセキュリティ担当。

まずこういう具合にデータを用意する。

data "google_secret_manager_secret_version" "terraform_ssh_private_key" {
    provider = google
    secret = "terraform-ssh-private-key"
    version = "1"
}

data "google_secret_manager_secret_version" "terraform_ssh_pub_key" {
    provider = google
    secret = "terraform-ssh-pub-key"
    version = "1"
}

そして、Google Compute Engineのインスタンス作成において次のように設定する。

resource "google_compute_instance" "debian10" {
    name = "debian10"
    machine_type = "e2-standard-4"

    ...()...

    metadata = {
        block-project-ssh-keys = "true"
        ssh-keys = "${var.gce_ssh_user}:${data.google_secret_manager_secret_version.terraform_ssh_pub_key.secret_data}"
    }

    provisioner "remote-exec" {
        inline = ["echo hello"]
        connection {
            type = "ssh"
            host = self.network_interface[0].access_config[0].nat_ip
            user = var.gce_ssh_user
            private_key = data.google_secret_manager_secret_version.terraform_ssh_private_key.secret_data
        }
    }

    provisioner "local-exec" {
        working_dir = "./ansible/"
        command = <<EOL
        ANSIBLE_HOST_KEY_CHEKING=False ansible-playbook -i hosts/inventory.gcp.yaml debian.yaml
        EOL
    }
}

ポイントはシークレットの値にアクセスする際に data.google_secret_manager_secret_version.<resource_name>.secret_data とすること。最後の値へのアクセスが secret_data になっているのが注意。

Cloud Buildで呼んだTerraform内でAnsibleを呼べるようにする

Cloud Buildの設定ではTerraformのbuilderに hashicorp/terraform:<version> を使っているが、これを使うと local-exec でAnsibleが呼び出せない。(イメージ内にAnsibleが入ってないので当たり前)

仕方ないので自分でCloud Builderを作成する。最後は ENTRYPOINT にしないと起動できないので注意。

FROM python:3.9-slim-buster as fetcher
ENV TERRAFORM_VER="1.0.2"
ENV ANSIBLE_VER="2.11.0"
RUN apt-get update && apt-get install -y wget unzip
WORKDIR /download
RUN wget -q -O terraform.zip "https://releases.hashicorp.com/terraform/1.0.2/terraform_${TERRAFORM_VER}_linux_amd64.zip" \
    && unzip terraform.zip \
    && rm terraform.zip

RUN wget https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
    && python3 get-pip.py && rm get-pip.py

COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN ansible-galaxy collection install google.cloud community.general
RUN ansible-galaxy install kewlfft.aur fubarhouse.golang

FROM python:3.9-slim-buster
COPY --from=fetcher /download/terraform /usr/local/bin/terraform
RUN chmod +x /usr/local/bin/terraform
COPY --from=fetcher /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=fetcher /usr/local/bin/ansible /usr/local/bin/ansible
COPY terraform.bash /usr/local/bin/terraform.bash
RUN chmod +x /usr/local/bin/terraform.bash
ENTRYPOINT ["/usr/local/bin/terraform.bash"]

また、ENTRYPOINT 用のコマンドがCloud Buildから引数を受け取れるように、このようにラッパーのシェルスクリプトを用意しておいて、そちらを指定するのが一般的。

echo "Running: terraform $@"
/usr/local/bin/terraform "$@"

Ansibleのhosts内で使うサービスアカウント情報を渡す部分

---
# This inventory is using gcp_compute plugin. Install the plugin in advance of the execution.
# $ ansible-galaxy collection install google.cloud
# https://docs.ansible.com/ansible/latest/collections/google/cloud/gcp_compute_inventory.html
plugin: gcp_compute
zones: # populate inventory with instances in these regions
  - asia-northeast1-b
  - asia-northeast1-a
  - asia-northeast2-b
  - asia-northeast2-a
  - asia-east1-b
projects: development-215403
service_account_file: /workspace/ansible-service-account.json
auth_kind: serviceaccount
scopes:
 - 'https://www.googleapis.com/auth/compute'
hostnames:
  # List host by name instead of the default public ip
  - 'name'
compose:
  # Set an inventory parameter to use the Public IP address to connect to the host
  # For Private ip use "networkInterfaces[0].networkIP"
  ansible_host: networkInterfaces[0].accessConfigs[0].natIP

service_account_file の情報を渡さないと ansible-playbook が実行が失敗するわけだけど、これは google_compute_instancelocal-exec からだと変数として値を渡せるものではない。

仕方がないのでCloud Buildの特徴を使って /workspace 以下にサービスアカウントファイルを持ってくる手順を cloudbuild.yaml に記述する。

- name: 'gcr.io/cloud-builders/gcloud-slim'
  entrypoint: 'bash'
  args: ['shellscript/fetch_service_account_file.bash']

この中で呼んでるシェルスクリプトはこんな感じ。

#!/bin/bash
set -ex

IAM_ACCOUNT="cloud-build-terraform-ansible@development-215403.iam.gserviceaccount.com"

keys=$( gcloud iam service-accounts keys list \
  --iam-account=${IAM_ACCOUNT} \
  --filter="key_type=USER_MANAGED" \
  --format="value(name)" )

if [ -n "$keys" ]; then
    for key in $keys; do
        gcloud iam service-accounts keys delete $key \
          --iam-account=${IAM_ACCOUNT}
    done
fi

gcloud iam service-accounts keys create ansible-service-account.json \
  --iam-account=${IAM_ACCOUNT}

Ansibleが “Authentication failed” となってapt関連を動かせない

これでめちゃくちゃはまった。

Error running command 'ANSIBLE_HOST_KEY_CHEKING=False ansible-playbook -i hosts/inventory.gcp.yaml debian.yaml': exit status 4. Output:
PLAY [debian]
******************************************************************

TASK [Gathering Facts]
*********************************************************
[WARNING]: Platform linux on host debian is using the discovered Python interpreter at /usr/bin/python3.7, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible/2.11/reference_appendices/interpreter_discovery.html for more information.
ok: [debian]

TASK [debian : Change default shell]
*******************************************
changed: [debian]

TASK [debian : Install preconditional apt packages]
****************************
failed: [debian] (item={'name': 'apt-transport-https', 'state': 'latest'}) => {"ansible_loop_var": "item", "item": {"name": "apt-transport-https", "state": "latest"}, "msg": "Failed to uthenticate: Authentication failed.", "unreachable": true}

実際はAnsibleは全然関係なくて、taskの順番で実際に変更後に使うshellをインストールする前にそのshellに変更してしまったから、shellに接続できなくて死んでたという話。エラーメッセージだけみてSSH周りがおかしいのだと思って混乱してた。再現する簡単な設定。

- name: Change default shell
  ansible.builtin.user:
    name: demo
    shell: /usr/bin/zsh
  become: yes

- name: Update package
  ansible.builtin.apt:
    name: zsh
    state: latest
  become: yes