GitHub ActionsでAWSのCI/CD環境を作りたい

# 環境が大事
人の成長に関連して良く耳にする言葉ですが、ITインフラやアプリ開発にも共通して言える事だと思います。
Web系言語を学び始めの時はとりあえずもの作る事を意識していましたが、少しずつ慣れてくると、環境を作れるようになりたいと思い始める訳ですね。

環境といってもAWSでいうアカウント構成とかログ取得環境とかありますが、ここでは諸CI/CDでリソースをデプロイ出来る環境から作成していきたいです。

マルチアカウント環境のデプロイについては別で記事を書いてます。

本記事はマルチアカウント構成作成時にProdOU配下に作ったBlogProdアカウントで作業を行います。
# GitHub Actions
CI/CDの構成には色々あると思いますが、慣れ親しんだGitHubを使いたいです。
この記事ではActionsでEC2を作って消す所までやります。
なんとなくの手順
1. リポジトリの作成とローカルと接続
1. CDKセットアップ
1. (CLIベースでEC2作成の確認)
1. Actionsの設定
1. CI/CDでEC2を作成出来るか確認

## 1.リポジトリの作成とローカルとの接続
いつものやつです。GitHubでリポジトリを作成してローカルからクローンします。
```
git clone https://github.com/...
```

## 2.CDKセットアップ
インストールして、アカウントのセットアップして、初回はBootstrapですね。
やっていきます。ちなみにCDKについてはこのワークショップが非常に良くまとめられています。
ちなみにCDKとAWS CLIのバージョンは下記です。
cdk version: 2.134.0
aws-cli version: 2.15.34

ひとまず、バックエンドリソースを配置したフォルダで以下のコマンドを実行します。
(言語はTypeScriptでいきます)
```
cdk init app --language typescript
```
このコマンドで基本の構成が出来上がるのが凄いです。余談ですが、.gitignoreファイルまで勝手に作成されています。git addコマンドでnomoduleやcdk.outなどが、リモートに上げないように設定されていて感動しました。

次にAWS CLIのセットアップが必要です。現在はIAM Identity Centerでユーザ管理をしているので、そこの認証情報取得のための設定を行います。
(参考:https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/sso-configure-profile-token.html)

```
$ aws configure sso
SSO session name (Recommended): my-sso
SSO start URL [None]: https://my-sso-portal.awsapps.com/start
SSO region [None]: ap-northeast-1
```
SSO start URLはIAM Identity Center→設定→アイデンティティソース→AWS アクセスポータルのURLで確認出来ます。
ここで設定した内容は~/.aws/configに保存され、今後は
```
aws sso login --profile "profile名"
```
でセッションを開始出来ます。

## 3.(CLIベースでEC2作成の確認)
Actionsの前にCLIからリソースを立てられるのか確認します。
とりあえず、簡単にEC2を立ててみます。

```ruby: backend-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    const vpc = new cdk.aws_ec2.Vpc(this, 'my-vpc', {
    });
    const instance = new cdk.aws_ec2.Instance(this, 'targetInstance', {
      vpc: vpc,
      instanceType: cdk.aws_ec2.InstanceType.of(cdk.aws_ec2.InstanceClass.BURSTABLE2, cdk.aws_ec2.InstanceSize.MICRO),
      machineImage: new cdk.aws_ec2.AmazonLinuxImage({ generation: cdk.aws_ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 }),
    });
  }
}
```

cdk.jsonのあるディレクトリでデプロイを実行します。
```
cdk deploy --profile "profile名"
```
エラーが起きずに実行が終了し、リソースが正常に作成されていれば確認終了です。

## 4.Actionsの設定
GitHub Actionsの設定といっても大きく2パターンあると思います。
1. IAMロールを作成して認証情報をGitHun Secretsに登録して使う方法
1. OpenID ConnectにてIAMロールの一時的な認証情報を発行する方法

後者の方がベストプラクティスに沿っていると思うので今回はそちらでいきます。こちらの記事を参考にしました。
https://dev.classmethod.jp/articles/create-resources-used-for-oidc-integration-with-github-with-aws-cdk/

手順:
1. OpenID Connectに必要なリソースを作る
1. GitHub Secretsを作成
1. Actionsの中身を書く

### 4-1.OpenID Connectプロバイダーを作る
以下のコードでIAMロールとIDプロバイダーをデプロイします。
```ruby: github-actions-setup-stack.ts
import { Construct } from 'constructs';
import { Stack, StackProps, aws_iam } from 'aws-cdk-lib';

export interface GitHubActionsSetupStackProps extends StackProps {
  principalFederatedSub: string;
}

export class GitHubActionsSetupStack extends Stack {
  constructor(scope: Construct, id: string, props: GitHubActionsSetupStackProps) {
    super(scope, id, props);

    const accountId = Stack.of(this).account;
    const region = Stack.of(this).region;

    const gitHubIdProvider = new aws_iam.OpenIdConnectProvider(
      this,
      'GitHubIdProvider',
      {
        url: 'https://token.actions.githubusercontent.com',
        clientIds: ['sts.amazonaws.com'],
      }
    );

    const oidcDeployRole = new aws_iam.Role(this, 'GitHubOidcRole', {
      roleName: 'github-oidc-role',
      assumedBy: new aws_iam.FederatedPrincipal(
        gitHubIdProvider.openIdConnectProviderArn,
        {
          StringLike: {
            'token.actions.githubusercontent.com:sub':
              props.principalFederatedSub,
          },
        },
        'sts:AssumeRoleWithWebIdentity' //これを忘れるとStatementのActionが'sts:AssumeRole'となりOIDCでのAssumeRoleで使えなくなる。
      ),
    });

    const deployPolicy = new aws_iam.Policy(this, 'deployPolicy', {
      policyName: 'deployPolicy',
      statements: [
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: [
            's3:getBucketLocation',
            's3:List*',
            'cloudformation:CreateStack',
            'cloudformation:CreateChangeSet',
            'cloudformation:DeleteChangeSet',
            'cloudformation:DescribeChangeSet',
            'cloudformation:DescribeStacks',
            'cloudformation:DescribeStackEvents',
            'cloudformation:ExecuteChangeSet',
            'cloudformation:GetTemplate',
          ],
          resources: [
            'arn:aws:s3:::*',
            `arn:aws:cloudformation:${region}:${accountId}:stack/CDKToolkit/*`,
            `arn:aws:cloudformation:${region}:${accountId}:stack/*/*`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['s3:PutObject', 's3:GetObject'],
          resources: [`arn:aws:s3:::cdk-*-assets-${accountId}-${region}/*`],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['ssm:GetParameter'],
          resources: [
            `arn:aws:ssm:${region}:${accountId}:parameter/cdk-bootstrap/*/version`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['iam:PassRole'],
          resources: [
            `arn:aws:iam::${accountId}:role/cdk-*-cfn-exec-role-${accountId}-${region}`,
          ],
        }),
      ],
    });

    oidcDeployRole.attachInlinePolicy(deployPolicy);
  }
}
```

### 4-2. GitHub Secretsを作成
こんな感じでGitHub Secretsに作成したロールのARNを登録します。
![Qiita](https://qiita-img-20240309.s3.ap-northeast-1.amazonaws.com/actions_secrets.png "Qiita")

### 4-3. Actionsの設定
次にブランチにpusgがされた時にcdk deployを行うworkflowを設定します。
```ruby:.github/workflows/cicd.yml
name: AWS Deploy
on: push

env:
  AWS_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}

permissions:
  id-token: write
  contents: read
jobs:
  aws-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: "20.x"

      - name: Setup dependencies
        run: npm ci
        working-directory: src/backend

      - name: Build
        run: npm run build
        working-directory: src/backend

      - name: Assume Role
        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

      - name: Deploy
        run: npm run cdk deploy BackendStack
        working-directory: src/backend
```
少しエラーにハマッた部分を共有します。
1. package-lock.jsonがない
パッケージの依存関係をインストールする際にpackage-lock.jsonが無いというエラーが表示されました。これは.gihubのファイル配下にpackage-lock.jsonが無いから発生します。任意のディレクトリ指定するために
```
working-directory: (path/to/package-lock.json)
```
の一文を追加する必要があります。
2. ❌ Deployment failed: Error: "--require-approval" is enabled 
cdk deploy "stack名"を実行すると以下のエラーが表示されました。
```
❌ Deployment failed: Error: "--require-approval" is enabled and stack includes security-sensitive updates, but terminal (TTY) is not attached so we are unable to get a confirmation from the user
```
"--require-approval"がオンになっているためと出ています。これは、deployを確認する入力を受け付けるかどうかの設定がonになっている事を意味しています。
CLI上で"cdk deploy"をすると(y/N)の入力を求められますよね？あれです。
Actions経由でデプロイする時はその入力が出来ないので、"--require-approval"をオフにしてあげる必要があります。今回は"cdk.json"に設定を書き加えて対応しました。
```ruby: cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/backend.ts",
  "requireApproval": "never",
  ...
}
```
## 5.CI/CDでEC2を作成出来るか確認
上記の設定を行って、ブランチに変更内容をpushするとワークフローが動きます。
以下の画像のように緑のチェック状態になれば成功です。
リソースが作成されている事も確認出来ます。
![Qiita](https://qiita-img-20240309.s3.ap-northeast-1.amazonaws.com/actions_success.png "Qiita")

![Qiita](https://qiita-img-20240309.s3.ap-northeast-1.amazonaws.com/actions_ec2.png "Qiita")

# 総じて
今回はゼロからGitHub ActionsでCDKを用いてリソースデプロイ出来る環境を作成しました。ローカルの開発環境に依存しない点でチームでの開発や、個人でも様々デバイスで開発を進める事が容易になるかと思います。
この環境を利用してブログ作成を進めていこうと思います。