Terraformでの plan, apply をGitHub Actions で自動化してみます
ゴール
↓の表のようなPR作成→マージの処理を作っていきます
No. | 処理 | 備考 |
1 | ブランチを作成し、PRを作成する | |
2 | PR作成時に terraform plan が実行される | 結果をPRにコメントで残す |
3 | planの結果をレビューする | |
4 | PRをマージする | |
5 | マージ時に terraform apply が実行される |
前提
- AWSやTerraformに関して、基本的な知識がある
→細かい説明は省くので、ご注意ください - AWS CLIから操作できる環境がそろっている
つくってみる
以下の流れで自動化していきます
- 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バケットを作成
各ファイルの中身
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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" } } |
次のコマンドを実行していき、S3を作成します
1 2 3 |
1. terraform init 2. terraform plan 3. terraform apply -auto-approve |
AWSコンソールで確認してみます。
無事作成できました
tfstateの保存先をS3へ変更
main.tfのterraformブロック内にbackendを追記します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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 を実行します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
(iam.tfの中身)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#============================================================================ # 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プロバイダを作成します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
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しておきます
1 2 3 |
$ git add . $ git commit -m "create_S3_OIDC" $ git push |
GitHub Actions でAWSにアクセスできるかテスト
mainブランチからテスト用のtestブランチをきります
1 2 3 4 5 6 7 |
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 を作成します
(ディレクトリ)
terraform_test
├── .github
│ └── workflows
│ └── aws_access_test.yml // 作成
├── .gitignore
├── README.md
├── iam.tf
└── main.tf
こちらを参考にaws_access_test.ymlに記述していきます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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して確認します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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ファイルに記述していきます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
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 }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
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を定義します
1 2 3 4 5 6 7 |
# EC2を定義 resource "aws_instance" "server" { ami = "ami-0c3fd0f5d33134a76" instance_type = "t2.micro" # サブネットIDを指定して下さい subnet_id = "<subnetのID>" } |
commit → push します
1 2 3 |
$ git add . $ git commit -m "create_ec2_test" $ git push origin test |
GitHubのサイトでPRを作成して少し待ってみると。。。
↓のようにPRにplanの結果がコメントされます
マージして、GitHub > リポジトリ > Actions で成功を確認したら、
AWSコンソールでも確認してみます
これで自動化できましたー
テストで作成したEC2は不要なので、削除しておきましょう
main.tfの下記の部分を削除してpush→PR作成・マージします
1 2 3 4 5 6 7 |
# EC2を定義 resource "aws_instance" "server" { ami = "ami-0c3fd0f5d33134a76" instance_type = "t2.micro" # サブネットIDを指定して下さい subnet_id = "<subnetのID>" } |
AWSコンソールから削除されていることを確認したらOKです!
やり残したこと
tfファイルに差分があるときのみplanやapplyを実行するようにしたいなーと思いました
いつか取り組んでみますー
参考
コメント