Compute EngineでAtlantisサーバーを構築してTerraform実行を自動化する方法

記事タイトルとURLをコピーする

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

構築方法

基本的には公式のインストールガイドに従って構築します。

  1. Git Host のアクセス資格情報を作成
    • アクセス資格情報は Atlantis が Git Host の API を呼び出す際に使います
    • Git Host には GitHub や GitLab 等が使えます
    • GitHub を Git Host として使う場合、Personal Access Token (PAT) ではなく GitHub App が推奨されます
  2. Webhook Secret を作成
    • Atlantis が Git Host からの Webhook が正しいものかを判別するために設定します
    • 24文字以上である必要があります
  3. Atlantis サーバーを構築
  4. Webhook を構成
    • Git Host に GitHub、アクセス資格情報に GitHub App を使っている場合は Webhook が自動生成されるため不要です
  5. プロバイダーの資格情報を構成
    • Google Cloud の場合、Atlantis サーバーを構築する基盤 (Compute Engine 等) のサービスアカウントが使われます

ロック機能と Web UI

Atlantis は、Terraform のロック機能 とは別に独自のロック機能を持っています。
複数のプルリクエストが同一のディレクトリや Terraform Workspaces を変更している場合、最初のプルリクエストがマージされるまで他のプルリクエストの変更は実行できないようになっています。

Atlantis は Web UI も提供しており、ここからロックの確認や解除ができます。

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 を置いています。

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