GitHub ActionsによるCDKアプリのデプロイ自動化 (エンタープライズ向け)

# はじめに

CDKアプリケーションの開発を行っていたプロジェクトにて、GitHub Actionsによりデプロイ自動化の仕組みを構築しました。
その際、エンタープライズでのチーム開発に最低限必要そうな機能を含めたのですが、その辺りがひと通りまとまっている記事があまり見当たらなかったため、簡単にまとめてみました。

- 想定読者
    - チーム開発向けにデプロイ自動化の仕組みを構築したい。
    - GitHub Actionsでワークフローを組んだことがある。
    - AWSのCDK、CloudFormation(以降CFNと記載)、IAMなどについて基礎的な知識がある。

- 主な技術要素
    - [GitHub Actions](https://docs.github.com/en/actions)
    - [AWS CDK](https://aws.amazon.com/jp/cdk/)


# この記事のポイント

- デプロイ自動化の実現に至るための要点に絞っています。(基礎的な説明は省略してます)

- ブランチ毎に異なるAWS環境(アカウント)へデプロイします。

- OICDプロバイダーを使用し、GitHub ActionsからAWSへのセキュアで効率的なアクセスを行います。


# 前提事項

構築時の環境情報は下記のとおりです。

| OS・MW  | Version   |
|:--------|:----------|
| Ubuntu  | 20.04 LTS |
| Node.js | 16.14.2   | 
| AWS CDK | 2.17.0    |
| AWS CLI | 2.4.25    |


この記事でデプロイしているCDKアプリケーションは、`cdk init`コマンドを実行しただけの簡単なサンプルアプリケーションです。

```sh
# サンプルアプリケーション作成時のコマンド例
$ mkdir sample-cdk-app
$ cd sample-cdk-app/
$ cdk init sample-app --language=typescript
```

デプロイ先のAWS環境には`cdk bootstrap`コマンドが実行済みで、CDKを扱うために必要な`CDKToolkit`スタックが作成済みであるものとします。

```sh
$ npm install -g aws-cdk
$ cdk bootstrap
```


# 本題

### AWSへのOIDCプロバイダーとIAMロールの作成

デプロイ先のAWS環境へ、OIDCプロバイダーとIAMロール(GitHub Actionsが使用)を作成します。OIDCプロバイダーによる認証を採用することで、下記の様なメリットがあります。
- GitHub ActionsがAWSへアクセスする際の専用IAMユーザーの作成が不要。
- AWSのクレデンシャル情報(アクセスキーやシークレット)をGitHub側に持たせる必要がない。
- 上記により、シークレットのローテーションといった運用の考慮が必要ない。

ここでいうAWS側のOIDCプロバイダーは、厳密には「IDプロバイダーを信頼するための設定」です。実際にOpenID Connect認証用のIDトークンを発行するプロバイダは、GitHub側に存在します。そのあたりの説明は、下記の記事が分かりやすく、参考にさせて頂きました。

https://zenn.dev/miyajan/articles/github-actions-support-openid-connect

下記CFNテンプレートにより、前述のリソースを含むスタックを作成できます。

```yaml:template.yaml
Parameters:
  RepoOwner:
    Description: Owner of repository.
    Type: String
  RepoName:
    Description: Name of repository where the GitHub Actions will be running.
    Type: String
  OIDCProviderArn: # ２回目以降に作成済みOIDC ProviderのARNを指定します
    Description: Arn for the GitHub OIDC Provider.
    Default: ''
    Type: String

Conditions:
  CreateOIDCProvider: !Equals
    - !Ref OIDCProviderArn
    - ''

Resources:
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${RepoName}-actions-role
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !If
                - CreateOIDCProvider
                - !Ref GithubOidc
                - !Ref OIDCProviderArn
            Condition:
              StringLike:
                token.actions.githubusercontent.com:sub: !Sub repo:${RepoOwner}/${RepoName}:*
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess # 権限の限定を推奨

  GithubOidc:
    Type: AWS::IAM::OIDCProvider
    Condition: CreateOIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com
      ThumbprintList:
        - 6938fd4d98bab03faadb97b34396831e3780aea1 # サムプリントの更新箇所

Outputs:
  Role:
    Value: !GetAtt Role.Arn
```

`OIDC Provider`はAWSアカウントにひとつのみしか存在できません。そのため、このCFNテンプレートで別スタックを作成する場合は、下記`Parameters`に作成済み`OIDC Provider`の`Arn`を渡してください。

```yaml:template.yaml (抜粋)
Parameters:
  OIDCProviderArn: # ２回目以降に作成済みOIDC ProviderのARNを指定します
```

下記のポリシーを指定している箇所は、デプロイするCDKアプリケーションに応じて、必要最低限の権限とすることを推奨します。今回はサンプルのため、強い権限を与えています。

```yaml:template.yaml (抜粋)
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess # 権限の限定を推奨
```

また、GitHubのIDプロバイダーでサムプリントが更新されていた場合、最新のものをCFNテンプレートに反映する必要があります。スタック作成前に、後述の更新手順を実施しておくのが確実と思います。

```yaml:template.yaml (抜粋)
  GithubOidc:
    Type: AWS::IAM::OIDCProvider
    Condition: CreateOIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com
      ThumbprintList:
        - 6938fd4d98bab03faadb97b34396831e3780aea1 # サムプリントの更新箇所
```

下記がAWSの公式ガイドで説明されているサムプリント取得方法です。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html

公式ガイドに記載の手順は少々複雑であるため、私はIAMコンソールを使用する下記の方法で取得しました。(公式ではガイドされていない方法のため、こちらは自己責任で実施されてください)

1. 「プロバイダのタイプ」で`OpenID Connect`を選択します。
   ![aws_iam_get_thumbprint_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599435/e5e517a9-7fa1-a049-fcb3-38af60504517.png)
1. 「プロバイダのURL」に`https://token.actions.githubusercontent.com`を指定することで取得できます。
   ![aws_iam_get_thumbprint_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599435/8a704a75-ee8f-7e64-a9b0-20aaafedbe78.png)

下記のようなAWS CLIコマンドで、前述のCFNテンプレートでスタックを作成します。

```sh
aws cloudformation create-stack \
--stack-name actions-base \
--template-body file://template.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=RepoOwner,ParameterValue=<Owner Name> ParameterKey=RepoName,ParameterValue=sample-cdk-app
# 名前指定のIAMロール作成があるため、`CAPABILITY_NAMED_IAM`オプションの指定が必要。
```


### GitHubのSecretsへAWSアカウント情報を格納

GitHub Actionsがデプロイ先を判断するために必要なAWSアカウント情報(アカウントIDとリージョン)を、リポジトリのSecretsへ格納します。これは後述のワークフロー定義内で作り込みました仕様によるもので、Secretsへ格納することでアカウント情報が漏出するリスクを低減することが目的です。

- 開発環境(dev)用のSecret名
    - `AWS_ACCOUNT_DEV`
    - `AWS_REGION_DEV`
- 本番環境(prd)用のSecret名
    - `AWS_ACCOUNT_PRD`
    - `AWS_REGION_PRD`

GitHubリポジトリのコンソール`Settings > Secrets > Actions`から追加できます。
![github_actions_secrets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599435/564e2890-eef5-48d9-c1e6-31fc5467937f.png)


### GitHub Actionsワークフロー定義の配置

デプロイ対象のCDKアプリケーションに、`.github/workflows/`というディレクトリを作成し、ワークフロー定義ファイル(yaml)を配置します。ファイル名は任意です。

今回作成したワークフローの仕様はざっくり下記の通りです。

- merge先/push先ブランチによってデプロイ先のAWS環境を切り替えます。
    - `develop`ブランチ → 開発環境(dev)
    - `main`ブランチ → 本番環境(prd)

- プルリクエストの作成時、または、プルリクエストの更新時
    - `cdk diff`を実行します。ワークフローの実行結果から、デプロイ済みスタックとの差分が確認できます。

- プルリクエストのmerge時、または、ブランチへの直接push時
    - `cdk diff`と`cdk deploy`を実行します。

```yaml:.github/workflows/cdk-deploy.yml
name: CDK Deploy

on:
  push:
    branches:
      - develop
      - main
  pull_request:
    branches:
      - develop
      - main
permissions:
  id-token: write
  contents: read

jobs:
  cdk-deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16
          registry-url: https://npm.pkg.github.com/
      - name: Install AWS CDK
        run: npm ci
      - name: Destination settings for dev enviroment
        if: (github.ref == 'refs/heads/develop') || (github.event.pull_request.base.ref == 'develop')
        run: |
          echo "Using dev environment settings."
          echo "AWS_ACCOUNT=${{secrets.AWS_ACCOUNT_DEV}}" >> $GITHUB_ENV
          echo "AWS_REGION=${{secrets.AWS_REGION_DEV}}" >> $GITHUB_ENV
      - name: Destination settings for prd enviroment
        if: (github.ref == 'refs/heads/main') || (github.event.pull_request.base.ref == 'main')
        run: |
          echo "Using prd environment settings."
          echo "AWS_ACCOUNT=${{secrets.AWS_ACCOUNT_PRD}}" >> $GITHUB_ENV
          echo "AWS_REGION=${{secrets.AWS_REGION_PRD}}" >> $GITHUB_ENV
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::${{env.AWS_ACCOUNT}}:role/${{github.event.repository.name}}-actions-role
          aws-region: ${{env.AWS_REGION}}
      - name: CDK Diff
        if: contains(github.event_name, 'pull_request') || contains(github.event_name, 'push')
        run: npm run cdk -- diff
      - name: CDK Deploy
        if: contains(github.event_name, 'push')
        run: npm run cdk -- deploy --require-approval never
```

ワークフロー定義は今回のキモとなる部分のため、工夫点など含め少し詳しく説明していきます。


下記ステップは、`package.json`に記載の`aws-cdk`などをインストールすることが目的ですが、`npm ci`とすることで、`package-lock.json`の構成管理を強制しています。`npm install`を実行してしまうと、依存パッケージのバージョンが安定しないため、意図せぬ変更を防いでいます。

```yaml:.github/workflows/cdk-deploy.yml (抜粋)
      - name: Install AWS CDK
        run: npm ci
```


下記ステップで、どのブランチへのイベントであるかによってデプロイ先を判断しています。リダイレクトの記述`>> $GITHUB_ENV`によって、ステップを跨る環境変数の引継ぎができます。

```yaml:.github/workflows/cdk-deploy.yml (抜粋)
      - name: Destination settings for dev enviroment
        if: (github.ref == 'refs/heads/develop') || (github.event.pull_request.base.ref == 'develop')
        run: |
          echo "Using dev environment settings."
          echo "AWS_ACCOUNT=${{secrets.AWS_ACCOUNT_DEV}}" >> $GITHUB_ENV
          echo "AWS_REGION=${{secrets.AWS_REGION_DEV}}" >> $GITHUB_ENV
      - name: Destination settings for prd enviroment
        if: (github.ref == 'refs/heads/main') || (github.event.pull_request.base.ref == 'main')
        run: |
          echo "Using prd environment settings."
          echo "AWS_ACCOUNT=${{secrets.AWS_ACCOUNT_PRD}}" >> $GITHUB_ENV
          echo "AWS_REGION=${{secrets.AWS_REGION_PRD}}" >> $GITHUB_ENV
```
[参考] [Workflow commands for GitHub Actions > Setting an environment variable](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable)


下記ステップで、`aws-actions/configure-aws-credentials`を使用し、AWS環境へ作成したOIDCプロバイダーとIAMロールを使用したクレデンシャルの取得を実施しています。冒頭の`permissions`で指定している`id-token: write`もセットで必要です。

```yaml:.github/workflows/cdk-deploy.yml (抜粋)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::${{env.AWS_ACCOUNT}}:role/${{github.event.repository.name}}-actions-role
          aws-region: ${{env.AWS_REGION}}
```
```yaml:.github/workflows/cdk-deploy.yml (抜粋)
permissions:
  id-token: write
  contents: read
```
[参考] [aws-actions/configure-aws-credentials: "Configure AWS Credentials" Action For GitHub Actions](https://github.com/aws-actions/configure-aws-credentials)


下記ステップで、プルリクエスト作成かmerge(or 直接push)かによって、実行するCDKコマンドを切り替えています。実行コマンドは`npm run cdk -- diff`というようにすることで、`package.json`で依存しているCDKを使用するようにしています。ワークフロー定義内でCDKのインストールを記述してしまうと、CDKのバージョン指定箇所が分散してしまうため、このようにしています。

```yaml:.github/workflows/cdk-deploy.yml (抜粋)
      - name: CDK Diff
        if: contains(github.event_name, 'pull_request') || contains(github.event_name, 'push')
        run: npm run cdk -- diff
      - name: CDK Deploy
        if: contains(github.event_name, 'push')
        run: npm run cdk -- deploy --require-approval never
```
```json:package.json (抜粋)
  "scripts": {
    "cdk": "cdk"
  },
  "devDependencies": {
    "aws-cdk": "2.17.0",
```

このワークフロー定義を格納した状態でGitHubへpushしますと、さっそくGitHub Actionsが動きだします。GitHub Actionsのコンソールから下記状況が確認できれば、ワークフローが正しく動いています。
![github_actions_job_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599435/f71271a8-9d97-22af-cb69-c82e556b4ecd.png)
※`GitHub Event Context Info`というステップは、前述のワークフロー定義からは削除しているため無視してください。

デプロイ先AWS環境のCFNコンソールからも、スタック作成に成功していることが確認できると思います。(この記事通りの場合、`SampleCdkAppStack`というスタック名)


# 改善したいポイント

- 本番環境向けデプロイへの承認プロセスの実装
    - 実際のプロダクトでは上長への申請→承認といったプロセスを経てデプロイ、が多いのではと思います。そのあたりもGitHubに存在する機能の中で実現できるといいなぁと思ってます。

- 環境差異の切り替え → Environmentsの使用
    - Environmentsという機能を使うと、環境に応じた切り替えをもっとスマートに実現できそうです。ただし、Free版ではパブリックリポジトリでしか使えず、プライベートリポジトリで使う場合はEnterprise版の契約が必要なようです。(Free版プライベートリポジトリユーザーであるため、手が出せませんでした・・・)
        - [Using environments for deployment - GitHub Docs](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)

- GitHub Actionsコンソール上での`cdk diff`の結果確認
    - 正直かなり見づらく実用的でないです。ローカル(VSCode)での見え方を再現できる方法があればいいのですが・・・

- OIDCプロバイダーとIAMロールを作成するCFNテンプレートのCDK化
    - 実害はない＆あまり変更が入らない定義と思われますが、CDKに揃えたい気持ちがちょっとあります。


# まとめ

エンタープライズやチーム開発での自動化の手始めとして、参考にして頂けましたら幸いです。最後まで読んでいただきありがとうございました。


# 参考文献

- [Configuring OpenID Connect in Amazon Web Services - GitHub Docs](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
- [OpenID Connect (OIDC) ID プロバイダーの作成 - AWS Identity and Access Management](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)
- [AWS federation comes to GitHub Actions | Aidan Steele's blog (usually about AWS)](https://awsteele.com/blog/2021/09/15/aws-federation-comes-to-github-actions.html)