AtlantisをFargateで導入してみた
はじめに
こんにちは。コンサルティング部の津谷です。みなさんは、Terraform使っていますでしょうか。クラウドインフラのIaC化も昨今ではだいぶ標準的になっていますが、開発フローの改善活動は皆さんも課題感を持っているかと思います。
今日検証するのは、Atlantisというツールになります。こちらはTerraformでのワークフローをpull requests上で実行するOSSになります。Terraformでインフラ管理をする場合は、コードリポジトリを使いますがAtlantisはGithubともネイティブに統合しており、pull requestsの中でterraform planを自動実行して、コメントに出力することが可能です。またatlantisコマンドを使った、コメント内でのterraform applyを実行することが可能です。申し上げた通り、GithubとはWebhookを介して接続しており、ブランチ保護ルールによる承認ルールなども組み込めます。
詳しくは、公式ドキュメントにも記載あるので日本語にGoogle翻訳して読んでみてください。
自分もAtlantisに入門して、少しずつ勉強をスタートしようと思います。まずは、Atlantisを導入してみて基本的な機能を確かめようと思います。ちなみに、Atlantisはインフラ環境を自分でホスティングする必要があります。今回は、ECS Fargateにデプロイしてみようと思います。さくっと試したい場合はローカルテストもできるのでお勧めです。
基盤構成図
構成は以下のようになります。

デプロイするリソースの用途を以下に記載しました。
- Route53
- AtlantisはECS Fargateでホスティングします。Fargateの前段に置くALBへの名前解決に利用するドメインを払い出しております。
- Certificate Manager
- ALBに付与するSSL/TLS証明書になります。指定のドメインでアクセスさせるのでドメイン名に準拠した形でこちらも利用してみます。
- Application Load Balancer
- Fargateでは、複数のタスクをマルチAZに配置するので負荷分散に利用します。
- ECS Fargate
- Atlantisを実行する(Terraformの実行)本体になります。
- Secrets Manager
- 外部からGithubリポジトリへのアクセスに必要なGithubトークン情報や、Webhookに利用するシークレット情報を格納します。
- Internet Gateway
- AtlantisにもWebコンソールがあります。コンソールでは、terraform planの自動実行がどのリポジトリやブランチ上で行われたのかを一覧取得することができます。また、pull requestsを挙げてマージ(Atlantis deploy)するまでは実行ステータスをロックすることができるのですが、こちらもコンソールで状況確認できます。このコンソールへはブラウザ経由でアクセスします。また、Webhook経由でGithubからAtlantisへアクセスするのでこちらもGithubからのアクセス経路が必要になります。
- Nat Gateway
- AtlantisはTerraformを実行するので、Fargate起動時にTerraformをインストールしたり、Terraform実行時(デプロイ時)にAWS APIを叩く必要があります。また、PR情報の取得や、terraform planの結果出力でGithubへのアクセス経路が必要です。
構築
Atlantisの実装はモジュールが公開されているので、これを適用してみます。 モジュールでも手動で作成しておくリソースがあるので、準備しておきます。
- VPC
- サブネット
- パブリック×2、プライベート×2
- Route53ホストゾーン
- レコード追加はモジュールを利用できますが、面倒なのでドメイン周りの設定は手動でやってしまいました。
- Intenet Gateway
- Nat Gateway
- Route Table
以下はモジュールで作成できますが手動で作っちゃいました。
- ALB
- 本体、リスナー、ターゲットグループ
- Secret Manager
- Githubトークン、Webhookシークレットを格納します。
- ACM
- ドメイン周りは手動でやっていたので、証明書発行・ALBへの付与はそのまま手動でやりました。
それ以外のリソースはモジュールを使ってデプロイしています。
- ECSクラスター/サービス/タスク定義
- IAMロール(タスク実行ロール)
- CloudWatchログ
上記の様にいくつか手動で作成したので、examples/github-separateリポジトリを参照しました。既存リソースがある場合はこちらが使い勝手いいですね。
ディレクトリ構造は以下です。
atlantis-infra/
├── terraform.tfvars # 固有の変数値(秘密情報は含めない)
├── provider.tf # プロバイダー設定(Terraform、AWS)
├── variables.tf # 入力変数の定義
├── main.tf # メインのリソース定義
└── outputs.tf # 出力変数の定義
各ファイルの詳細は以下に記載します。
-
provider.tf
terraform { required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" version = ">= 6.28" } } } provider "aws" { region = var.aws_region default_tags { tags = { Environment = var.environment Terraform = "true" ManagedBy = "Terraform" } } } -
main.tf
module "atlantis" { source = "terraform-aws-modules/atlantis/aws" name = "atlantis" vpc_id = var.vpc_id # Existing cluster create_cluster = false cluster_arn = var.cluster_arn # Existing ALB create_alb = false alb_target_group_arn = var.alb_target_group_arn alb_security_group_id = var.alb_security_group_id # Existing ACM certificate create_certificate = false certificate_arn = var.certificate_arn validate_certificate = false # Route53 configuration route53_zone_id = var.route53_zone_id create_route53_records = false # ECS Container Definition atlantis = { fqdn = var.atlantis_fqdn environment = [ { name = "ATLANTIS_GH_USER" value = var.github_user }, { name = "ATLANTIS_REPO_ALLOWLIST" value = var.github_repo_allowlist }, ] secrets = [ { name = "ATLANTIS_GH_TOKEN" valueFrom = var.github_token_secret_arn }, { name = "ATLANTIS_GH_WEBHOOK_SECRET" valueFrom = var.github_webhook_secret_arn }, ] } # ECS Service service = { subnet_ids = var.subnet_ids task_exec_secret_arns = [ var.github_token_secret_arn, var.github_webhook_secret_arn, ] # Provide Atlantis permission necessary to create/destroy resources tasks_iam_role_policies = { AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess" } } tags = { Environment = var.environment Terraform = "true" } }
sourceでモジュールを参照しています。モジュールではboolean変数でリソースの作成可否を決められるので、既に作成済みのリソースはfalseにしています。
-
variables.tf
variable "aws_region" { description = "AWS region for resources" type = string default = "ap-northeast-1" } variable "environment" { description = "Environment name" type = string default = "tsuya-test" } variable "vpc_id" { description = "VPC ID where Atlantis will be deployed" type = string } variable "cluster_arn" { description = "Existing ECS cluster ARN" type = string } variable "alb_target_group_arn" { description = "Existing ALB target group ARN" type = string } variable "alb_security_group_id" { description = "Existing ALB security group ID" type = string } variable "subnet_ids" { description = "List of subnet IDs for ECS service" type = list(string) } variable "certificate_arn" { description = "Existing ACM certificate ARN for HTTPS" type = string } variable "route53_zone_id" { description = "Route53 hosted zone ID" type = string } variable "atlantis_fqdn" { description = "FQDN for Atlantis" type = string } variable "github_user" { description = "GitHub username for Atlantis" type = string } variable "github_repo_allowlist" { description = "GitHub repositories that Atlantis can manage" type = string } variable "github_token_secret_arn" { description = "AWS Secrets Manager ARN for GitHub token" type = string } variable "github_webhook_secret_arn" { description = "AWS Secrets Manager ARN for GitHub webhook secret" type = string } -
output.tf
output "atlantis_url" { description = "URL to access Atlantis" value = module.atlantis.url } output "ecs_service" { description = "ECS service details" value = module.atlantis.service sensitive = true } -
terraform.tfvars
# AWS Region aws_region = "ap-northeast-1" environment = "tsuya-test" # Network Configuration vpc_id = "vpc-xxx" subnet_ids = ["subnet-xxx", "subnet-xxx"] # Existing ECS Cluster cluster_arn = "arn:aws:ecs:ap-northeast-1:[アカウントID]:cluster/atlantis-cluster" # Existing ALB Configuration alb_target_group_arn = "arn:aws:elasticloadbalancing:ap-northeast-1:[アカウントID]:targetgroup/atlantis-tg/xxx" alb_security_group_id = "sg-xxx" # ACM Certificate certificate_arn = "arn:aws:acm:ap-northeast-1:xxx:certificate/xxx" # Route53 Configuration route53_zone_id = "xxx" # Atlantis FQDN atrantis_fqdn = "xxx.com" # GitHub Configuration github_user = "xxx" github_repo_allowlist = "github.com/[gitユーザ]/[リポジトリ名]" # AWS Secrets Manager ARNs github_token_secret_arn = "arn:aws:secretsmanager:ap-northeast-1:[アカウントID]:secret:xxx" github_webhook_secret_arn = "arn:aws:secretsmanager:ap-northeast-1:[アカウントID]:secret:xxx"
これでAtlantisの基盤自体はデプロイが成功します。
Github上での設定
実際にAtlantisを動かしてみましょう。今回は、同じアカウント上にVPCを1つTerraformで作成してみようと思います。コードを格納するテスト用のリポジトリを作成して、リポジトリ上の設定をいくつかやっていきます。
以下の個人アカウントでリポジトリを作成します。

プライベートリポジトリにしています。
次にAtlantisがpull requests上でterraform plan結果を出力するためのGithubユーザを指定する必要があります。今回は検証なので自分のGithubアカウントユーザを利用しますが、誰からコメントされているのか混在させないためにAtlantis用のGithubアカウント(bot)を作成しておくと良いかもしれません。Githubへのアクセストークンも作成しておきましょう。今回は、classicトークンを発行しましたが、個別リポジトリ単位、API単位で制限することがよりセキュアなのでFine-grainedトークンを使ったり、GitAppによる認証を使ったりすることが推奨されます。
Atlantisも公式でリポジトリ単位でのトークン作成を推奨しています。
今回はリポジトリへの操作権限をフルで与えていますが、最小に絞る場合は以下の権限だけ付与すればよいです。
- Commitのステータスの読み取り・編集権限
- リポジトリのファイルに対する読み取り権限
- リポジトリをクローンして、コードファイル等を読み取り差分を検出します。情報を基にAtlantisのローカル環境でterraform planを実行しています。
- リポジトリのメタデータに対する読みとり権限(自動有効なので何もしなくていい)
- AtlantisのWebUIにpull request中のリポジトリやブランチ情報等が出力されます。
- プルリクエストへのコメント権限
- terraform plan/applyの結果出力・エラーメッセージの出力、コメントコマンドの検出を行います。(atlantis plan等)
Fine-grainedトークンを発行する場合は以下のように権限をつけてあげると良いです。

次に、Webhookシークレット作成しましょう。Webhookはpull requestsをトリガーにAtlantisのURLを呼び出して自動planを実行してもらうために必要になります。GithubのGIPで正当性を確認することも可能ですが、可変だと追従が難しいのでシークレットを付与することでGitからのWebhookが正当なものだと確かめることが可能です。Webhookの設定で必要になるので先に設定しておきます。
次に、Webhookの設定をしていきます。

PayloadURLは、AtlantisのWebUIにアクセスするためのドメインを指定してあげます。ドメインはRoute53で払い出したドメインを利用します。「このリポジトリ内でpull requestが発生したらこのドメイン宛てに送ってね」の情報です。公式ドキュメントにも記載ありますが、/eventsパスを追加しておきましょう。Content Typeはjsonを指定します。Atlantisは、Webhookで受け取るイベント(リクエストボディ)をJsonでパースするためです。
次に先ほどジェネレータで作成した文字列をシークレットとして登録します。

次にWebhookのトリガーイベントを設定します。ここは「Let me select individual events」を選択し、個別にイベントを設定しておきます。

イベントは以下を指定しています。
- Issue comments
- atlantis plan/applyといったTerraformコマンド操作を実行する際に、pull requests上のコメントで記載するので、commentをトリガーにAtlantisをhookする必要があります。
- Pull request reviews
- Atlantis側で、レビュー状況の変更をトリガーにhookします。「terraform実行までのワークフローで例えば、plan実行後にレビュアーに承認されないといけない」といったケースでポリシーを作りこんだ場合に、レビュー状況を知らせてもらう必要があります。
- Pull requests
- Pull requestsが作成・更新・クローズされたことをAtlantisに知らせるために必要です。PRが開かれたときに自動でplan を実行したり、マージ・クローズ時にロックを解放したりします。
- Pushes
- PRのブランチに追加のコミットがプッシュされたことを検知するためにhookします。
ここまで出来たら、Atlnatisを実行する準備は完了です。
動作確認
VPCを作成してみようと思います。
ローカルで開発を始める前に、先ほど作ったリポジトリはクローンしておきます。ディレクトリ構造は以下です。
atlantis-test/
├── atlantis.yaml # 固有の変数値(秘密情報は含めない)
├── provider.tf # プロバイダー設定(Terraform、AWS)
├── main.tf # メインのリソース定義
├── variables.tf # 入力変数の定義
└── outputs.tf # 出力変数の定義
-
atlantis.yaml
version: 3 projects: - name: vpc-project dir: . workspace: default terraform_version: v1.6.0 autoplan: when_modified: - "*.tf" - "*.tfvars" enabled: true
ちなみにですが、Atlantis.ymlはなくても動作します。その場合は、リポジトリのルート直下にtfファイル等が必要です。
各変数について整理します。
| 項目 | 説明 |
|---|---|
| version | atlantis.ymlの書式のバージョンです。最新は3です。 |
| projects | リポジトリで管理するTerraformを環境別に分ける場合などに便利です。atlantisコマンドもプロジェクトごとに実行できます。 |
| dir | tfファイルがあるディレクトリを指しています。今回はルート直下におくので「.」を指定します。 |
| workspace | Terraformのワークスペースです。環境はディレクトリ分離するパターンが多いように感じますが、こちらも1つのコードでワークスペースを切り替えて流用できます。 |
| terraform_version | Terraformのバージョンを明示的に指定することが可能です。 |
| autoplan | pull requestsが作成された際に自動でteraform plan結果を発動するか決められます。どのファイルの変更をトリガーにするかも指定できます。今回は.tfと.tfvarsのみです。 |
あとは、省略します。
リポジトリにpushするだけです。pull requestsを作成してみると、自動でterraform planが実行されているのがわかりますね。

さいごに
今回はAtlantisのセットアップをしてみました。セットアップがメインだったのでAtlantisの機能については次回試してみようと思います。今回もお読みいただきありがとうございました。








