概要
CodeBuildサービスを利用してterraformリポジトリの継続的インテグレーションが行えるようになるまでの設定手順をハンズオンライクに記載します。
前提
- Githubでtfファイルを管理している
- AWSアカウントを保持している
- ステートファイルをS3バケットで管理している
実施すること
AWS上のCodeBuild
サービスを用いて、Github
で管理しているterraform
コードに修正が行われた際、修正プルリクエストの更新等をトリガーとして、自動的に以下を行うようなプロジェクトを作成します。
- フォーマット確認(format)
- バリデーション(validate)
- 静的解析(lint)
- 実行計画(plan)
- 実行計画の通知(tfnotify)
フォーマット確認
terraform fmt
コマンドではファイルにフォーマットをかけたり、未フォーマットのファイルを検知を行ったりします。
$ terraform fmt -recursive -check
-
-recursive
オプション:現在のディレクトリから再帰的にtfファイルを探して実行する -
-check
オプション:formatの正しくないファイルがあった場合にexit codeが0以外になる
なお、-check
オプションをつけない場合は、自動的に全ファイルのフォーマットが行われ、フォーマット対象となったファイル名一覧が返却されます。
バリデーション
terraform validate
コマンドで、変数が未定義かどうか、構文にエラーが存在するかといったことをチェックします。
ただし、validate
を行う前にinit
を実行しておく必要があります。
$ terraform init
$ terraform validate
validate
をはじめ、これ以降のコマンドに関してはfmt
のように再帰的に実行可能なオプションが用意されていないため、自前でtfファイルが存在するディレクトリ一覧を取得し、それぞれのディレクトリに対してコマンド実行するといった工夫が必要となります。
シェルコマンドでこれを解決する一例を以下に示しておきます。
$ find ${base_dir} -type f -name "*.tf" -exec dirname {} \; | sort -u | xargs -I {} terraform validate {}
静的解析
tflintを用いることでtfファイルの静的解析を行います。
手元で使う場合はbrewによるインストールが必要となります。
$ brew install tflint
たとえば、次のように存在しないリソース種別を定義しているファイルを検知することが可能です。
resource "aws_instance" "tutorial" {
ami = data.aws_ami.latest.image_id
instance_type = "t2.micro2" # 存在しないEC2インスタンスタイプを指定
iam_instance_profile = "unknown_profile" # 存在しないプロファイルを指定
}
$ tflint
1 issue(s) found:
Error: "t2.micro2" is an invalid value as instance_type (aws_instance_invalid_type)
on main.tf line 33:
33: instance_type = "t2.micro2"
AWS APIを用いてより詳細な解析を行うために--deep
オプションが用意されています。
上記の例では、このオプションを付与することでiam_instance_profile
が不正であることも検知することが可能となります。
なお、--deep
オプションを利用する際にはAWS APIを利用するため、実行時のregion指定およびAWSへのCredentials設定が必要となります。
$ tflint --deep --region=ap-northeast-1
2 issue(s) found:
Error: "t2.micro2" is an invalid value as instance_type (aws_instance_invalid_type)
on main.tf line 33:
33: instance_type = var.aws_instance_type
Error: "unknown_profile" is invalid IAM profile name. (aws_instance_invalid_iam_profile)
on main.tf line 34:
34: iam_instance_profile = "unknown_profile"
validate
tflint
のそれぞれに得意・不得意があるようなので、組み合わせて使うのが良いのではないかと考えています。
実行計画
terraform plan
を利用することで、実行計画を取得します。
通常実行するケースでは特にオプションをつける必要性はありませんが、今回のCodeBuildやcircleci上で実行する場合、実行結果ログを見やすくするために-no-color
オプションを付与することが好ましいです。(実行結果が色つきではなくANSIエスケープコードで表示されるため)
また、plan
を行うためにはinit
が前提となります。
$ terraform init
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
data.aws_ami.latest: Refreshing state...
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.tutorial will be created
+ resource "aws_instance" "tutorial" {
+ ami = "ami-0a1c2ec61571737db"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ id = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
・・・(中略)
}
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
実行計画の通知
tfnotifyを用いることで、GithubのPRやSlackへplan
の実行結果を通知することができます。あらかじめ実行計画を見ることができる点が非常に便利です。
対象が複数環境となる場合でGithub PRへの通知設定を行っている場合、ややPRのコメントが流れやすくなってしまうため多少運用面での工夫が必要となるかもしれません。
terraform plan
をパイプラインで渡して実行します。
$ terraform plan | tfnotify plan --config tfnotify.yml
通知先や通知内容の設定は任意のymlファイルに記載することができます。(詳細はこちらを参照)
なお、Githubへ連携を行うためにはアクセス用トークンを発行する必要があります。(後述)
一例を以下に示します。
ci: codebuild # ciを行う場所 circleciならcircleci
notifier:
github: # 今回はgithubへ通知
token: $GITHUB_TOKEN # github連携用トークン
repository:
owner: "mintak21" # 通知先リポジトリのオーナーユーザー名称
name: "terraform" # 通知先リポジトリ名称
terraform:
plan:
# 通知結果表示テンプレートを定義(html利用可能)
template: |
{{ .Title }}
{{ .Message }}
{{if .Result}}<pre><code> {{ .Result }} </pre></code>{{end}}
<details><summary>Details (Click me)</summary>
<pre><code> {{ .Body }} </pre></code></details>
# 実行結果ごとのアクション定義(Github限定)
when_add_or_update_only: # addかupdateだけの場合、add-or-updateラベルをつける
label: "add-or-update"
when_no_changes:
label: "no-changes"
when_destroy: # destroyがある場合、destroyラベルをつけ、WARNINGを付記
label: "destroy"
template: |
## :warning: WARNING: Resource Deletion will happen :warning:
This plan contains **resource deletion**. Please check the plan result very carefully!
when_plan_error:
label: "error"
使用できるテンプレート変数
変数 | 概要 |
---|---|
{{ .Title }} | ## Plan result |
{{ .Message }} | tfnotify コマンド実行時の --message オプションで指定した内容 |
{{ .Result }} |
plan 最後の1行 ex: Plan: 1 to add, 0 to change, 0 to destroy.
|
{{ .Body }} |
plan の内容 |
{{ .Link }} |
tfnotify を実行したCIページへのリンク |
CodeBuildプロジェクトの作成
CIにて確認したい内容、確認の方法を上記で見ることができたので、ここからそれを実行するためのCodeBuildプロジェクトを作成していきましょう。もちろんterraform
で作成します。
次のようなディレクトリ構成とします。
コードの全容はリポジトリをご参照ください。
├── cicd/ # CodeBuild設定管理ファイル
│ ├── backend.tf
│ ├── buildspec.yml # ビルドフェーズ定義ファイル
│ ├── main.tf # エントリポイント
│ ├── output.tf # 出力定義
│ ├── providers.tf
│ ├── terraform.tfvars
│ ├── tfnotify.yml # tfnotify設定
│ └── variables.tf
├── files/ # CIの対象とするファイル群(階層はイメージ)
│ ├── development/
│ │ └── main.tf
│ └── staging/
│ └── main.tf
├── module/ # モジュール
└── scripts/ # CodeBuildビルドフェーズで使用するスクリプト群
└── ci/
├── format.sh
├── install_packages.sh
├── lint_aws.sh
├── plan.sh
├── settings.sh
└── validate.sh
1. Github連携トークンの払い出し
githubと連携を行うため必要なトークンを作成します。
こちらを参照して発行を行ってください。
2. パラメータストアへのトークン登録
連携トークンは秘匿情報にあたります。CodeBuild
の環境変数に設定してもよいのですが、よりセキュアに管理するためトークン情報はAWS Systems Manager パラメータストアに保存し、CodeBuild
からパラメータ参照することとします。ここではaws-cli
コマンドを使用して保存します。
aws ssm put-parameter --type SecureString --name パラメータ名 --value Githubトークン
3. CodeBuild用IAMロール払い出し
CodeBuild用のIAMロール作成を行います。
モジュールの詳細を割愛しますが、指定したポリシードキュメントを持つIAMポリシーを作成、サービスロールとしてIAMロールを作成し、これらの関連付けを行っています。
また、少し楽をするため、アタッチするポリシーはCodeBuild全権限としています。通常は必要な権限を絞って適用すべきである点ご注意ください。
data "aws_iam_policy" "administrator" {
arn = "arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess" // !!!CodeBuildAdmin権限注意!!!
}
module "service_role_for_continuous_check" {
source = "./../module/aws/iam"
role_name = var.codebuild_role_name
policy_name = var.codebuild_role_policy_name
policy = data.aws_iam_policy.administrator.policy
principal_service_identifiers = ["codebuild.amazonaws.com"]
}
4. CodeBuildプロジェクト定義
次にCodeBuildプロジェクト作成のためのコードを書いていきます。
4.1. CI実行用スクリプトの作成
ビルドフェーズでlint
やplan
を行うにあたり対象とするディレクトリに対して再帰的に実行したいため、ラッパーシェルを作成します。例えばplan
を行うシェルは以下のように作成しています。
なおここで使用しているCODEBUILD_SRC_DIR
という環境変数を使用することでCodeBuild
でビルドする際のルートディレクトリを取得することが可能です。
target_dirs=`find ../../${base_dir} -type f -name "*.tf" -exec dirname {} \; | sort -u`
for target in ${target_dirs}
do
cd ${target}
terraform init -input=false -no-color
terraform plan -input=false -no-color | \
tfnotify --config ${CODEBUILD_SRC_DIR}/cicd/tfnotify.yml plan --message "$(date)"
done
また、今回ベースイメージにterraform:lightを使用しますが、tflint
やtfnotify
は含まれていないため、installフェーズでこれらを取得するためのスクリプトも作成しておきます。(専用のカスタムイメージを作成し、これを使用するようにすればinstallフェーズ自体のスキップすることができるかもしれません)
TF_LINT_VER="v0.25.0"
TF_NOTIFY_VER="v0.7.0"
install_tflint() {
REPO_URL="https://github.com/terraform-linters/tflint/releases/download"
DL_LINK="${REPO_URL}/${TF_LINT_VER}/tflint_linux_amd64.zip"
wget ${DL_LINK} -P /tmp
unzip /tmp/tflint_linux_amd64.zip -d /tmp
mv /tmp/tflint /bin/tflint
echo "install tflint"
}
install_tfnotify() {
REPO_URL="https://github.com/mercari/tfnotify/releases/download"
DL_LINK="${REPO_URL}/${TF_NOTIFY_VER}/tfnotify_linux_amd64.tar.gz"
wget ${DL_LINK} -P /tmp
tar zxvf /tmp/tfnotify_linux_amd64.tar.gz -C /tmp
mv /tmp/tfnotify /bin/tfnotify
echo "install tfnotify"
}
install_tflint
install_tfnotify
4.2. ビルドフェーズ定義ファイルの作成
CodeBuild
でのビルドはSUBMITTED
フェーズ〜FINALIZING
フェーズまで、一連のフェーズにて行われます。
これら各種フェーズで何を行うかをymlファイルで設定することが可能です。
今回はINSTALL
フェーズで必要となるパッケージをインストールし、BUILD
フェーズでformat/validate/lint/planを行う(シェルを呼び出す)ように設定します。
version: 0.2
env:
parameter-store:
GITHUB_TOKEN: "2で指定したパラメータ名"
phases:
install:
commands:
- ${CODEBUILD_SRC_DIR}/scripts/ci/install_packages.sh
build:
commands:
- ${CODEBUILD_SRC_DIR}/scripts/ci/format.sh
- ${CODEBUILD_SRC_DIR}/scripts/ci/validate.sh
- ${CODEBUILD_SRC_DIR}/scripts/ci/lint_aws.sh
- ${CODEBUILD_SRC_DIR}/scripts/ci/plan.sh
4.3. プロジェクトの作成ファイル
一例を示しますが、重要な項目は次となります。
-
service_role
:紐付けるサービスロール。払い出したロールのarn値を指定する -
source
ブロック:CIの実行対象とするリポジトリの定義-
report_build_status
:true
: CI結果の通知を行う。デフォルトfalse
なので有効化しない限りGithub側へ反映されない
-
-
environment
ブロック:CIを行う環境の準備-
image
:使用するDockerイメージ -
compute_type
:マシンスペック。特に意識しなければ最小構成のBUILD_GENERAL1_SMALL
を選択すべし
-
-
logs_config
ブロック:ログ出力設定。CloudWatch
はONにしておくとよいかと
resource "aws_codebuild_project" "continuous_check" {
// プロジェクトの設定
name = var.codebuild_project_name
description = "continuous integration project for terraform repogistory"
badge_enabled = false
tags = {
Author = "mintak21"
}
// ソース
source {
type = "GITHUB"
location = var.github_repository_location
git_clone_depth = 1
report_build_status = true // リポジトリ側へ結果通知
buildspec = var.buildspec_settings
}
// 環境
environment {
image = "hashicorp/terraform:light" // カスタムイメージURL
type = "LINUX_CONTAINER" // 環境タイプ
compute_type = "BUILD_GENERAL1_SMALL" // コンピューティングタイプ
privileged_mode = false
}
// サービスロール
service_role = module.service_role_for_continuous_check.this_aws_iam_role_arn
// タイムアウト
build_timeout = "30"
// キュータイムアウト
queued_timeout = "60"
// アーティファクト
artifacts {
type = "NO_ARTIFACTS"
}
// キャッシュ
cache {
type = "LOCAL"
modes = ["LOCAL_SOURCE_CACHE"]
}
// ログ
logs_config {
cloudwatch_logs {
status = "ENABLED"
group_name = "mintak"
stream_name = "logs-for-${var.codebuild_project_name}"
}
s3_logs {
status = "DISABLED"
}
}
}
5. Webhook作成ファイル
プロジェクトを作成しましたが、このままではGithub
にpushやPRの作成等を行っても、CodeBuild
へ連携されることがありません。そこでこれらのイベントをトリガーとしてプロジェクトのビルドが実行されるようにWebhookを作成します。
filter
ブロックで検知するイベントの種別、パターンを記述します。ここでは以下をトリガーとしてビルド実行される設定を行います。
- PR作成・更新
- masterブランチへのpush
resource "aws_codebuild_webhook" "continuous_check" {
project_name = aws_codebuild_project.continuous_check.name
// PR作成・更新時
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_CREATED"
}
}
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_UPDATED"
}
}
// masterブランチpush時
filter_group {
filter {
type = "EVENT"
pattern = "PUSH"
}
filter {
type = "HEAD_REF"
pattern = "master"
}
}
}
6. プロジェクト作成
ここまでで必要なコード類がそろったので、いよいよapply
でプロジェクトを作成します。
$ cd cicd
$ terraform init
$ terraform apply
7. Githubブランチの設定
ここまででCIは実行されるようになりましたが、CIに失敗した場合にPRがマージが可能な状態は避けたいものです。
そこでGithubリポジトリのブランチ保護設定を行い、PRのマージ可能条件に「CIの成功」を設定します。master
ブランチおよびdevelop
ブランチに設定するのが良いでしょう。
試してみる
では実際にfiles
ディレクトリ以下のtfファイルに任意の変更を行い、プルリクエストを作成してみましょう。
CodeBuild
が実行され、以下が通知されれば正しく設定が完了しています。
-
tfnotify
によりplan
結果(コメント) - ビルドステータス
おつかれさまでした!