概要
この記事ではTerraformのチーム運用にあたり、どのようにTerraformの継続的applyを整えたかをまとめます。
インフラ環境はAWSで、主要な事は実践Terraformの第27章 継続的applyとTerraform どこで実行していますか?という記事にまとめられている所とほぼ同じで、実際の環境作成でもこの記事を非常に参考にさせていただいたのですが、一つのサンプルケースとして参考になれば幸いです。
前提
利用しているのは実際のサービスで、AWSは本番用と開発用で2アカウント利用しています。本番ではproductionとstagingとという環境が、開発ではdevelopmentという環境が動いています。terraformのソースはアプリケーションのコードと合わせてGitHubで管理しており、GitFlowでの運用を行っています。tfファイルはterraform cloudで管理しており、planやapplyは実質一人がローカルで実施しているという状態でした。
フォルダ構成は以下のようになってます。
app/ ... アプリケーションのコード
terraform/prod/ ... 本番AWSアカウント用のディレクトリ
/common ... 本番AWSアカウントで共通に使うもののtfファイル群
/prod ... production環境で使うもののtfファイル群
/stg ... staging環境で使うもののtfファイル群
/dev/ ... 開発AWSアカウント用のディレクトリ
/common ... 開発AWSアカウントで共通に使うもののtfファイル群
/dev ... develop環境で使うもののtfファイル群
今回達成したい事
- AWS上でplan,applyをさせたい
- 実行に時間がかかるものをローカルで実行されるのは途中で落ちないか不安
- 複数人運用しても事故が起こらないようにしたい
- 常に最新の状態でapplyがされるようにしたい
- terraformの内容をレビューし合えるようしたい
- 本番環境以外はある程度柔軟に操作できるようにしたい
- 開発環境ではトライアンドエラーがしやすいようにしたい。
# 最終的に実現できたもの
- GitHubでプルリク時にCodeBuild上でplan実行、マージ時にapply実行
- plan,apply実行時にはGitHubのコメントとSlack両方に通知する
- GitHubだけだとマージ後にapplyを行った際、終わったかを見に行かなければいけないのが手間なので
- 本番と本番共通のterraformはmasterのマージ時にそれ以外はdevelopのマージ時に行う。
- ローカルではapplyとplan両方が実行できてしまうが、applyは実行しないというルールにする
- planができないのは不便なため。applyができるのは塞ぎたかったがうまくできず今後の検討事項
実装の詳細
注意事項
説明したコードは、あくまでもお使いの環境や要件に合わせて変更し、動作確認を十分におこなった上でご利用ください。
また、この運用自体も成熟しているとは言い難いので今後修正していく可能性が大いにあります。より良い方法についての指摘がもしあればいただけると非常にありがたいです!
フォルダ構成
今回の仕組み作るにあたって以下を追加しています。
terraform/continuous_apply/script/apply.sh ... apply用のシェル
/build.sh ... applyかplanかで振り分ける用のシェル
/install.sh ... tfnotifyのインストール用
/plan.sh ... plan用のシェル
/_terraformrc ... terraform cloudの設定ファイル用のテンプレート
/buildspec.yml ... CodeBuild用のyaml
/tfnotify_github.yml ... tfnotifyの設定ファイル、github用
/tfnotify_slack.yml ... tfnotifyの設定ファイル、slack用
それぞれのファイルを見ていきます。
CodeBuildで実行されるシェルスクリプト
install.shではtfnotifyをインストールしています。これが既に組み込まれたコンテナImageがあれば良いので、いずれ対応する予定です。なお、tfnotifyは現状の最新を入れたため、実践Terraformにあったサンプルスクリプトをアレンジしてます。
#!/bin/sh
VERSION="v0.6.1"
BASE_URL=https://github.com/mercari/tfnotify/releases/download
DOWNLOAD_URL="${BASE_URL}/${VERSION}/tfnotify_linux_amd64.tar.gz"
wget ${DOWNLOAD_URL} -P /tmp
mkdir -p /tmp/tfnotify_linux_amd64
tar zxvf /tmp/tfnotify_linux_amd64.tar.gz -C /tmp/tfnotify_linux_amd64
mv /tmp/tfnotify_linux_amd64/tfnotify /usr/local/bin/tfnotify
build.shではマージ時とそうでないかの振り分け、さらにマージ時では本番環境とそれ以外をマージ先で切り分けています。
本当はplanも切り分けたかったのですが、現状まだできていません。
#!/bin/sh
set -x
if [[ ${CODEBUILD_WEBHOOK_TRIGGER} = 'branch/master' ]]; then
${CODEBUILD_SRC_DIR}/terraform/continuous_apply/scripts/apply.sh master
elif [[ ${CODEBUILD_WEBHOOK_TRIGGER} = 'branch/develop' ]]; then
${CODEBUILD_SRC_DIR}/terraform/continuous_apply/scripts/apply.sh develop
else
${CODEBUILD_SRC_DIR}/terraform/continuous_apply/scripts/plan.sh
fi
planでは変更があったフォルダを取得し、terraform applyを実施します。GithubとSlack両方に通知したいので二回パイプしてます。
#!/bin/sh
DIRS=$(git --no-pager diff origin/develop..HEAD --name-only | xargs -I {} dirname {} | grep "terraform" | uniq)
if [ -z "$DIRS" ]; then
echo "No directories for apply."
exit 0
fi
for dir in $DIRS
do
if [ ! -e $dir/terraform.tf ]; then
continue
fi
echo $dir
(cd $dir && terraform init -input=false -no-color)
(cd $dir && terraform plan -input=false -no-color | tfnotify --config ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/tfnotify_github.yml plan --message "$dir" | tfnotify --config ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/tfnotify_slack.yml plan --message "$dir" )
done
apply用のシェルでは基本的に流れはplanと同じですが、本番用かそれ以外かを切り分けています。
#!/bin/sh
MODE=$1
PROD_DIRS='terraform/prod/prod|terraform/prod/common'
echo "Mode: ${MODE}"
MESSAGE=$(git log ${CODEBUILD_SOURCE_VERSION} -1 --pretty=format:"%s")
CODEBUILD_SOURCE_VERSION=$(echo ${MESSAGE} | cut -f4 -d' ' | sed 's/#/pr\//')
get_dir_list () {
if [ $1 = 'master' ]; then
git --no-pager diff HEAD^..HEAD --name-only | xargs -I {} dirname {} | grep "terraform" | egrep "$2" | uniq
else
git --no-pager diff HEAD^..HEAD --name-only | xargs -I {} dirname {} | grep "terraform" | egrep -v "$2" | uniq
fi
}
DIRS=$(get_dir_list $MODE $PROD_DIRS)
echo $DIRS
if [ -z "$DIRS" ]; then
echo "No directories for apply."
exit 0
fi
for dir in $DIRS
do
if [ ! -e $dir/terraform.tf ]; then
continue
fi
echo $dir
(cd $dir && terraform init -input=false -no-color)
(cd $dir && terraform apply -input=false -no-color -auto-approve | tfnotify --config ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/tfnotify_github.yml apply --message "$dir" | tfnotify --config ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/tfnotify_slack.yml apply --message "$dir" )
done
buildspec.yml、tfnotifyの設定
以下がCodeBuildで実装する用のbuilspecです。
ポイントとしては、install時にtfnotifyを入れているのと、terraform cloudの設定ファイルをおくための処理を入れてます。
version: 0.2
env:
parameter-store:
GITHUB_TOKEN: "/continuous_apply/github_token"
TERRAFROM_CLOUD_TOKEN: "/continuous_apply/terraform_cloud_token"
SLACK_TOKEN: "/continuous_apply/slack_token"
phases:
install:
commands:
- ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/scripts/install.sh
- sed -e "s/_TOKEN_/${TERRAFROM_CLOUD_TOKEN}/" ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/_terraformrc > ~/.terraformrc
build:
commands:
- ${CODEBUILD_SRC_DIR}/terraform/continuous_apply/scripts/build.sh
credentials "app.terraform.io" {
token = "_TOKEN_"
}
tfnotitfyの設定は以下のようになってます。読みやすくするためGitHubとSlackでフォーマットを変えてます。
ci: codebuild
notifier:
github:
token: $GITHUB_TOKEN
repository:
owner: "(__secret__)"
name: "(__secret__)"
terraform:
plan:
template: |
{{ .Title }}
{{ .Message }}
{{if .Result}}<pre><code> {{ .Result }} </pre></code>{{end}}
<details><summary>Details (Click me)</summary>
<pre><code> {{ .Body }} </pre></code></details>
apply:
template: |
{{ .Title }}
{{ .Message }}
{{if .Result}}<pre><code> {{ .Result }} </pre></code>{{end}}
<details><summary>Details (Click me)</summary>
<pre><code> {{ .Body }} </pre></code></details>
ci: codebuild
notifier:
slack:
token: $SLACK_TOKEN
channel: "(__secret__)"
bot: "(__secret__)"
terraform:
plan:
template: |
{{ .Title }}
{{ .Message }}
{{if .Result}}
```
{{ .Result }}
```
{{end}}
```
{{ .Body }}
```
apply:
template: |
{{ .Title }}
{{ .Message }}
{{if .Result}}
```
{{ .Result }}
```
{{end}}
```
{{ .Body }}
```
AWSの設定
terraformで書いています。実際には本番用と開発用が必要で、以下は開発環境用です。
不要にビルドしないようにここでも変更フォルダでフィルタリングしています。
resource "aws_codebuild_project" "continuous_apply" {
name = "${var.project}-common-continuous-apply"
service_role = module.continuous_apply_codebuild_role.iam_role_arn
source {
type = "GITHUB"
location = "(_secret_)"
buildspec = "terraform/continuous_apply/buildspec.yml"
}
artifacts {
type = "NO_ARTIFACTS"
}
environment {
type = "LINUX_CONTAINER"
compute_type = "BUILD_GENERAL1_SMALL"
image = "hashicorp/terraform:0.12.25"
privileged_mode = false
}
provisioner "local-exec" {
command = <<-EOT
aws codebuild import-source-credentials \
--server-type GITHUB \
--auth-type PERSONAL_ACCESS_TOKEN \
--token $GITHUB_TOKEN
EOT
environment = {
GITHUB_TOKEN = data.aws_ssm_parameter.github_token.value
}
}
}
resource "aws_codebuild_webhook" "continuous_apply" {
project_name = aws_codebuild_project.continuous_apply.name
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_CREATED"
}
filter {
exclude_matched_pattern = false
pattern = "^terraform/dev/"
type = "FILE_PATH"
}
}
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_UPDATED"
}
filter {
exclude_matched_pattern = false
pattern = "^terraform/dev/"
type = "FILE_PATH"
}
}
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_REOPENED"
}
filter {
exclude_matched_pattern = false
pattern = "^terraform/dev/"
type = "FILE_PATH"
}
}
filter_group {
filter {
type = "EVENT"
pattern = "PUSH"
}
filter {
type = "HEAD_REF"
pattern = "develop"
}
filter {
exclude_matched_pattern = false
pattern = "^terraform/dev/"
type = "FILE_PATH"
}
}
}
GitHubの設定
細かな説明は省きますが、ブランチに対して、事故防止のため以下の設定を入れています。
- Require pull request reviews before merging
- Require branches to be up to date before merging
まとめ
以上で継続的applyの仕組みを整えることができました。完全にガッチリとした運用、仕組みとは言い難いのですが、一人でしか扱えない状態に比べては格段に運用しやすくなったかと思います。参考にしつつ仕組みを組んでいただければ幸いです。
参考
冒頭に述べたものとほぼ同じですですが、改めてまとめます。これらの内容は非常に参考になりました。ありがとうございます!