Github ActionsでAWS CDKを自動デプロイしてみる

#はじめに
はじめまして！株式会社オークンのNHatanakaと申します！

現在会社の同僚と趣味でフットサルのLINEアプリケーションを作っていて、
せっかくなので初めてGithub Actionsを触ってみました！
なので今回はその時実装したコードを紹介したいと思います！

#使ったもの
AWS CDK
Github Actions
direnv

#構成
まず構成としては下図のようなパイプラインでデプロイ時にAPIGatewayとLambdaが作成されます。
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2347292/d2fcb710-c34f-0aa9-b3b0-6dc1b8d80d7e.png)

#ディレクトリ構成
iam：GithubActions上で使用するロールを作成するプロジェクトです。
futsal-backend：今回自動デプロイを組むプロジェクトでLambdaとAPIGatewayを作成します。

```ruby:全体のディレクトリ構成
.
├── README.md
├──.github
│ 　　　　└──workflows
│ 　　　　　　　　　　　　└──cdk-deploy.yml
├── futsal-backend
│   └── ...
└── iam
│   └── ...
└──.envrc
```

```ruby:futsal-backendプロジェクトのディレクトリ構成
.
├── README.md
├── bin
│   └── futsal-backend.ts
├── cdk.json
├── config
│   └── stack-const.ts
├── functions
│   └── message-api
│       └── index.py
├── jest.config.js
├── lib
│   └── futsal-backend-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── futsal-backend.test.ts
└── tsconfig.json

```

```ruby:iamプロジェクトのディレクトリ構成
.
├── README.md
├── bin
│   └── iam.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── iam-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── iam.test.ts
└── tsconfig.json
```
#iamスタックの内容
Github Actions上で使用するロールを作成するスタックです。
環境変数として.envrcに登録が必要な情報としては下記になります。
GITHUB_ORG：GitHubリポジトリのOrganization名
REPOSITORY_NAME：GitHubリポジトリ名

<details><summary>iam-stack.ts</summary><div>

```typescript

import * as cdk from '@aws-cdk/core';
import * as iam from '@aws-cdk/aws-iam';
import {Duration} from '@aws-cdk/core';

export class IamStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        const githubProvider = new iam.OpenIdConnectProvider(this, 'GithubProvider', {
            url: 'https://token.actions.githubusercontent.com',
            clientIds: ['sts.amazonaws.com'],
            thumbprints: ['a031c46782e6e6c662c2c87c76da9aa62ccabd8e']
        });

        const deploy_role = new iam.Role(this, 'FutsalDeployRole', {
            assumedBy: new iam.FederatedPrincipal(
                githubProvider.openIdConnectProviderArn,
                {
                    StringLike: {
                        'token.actions.githubusercontent.com:sub': `repo:${process.env.GITHUB_ORG}/${process.env.REPOSITORY_NAME}:*`
                    }
                },
                'sts:AssumeRoleWithWebIdentity',
            ),
            roleName: `FutsalDeployRole`,
            maxSessionDuration: Duration.seconds(3600)
        });
        deploy_role.addToPolicy(
            new iam.PolicyStatement({
                resources: ['*'],
                actions: [
                    'cloudformation:CreateStack',
                    'cloudformation:CreateChangeSet',
                    'cloudformation:DeleteChangeSet',
                    'cloudformation:DescribeChangeSet',
                    'cloudformation:DescribeStacks',
                    'cloudformation:ExecuteChangeSet',
                    'cloudformation:GetTemplate',
                    'cloudformation:DescribeStackEvents',
                    'cloudformation:DeleteStack',
                    's3:ListBucket',
                    's3:getBucketLocation',
                    's3:CreateBucket',
                    's3:*Object',
                    'iam:PassRole',
                ],
            })
        );

        new iam.Role(this, 'FutsalBackendStackDeployRoleForCloudFormation', {
            assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'),
            roleName: `FutsalBackendStackDeployRoleForCloudFormation`,
            maxSessionDuration: Duration.seconds(3600),
            managedPolicies: [
                iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambda_FullAccess'),
                iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCloudFormationFullAccess'),
                iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonAPIGatewayAdministrator'),
                iam.ManagedPolicy.fromAwsManagedPolicyName('IAMFullAccess'),
                iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'),
            ],
        });
    }
}

```

</div></details>

上記のコードを手動でデプロイして作成されたロールのARNをGitHubにSecretに登録します。
余談ですがGithub Actionsは最近AWSクレデンシャルを直接渡さずにIAMロールが使えるように
なるという最高すぎることがあり、なんとなくCloudFormationを眺めながらCDKにしてみました。(あまり意味がないことを理解しつつ)

#futsal-backendの内容
ここでは実際にGithub Actionsで自動デプロイするリソースの定義を行なっています。

<details><summary>futsal-backend-stack.ts</summary><div>

```typescript
import * as cdk from '@aws-cdk/core';
import {Duration} from '@aws-cdk/core';
import {resolve} from 'path';
import lambda = require('@aws-cdk/aws-lambda');
import apigw = require('@aws-cdk/aws-apigateway');

export class FutsalBackendStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, systemEnv: string, props?: cdk.StackProps) {
        super(scope, id, props);

        const messageApiFunction = new lambda.Function(this, 'MessageApiFunction', {
            functionName: `MessageApiFunction${systemEnv}`,
            code: lambda.Code.fromAsset(resolve(__dirname, '../functions/message-api')),
            timeout: Duration.minutes(5),
            handler: 'index.handler',
            runtime: lambda.Runtime.PYTHON_3_8,
            environment: {
                CHANNEL_ACCESS_TOKEN: String(process.env.CHANNEL_ACCESS_TOKEN),
                SECRET_KEY: String(process.env.SECRET_KEY)
            }
        })

        new apigw.LambdaRestApi(this, 'Endpoint', {
            handler: messageApiFunction,
            deployOptions: {
                stageName: systemEnv
            }
        });
    }
}
```

</div></details>

またbin/futsal-backend.tsで環境名(systemEnv)を環境変数から取得するようにし
その値をCloudFormationやリソースに含めることで環境別にリソースが生成されるようにしています。

<details><summary>futsal-backend.ts</summary><div>

```typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { FutsalBackendStack } from '../lib/futsal-backend-stack';

const systemEnv: string = process.env.SYSTEM_ENV || '';

const app = new cdk.App();
new FutsalBackendStack(app, `FutsalBackendStack${systemEnv}`, systemEnv);

```

</div></details>

#Github Actionsのワークフロー
さてやっとGithub Actionsのワークフローです。
今回はGitLabFlowを想定しておりmain・staging・productionの各ブランチに
マージされた際に起動しマージされたブランチ名を取得してSYSTEM_ENVに対応した環境名を
設定するように設計しました。

<details><summary>cdk-deploy.yml</summary><div>

```yaml
name: AWS CDK
on:
  push:
    branches:
      - main
      - staging
      - production
    paths:
      - futsal-backend/**
env:
  FUTSAL_DEPLOY_ROLE: ${{ secrets.FUTSAL_DEPLOY_ROLE }}
  FUTSAL_BACKEND_STACK_DEPLOY_ROLE: ${{ secrets.FUTSAL_BACKEND_STACK_DEPLOY_ROLE }}

permissions:
  id-token: write
  contents: read
jobs:
  aws_cdk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: "${{ env.FUTSAL_DEPLOY_ROLE }}"
          aws-region: ap-northeast-1
          role-duration-seconds: 900
          role-session-name: GitHubActionsTestSession

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '14'

      - name: npm ci
        run: npm ci
        working-directory: ./futsal-backend

      - name: Set System Development
        if: contains(toJSON(github.ref), 'main')
        run: echo SYSTEM_ENV=Dev >> $GITHUB_ENV

      - name: Set System Env Staging
        if: contains(toJSON(github.ref), 'staging')
        run: echo SYSTEM_ENV=Stg >> $GITHUB_ENV

      - name: Set System Env Production
        if: contains(toJSON(github.ref), 'production')
        run: echo SYSTEM_ENV=Prod >> $GITHUB_ENV

      - name: CDK Deploy
        if: contains(github.event_name, 'push')
        run: npx cdk deploy --require-approval never --role-arn "${{ env.FUTSAL_BACKEND_STACK_DEPLOY_ROLE }}"
        working-directory: ./futsal-backend
```

</div></details>

#検証
いい感じですね。
![スクリーンショット 2021-12-04 1.19.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2347292/755b2719-f706-3bda-01b2-89d24442bee7.png)


#感想
今回初めてGithub Actionsを触って見ましたが非常に勉強になりました！
普段の業務では主にCircleCIを使っているのですが今後の新しいプロジェクトでは
Github Actionsも選択肢に入る気がします。(IAMユーザー作らなくていいのが大きすぎる)
また今回私自身ブログの投稿が初めてでエンジニア経験がそこまで長いわけではないので、
おかしなところもあると思いますがご容赦頂けたら幸いです。