LoginSignup
26
16

More than 3 years have passed since last update.

[Terraform]CodeBuildでTerraformリポジトリのCIを行う

Last updated at Posted at 2020-07-19

概要

CodeBuildサービスを利用してterraformリポジトリの継続的インテグレーションが行えるようになるまでの設定手順をハンズオンライクに記載します。

Untitled Diagram (1).png

前提

  • Githubでtfファイルを管理している
  • AWSアカウントを保持している
  • ステートファイルをS3バケットで管理している

実施すること

AWS上のCodeBuildサービスを用いて、Githubで管理しているterraformコードに修正が行われた際、修正プルリクエストの更新等をトリガーとして、自動的に以下を行うようなプロジェクトを作成します。

  • フォーマット確認(format)
  • バリデーション(validate)
  • 静的解析(lint)
  • 実行計画(plan)
  • 実行計画の通知(tfnotify)

フォーマット確認

terraform fmtコマンドではファイルにフォーマットをかけたり、未フォーマットのファイルを検知を行ったりします。

Terminal
$ terraform fmt -recursive -check
  • -recursiveオプション:現在のディレクトリから再帰的にtfファイルを探して実行する
  • -checkオプション:formatの正しくないファイルがあった場合にexit codeが0以外になる

なお、-checkオプションをつけない場合は、自動的に全ファイルのフォーマットが行われ、フォーマット対象となったファイル名一覧が返却されます。

バリデーション

terraform validateコマンドで、変数が未定義かどうか、構文にエラーが存在するかといったことをチェックします。
ただし、validateを行う前にinitを実行しておく必要があります。

Terminal
$ terraform init
$ terraform validate

validateをはじめ、これ以降のコマンドに関してはfmtのように再帰的に実行可能なオプションが用意されていないため、自前でtfファイルが存在するディレクトリ一覧を取得し、それぞれのディレクトリに対してコマンド実行するといった工夫が必要となります。
シェルコマンドでこれを解決する一例を以下に示しておきます。

Terminal
$ find ${base_dir} -type f -name "*.tf" -exec dirname {} \; | sort -u | xargs -I {} terraform validate {}

静的解析

tflintを用いることでtfファイルの静的解析を行います。
手元で使う場合はbrewによるインストールが必要となります。

Terminal
$ brew install tflint

たとえば、次のように存在しないリソース種別を定義しているファイルを検知することが可能です。

main.tf
resource "aws_instance" "tutorial" {
  ami           = data.aws_ami.latest.image_id
  instance_type = "t2.micro2"               # 存在しないEC2インスタンスタイプを指定
  iam_instance_profile = "unknown_profile"  # 存在しないプロファイルを指定
}
Terminal
$ 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設定が必要となります。

Terminal
$ 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が前提となります。

Terminal
$ 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をパイプラインで渡して実行します。

Terminal
$ terraform plan | tfnotify plan --config tfnotify.yml

スクリーンショット 2020-06-27 15.37.51.png

通知先や通知内容の設定は任意のymlファイルに記載することができます。(詳細はこちらを参照)
なお、Githubへ連携を行うためにはアクセス用トークンを発行する必要があります。(後述)
一例を以下に示します。

tfnotify.yml
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コマンドを使用して保存します。

Terminal
aws ssm put-parameter --type SecureString --name パラメータ名 --value Githubトークン

3. CodeBuild用IAMロール払い出し

CodeBuild用のIAMロール作成を行います。
モジュールの詳細を割愛しますが、指定したポリシードキュメントを持つIAMポリシーを作成、サービスロールとしてIAMロールを作成し、これらの関連付けを行っています。
また、少し楽をするため、アタッチするポリシーはCodeBuild全権限としています。通常は必要な権限を絞って適用すべきである点ご注意ください。

main.tf
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実行用スクリプトの作成

ビルドフェーズでlintplanを行うにあたり対象とするディレクトリに対して再帰的に実行したいため、ラッパーシェルを作成します。例えばplanを行うシェルは以下のように作成しています。
なおここで使用しているCODEBUILD_SRC_DIRという環境変数を使用することでCodeBuildでビルドする際のルートディレクトリを取得することが可能です。

plan.sh
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を使用しますが、tflinttfnotifyは含まれていないため、installフェーズでこれらを取得するためのスクリプトも作成しておきます。(専用のカスタムイメージを作成し、これを使用するようにすればinstallフェーズ自体のスキップすることができるかもしれません)

install_packages.sh
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を行う(シェルを呼び出す)ように設定します。

buildspec.yml
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_statustrue: CI結果の通知を行う。デフォルトfalseなので有効化しない限りGithub側へ反映されない
  • environmentブロック:CIを行う環境の準備
    • image:使用するDockerイメージ
    • compute_type:マシンスペック。特に意識しなければ最小構成のBUILD_GENERAL1_SMALLを選択すべし
  • logs_configブロック:ログ出力設定。CloudWatchはONにしておくとよいかと
main.tf
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
main.tf
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でプロジェクトを作成します。

Terminal
$ cd cicd
$ terraform init
$ terraform apply

7. Githubブランチの設定

ここまででCIは実行されるようになりましたが、CIに失敗した場合にPRがマージが可能な状態は避けたいものです。
そこでGithubリポジトリのブランチ保護設定を行い、PRのマージ可能条件に「CIの成功」を設定します。masterブランチおよびdevelopブランチに設定するのが良いでしょう。

スクリーンショット 2020-06-28 1.37.40.png

試してみる

では実際にfilesディレクトリ以下のtfファイルに任意の変更を行い、プルリクエストを作成してみましょう。
CodeBuildが実行され、以下が通知されれば正しく設定が完了しています。

  • tfnotifyによりplan結果(コメント)
  • ビルドステータス スクリーンショット 2020-06-28 2.03.59.png

おつかれさまでした!

References

26
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
16