🚀

GitHubの機能を活用した Terraform CI/CD Template Repository

2021/12/07に公開

この記事はTerrafrom Advent Calendar 2021の7日目の記事です。

はじめに

2019年12月のTerraform Meetup Tokyo#3にて、「GitHub Actionsで Terraformをplan&applyしてみた」 でLTしてから早2年が過ぎました・・

この時はGitHub ActionsがGAした直後でしたが、2年の月日が経ち、Terraform公式のActionsも変わっていたり、他にも便利な機能が増えている為、Actionsを活用したTerraformのGitHubテンプレートリポジトリを作成してみました。

大規模で複雑なシステムにおいてはそのまま活用はできないかもしれませんが、これからTerraformを始める方や、簡単なシステムを作りたい方であればそのまま利用してもらえると思うので、是非参考にしていただければと思います。

想定する読者

  • Terraformの基本的な知識はあり、これから活用していきたい
  • GitHub Actionsを活用し、Terraform周りの運用を自動化したい

前提条件

  • インフラはAWSを想定
  • 本番(Production)、ステージング(Staging)、開発(Develop)の3環境を運用
  • 3環境はAWSアカウントを分けて運用
  • 環境毎の構成ファイルはディレクトリを分けて管理

テンプレートリポジトリ

https://github.com/dehio3/terraform-template

リポジトリ構成

├── .github
│   ├── actions
│   │   ├── terraform-apply
│   │   │   └── action.yml
│   │   └── terraform-plan
│   │       └── action.yml
│   ├── dependabot.yml
│   └── workflows
│       ├── apply-develop.yml
│       ├── apply-production.yml
│       ├── apply-staging.yml
│       ├── plan-dependabot.yml
│       ├── plan-develop.yml
│       ├── plan-production.yml
│       └── plan-staging.yml
├── .tfsec
│   └── config.yml ### tfsec用定義ファイル
├── README.md
├── cf-github-actions-role.yml ### Terraform実行用ロールCFnテンプレート
├── develop ### 開発環境
│   ├── .tfsec
│   │   └── config.yml -> ../../.tfsec/config.yml
│   ├── backend.tf
│   ├── provider.tf -> ../shared/provider.tf
│   ├── terraform.tf -> ../shared/terraform.tf
│   ├── terraform.tfvars
│   └── variables.tf -> ../shared/variables.tf
├── production ### 本番環境
│   ├── .tfsec
│   │   └── config.yml -> ../../.tfsec/config.yml
│   ├── backend.tf
│   ├── provider.tf -> ../shared/provider.tf
│   ├── terraform.tf -> ../shared/terraform.tf
│   ├── terraform.tfvars
│   └── variables.tf -> ../shared/variables.tf
├── shared
│   ├── provider.tf
│   ├── terraform.tf
│   └── variables.tf
└── staging ### ステージング環境
    ├── .tfsec
    │   └── config.yml -> ../../.tfsec/config.yml
    ├── backend.tf
    ├── provider.tf -> ../shared/provider.tf
    ├── terraform.tf -> ../shared/terraform.tf
    ├── terraform.tfvars
    └── variables.tf -> ../shared/variables.tf

環境で共有する定義

バージョンや変数など、運用する上で環境差異をつけない項目については、各環境用ディレクトリにはシンボリックリンクを配置し、実体としては一つの定義ファイルで管理するようにしています。

shared/version.tf

terraform及びproviderのバージョンを指定します。

shared/version.tf
terraform {
  required_version = "1.0.8"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.64.0"
    }
  }
}

shared/provider.tf

providerの設定を記載します。

shared/provider.tf
provider "aws" {
  default_tags {
    tags = local.tags
  }
}

https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags

shared/variables.tf

変数を定義します。

shared/variables.tf
variable "environment" {
  description = "environment prefix"
  type        = string
}

variable "service" {
  description = "service name"
  type        = string
}

locals {
  prefix = "${var.environment}-${var.service}"
  tags = {
    "Terraform"   = "true"
    "Environment" = var.environment
    "Service"     = var.service
  }
}

リソース名やタグ情報として、環境とサービス名は利用することが多いので、変数としてenvironmentserviceを定義しています。

また、設定した変数からローカル変数としてprefixtagsを定義しています。

  • local.prefix ... 環境とサービス名を繋げて、リソース名の識別子なとで利用
  • local.tags ... リソースに必須で設定したいタグを定義し、default_tagsに指定

.tfsec/config.yml

tfsecの設定を定義します。

https://aquasecurity.github.io/tfsec/v0.61.0/

Terraform Plan実行時に、静的セキュリティ解析を実行するために利用しています。
解析を除外したい条件など、セキュリティ指針は環境ごとでの差異はもたせず、同じファイルを参照するようにしています。

環境固有の定義

backend.tf

tfstateファイルを管理するS3バケット情報を定義します。

develop/backend.tf
terraform {
  backend "s3" {
    bucket  = "<S3 bucket name>"
    key     = "terraform.tfstate"
    region  = "<S3 bucket region>"
    encrypt = true
  }
}

https://www.terraform.io/docs/language/settings/backends/index.html

terraform.tfvars

変数の値を定義します。

develop/terraform.tfvars
environment = "dev"
service     = "sample"

変数については複数のリソースを作成していくうちに増加していくと思うので、変数追加時はvariables.tfと合わせて修正します。

初期設定

リポジトリを作成後、Terraformを実行するために以下の初期設定が必要です。

tfstate保存バケット作成

Terraformのステートファイルを保存する為のS3バケットを各AWSアカウントに作成します。
作成したバケット名とリージョンの情報を各環境ディレクトリのbackend.tfに記載します。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/create-bucket-overview.html

Terraform実行用ロール作成

GitHub ActionsからTerraformを実行するためにロールを作成します。

https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/

cf-github-actions-role.ymlCFnテンプレートを使用し、スタックを作成します。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-create-stack.html

スタックのパラメータとして以下を入力します。

パラメータ 説明
GitHubOrg GitHub Organization Name terraformコードを管理するOrganization
RepositoryName Terraform Repository Name terraformコードを管理するリポジトリ
OIDCProviderArn ID Provider ARN 既にIDプロバイダーを設定済みの場合に入力

尚、CFnテンプレートは以下の公式のSampleがあるのでそちらを利用できます。
差分としては、ロール名の指定とadminポリシーの付与が追加されています。
https://github.com/aws-actions/configure-aws-credentials#sample-iam-role-cloudformation-template

GitHub Secrets設定

GitHub Actionsのワークフロー内で指定するTerraform実行用ロールARNはGitHub Secretsから取得するため、作成した各環境のTerraform実行用ロールをGitHub Secretsに追加します。
https://docs.github.com/ja/actions/security-guides/encrypted-secrets

パラメータ
DEV_AWS_ASSUME_ROLE_ARN <開発環境に作成したTerraform実行用ロールのARN>
STG_AWS_ASSUME_ROLE_ARN <ステージング環境作成したTerraform実行用ロールのARN>
PRO_AWS_ASSUME_ROLE_ARN <本番環境作成したTerraform実行用ロールのARN>

プルリクエスト作成時の動作

試しにS3バケットの定義を作成し、プルリクエストを作成します。

s3.tf
resource "aws_s3_bucket" "b" {
  bucket = "my-tf-test-bucket"
  acl    = "private"

  tags = {
    Name = "test"
  }
}

plan結果のコメント出力

planの出力について、コメントが追加されます。

fmtvalidateも合わせて実行しており、エラーとなった場合はエラーメッセージがコメント出力されます。

tfsecによるセキュリティ静的解析

planと同時に、tfsecにより静的セキュリティ解析が行われます。
解析結果がコード内のコメントとして追記されます。

https://tfsec.dev/pr-commenter/

.tfsec/config.ymlにて動作を制御できるので、以下の例ではS3バケットのロギングの設定とバージョニング設定をチェック対象から除外しています。

.tfsec/config.yml
---
exclude:
  - aws-s3-enable-bucket-logging
  - aws-s3-enable-versioning

マージ時の動作

マージするとapplyが実行されます。

AWSプロバイダーの自動アップデート

AWSプロバイダーのバージョンがリリースされると、develop/version.tfを自動で書き換えプルリクを作成します。
プルリクが作成されると、アップデートしたバージョンでのplanが実行さる為、アップデートしたことによる既存への影響がない事を確認した上でマージします。

Dependabotの実装

バージョンのアップデートにはGitHubにてデフォルトで利用できるDependabot version updatesを利用しています。

https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates

version.tfの実体ファイルが存在するsharedディレクトリを指定し、毎朝9時にアップデートをチェックします。

.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "terraform"
    directory: "/shared"
    schedule:
      interval: "daily"
      time: "09:00"
      timezone: "Asia/Tokyo"

InsightsDependency graph画面にて動作結果が確認できます。
Check for updatesをクリックすると、手動でチェック処理をうご描くことができます。

やれてないこと

間に合わず実装できてないです・・

slack通知

Actionsの実行結果や、applyの結果などはSlack通知する方がいいと思います。
slack通知系やActionsが豊富なので、運用に従ってどういう通知をしたいか検討すればいいかなと思います。

AWSプロバイダーの自動アップデートマージ後のapply

AWSプロバイダーの自動アップデートのプルリクエストをマージしてもapplyまでは実行されないです。
バージョンを上げた場合の影響の有無を確認できればいいので、plan差分がなければマージし、次回のリソース作成時などに合わせてapplyされる動きになってます。

まとめ

なるだけ凝ったことは行わず、GitHub既存の機能やGitHub Actionsを活用した構成例を作ってみました。

Terraformの運用自動化についてはいろいろな情報があるので、他の記事も参考にしつつ「ぼくのかんがえたさいきょうのTerraformCI/CD構造」を目指してもらえればと思います。

追記

2022.01.10

plan,apply時のコメントの実装をtfcmtに変更
https://zenn.dev/shunsuke_suzuki/articles/improve-terraform-cicd-with-tfcmt

Discussion