【POC】CloudFormationでもAtlantisでGitOpsがしたい!

こんにちは、かたいなかです。

以前、AtlantisでGitHub上からTerrraformのコードの変更を適用する方法をご紹介しました。

TerraformをPull Request上のコマンドで実行!Atlantisを試してみた

AtlantisはTerraformを適用するためのプリセットのコマンドだけでなく、自分で指定したコマンドをワークフローの中で実行することができます。

そこで、今回はその機能を使ってCloudFormationの適用をGitOps化する方法をご紹介します。

今回のコード

cfn-atlantis-gitops-poc

なぜAtlantisを使いたいのか

CircleCIを使用してCloudFormationを適用するフローも検討したのですが、以下の点でAtlantisでの適用が良いと考えました。

  • CloudFormation開発時はトライアンドエラーを繰り返すが、「masterブランチの場合はCloudFormationを実行する」というようなフローだと、実行が失敗するコードがmasterブランチに残った状態となる
  • 大量の環境を管理するにあたって、変更の内容をもとに必要なテンプレートに限ってだけCloudFormationを適用したい
  • Atlantis + Terraformの運用がチーム内で好評であり、同じようなフローでCloudFormationも運用したいというチーム内の意見

CloudFormation適用用のツールを自作するという手もあったのですが、チーム内のリソース状況を鑑みて見送ることにしました。

前提

今回は以下のようなディレクトリ構造のリポジトリでGitHubの適用を自動化します。

├── atlantis.yaml # 記事の中で後ほど作成します
└── cloudformation
    └── stack_name # ディレクトリ名をそのままスタック名にします
        ├── parameters.txt # パラメータをKEY=VALUEの形式で一行ずつ列挙します。
        └── template.yaml # CloudFormationテンプレートです

実際の構築

Dockerイメージの作成・プッシュ

スクリプトを用意

planapplyで使用するスクリプトを用意します。

planスクリプト

ChangeSetを作成して内容を表示します。

ChangeSetを作成した場合は、dummy.tfplanというファイルを作成しています。これは、Atlantisに対して terraform plan を実行したように見せかけるためです。

describe-change-setを実行するコマンドを取得する部分が少し闇な感じになっています。実プロダクトに適用するときはaws cloudformation deployを使わないように書き直すと良いでしょう。

#! /bin/bash
set -eux

stack_name=$1
template_path=$2
parameters_path=$3

describe_change_set_command="$(aws cloudformation deploy \
  --template-file ${template_path} \
  --stack-name ${stack_name} \
  --no-execute-changeset \
  --no-fail-on-empty-changeset \
  --parameter-overrides $(cat ${parameters_path} | tr '\n' ' ') | \
  grep 'aws cloudformation describe-change-set' || true)"

if [ -n "$describe_change_set_command" ]
then
  ## Atlantisに対して`terraform plan`実行したと見せかける
  touch dummy.tfplan

  $describe_change_set_command
fi
applyスクリプト

apply用のスクリプトです。こちらは普通に適用するだけで特別なことはしていません。

#! /bin/bash
set -eux

stack_name=$1
template_path=$2
parameters_path=$3

aws cloudformation deploy \
  --template-file ${template_path} \
  --stack-name ${stack_name} \
  --no-fail-on-empty-changeset \
  --parameter-overrides $(cat ${parameters_path} | tr '\n' ' ') || \
  (aws cloudformation describe-stack-events --stack-name ${stack_name} && false)

イメージをビルド・プッシュ

以下のようなDockerfileでイメージをビルドし、ECRにプッシュしておきます。

ベースのイメージにAWS CLIをインストールし、スクリプトを追加してパスを通しています。

FROM runatlantis/atlantis:v0.8.2

RUN apk --update add python sudo

# awscli
RUN curl "https://bootstrap.pypa.io/get-pip.py" | python && \
    pip install awscli==1.16.211

# スクリプト
RUN curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "/tmp/awscli-bundle.zip" && \
  unzip /tmp/awscli-bundle.zip && \
  sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws


COPY ./scripts/* /usr/local/cfn-utils/
RUN sudo chmod +x /usr/local/cfn-utils/*
ENV PATH $PATH:/usr/local/cfn-utils/

Terraformでデプロイ

前回Atlantisを紹介した際のブログでの内容とほぼ同様ですが、イメージの指定と、workflowをリポジトリ側の設定で指定できるような設定を追加しています。

前回記事との差分の部分はハイライトさせています。大きく3つの変更を加えています。

  • 自分で作成したイメージを指定
  • リポジトリ側の設定でworkflowを指定できるよう許可
  • PRをマージした結果をもとにPlanやApplyを実行(今回の記事の内容とはあまり関係ないのでハイライトしていません)
provider "aws" {
  version = "~> 2.0"
  region  = "ap-northeast-1"
}

module "atlantis" {
  source  = "terraform-aws-modules/atlantis/aws"
  version = "~> 2.0"

  name = "atlantis"

  cidr            = "10.20.0.0/16"
  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  private_subnets = ["10.20.1.0/24", "10.20.2.0/24"]
  public_subnets  = ["10.20.101.0/24", "10.20.102.0/24"]

  # 自分でビルドしたイメージを使用する
  atlantis_image = "<プッシュしておいたイメージのURI>"

  route53_zone_name = "<atlantisの名前を登録するRoute53のPublic HZのドメイン名>"

  policies_arn = [
    "arn:aws:iam::aws:policy/AdministratorAccess"
  ]

  atlantis_github_user       = "GitHubのボットユーザの名前"
  atlantis_github_user_token = "GitHubのボットユーザのトークン"
  atlantis_repo_whitelist    = ["github.com/<ユーザ名やOrg名に応じて>/*"]

  custom_environment_variables = [
    # リポジトリ側の設定でworkflowを指定できるよう許可
    {
      name  = "ATLANTIS_REPO_CONFIG_JSON"
      value = "{\"repos\":[{\"id\":\"/.*/\",\"allowed_overrides\":[\"apply_requirements\",\"workflow\"],\"allow_custom_workflows\":true}]}"
    },
    # PRをマージした結果をもとにPlanやApplyを実行
    {
      name  = "ATLANTIS_CHECKOUT_STRATEGY",
      value = "merge"
    }
  ]
}

output "webhook_url" {
  description = "Github webhook URL"
  value       = module.atlantis.atlantis_url_events
}

output "webhook_secret" {
  description = "Github webhook secret"
  value       = module.atlantis.webhook_secret
}

出力されたwebhook_secretとwebhook_urlの2つの値は後ほどGitHubのリポジトリでWebhookを登録する際に使用するのでメモっておきます。

Webhookの設定

次にWebhookを登録します。Atlantisのドキュメントを参考に設定していきます。

GitHubのリポジトリのトップ画面から Settings -> Webhook -> Add Webhook と順番に選択し、Webhookの追加画面に移動します。

その画面で以下のような設定を行い、Webhookを作成します。

今回は個人リポジトリでしたが、Orgs配下のリポジトリではTerraformで適用することも検討すると良いと思います。

項目 設定値
Payload URL <webhook_url として出力されたURL>
Content type application/json
Secret <webhook_secret として出力された文字列>
Which events would you like to trigger this webhook?  Let me select individual events
イベントの種類 設定値
Pull request reviews
Pushes
Issue comments
Pull requests
その他のイベント チェックしない

リポジトリ側の設定

最後に以下のような名前のファイルをCloudFormationのリポジトリのルートにatlantis.yamlという名前でコミットし、プッシュしておきます。

version: 3
automerge: true
projects:
  - name: <名前>
    dir: <Cloudformationがあるディレクトリ>
    autoplan:
      when_modified: ["*"]
      enabled: true
    workflow: cloudformation
workflows:
  cloudformation:
    plan:
      steps:
        - run: cfn-plan.sh $(basename $(pwd)) template.yaml parameters.txt
    apply:
      steps:
        - run: cfn-apply.sh $(basename $(pwd)) template.yaml parameters.txt

実際に動かしてみる

この状態でリポジトリ上に変更を加えると、自動でCloudFormationのChange Setが作成され、結果がコメントとして追加されます。

この状態で atlantis apply とコメントすると、CloudFormationのテンプレートが適用され、自動でPRがマージされます。

apply に失敗した場合は以下の画像のように、エラーメッセージが表示され、裏ではCloudFormationのロールバックが実行されます。

CloudFormationのGitOpsが実現されました!

まとめ

AtlantisでCloudFormationの適用を行う方法を紹介しました。.tfplanファイルを作成している部分は無理している感がありましたが、その他は問題なく実行できていそうです。AtlantisでのGitHub上で完結する運用がCloudFormationで構築された環境でも適用できると、大きく運用負荷を下げられそうだと考えています。

参考資料