G-gen の藤岡です。当記事では Atlantis を使って GitHub のプルリクエスト上で Terraform を実行する方法を紹介します。
当記事で扱うツール
Terraform
概要
Terraform は Infrastructure as Code (IaC) を実現する OSS のツールです。
IT インフラをコードによって構築、管理し CI/CD (継続的インテグレーション / 継続的デリバリ) を可能にします。IT インフラをコードで定義できることのメリットの1つとして、Git によるバージョン管理ができる点が挙げられます。
Terraform を Google Cloud で使う際は、以下の記事をご参照ください。 blog.g-gen.co.jp
ローカルから実行する場合の注意点
複数のエンジニアによって開発される場合、どこから Terraform を実行するか注意が必要です。
例えばそれぞれのローカルマシンから Terraform を実行する運用では、ヒューマンエラーによる実行ミスのリスクや実行結果の履歴が残らない等の課題があり、チームでの作業の見える化や過去の変更を追いにくくなる恐れがあります。
自動化ツール
Terraform 実行の自動化ツールとして、当記事で紹介する Atlantis の他にも Terraform Cloud や GitHub Actions 等があります。
Atlantis ではプルリクエスト上で Terraform の実行ができるため、前述のローカルマシンから実行する時に生じる課題を解消できます。
GitHub Actions を使った自動化は以下の記事をご参照ください。 blog.g-gen.co.jp
Atlantis
概要
Atlantis は プルリクエストを使って Terraform の実行を自動化する OSS ツールです。
以下のように、プルリクエストを作成すると自動で terraform plan
が実行され、実行結果がプルリクエストのコメントに記載されます。
そのプルリクエストで atlantis apply
をコメントすると terraform apply
が実行され、その結果が同じようにコメントに記載されます。
また、atlantis
コマンドの実行条件 (main ブランチへマージ可能な状態であることやプルリクエストが Approve されていること等) を定義できます。
例えば、Terraform で IT インフラを構築する際に、terraform plan
は通ったとしても terraform apply
で失敗することもあります。
GitHub でコードを管理している場合に、terraform plan
が通ったものを main ブランチにマージして terraform apply
を実行するような運用フローでは、terraform apply
で失敗した時に再度ブランチを切るなど手戻りが発生します。
Atlantis では、プルリクエストのコメント上で Terraform の実行ができるため、実環境へ適用されたものを main ブランチにマージすることができ手戻りが発生しにい等のメリットもあります。
アーキテクチャ
Atlantis は以下のように動作します。
Atlantis はセルフホスト型のため、自身で Atlantis サーバーを構築しなければなりません。 Google Cloud の場合、Compute Engine と Google Kubernetes Engine のモジュールが提供されているため、必要な設定を入れるだけで容易に構築ができます。
当記事では公式から提供されているモジュールを使って Compute Engine 上に Atlantis サーバーを構築し、動作を確認します。
参考:Deployment
構築方法
基本的には公式のインストールガイドに従って構築します。
- Git Host のアクセス資格情報を作成
- アクセス資格情報は Atlantis が Git Host の API を呼び出す際に使います
- Git Host には GitHub や GitLab 等が使えます
- GitHub を Git Host として使う場合、Personal Access Token (PAT) ではなく GitHub App が推奨されます
- Webhook Secret を作成
- Atlantis が Git Host からの Webhook が正しいものかを判別するために設定します
- 24文字以上である必要があります
- Atlantis サーバーを構築
- Webhook を構成
- Git Host に GitHub、アクセス資格情報に GitHub App を使っている場合は Webhook が自動生成されるため不要です
- プロバイダーの資格情報を構成
- Google Cloud の場合、Atlantis サーバーを構築する基盤 (Compute Engine 等) のサービスアカウントが使われます
ロック機能と Web UI
Atlantis は、Terraform のロック機能 とは別に独自のロック機能を持っています。
複数のプルリクエストが同一のディレクトリや Terraform Workspaces を変更している場合、最初のプルリクエストがマージされるまで他のプルリクエストの変更は実行できないようになっています。
Atlantis は Web UI も提供しており、ここからロックの確認や解除ができます。
但し、この Web UI にはデフォルトでは認証機能がありません。 そのため、Atlantis がデプロイされるとインターネット上からこの Web UI が閲覧できてしまいセキュリティリスクが高まります。
Atlantis からは Basic 認証が提供されており、また Google Cloud の場合は Identity-Aware Proxy (IAP) と組み合わせることで閲覧制限がかけられます。
また、このロック機能の実現には外部データベース (永続ディスク) が必要です。
Atlantis は外部データベースに terraform plan
やプルリクエストのマージ情報などを保存します。
当記事で使うモジュールでも Compute Engine に永続ディスクがアタッチされています。
以下は atlantis apply
をした時にプルリクエストのコメントに記載されたエラーログです。Atlantis は /home/atlantis/.atlantis
配下に情報を保存しています。
running "/usr/local/bin/terraform apply -input=false -no-color \"/home/atlantis/.atlantis/repos/REPO_OWNER/REPO_NAME/5/default/default.tfplan\"" in "/home/atlantis/.atlantis/repos/REPO_OWNER/REPO_NAME/5/default": exit status 1 google_compute_network.vpc_network: Creating... Error: Error creating Network: googleapi: Error 409: The resource 'projects/PROJECT_ID/global/networks/atlantis-vpc' already exists, alreadyExists with google_compute_network.vpc_network, on main.tf line 12, in resource "google_compute_network" "vpc_network": 12: resource "google_compute_network" "vpc_network" {
構築にあたり
アーキテクチャ
当記事で作成する構成と、Atlantis サーバーによって作られるリソースは以下の通りです。
前提と注意点
当記事では検証目的のため簡易的な構成としています。本番運用する際は、以下の前提と注意点を踏まえて構築してください。
- 提供されているモジュールの examples/complete を使用します
- examples/complete/main.tf では 106 行目で Cloud DNS へ Cloud Load Balancing の IP アドレスを A レコードに登録しています
- そのため、既にドメインを取得しており、Cloud DNS でゾーンを管理していることが前提となります
- Web UI の認証はかけていません
- 前述の通り、Atlantis には Web UI が提供されますが当記事では認証を設定していません
- examples/complete/main.tf は以下の点を踏まえ一部変更します
- Git Host アクセス資格情報に Personal Access Token (PAT) ではなく GitHub App を使用します (PAT は個人に紐づく点や有効期限がある等の観点から望ましくありません)
- GitHub App の Private Key を main.tf の local 変数として記載していますが、実際はセキュリティ上の観点から Secret Manager 等で管理することが推奨されます
事前準備
GitHub App の作成
ドキュメントに従って GitHub App を作り、対象のリポジトリへインストールします。
固有の設定箇所は以下の通りです。
項目 | 設定値 | 備考 |
---|---|---|
Webhook URL | https://<ドメイン名>/events | Atlantis は Webhook を /events で受け付ける |
Webhook secret (optional) | 24 文字以上の任意の文字列 | |
Permissions | Administration: Read-only Checks: Read and write Commit statuses: Read and write Contents: Read and write Issues: Read and write Metadata: Read-only (default) Pull requests: Read and write Webhooks: Read and write Members: Read-only |
ドキュメントに記載 |
Subscribe to events | Check run / Create / Delete / Issue comment / Issues / Pull request / Pull request review / Pull request review comment / Push | Atlantis によって自動生成した際の項目を手動でも設定 |
作成後、Private keys を生成します。
GitHub App の App ID、Installations ID、Private keys は Terraform で使います。
Terraform ファイル
公式モジュールの main.tf を以下のように変更します。
# main.tf locals { project_id = "<your-project-id>" region = "<your-region>" zone = "<your-zone>" domain = "<example.com>" managed_zone = "<your-managed-zone>" github_repo_allow_list = "github.com/<repo-owner>/<repo-name>" github_app_id = "<your-github-app-id>" github_app_installation_id = "<your-github-app-installation-id>" github_webhook_secret = "<your-github-webhook-secret>" github_app_key = <<-EOT -----BEGIN RSA PRIVATE KEY----- <your-github-app-private-key> -----END RSA PRIVATE KEY----- EOT services = toset([ "compute.googleapis.com", ]) } # Enable APIs resource "google_project_service" "service" { for_each = local.services project = local.project_id service = each.value disable_dependent_services = false disable_on_destroy = false } # Create a service account and attach the required Cloud Logging permissions to it resource "google_service_account" "atlantis" { account_id = "atlantis-sa" display_name = "Service Account for Atlantis" project = local.project_id } resource "google_project_iam_member" "atlantis_log_writer" { role = "roles/logging.logWriter" member = "serviceAccount:${google_service_account.atlantis.email}" project = local.project_id } resource "google_project_iam_member" "atlantis_metric_writer" { role = "roles/monitoring.metricWriter" member = "serviceAccount:${google_service_account.atlantis.email}" project = local.project_id } resource "google_compute_network" "default" { name = "example-network" auto_create_subnetworks = false project = local.project_id } resource "google_compute_subnetwork" "default" { name = "example-subnetwork" ip_cidr_range = "10.2.0.0/16" region = local.region network = google_compute_network.default.id project = local.project_id private_ip_google_access = true log_config { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 0.5 metadata = "INCLUDE_ALL_METADATA" } } # Create a router, which we associate the Cloud NAT too resource "google_compute_router" "default" { name = "example-router" region = google_compute_subnetwork.default.region network = google_compute_network.default.name bgp { asn = 64514 } project = local.project_id } # Create a NAT for outbound internet traffic resource "google_compute_router_nat" "default" { name = "example-router-nat" router = google_compute_router.default.name region = google_compute_router.default.region nat_ip_allocate_option = "AUTO_ONLY" source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" project = local.project_id } module "atlantis" { source = "bschaatsbergen/atlantis/gce" name = "atlantis" network = google_compute_network.default.name subnetwork = google_compute_subnetwork.default.name region = local.region zone = local.zone service_account = { email = google_service_account.atlantis.email scopes = ["cloud-platform"] } # Note: environment variables are shown in the Google Cloud UI # See the `examples/secure-env-vars` if you want to protect sensitive information env_vars = { ATLANTIS_ATLANTIS_URL = "https://${local.domain}" ATLANTIS_REPO_ALLOWLIST = local.github_repo_allow_list ATLANTIS_WRITE_GIT_CREDS = true ATLANTIS_REPO_CONFIG_JSON = jsonencode(yamldecode(file("${path.module}/server-atlantis.yaml"))) ATLANTIS_GH_APP_ID = local.github_app_id ATLANTIS_GH_INSTALLATION_ID = local.github_app_installation_id ATLANTIS_GH_WEBHOOK_SECRET = local.github_webhook_secret ATLANTIS_GH_APP_KEY = local.github_app_key } domain = local.domain project = local.project_id } # As your DNS records might be managed at another registrar's site, we create the DNS record outside of the module. # This record is mandatory in order to provision the managed SSL certificate successfully. resource "google_dns_record_set" "default" { name = "${local.domain}." type = "A" ttl = 60 managed_zone = local.managed_zone rrdatas = [ module.atlantis.ip_address ] project = local.project_id } resource "google_compute_ssl_policy" "default" { name = "example-ssl-policy" profile = "RESTRICTED" min_tls_version = "TLS_1_2" project = local.project_id }
Atlantis サーバーの構築
Terraform の実行
Atlantis サーバーと必要なリソースを作成します。
# 初期化 fujioka@penguin:~$ terraform init Initializing the backend... ... fujioka@penguin:~$ # 適用確認 fujioka@penguin:~$ terraform plan ... Plan: 23 to add, 0 to change, 0 to destroy. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. fujioka@penguin:~$ # 適用 fujioka@penguin:~$ terraform apply ... Apply complete! Resources: 23 added, 0 changed, 0 destroyed. fujioka@penguin:~$
動作確認
サービスアカウントへ権限付与
Terraform の実行により、atlantis-sa@PROJECT_ID-prj.iam.gserviceaccount.com
のサービスアカウントが作られます。
GitHub のプルリクエスト上で atlantis コマンドを実行すると Atlantis サーバー (Compute Engine) のサービスアカウントの権限で Terraform が実行されます。
今回は Atlantis サーバーが構築されたプロジェクトとは異なるプロジェクトに対して VPC を作ります。
そのため、VPC を作成するプロジェクトで atlantis-sa@PROJECT_ID-prj.iam.gserviceaccount.com
のサービスアカウントに編集者 (roles/editor) ロールを付与して動作を確認します。
プルリクエストを作成
GitHub App をインストールしたリポジトリで main.tf を作成し、プルリクエストを作成します。
今回は検証目的のため、ルートに main.tf を置いています。
プルリクエストを作成するとリポジトリにインストールされた GitHub App の Subscribe to events に該当するため、Atlantis サーバーへ Webhook され terraform plan
の結果がコメントされます。
適用
プルリクエスト上で atlantis apply
をコメントすると、実環境へ適用されリソースが作られます。
藤岡 里美 (記事一覧)
クラウドソリューション部
数年前までチキン売ったりドレスショップで働いてました!2022年9月 G-gen にジョイン。ハイキューの映画を4回は見に行きたい。
Google Cloud All Certifications Engineer / Google Cloud Partner Top Engineer 2024
Follow @fujioka57621469