はじめに
この記事はFLINTERS10周年記念ブログリレーの36日目の投稿です
グループ企業向けにシステム開発などを担当しているチームでエンジニアをしている脇田です。 最近では担当projectが落ち着いて時間がとれたのでDevOpsに相当するような取り組みをいくつか行なっています、今回はその中の取り組みの一つを紹介します。
課題
私のチームでは複数プロダクトを管理している都合上インフラを定義しているTerraformリポジトリが4,5個あり、これからも管理リポジトリが増えることも予想されます。 そして各々のリポジトリでTerraformによるCICDのGithubActionsをコピー&ペーストのように管理しているため保守コストが高くなってしまっています。 そのためGithubActionsを共通化し保守コストを下げる対応をしました。
別リポジトリに共通化用のworkflowを導入
現状のCICD整理
導入するにあたってまず現状のCICDを整理します。 主に使われているworkflowは
plan,fmt,validateなどを行うtest workflow
env: TERRAFORM_WORKDIR: ... GOOGLE_CREDENTIALS: ... jobs: plan: runs-on: ubuntu-latest defaults: run: working-directory: ${{ env.TERRAFORM_WORKDIR }} steps: - uses: actions/checkout@v2 - uses: hashicorp/setup-terraform@v1 with: terraform_version: 1.4.5 - name: Terraform fmt id: fmt run: terraform fmt -check continue-on-error: true - name: Terraform Init id: init run: terraform init - name: Terraform Validate id: validate run: terraform validate -no-color - name: Terraform Plan id: plan run: terraform plan -no-color continue-on-error: true - name: Create Issue Comment uses: actions/github-script@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: ...
applyを行うdeploy workflow
env: TERRAFORM_WORKDIR: ... GOOGLE_CREDENTIALS: ... jobs: apply: runs-on: ubuntu-latest defaults: run: working-directory: ${{ env.TERRAFORM_WORKDIR }} steps: - uses: actions/checkout@v2 - uses: hashicorp/setup-terraform@v1 with: terraform_version: 1.4.5 - name: Terraform Init id: init run: terraform init - name: Terraform Plan id: plan run: terraform plan -no-color continue-on-error: true - name: Terraform Apply id: apply run: terraform apply -auto-approve -no-color continue-on-error: true - name: Create Issue Comment with: github-token: ${{ secrets.GITHUB_TOKEN }} script: ...
上記の例ではGCP用のworkflowですが同じような内容でaws providerに対するものがある、それぞれstaging,production用に分かれている現状でした。 これらのworkflowの依存している値を抽出すると
- terraformのversion
- terraformの実行ディレクトリ
- terraformを適用するcloudに対するcredentials
- 実行結果の通知をPR上にコメントするためのGithub token
が挙げられます、これらを外部から与えて動作する共通workflowを考えてみます。
導入後のCICDを考える
まず外部から呼び出すための方法を考えます。
共通化の手段として利用可能なものは
- job粒度で再利用できる reusable workflow
- step粒度で再利用できる composite action
があります、今回はjob粒度で再利用を行いたいのでreuseable workflowを選択しました。
実装
reusable workflowを使って共通用のworkflowを実装していきます。
新しくリポジトリを作り、まず外部リポジトリからの呼び出しを許可する設定を行います。
reusable workflowを管理するリポジトリからSettings->Actions-General
に遷移し、Actions permissionsをAllow actions and reusable workflows
*1をONにします。
リポジトリの設定をしたところで実際にreusable workflowを実装していきます。
まず発火条件として外部から呼び出されるのみなのでworkflow_call
を指定し、inputsとして先ほど抽出した依存値を指定していきます。
なおcloudのクレデンシャルはOIDCを前提としたものとしています。(この共通workflow使いたいならoidc設定をしろ、という強いメッセージをinterfaceに込めます)
.github/workflows/test.yaml
on: workflow_call: inputs: terraform_version: required: true type: number terraform_working_dir: required: true type: string terraform_provider: required: true type: string description: \"aws\" or \"gcp\" # For gcp gcp_workload_identity_provider: required: false type: string description: When using terraform_provider=gcp, Require this parameter gcp_service_account: required: false type: string description: When using terraform_provider=gcp, Require this parameter # For aws aws_role_to_assume: required: false type: string description: When using terraform_provider=aws, Require this parameter aws_region: required: false type: string description: When using terraform_provider=aws, Require this parameter secrets: token: required: true
次はjob部分です。 gcp,awsどちらのproviderにも対応するため両クラウドのOIDCで必要となるパラメータを受け取り、指定されたproviderで必要となるパラメータの検証をgithub-scriptのjavascriptで行ないます。 こちらは後述する実際のtest,deployのreusable workflowから利用するものになります。
.github/workflows/assert-inputs.yaml
on: workflow_call: inputs: terraform_provider: required: true type: string gcp_workload_identity_provider: required: false type: string gcp_service_account: required: false type: string aws_role_to_assume: required: false type: string aws_region: required: false type: string jobs: main: runs-on: ubuntu-latest steps: - uses: actions/github-script@v6 env: TERRAFORM_PROVIDER: ${{ inputs.terraform_provider }} AWS_ROLE_TO_ASSUME: ${{ inputs.aws_role_to_assume }} AWS_REGION: ${{ inputs.aws_region }} GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ inputs.gcp_workload_identity_provider }} GCP_SERVICE_ACCOUNT: ${{ inputs.gcp_service_account }} with: script: | const { TERRAFORM_PROVIDER, AWS_ROLE_TO_ASSUME, AWS_REGION, GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT } = process.env; switch(TERRAFORM_PROVIDER) { case 'aws': if (!AWS_ROLE_TO_ASSUME) throw new Error("aws_role_to_assume parameter is missing, require when using terraform_provider=aws") if (!AWS_REGION) throw new Error("aws_region parameter is missing, require when using terraform_provider=aws") break; case 'gcp': if (!GCP_WORKLOAD_IDENTITY_PROVIDER) throw new Error("gcp_workload_identity_provider parameter is missing, require when using terraform_provider=gcp") if (!GCP_SERVICE_ACCOUNT) throw new Error("gcp_service_account parameter is missing, require when using terraform_provider=gcp") break; default: throw new Error(`terraform_provider=${TERRAFORM_PROVIDER} is invalid, expected 'aws' or 'gcp'`) }
次に実際に外部から呼び出されるtest,deployのworkflowです。差分がほぼないため説明としてtest workflowを挙げます。
jobは先ほど説明したinputsを検証するassert-inputs
と実際にterraformを実行するmain
に分かれます。
要点としては
- assert-inputsをmainのneedsにしておいてassertが落ちたら実行をskipさせる
- inputs.terraform_providerの値ごとにOIDCのactionを切り替える
- permissionsでid-token,contentsを指定しているが明示的にpermissionsを指定するとdefaultで指定されているpermissionが無効になります。そのため今回はPRにコメントするための権限としてdefaultで許可されているpull-requests,issuesを追加で定義しています。
.github/workflows/test.yaml
jobs: assert-inputs: uses: ./.github/workflows/assert-inputs.yaml with: terraform_provider: ${{ inputs.terraform_provider }} gcp_workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} gcp_service_account: ${{ inputs.gcp_service_account }} aws_role_to_assume: ${{ inputs.aws_role_to_assume }} aws_region: ${{ inputs.aws_region }} main: runs-on: ubuntu-latest needs: - assert-inputs permissions: pull-requests: write issues: write id-token: write contents: read defaults: run: working-directory: ${{ inputs.terraform_working_dir }} steps: - uses: actions/checkout@v4 - name: Auth gcp if: ${{ inputs.terraform_provider == 'gcp' }} uses: google-github-actions/auth@v1 with: workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} service_account: ${{ inputs.gcp_service_account }} - name: Auth aws if: ${{ inputs.terraform_provider == 'aws' }} uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ inputs.aws_region }} role-to-assume: ${{ inputs.aws_role_to_assume }} - uses: hashicorp/setup-terraform@v2 with: terraform_version: ${{ inputs.terraform_version }} - name: Terraform fmt id: fmt run: terraform fmt -check continue-on-error: true - name: Terraform Init id: init run: terraform init - name: Terraform Validate id: validate run: terraform validate -no-color - name: Terraform Plan id: plan run: terraform plan -no-color continue-on-error: true - name: Create Issue Comment uses: actions/github-script@v6 env: with: github-token: ${{ secrets.token }} script: ...
ディレクトリ構成、全体のworkflowは下記のようになりました。
ディレクトリ構成
.github └── workflows ├── assert-inputs.yaml ├── deploy.yaml └── test.yaml
.github/workflows/test.yaml
on: workflow_call: inputs: terraform_version: required: true type: number terraform_working_dir: required: true type: string terraform_provider: required: true type: string description: \"aws\" or \"gcp\" # For gcp gcp_workload_identity_provider: required: false type: string description: When using terraform_provider=gcp, Require this parameter gcp_service_account: required: false type: string description: When using terraform_provider=gcp, Require this parameter # For aws aws_role_to_assume: required: false type: string description: When using terraform_provider=aws, Require this parameter aws_region: required: false type: string description: When using terraform_provider=aws, Require this parameter secrets: token: required: true jobs: assert-inputs: uses: ./.github/workflows/assert-inputs.yaml with: terraform_provider: ${{ inputs.terraform_provider }} gcp_workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} gcp_service_account: ${{ inputs.gcp_service_account }} aws_role_to_assume: ${{ inputs.aws_role_to_assume }} aws_region: ${{ inputs.aws_region }} main: runs-on: ubuntu-latest needs: - assert-inputs permissions: pull-requests: write issues: write id-token: write contents: read defaults: run: working-directory: ${{ inputs.terraform_working_dir }} steps: - uses: actions/checkout@v4 - name: Auth gcp if: ${{ inputs.terraform_provider == 'gcp' }} uses: google-github-actions/auth@v1 with: workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} service_account: ${{ inputs.gcp_service_account }} - name: Auth aws if: ${{ inputs.terraform_provider == 'aws' }} uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ inputs.aws_region }} role-to-assume: ${{ inputs.aws_role_to_assume }} - uses: hashicorp/setup-terraform@v2 with: terraform_version: ${{ inputs.terraform_version }} - name: Terraform fmt id: fmt run: terraform fmt -check continue-on-error: true - name: Terraform Init id: init run: terraform init - name: Terraform Validate id: validate run: terraform validate -no-color - name: Terraform Plan id: plan run: terraform plan -no-color continue-on-error: true - name: Create Issue Comment uses: actions/github-script@v6 env: PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" with: github-token: ${{ secrets.token }} script: ...
reusable workflowを呼び出す
最後に上記で実装したreusable workflowを呼び出します。
on: pull_request: types: - synchronize - opened - reopened - ready_for_review workflow_dispatch: jobs: test: uses: organization/repository/.github/workflows/test.yaml@main with: terraform_provider: gcp terraform_version: 1.6 terraform_working_dir: terraform/gcp/entrypoint gcp_workload_identity_provider: ... gcp_service_account: ... secrets: token: ${{ secrets.GITHUB_TOKEN }}
まとめ
これでterraformのCICDを共通化することで保守コストを下げ、新規に導入する際のコストも大幅に下げることができました。 今後tflintやtrivyなどの静的検査ツールを組み込む際もreusable workflowのみを対応すれば良いので非常に楽です。
*1:今回は例としてAllow actions and reusable workflowsを選択していますがprojectに合わせて適切なpermissionを選択してください。