最近、AWS環境の構築・管理にはTerraformを利用しています。
Terraformの実行環境はDockerコンテナを作りこんで使っていますが……
- ローカルで動かしてると重い
- 運用保守フェーズに入ると滅多に使わないから環境を維持しておきたくない
- ローカルの環境を消した時に限って必要になって焦る
みたいな事があり、CodePipelineから実行できるようにしました。 (Terraform Cloudはいったん置いておきます)
※ Terraform実行のためのPipelineですが、Terraformで構築しています。
※ 設定ポイントのみ記載し、Terraformテンプレート全文は載せていません。
※ CodeBuild/CodePipelineの基本的な構築はできる方が対象になります。
概要
CodePipelineは以下のようなフローになります。
- Source: CodeCommitからファイルを取得
- Plan: CodeBuildで
terraform init
とterraform plan
を実行 - Approval: 手動の承認アクション
- Apply: CodeBuildで
terraform apply
を実行して環境へ反映
これを利用する時は、以下のような手順になります。
- Terraformテンプレートを修正してCommit
- CodePipelineを実行(自動実行の設定はしてないので)
- CodeBuildの実行結果からPlanの結果を確認
- Plan結果に問題無ければCodePipelineで承認、問題があれば却下
- 承認した場合は実行結果を確認
Planの結果をS3等へアップして承認アクションのリンクから確認ができたらさらに素敵かもしれませんが、そこまでは考慮しません。
おとなしくCodePipelineのリンクを辿ってCodeBuildを確認します。
Terraformの自動化
通常、terraformコマンドを使ってapplyする時は更新内容を確認後に『yes』と手で入力する必要があります。
しかし、このままではCodeBuildで実行できません。
applyの対話式の入力を防ぐために
-auto-approve
オプションでスキップする- plan結果を出力してapply時に指定する
というような手法を取ることができます。
上の-auto-approve
オプションは、planの結果確認をせずにapplyを実行します。
一見すると事前にplanで確認すれば問題ないように見えますが、planからapplyまでの間に変更が入った場合は意図しない更新がされてしまう場合があります。
上記の問題を解決するために、planコマンドの-out
オプションを使います。
-out
オプションでplanの結果をファイル出力すると、apply時にそのファイルを読み込ませることが可能です。
これにより、planで検知された変更だけがapplyで適用されます。
また、terraformのinit、plan、applyにinput=false
オプションを付与すれば対話プロンプトが出ないようにできます。
buildspec
buildspec.ymlはplanとapplyの2つを用意し、Terraformテンプレートと一緒に配置します。
buildspec - Plan
version: 0.2 phases: pre_build: on-failure: ABORT commands: - "cd ${CODEBUILD_SRC_DIR}/terraform/env/${TERRAFORM_ENV_DIR}" - "terraform init -var-file=./variables.tfvars -input=false" build: commands: - "cd ${CODEBUILD_SRC_DIR}/terraform/env/${TERRAFORM_ENV_DIR}" - "terraform plan -var-file=./variables.tfvars -input=false -out=./.tfplan" artifacts: base-directory: ${CODEBUILD_SRC_DIR} files: - "**/*"
環境変数TERRAFORM_ENV_DIR
にTerraformを実行する対象のディレクトリ名を設定することで、1ファイルで複数ディレクトリへ対応するようにしました。
また、.tfvarsファイルもすべてのディレクトリに同じファイル名で作成しておき、同じコマンドで実行できる環境にしています。
このbuidspecではinitとplanを実行し、plan結果を同じディレクトリに保存しています。
この後のapplyで利用できるように、init/plan実行結果を含むソース全体をArtifactとして出力しておくのもポイントです。
buildspec - Apply
version: 0.2 phases: build: commands: - "cd ${CODEBUILD_SRC_DIR}/terraform/env/${TERRAFORM_ENV_DIR}" - "terraform show ./.tfplan" - "terraform apply -input=false ./.tfplan"
環境変数TERRAFORM_ENV_DIR
はplanと同じものが設定されます。
applyの引数にはplanで作成したファイル名です。前工程でinit/planされたソースを使うことが前提となります。
terraform show
しているのは、Apply失敗時に調査しやすいようにPlanファイルの内容を出力しています。
CodeBuild
イメージ
CodeBuildで利用するDockerイメージはECR Public GalleryにあるTerraformイメージを利用します。
locals { cicd_terraform_codebuild_repository_uri = "public.ecr.aws/hashicorp/terraform:1.3.3" }
CodeBuildのカスタムイメージは実行時にENTRYPOINT
が書き換えられるため、このイメージをそのまま利用可能です。
CodeBuild - Plan
resource "aws_codebuild_project" "cicd_terraform_plan" { for_each = var.cicd_terraform_pipelines ~<中略>~ build_timeout = 90 source { type = "CODEPIPELINE" buildspec = "terraform/buildspec_terraform_plan.yml" } ~<中略>~ environment { compute_type = "BUILD_GENERAL1_SMALL" image = local.cicd_terraform_codebuild_repository_uri type = "LINUX_CONTAINER" image_pull_credentials_type = "SERVICE_ROLE" privileged_mode = false environment_variable { name = "AWS_DEFULT_REGION" value = data.aws_region.current.name } environment_variable { name = "TERRAFORM_ENV_DIR" value = lookup(each.value, "env_dir", each.key) } } }
CodeBuild設定のポイントは以下の通りです。
source.buildspec
にplan用のbuildspecファイルを指定するenvironment.image
に前述のTerraformイメージを指定する- build_timeoutは十分に長い時間を設定する(リソースが増えた場合等に
plan
でも時間のかかる場合があります) - 環境変数
TERRAFORM_ENV_DIR
を設定する
CodeBuild - Apply
基本的にPlanの内容からbuildspec
を変更したものです。
resource "aws_codebuild_project" "cicd_terraform_apply" { for_each = var.cicd_terraform_pipelines ~<中略>~ build_timeout = 90 source { type = "CODEPIPELINE" buildspec = "terraform/buildspec_terraform_apply.yml" } ~<中略>~ environment { compute_type = "BUILD_GENERAL1_SMALL" image = local.cicd_terraform_codebuild_repository_uri type = "LINUX_CONTAINER" image_pull_credentials_type = "SERVICE_ROLE" privileged_mode = false environment_variable { name = "AWS_DEFULT_REGION" value = data.aws_region.current.name } environment_variable { name = "TERRAFORM_ENV_DIR" value = lookup(each.value, "env_dir", each.key) } } }
RDS作成等の時間のかかる処理もあるため、build_timeoutの設定は慎重に考える必要があるかと思います。
CodePipeline
基本的には上で作成したCodeBuildを順番に動かすだけです。
Planのoutput_artifacts
をApplyのinput_artifacts
にしているところだけ注意が必要です。
resource "aws_codepipeline" "cicd_terraform" { for_each = var.cicd_terraform_pipelines ~<中略>~ stage { name = "Source" ~<中略>~ } stage { name = "Plan" action { name = "${each.key}-plan" category = "Build" owner = "AWS" provider = "CodeBuild" input_artifacts = ["terraform_sources"] output_artifacts = ["planned_templates"] version = "1" configuration = { ProjectName = aws_codebuild_project.cicd_terraform_plan[each.key].name PrimarySource = "terraform_sources" } } } stage { name = "Approval" action { name = "${each.key}-approval" category = "Approval" owner = "AWS" provider = "Manual" version = "1" input_artifacts = [] configuration = { NotificationArn = aws_sns_topic.cicd_terraform.arn CustomData = "Terraform apply for ${lookup(each.value, "env_dir", each.key)}." } } } stage { name = "Apply" action { name = "${each.key}-apply" category = "Build" owner = "AWS" provider = "CodeBuild" input_artifacts = ["planned_templates"] version = "1" configuration = { ProjectName = aws_codebuild_project.cicd_terraform_apply[each.key].name PrimarySource = "terraform_sources" } } } }
最後に
最初の構築フェーズでテンプレートを作成する時は更新とplanを繰り返すと思います。その時は、わざわざCodePipelineで確認するのは時間がかかるし面倒です。
そのため、最初はローカルにTerraform実行環境が必要だと感じており、Terraform構築環境自体をTerraformで作るという方向にしました。
社内のIaC標準化を進める中で、環境の精査は今後も続けていく必要があると感じています。