【Terraform】GitHub Actions を使って、planやapplyを自動化しよう
Terraform での plan, apply を GitHub Actions で自動化してみます
ゴール
↓ の表のような PR 作成 → マージの処理を作っていきます
No. | 処理 | 備考 |
---|---|---|
1 | ブランチを作成し、PRを作成する | |
2 | PR作成時に terraform plan が実行される | 結果をPRにコメントで残す |
3 | planの結果をレビューする | |
4 | PRをマージする | |
5 | マージ時に terraform apply が実行される |
つくってみる
以下の流れで自動化していきます
- tfstate ファイルを S3 で管理
- OIDC(OpenID Connect)を作成
- GitHub Actions で AWS にアクセスできるかテスト
- GitHub Actions に plan と apply を組み込む
今回は「terraform_test」という名前の Git リポジトリを使用します
tfstate ファイルを S3 で管理
下記のように 2 つのファイルを作成します
terraform_test
├── .gitignore // 作成
├── README.md
└── main.tf // 作成
tfstate を保存する S3 バケットを作成
各ファイルの中身
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Ignore any .tfvars files that are generated automatically for each Terraform run. Most
# .tfvars files are managed as part of configuration and so should be included in
# version control.
#
# example.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
*.hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "ap-northeast-1"
}
# S3バケットの定義
resource "aws_s3_bucket" "terraform_state" {
# バケット名は一意の必要があるので、bucketの値は各自変更してください
bucket = "terraform-state-hisui"
}
# バージョニング設定
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
main.tf 内のコメントにも書きましたが、バケット名は一意である必要がありますなので、バケット名は変更してください!!
次のコマンドを実行していき、S3 を作成します
1. terraform init
2. terraform plan
3. terraform apply -auto-approve
AWS コンソールで確認してみます。
無事作成できました
tfstate の保存先を S3 へ変更
main.tf のterraform ブロック内に backend を追記します
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
+ # tfstateの保存先
+ backend "s3" {
+ bucket = "terraform-state-hisui" # 作成したS3バケット
+ region = "ap-northeast-1"
+ # バケット内の保存先
+ # 適宜変更してください
+ key = "test/terraform.tfstate"
+ encrypt = true
+ }
}
provider "aws" {
region = "ap-northeast-1"
}
# S3バケットの定義
resource "aws_s3_bucket" "terraform_state" {
# バケット名は一意の必要があるので、bucketの値は各自変更してください
bucket = "terraform-state-hisui"
}
# バージョニング設定
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
backend の設定をしたので、terraform init を実行します
terraform_test$ terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
// ローカルのtfstateファイルをS3へコピーするか聞かれているので、「yes」
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
tfstate ファイルが S3 にコピーされているか確認します
コピーされていますねー
OIDC(OpenID Connect)を作成
GitHub の Actions secrets and variables から AWS の認証情報を読み込む方法もありですが、 セキュリティ面を考慮して OIDC を使います。
こちらを参考にiam.tf を作成します
(ディレクトリ)
terraform_test
├── .gitignore
├── README.md
├── iam.tf // 作成
└── main.tf
#============================================================================
# Github Actions 用ロール
#============================================================================
# -----------------------------------------------------------------------------
# GitHub Actions プロバイダー設定
# -----------------------------------------------------------------------------
resource "aws_iam_openid_connect_provider" "terraform_cicd" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# このコードは固定値
# OIDC ID プロバイダーのサムプリント
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
# -----------------------------------------------------------------------------
# GitHub Actions 用ロール作成
# -----------------------------------------------------------------------------
resource "aws_iam_role" "terraform_cicd_oidc_role" {
name = "TerraCICDDemoOIDCRole"
path = "/"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.terraform_cicd.arn
}
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = [
# リポジトリ制限
# xxxxx:GitHubのアカウント名
"repo:xxxxx/terraform_test:*",
]
}
}
}]
})
}
# -----------------------------------------------------------------------------
# ポリシーのアタッチ(AdministratorAccess_attachment)
# -----------------------------------------------------------------------------
resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
role = aws_iam_role.terraform_cicd_oidc_role.name
# Admin権限を指定
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
上記を terraform apply してID プロバイダを作成します
terraform_test$ terraform plan
aws_s3_bucket.terraform_state: Refreshing state... [id=terraform-state-hisui]
aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=terraform-state-hisui]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_openid_connect_provider.terraform_cicd will be created
+ resource "aws_iam_openid_connect_provider" "terraform_cicd" {
+ arn = (known after apply)
+ client_id_list = [
+ "sts.amazonaws.com",
]
+ id = (known after apply)
+ tags_all = (known after apply)
+ thumbprint_list = [
+ "6938fd4d98bab03faadb97b34396831e3780aea1",
]
+ url = "https://token.actions.githubusercontent.com"
}
# aws_iam_role.terraform_cicd_oidc_role will be created
+ resource "aws_iam_role" "terraform_cicd_oidc_role" {
+ arn = (known after apply)
+ assume_role_policy = (known after apply)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = (known after apply)
+ max_session_duration = 3600
+ name = "TerraCICDOIDCRole"
+ name_prefix = (known after apply)
+ path = "/"
+ role_last_used = (known after apply)
+ tags_all = (known after apply)
+ unique_id = (known after apply)
}
# aws_iam_role_policy_attachment.AdministratorAccess_attachment will be created
+ resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
+ id = (known after apply)
+ policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
+ role = "TerraCICDOIDCRole"
}
Plan: 3 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
terraform_test$ terraform apply -auto-approve
aws_s3_bucket.terraform_state: Refreshing state... [id=terraform-state-hisui]
aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=terraform-state-hisui]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_openid_connect_provider.terraform_cicd will be created
+ resource "aws_iam_openid_connect_provider" "terraform_cicd" {
+ arn = (known after apply)
+ client_id_list = [
+ "sts.amazonaws.com",
]
+ id = (known after apply)
+ tags_all = (known after apply)
+ thumbprint_list = [
+ "6938fd4d98bab03faadb97b34396831e3780aea1",
]
+ url = "https://token.actions.githubusercontent.com"
}
# aws_iam_role.terraform_cicd_oidc_role will be created
+ resource "aws_iam_role" "terraform_cicd_oidc_role" {
+ arn = (known after apply)
+ assume_role_policy = (known after apply)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = (known after apply)
+ max_session_duration = 3600
+ name = "TerraCICDOIDCRole"
+ name_prefix = (known after apply)
+ path = "/"
+ role_last_used = (known after apply)
+ tags_all = (known after apply)
+ unique_id = (known after apply)
}
# aws_iam_role_policy_attachment.AdministratorAccess_attachment will be created
+ resource "aws_iam_role_policy_attachment" "AdministratorAccess_attachment" {
+ id = (known after apply)
+ policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
+ role = "TerraCICDOIDCRole"
}
Plan: 3 to add, 0 to change, 0 to destroy.
aws_iam_openid_connect_provider.terraform_cicd: Creating...
aws_iam_openid_connect_provider.terraform_cicd: Creation complete after 1s [id=arn:aws:iam::××××××××××××:oidc-provider/token.actions.githubusercontent.com]
aws_iam_role.terraform_cicd_oidc_role: Creating...
aws_iam_role.terraform_cicd_oidc_role: Creation complete after 1s [id=TerraCICDOIDCRole]
aws_iam_role_policy_attachment.AdministratorAccess_attachment: Creating...
aws_iam_role_policy_attachment.AdministratorAccess_attachment: Creation complete after 1s [id=TerraCICDOIDCRole-××××××××××××]
AWS コンソールでも作成が確認できました
いったんここまでの内容を main ブランチに commit、push しておきます
$ git add .
$ git commit -m "create_S3_OIDC"
$ git push
GitHub Actions で AWS にアクセスできるかテスト
main ブランチからテスト用の test ブランチをきります
terraform_test$ git branch
* main
terraform_test$ git checkout -b test
Switched to a new branch 'test'
terraform_test$ git branch
main
* test
GitHub Actions のワークフローを定義するため、 .github/workflows/aws_access_test.ymlを作成します
.github/workflows/ 配下の YAML ファイルに ワークフローを定義することで GitHub Actions を使用できます
(ディレクトリ)
terraform_test
├── .github
│ └── workflows
│ └── aws_access_test.yml // 作成
├── .gitignore
├── README.md
├── iam.tf
└── main.tf
こちらを参考に aws_access_test.yml に記述していきます
name: AWS Deploy
on:
push:
branches:
# 作業ブランチ
- test
env:
# ×××××××××:AWSアカウントID
AWS_ROLE_ARN: arn:aws:iam::×××××××××:role/TerraCICDOIDCRole
permissions:
id-token: write
contents: read
jobs:
aws-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- run: aws sts get-caller-identity
test ブランチに push した際に
aws sts get-caller-identity
が実行されるワークフローが定義できました!
このワークフローでGitHub Actions から AWS にアクセスできるか push して確認します
terraform_test$ git add .
terraform_test$ git commit -m "commit aws_access_test.yml"
terraform_test$ git push origin test
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 743 bytes | 743.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'test' on GitHub by visiting:
remote: https://github.com/×××××××××/terraform_test/pull/new/test
remote:
To https://github.com/×××××××××/terraform_test.git
* [new branch] test -> test
GitHub > リポジトリ > Actions から結果を確認できます
成功していれば、.github/workflows/aws_access_test.yml は もういらないので削除しておきます
GitHub Actions に plan と apply を組み込む
plan 用と apply 用のワークフローを作成します
ポイントは
- PR 作成時に plan を実行し、結果をコメントに残す
- PR マージ時に apply を実行する
.github/workflows/に plan.yml と apply.yml の2つのファイルを作成します
(ディレクトリ)
terraform_test
├── .github
│ └── workflows
│ └── plan.yml // 作成
│ └── apply.yml // 作成
├── .gitignore
├── README.md
├── iam.tf
└── main.tf
こちらを参考に yml ファイルに記述していきます
name: TerraformPlan
on:
pull_request:
branches:
- main
env:
TF_VERSION: 1.4.2
AWS_DEFAULT_REGION: ap-northeast-1
# ×××××××××:AWSアカウントID
AWS_ROLE_ARN: arn:aws:iam::xxxxxxxx:role/TerraCICDOIDCRole
permissions:
id-token: write
contents: read
actions: read
pull-requests: write
jobs:
Terraform_plan_and_Comment:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
# リポジトリのチェックアウトをする。
- name: Check out repository code
uses: actions/checkout@v3
# OICDでAssumeRoleをする。
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ env.AWS_DEFAULT_REGION }}
role-to-assume: ${{ env.AWS_ROLE_ARN }}
- name: Setup Terraform
# バージョン2を使用する。
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Exec Terraform fmt check
id: fmt
run: terraform fmt -check -recursive
# exit code 3でエラーになり終了してしまうため
# continue-on-error: true で後続の処理も続ける。
continue-on-error: true
- name: Exec Terraform init
id: init
run: terraform init
- name: Validate
run: terraform validate
- name: Exec Terraform plan
id: plan
run: terraform plan -no-color
# terraform plan の結果をコメント欄に出力する。
- name : comment
uses: actions/github-script@v4
env:
# ここのstdoutでterraform planの結果をPLANに保存している。
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
script: |
const output = `### terraform cicd
#### Terraform Format and Style \`${{ steps.fmt.outcome }}\`
#### Terraform Initialization \`${{ steps.init.outcome }}\`
#### Terraform Plan \`${{ steps.plan.outcome }}\`
#### Terraform Validation \`${{ steps.validate.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`${process.env.PLAN}\`\`\`
</details>`;
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
name: TerraformApply
on:
push:
branches:
- master
env:
TF_VERSION: 1.4.2
AWS_DEFAULT_REGION: ap-northeast-1
# ×××××××××:AWSアカウントID
AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxx:role/TerraCICDOIDCRole
permissions:
id-token: write
contents: read
actions: read
pull-requests: write
jobs:
Terraform_Apply:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
# リポジトリのチェックアウトをする。
- name: Check out repository code
uses: actions/checkout@v3
# OICDでAssumeRoleをする。
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ env.AWS_DEFAULT_REGION }}
role-to-assume: ${{ env.AWS_ROLE_ARN }}
- name: Setup Terraform
# バージョン2を使用する
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Exec Terraform fmt check
id: fmt
working-directory: "${{ env.WORK_DIR }}"
run: terraform fmt -recursive -check
# exit code 3でエラーになり終了してしまうため
# continue-on-error: true で後続の処理も続ける。
continue-on-error: true
- name: Exec Terraform init
id: init
working-directory: "${{ env.WORK_DIR }}"
run: terraform init
- name: terraform apply
id: apply
working-directory: "${{ env.WORK_DIR }}"
run: terraform apply -auto-approve
ためしてみる
GitHub Actions のワークフローが作成できたので、テストしてみます
main.tf に下記を追記して、EC2 を定義します
# EC2を定義
resource "aws_instance" "server" {
ami = "ami-0c3fd0f5d33134a76"
instance_type = "t2.micro"
# サブネットIDを指定して下さい
subnet_id = "<subnetのID>"
}
commit → push します
$ git add .
$ git commit -m "create_ec2_test"
$ git push origin test
GitHub のサイトで PR を作成して少し待ってみると。。。
↓ のように PR に plan の結果がコメントされます
Q:PR 作成後に追加でコミットしたら plan は再実行されるのでしょうか? A:plan.yml で定義した内容が再実行されます。
マージして、GitHub > リポジトリ > Actions で成功を確認したら、 AWS コンソールでも確認してみます
これで自動化できましたー
テストで作成した EC2 は不要なので、削除しておきましょう
main.tf の下記の部分を削除して push→PR 作成・マージします
# EC2を定義
resource "aws_instance" "server" {
ami = "ami-0c3fd0f5d33134a76"
instance_type = "t2.micro"
# サブネットIDを指定して下さい
subnet_id = "<subnetのID>"
}
AWS コンソールから削除されていることを確認したら OK です!
やり残したこと
tf ファイルに差分があるときのみ plan や apply を実行するようにしたいなーと思いました
いつか取り組んでみますー
参考
Discussion