AWS CDK Code Pipelineを作成 - CICD

本記事は[Part2](https://qiita.com/kennyQiita/items/7d684a0e9792c8e808a7)までで作成したAWSスタックに対して、CICDパイプラインを作成していきます。Part2までのスタックを作成してない場合は参考記事として参照いただければ幸いです。

## 環境情報
* 作業環境はAWS Cloud9を利用
* cdk versionは2.63.2

## はじめに
AWS CDKを用いた場合でも、直接デプロイするのではなくCodeCommitやCodePipelineを使ってCICDサイクルを管理したいケースは多々あると思います。これまでのPartsでは作成したスタックを直接`cdk deploy`で環境に反映していましたが、今回はしっかりとPipelineを通してデプロイされるように構築していきたいと思います。

## CDK Pipelineの作成
### Pipeline用のスタックを作成する
まずはパイプラインのスタックを作成していきます。このスタックは実際のアプリケーションとは異なるため、アプリケーションとは関係なく独立したスタックとして作成していきます。
`lib`フォルダ配下に新たに`lib/pipeline-stack.ts`ファイルを作成し、以下のコードを記述していきます。
```typescript:lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class WorkshopPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    // Pipeline code goes here
  }
}
```

### CDKデプロイエントリーポイントの更新
これまではアプリケーション用のスタックが直接デプロイされていましたが、今後はPipeline用のスタックを介してデプロイするため直接アプリ用のスタックがデプロイされないようにします。代わりに、Pipeline用のスタックがデプロイ時のエントリーポイントとなるように修正します。
`bin/cdk-workshop.ts`ファイルを以下のように更新します。
```typescript:bin/cdk-workshop.ts
import * as cdk from 'aws-cdk-lib';
import { WorkshopPipelineStack } from '../lib/pipeline-stack';

const app = new cdk.App();
new WorkshopPipelineStack(app, 'CdkWorkshopPipelineStack');
```
下準備ができたので、パイプラインをつくっていきましょう！

### リポジトリの作成
まずはソースコントロール用のリポジトリを作成していきます。CodeCommitリポジトリを作成していきましょう。
`lib/pipeline-stack.ts`を以下のように編集します。
```typescript:lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import { Construct } from 'constructs';

export class WorkshopPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    // Create a CodeCommit repository
    new codecommit.Repository(this, 'WorkshopRepo', {
      repositoryName: "WorkshopRepo"
    });
  }
}
```
ここまでで一旦デプロイしてみます。
```
cdk deploy
```
レポジトリができていますね！
![repo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/6148bc05-7d61-869a-f84a-151412563760.png)

### リポジトリのクレデンシャルを取得し、コミットする
まずはリポジトリ接続用のクレデンシャルを取得します。この手順は本記事の内容をVisualStudioCodeなどで実施している方向けです。筆者の場合はCloud9にawsのcredentialファイルを配置しているため、CodeCommitリポジトリ専用のGitクレデンシャルを取得する必要はありません。
クレデンシャルはIAMコンソール >> Users（任意ユーザー） >> Security credentials >> HTTP Git credentials for AWS CodeCommitの順番に遷移し、"Generate credentials"より作成して取得してください。
![credentials.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ab31f402-d8c9-f203-91f5-17726aabe27a.png)

次にCodeCommitコンソールから今回作成したリポジトリのURLを取得します。
![スクリーンショット 2023-02-15 15.02.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ab5752d6-d7ee-b3cc-d1b4-23703ed04d02.png)

`.gitignore`ファイルを開き、`!lambda/*js`を追記します。これを追記しないとgit commitにlambda用のファイルが含まれません。
```.gitignore
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
!lambda/*.js
```
ちなみにCloud9利用の場合は設定から"Show Hidden Files"をONにすることで確認できます。
![スクリーンショット 2023-02-15 15.06.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/dd0f024c-cbc9-afc0-e1ee-608552fb34de.png)

ファイルをローカルにコミットしていきます。
```
git add -A && git commit -m "Workshop Stack Initial Commit"
```
次にリモートリポジトリの情報を取得し（`XXXX`の部分は先ほどコピーしたURL）、
```
git remote add origin XXXXX
```
実際にリモートリポジトリに反映します。(`--set-upstream`を指定することで、masterブランチの内容を上書きしています)
```
git push --set-upstream origin master
```
CodeCommitのコンソールを確認すると、しっかりlambdaフォルダも含まれてプッシュされていますね！
![スクリーンショット 2023-02-15 15.12.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/31e068a8-8987-b781-e930-5fa7c1586e6d.png)

### パイプラインの定義と作成
それではパイプラインを定義していきます。`lib/pipeline-stack.ts`を以下のように編集していきます。
```typescript:lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import { Construct } from 'constructs';
import { CodeBuildStep, CodePipeline, CodePipelineSource } from 'aws-cdk-lib/pipelines';

export class WorkshopPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    // Create a CodeCommit repository
    const repo = new codecommit.Repository(this, 'WorkshopRepo', {
      repositoryName: "WorkshopRepo"
    });
    
    // Declare CodePipeline
    const pipeline = new CodePipeline(this, 'Pipeline', {
      pipelineName: 'WorkshopPipeline',
      synth: new CodeBuildStep('SynthStep', {
        input: CodePipelineSource.codeCommit(repo, 'master'),
        installCommands: [
          'npm install -g aws-cdk'
        ],
        commands: [
          'npm ci',
          'npm run build',
          'npx cdk synth'
        ]
      })
    });
  }
}
```
* `new CodePipeline(...) `:Pipelineを初期化しています。
* `synth(...)`:dependenciesのインストールやビルド、synthを実行するコマンドを記述していきます。決まりとして`synth`するコマンドは一番最後に記述します。NPMベースのプロジェクトの場合は`npx cdk synth`となります。
* `input`: CDKのソースコードがストアされているリポジトリを指定します。

一旦ここまでの内容をリポジトリにコミットしていきます。
```
$ git commit -am "Added Pipeline" && git push
```

デプロイしていきます。
```
$ cdk deploy
```
パイプラインが作成されました！
![スクリーンショット 2023-02-15 16.26.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a22d8159-8f67-c56f-0a53-ccf2a64d4847.png)

### パイプラインにアプリケーションを紐づける
この時点でのパイプラインは、コミットされるごとに自動でトリガーされますが、アプリケーションを”デプロイ”するためのステージが必要です。ステージの中で実際のアプリケーションを紐付けます。その後デプロイ用のステージをパイプラインに組み込んでいきましょう。
まずは`lib`配下に新規ファイル`pipeline-stage.ts`ファイルを以下のように作成します。
```typescript:lib/pipeline-stage.ts
import { CdkWorkshopStack } from './cdk-workshop-stack';
import { Stage, StageProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class WorkshopPipelineStage extends Stage {
  constructor(scope: Construct, id: string, props?: StageProps) {
    super(scope, id, props);
    
    new CdkWorkshopStack(this, 'WebService');
  }
}
```
上記のコードではパイプライン上に組み込む新規の`Stage`を定義して、アプリケーションを紐付け（インスタンス化）ました。
ただこの状態だとエディター上でエラーが表示されているかと思います。これはアプリケーションスタックがパイプラインを通してデプロイされる状態にまだ調整されていないからです。
では調整していきます。`lib/cdk-workshop-stack.ts`を以下のように編集します。
```typescript:lib/cdk-workshop-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs'; 
import { HitCounter } from './hitcounter';

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

    // defines an AWS Lambda resource
...
```
上記コードについて、変更前の`scope`パラメータは`cdk.App`となっていました。これはコンストラクトツリーの中ではappの子要素であることを意味します。今回の変更でこのスタックはパイプラインを通してデプロイされるように変更したので、appの子要素である必要はなくなったため`Construct`をセットしています。

では作成したステージをパイプラインに紐づけていきます。

`lib/pipeline-stack.ts`を以下のように編集していきます。
```typescript:lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import { Construct } from 'constructs';
import { WorkshopPipelineStage } from './pipeline-stage';
import { CodeBuildStep, CodePipeline, CodePipelineSource } from 'aws-cdk-lib/pipelines';

export class WorkshopPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    // Create a CodeCommit repository
    const repo = new codecommit.Repository(this, 'WorkshopRepo', {
      repositoryName: "WorkshopRepo"
    });
    
    // Declare CodePipeline
    const pipeline = new CodePipeline(this, 'Pipeline', {
      pipelineName: 'WorkshopPipeline',
      synth: new CodeBuildStep('SynthStep', {
        input: CodePipelineSource.codeCommit(repo, 'master'),
        installCommands: [
          'npm install -g aws-cdk'
        ],
        commands: [
          'npm ci',
          'npm run build',
          'npx cdk synth'
        ]
      })
    });
    
    const deploy = new WorkshopPipelineStage(this, 'Deploy');
    const deployStage = pipeline.addStage(deploy);
  }
}
```
ここでは`WorkshopPipelineStage`をインスタンス化し、このインスタンス化したステージをパイプラインに追加しています（`pipeline.addStage(deploy)`）。


`git add .`して、コミットします！（もう`cdk deploy`は不要ですね）
```
$ git add .
$ git commit -am "Add deploye stage to pipeline" && git push
```
ちょっと時間はかかりますが、、、完了しました！
![スクリーンショット 2023-02-15 17.41.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/26ec6c23-3291-f724-48c6-e897ee269354.png)
デプロイステージも追加されています。
![スクリーンショット 2023-02-15 17.42.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/11c72c81-6c78-2d99-8c00-2c3de3fe17b7.png)
これでひとまずやりたかったことは実現できました！

### アプリケーションコードを更新してテスト
では実際にアプリケーションコードを更新してデプロイし、テストしてみましょう！Lambda関数を編集します。
```javascript:hello.js
exports.handler = async function(event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    /*bodyをGreatJobに編集*/
    body: `GreatJob, CDK! You've hit ${event.path}\n`
  };
};
```
保存してコミットします。
```
$ git commit -am "Update hello.js lambda" && git push  
```
パイプラインが動き始めました！ドキドキ
![スクリーンショット 2023-02-15 17.47.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/284c5bb1-2b97-d1b5-06cf-3c3e4d6d3e77.png)

無事に成功したようです！（スクショ割愛）
URLに対してテストをしたいのですが、`cdk deploy`コマンドを使ってないのでターミナル上では確認できないですね。
CloudFormationコンソールから"Deploy-WebService"スタックを選択し、Outputsタブを参照してください。こちらにURLが出力されています。
![スクリーンショット 2023-02-15 17.57.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/6af2f3e4-68d4-761b-4125-85f8b869e14b.png)
このURLをHitすると、先ほど更新した内容が反映されていますね！
```
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/
GreatJob, CDK! You've hit /
```
## さらに実施したい方向け
ここからはアディショナルな内容となりますが、[こちら](https://cdkworkshop.com/20-typescript/70-advanced-topics/200-pipelines/5000-test-actions.html)のサイトで以下の2点の実装方法を解説しています。興味のある方は是非実施してみてください！
* URLエンドポイントをよりわかりやすくCloudFormationのOutPutsタブに表示する方法
* デプロイ時にURLエンドポイントに対してテストを実施するステップをパイプラインに組み込む方法

## まとめ
今回はCDKを用いたCICDパイプラインの構築を実践してみました！[Part1](https://qiita.com/kennyQiita/items/907b563048428e8264a1)から続くこの内容で、かなりCDKに対する理解が深まったと思います。
CDKを利用することで、アプリケーション作成の際に必要な細かいAWSリソース（IAMロールなど）も自動で作成できますし、同じ内容をCloudFormationテンプレートで記述するよりも記述量はかなり少なかった印象です。また、デプロイの中にテストを組み込めるのも利点かとおもいました。
シーンに応じでAWS CDKを今後も利用していきたいと思います。

以上です。

## 参考サイト
https://cdkworkshop.com/20-typescript/70-advanced-topics/200-pipelines.html