Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

CodePipelineからTerraformの実行

最近、AWS環境の構築・管理にはTerraformを利用しています。
Terraformの実行環境はDockerコンテナを作りこんで使っていますが……

  • ローカルで動かしてると重い
  • 運用保守フェーズに入ると滅多に使わないから環境を維持しておきたくない
  • ローカルの環境を消した時に限って必要になって焦る

みたいな事があり、CodePipelineから実行できるようにしました。 (Terraform Cloudはいったん置いておきます)

※ Terraform実行のためのPipelineですが、Terraformで構築しています。
※ 設定ポイントのみ記載し、Terraformテンプレート全文は載せていません。
※ CodeBuild/CodePipelineの基本的な構築はできる方が対象になります。

概要

CodePipelineは以下のようなフローになります。

CodePipeline処理フロー

  1. Source: CodeCommitからファイルを取得
  2. Plan: CodeBuildでterraform initterraform planを実行
  3. Approval: 手動の承認アクション
  4. Apply: CodeBuildでterraform applyを実行して環境へ反映

これを利用する時は、以下のような手順になります。

  1. Terraformテンプレートを修正してCommit
  2. CodePipelineを実行(自動実行の設定はしてないので)
  3. CodeBuildの実行結果からPlanの結果を確認
  4. Plan結果に問題無ければCodePipelineで承認、問題があれば却下
  5. 承認した場合は実行結果を確認

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標準化を進める中で、環境の精査は今後も続けていく必要があると感じています。