CDKで一発作成！S3→Codebuild→ECRでDockerイメージをpushするCodePipeline  

## 記事概要
本記事では、CDKで以下の構成をデプロイします。  

![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261789/3e160c9c-45e7-4ad9-8cf9-44079ac1bbf5.png)

以下の記事で、S3にアップロードしたソースコードからDockerイメージを作成しECRにプッシュする、というプロセスを自動化するCodePipelineを作成しました。  
- [DockerイメージのビルドとAmazon ECRへのプッシュをCodeBuildで自動化する](
https://qiita.com/e52yamada/items/9ef696737d20285c7498)
- [S3へのコードのアップロードを検知してCodeBuildを実行するCodePipelineの作成](https://qiita.com/e52yamada/items/21d725b8a7aa3e925080)  

S3バケットやECRリポジトリ、CodeBuildやCodePipelineなど、マネジメントコンソールで作成してきたAWSリソースを一発で作成できるCDKコードを作成したので、デプロイ方法を紹介します。  

CDKの練習を兼ねて作成したコードのため、とりあえずデプロイできるというクオリティです。可読性、保守性などを踏まえて後々ブラッシュアップしたいと思います…
## CDKでデプロイされるリソース
CDKをデプロイすると、以下が作成されます。
- ECRプライベートリポジトリ
    - 作成されたDockerイメージのpush先となるリポジトリ
- S3バケット
    - DockerイメージのもとになるDockerfileやその他のソースコードのアップロード先
    - ソースコード類はまとめてzipファイルにし、作成したS3バケットに直接配置する構成になっています
- CodeBuildプロジェクト
    - buildspecをコマンドで挿入しているため、今回buildspec.ymlファイルの準備は不要です
- CodePipelineのパイプライン    
    - ソースステージで作成したS3バケットをプロバイダーとして指定
    - ビルドステージで作成したCodeBuildプロジェクトをプロバイダーとして指定

S3にアップロードするzipファイルは自分で用意してS3バケットにアップロードします。アップロードのトリガーにパイプラインが実行されます。    
※ buildspec.ymlはzipファイルに含める必要はありません
## CDKのデプロイ方法  
### 動作環境
今回はCloudShellで実行したため、そのままCDKコマンドが実行できます。それ以外の環境で実行する場合、[AWS CDK の前提条件](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/prerequisites.html)を確認のうえ、必要なセットアップを行ってください。  

### CDKプロジェクトの作成

以下のようにディレクトリを1つ作成し、そのディレクトリ内で `cdk init` コマンドによりプロジェクトの初期化を行います。ディレクトリ名に合わせてCDKコードが生成されるため、 **本記事のCDKを実行する場合はディレクトリ名を `cdk` にすることをおすすめします。**   

```sh
mkdir cdk
cd cdk
cdk init app --language typescript
```
実行すると、`bin` ディレクトリや `lib` ディレクトリ、`cdk.json`ファイルなどが作成されます。  

### CDKコードの挿入
今回はすでに作成されている `/lib/cdk-stack.ts` を[下記のCDKコード](https://qiita.com/e52yamada/items/5af111df7c3890bc364a#cdk%E3%82%B3%E3%83%BC%E3%83%89)に書き換えます。必要に応じて一部カスタマイズしてください。  
CloudShellだと編集しづらいため、ローカル環境でファイルを用意してから、CloudShellへアップロードすることをおすすめします。

### CDKコードのカスタマイズ
- アップロードするzipファイルを変更したい場合は、以下の `'artifact.zip'` を変更してください。  

※ 作成したS3バケット直下にzipファイルを配置する構成になっています
```ts
const Resourcepath = 'artifact.zip'
```

- リソースを作成するリージョンを変更したい場合は、以下の `"ap-northeast-1"` を変更してください。
```ts
"AWS_DEFAULT_REGION": "ap-northeast-1",
```

変数部分は `cdk.json` など別ファイルに外だしするのが一般的だと思いますが、追々アップデートしていきたいと思います。

### CDKブートストラップ・デプロイ
CDKでAWSリソースをデプロイするためのブートストラップとして、S3やIAMロールなどを作成するプロセスとしてブートストラップを行います。作成したディレクトリ(今回だと`cdk`ディレクトリ)で以下のコマンドを実行します。  
```sh
cdk bootstrap
```  
:::note info  
ブートストラップはCDKプロジェクト作成時に一度だけ実行します  
:::  

最後に、以下のコマンドでデプロイを行います。以後、CDKコードを変更しその内容をデプロイしたい場合は、以下のコマンドだけ実行すればOKです。  
```sh
cdk deploy
```  

デプロイ前に、必要なIAMリソースを作成してよいか聞かれるため `y` を入力してください。  
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261789/77ab1bb7-d3d5-1ac5-5254-ae04e96bcd2f.png)


## CDKコード
`/lib/cdk-stack.ts` の内容を以下に書き換えます。  

```ts
import * as cdk from 'aws-cdk-lib';
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as cb from "aws-cdk-lib/aws-codebuild";
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import { Construct } from 'constructs';

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

    // ECRプライベートリポジトリ
    const EcrRepository = new ecr.Repository(this, 'Repository', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      imageTagMutability: ecr.TagMutability.MUTABLE,
      emptyOnDelete: true
    });

    // S3バケット
    const ResourceBucket = new s3.Bucket(this, 'CreateBucket' ,{
      versioned: true,
    });
    const Resourcepath = 'artifact.zip'
    
    //buildspecオブジェクト
     const buildSpecObject = {
    "version": "0.2",
    "env": {
      "variables": {
        "AWS_DEFAULT_REGION": "ap-northeast-1",
        "DOCKER_USER": "AWS",
        "REPOSITORY_URI": EcrRepository.repositoryUri,
        "IMAGE_NAME": EcrRepository.repositoryName,
        "ACCOUNT_ID": cdk.Stack.of(this).account
      }
    },
    "phases": {
      "pre_build": {
        "commands": [
          "echo Logging in to Amazon ECR...",
          "aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username $DOCKER_USER --password-stdin $REPOSITORY_URI"
        ]
      },
      "build": {
        "commands": [
          "echo Building the Docker image...",
          "docker build -t \"$REPOSITORY_URI:latest\" ."
        ]
      },
      "post_build": {
        "commands": [
          "echo Pushing the Docker image...",
          "docker push \"$REPOSITORY_URI:latest\""
        ]
      }
    }
  }

        
    //CodeBuildプロジェクト
    const CodebuildProject = new cb.Project(this, 'Ci-Project', {
    source: cb.Source.s3({
      bucket: ResourceBucket,
      path: Resourcepath
      }),
      buildSpec: cb.BuildSpec.fromObject(buildSpecObject),
    });
    
    // パイプラインの作成
    const pipeline = new codepipeline.Pipeline(this, 'S3TriggeredPipeline', {
    });
    
     // ソースステージの追加
    const sourceOutput = new codepipeline.Artifact();
    const sourceAction = new codepipeline_actions.S3SourceAction({
      actionName: 'S3Source',
      bucket: ResourceBucket,
      bucketKey: Resourcepath,
      output: sourceOutput,
    });
    pipeline.addStage({
      stageName: 'Source',
      actions: [sourceAction],
    });
    
    // ビルドステージの追加
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild',
      project: CodebuildProject,
      input: sourceOutput,
      outputs: [new codepipeline.Artifact()],
    });
    pipeline.addStage({
      stageName: 'Build',
      actions: [buildAction],
    });
    
  // EventBridgeルールの作成
    const rule = new events.Rule(this, 'S3ObjectCreatedRule', {
      eventPattern: {
        source: ['aws.s3'],
        detailType: ['Object Created'],
        detail: {
          bucket: {
            name: [ResourceBucket.bucketName]
          },
          object: {
            key: [Resourcepath]
          }
        }
      }
    });

    // EventBridgeルールのターゲットとしてパイプラインを追加
    rule.addTarget(new targets.CodePipeline(pipeline));
  }
};
```
---
本記事はここまでとなります。