CDKでサクッとCodeCommit+CodePipeline

# はじめに
本投稿は、AWS CDKの勉強を兼ねて試作してみたCodeCommit+CodePipelineでS3にファイルをアップロードする機構について紹介しています。
エンジニアチーム内で「触ってみたいですよね」という声があり興味があった所に、別部署のエンジニアの方からも「CDKいいっすよ〜」という評判を聞いたので触ってみました。

# AWS CDKとは？
[AWS CDK](https://aws.amazon.com/jp/cdk/)とは、`AWS クラウド開発キット (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースをモデル化およびプロビジョニングするためのオープンソースのソフトウェア開発フレームワーク`だそうです。PythonやTypeScriptなんかでインフラの定義がかけて便利です。

# 背景
## 最初期
過去、制作用の環境としてCloud9が使用されていたのですが、その時の選定要件は、

1. 同時編集できる
2. 成果物をプレビューできる
3. プレビューにBasic認証入れられる
4. バージョン管理できる

だったので、Cloud9+Codecommit+Lambda+CloudFront+Lambda@edge+S3でCloud9からCodeCommit経由でS3にhtmlファイル・画像ファイルがアップロードされ、CloudFront経由でプレビューできるというものを作っていました。最初はうまく回っているように見えていました。

## 問題点
最初はうまく回っているように見えていたCloud9も徐々に問題点が出てきました。

1. 一部ファイルがS3に上がっていない。（これはCodeCommit→S3をLambdaでやっている所が怪しいと睨んでいます）
1. 案件が増えてきてCloud9の容量が圧迫されている（画像もりもりの制作物なので…）
1. そもそも同時編集機能が活用されてない気配がある…

ということで、Cloud9である必要性がなくなっていました。であればローカルで作ってS3に上がればいいよねってことで作り直しの機運が高まりました。（実際にコーディングするメンバーはAWSを触る人たちではないので極力AWSと直接相対しない形を提供してあげたいという思いもありました。）

# 本題
作り直しにあたり、社内で噂を聞いたCDKを採用しました。下図の左側CodeCommit・CodePipeline・CodeBuildのリソース作成をCDKが担当します。（S3+CloudFront+Lambda@edgeは以前のものを流用しました。）

![codecommit_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575891/ed3242fc-387c-8764-d238-18e4a5d8dedc.png)

Cloud9時代には、`1Cloud9(1EC2インスタンス)=複数案件=1リポジトリ`だったものを、`1案件=1リポジトリ`にしました。案件着手時に`cdk deploy`することで、CodeCommit上にリポジトリの作成とCodePipelineが整備されて、masterにpushするごとに、CodeBuildでS3にファイルがアップロードされるという状態です。

## 実装
[ドキュメント](https://docs.aws.amazon.com/cdk/latest/guide/hello_world.html)に従って、

```sh
$ npm install -g aws-cdk
$ mkdir test
$ cd test
$ cdk init test --language python
$ source .env/bin/activate
$ pip install -r requirements.txt
```
としてスタートします。

```python:app.py
#!/usr/bin/env python3
import os
from os.path import join, dirname
from dotenv import load_dotenv

from aws_cdk import core
from test.test_stack import TestStack


dotenv_path = join(dirname(__file__), '.env_file')
load_dotenv(dotenv_path)


app = core.App()
TestStack(app,
          "test",
          repo_name=os.environ["REPOSITORY_NAME"],
          env={"account": "xxxxxxxxxxxx", "region": "ap-northeast-1"})
app.synth()
```

```python:test_stack.py
from aws_cdk import core
from aws_cdk import aws_codecommit as codecommit
from aws_cdk import aws_codebuild as codebuild
from aws_cdk import aws_codepipeline as codepipeline
from aws_cdk import aws_codepipeline_actions as codepipeline_actions
from aws_cdk import aws_iam as iam

class TestStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, repo_name: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # output先のバケット名
        s3_bucket_name = "test-bucket"
    
        # CodeCommitのRepository作成
        repo = codecommit.Repository(self,
                                     "Repository",
                                     repository_name=repo_name,
                                     description="test.")
        repository = codecommit.Repository.from_repository_arn(self, repo_name, repo.repository_arn)

        # CodePipelineの定義
        pipeline = codepipeline.Pipeline(self,
                                         id=f"test-pipeline-{repo_name}",
                                         pipeline_name=f"test-pipeline-{repo_name}")
        source_output = codepipeline.Artifact('source_output')

        # CodeCommitの追加
        source_action =  codepipeline_actions.CodeCommitSourceAction(repository=repository,
                                                                     branch='master',
                                                                     action_name='source_collect_action_from_codecommit',
                                                                     output=source_output,
                                                                     trigger=codepipeline_actions.CodeCommitTrigger.EVENTS)
        pipeline.add_stage(stage_name='Source', actions=[source_action])

        # CodeBuildの追加
        cdk_build = codebuild.PipelineProject(self,
                                              "CdkBuild",
                                              build_spec=codebuild.BuildSpec.from_object(dict(
                                                  version="0.2",
                                                  phases=dict(
                                                      build=dict(
                                                          commands=[f"aws s3 sync ./ s3://{s3_bucket_name}/"]
                                                      )
                                                  )
                                              )))
        cdk_build.add_to_role_policy(
            iam.PolicyStatement(
                resources=[f'arn:aws:s3:::{s3_bucket_name}', f'arn:aws:s3:::{s3_bucket_name}/*'],
                actions=['s3:*']
            )
        )

        build_output = codepipeline.Artifact("CdkBuildOutput")

        build_action = codepipeline_actions.CodeBuildAction(
                            action_name="CDK_Build",
                            project=cdk_build,
                            input=source_output,
                            outputs=[build_output])

        pipeline.add_stage(
            stage_name="Build",
            actions=[build_action]
        )
```

### CodeBuild
今にして思うと、`s3 sync`してるだけのCodeBuildってCodeDeployで良かったのでは感があります。
`s3 sync`する時にs3バケット側の階層を一段深くしたくてこうしていますが、良い解決策はないものでしょうか…

```python:codebuildの部分
# CodeBuildの追加
cdk_build = codebuild.PipelineProject(self,
                                      "CdkBuild",
                                      build_spec=codebuild.BuildSpec.from_object(dict(
                                        version="0.2",
                                        phases=dict(
                                        build=dict(commands=[f"aws s3 sync ./ s3://{s3_bucket_name}/"])
                                      ))))
```

## デプロイ
あとは

```sh
$ cdk deploy
```
としてやればデプロイできますが、使う人向けに以下のようにスクリプトにしておきました。

```shell:make.sh
#!/bin/bash

if [ $# -ne 1 ]; then
  echo "実行するには1個の引数が必要です。" 1>&2
  echo "引数にはプロジェクトコードを指定してください。（プレビュー時のURLに一部使用されます）"
  exit 1
fi
echo REPOSITORY_NAME=$1 > .env_file
cdk deploy test --profile xxxxxxx
# commitぶっこむ
git clone ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/$1 ../$1
mkdir ../$1/$1
touch ../$1/$1/.gitkeep
cd ../$1
git checkout -b master
git add -A
git commit -m "initial commit"
git push origin master
```

これで、作成するリポジトリ名を引数として

```sh
$ ./make.sh test_repository
```
とすればOKです。

## 懸念点
- 今はCDKの中でCodeCommitのリポジトリを作成しているため、スタックを消してしまうとリポジトリごと消えてしまうというリスクがあります。
- 実は初回のdeploy時にはCodeCommit側でmasterブランチが作成されていない（？）ためBuild時に失敗してしまいます。git pushしてあげると正しく動作するので現在のところは放置しています。（が少し気持ち悪い。）

# 最後に
今回はCDKを使ってCodeCommitからCodeBuild経由でS3にファイルをアップロードする機構を作成しました。
Cloud9時代に比べて評判がいいようなので、このまま何事もなく使ってもらえるといいなと思いつつフィードバックを待ちます。
CDK自体はCloudFormationと直に戦わなくていいので良いものだと思います。