AWS CDKをデプロイするCodepipelineをCDK(Python)で構築する

# 前回やったこと
下記の記事でAWS CDKでPythonのFargateバッチをデプロイする方法を紹介させていただきました。
前回記事： [Fargateで起動するPythonの定期バッチをAWS-CDKで構築する](https://qiita.com/joe-king-sh/items/0c06911682f85ba0f0bd)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/b5d68a9e-d88c-917a-d1b3-7a03d2d80b4f.png)


# 今回やること
前回作成したCDKをデプロイするCodePipelineをCDKの別スタックで構築します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/b6b286a5-904f-206f-8b68-e9e7c79db19d.png)

CodePipelineを構築するスタックの初回デプロイのみ、手動で「cdk deploy」コマンドを打つ必要がありますが、
その後はGitHubにプッシュするだけで、CodePipelineによってCDKがデプロイされるようにします。

少し図が複雑になったように見えますが、
CDKを使用すれば、エディタの恩恵を受けながら、100行程度のPythonコードで実現できます。

ソースコードは前回同様全て[こちらのリポジトリ](https://github.com/joe-king-sh/aws-cdk-fargate-batch)にあります。
Cloud9の環境構築やCDKの基本的な使い方については[前回の記事](https://qiita.com/joe-king-sh/items/0c06911682f85ba0f0bd)を参照してください。

それでは、書いていきましょう！
# 構築開始
## 必要ライブラリのインストール

```bash
# 仮想環境へ入る
$ pipenv shell
# 必要なライブラリをインストール
$ pipenv install boto3 aws_cdk.core aws_cdk.aws_iam aws_cdk.aws_codepipeline_actions aws_cdk.aws_codepipeline aws_cdk.aws_codebuild aws_cdk.app_delivery
```
※コマンドはpipenvを使用している想定なので、各自使用しているパッケージマネージャに読み替えてください。

## 前回の構成
前回FargateのPythonバッチを作成した時点では、
CDKのプロジェクトは下記のようなフォルダ配置になっていました。

```bash
$ tree
├── app.py
├── aws_cdk_fargate_batch
│   ├── aws_cdk_fargate_batch_stack.py  # VPCやECR,ECSのリソース定義　
│   └── __init__.py
├── batch
├── cdk.context.json
├── cdk.json
├── cdk.out
├── Pipfile
├── Pipfile.lock
└── README.md
```

app.pyも下記のようにAwsCdkFargateBatchStackだけをデプロイするシンプルな構成です。

```python:app.py
#!/usr/bin/env python3
from aws_cdk import core
from aws_cdk_fargate_batch.aws_cdk_fargate_batch_stack import AwsCdkFargateBatchStack

app = core.App()
AwsCdkFargateBatchStack(app, "aws-cdk-fargate-batch")

app.synth()
```

## CodePipelineStack用のディレクトリとファイル作成
今回作成するCodePipelineのリソースは、
これまで作成したリソースと、次元が違いライフサイクルが異なるので、
別ディレクトリを作成し下記のようなファイルを作成しました。

```bash
$ tree
├── app.py
├── aws_cdk_fargate_batch
│   ├── aws_cdk_fargate_batch_stack.py
│   └── __init__.py
├── continuous_delivery                 # 新規作成
│   ├── continuous_delivery_stack.py    # 新規作成
│   └── __init__.py                     # 新規作成
├── batch
├── cdk.context.json
├── cdk.json
├── cdk.out
├── Pipfile
├── Pipfile.lock
└── README.md
```

## 実装 - continuous_delivery.py

新しく作成したファイルcontinuous_delivery.pyに、
CodePipelineのリソースを定義した全文がこちらです。

```python:continuous_delivery.py
from aws_cdk import core
from aws_cdk import aws_iam
from aws_cdk import app_delivery
from aws_cdk import aws_codebuild
from aws_cdk import aws_codecommit
from aws_cdk import aws_codepipeline
from aws_cdk import aws_codepipeline_actions
import boto3

class ContinuousDeliveryStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, deploy_stack: core.Stack, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        
        # ========================================
        # CodePipeline
        # ========================================
        codepipeline = aws_codepipeline.Pipeline(
            self,
            id='sample_pipeline',
            pipeline_name='sample_pipeline',
        )
        
        
        # ============ source stage start ============
        source_output = aws_codepipeline.Artifact('source_output')

        # Change to your setting.
        owner = 'joe-king-sh'
        repo = 'aws-cdk-fargate-batch'
        branch = 'master'
        oauth_token = get_parameters('GITHUB_OAUTH_TOKEN')
        
        # Create source collect stage.
        source_action = aws_codepipeline_actions.GitHubSourceAction(
            action_name='source_collect_action_from_github',
            owner=owner,
            repo=repo,
            trigger=aws_codepipeline_actions.GitHubTrigger.POLL,
            oauth_token=core.SecretValue.plain_text(oauth_token),
            output=source_output
        )
        # Add source stage to my pipeline.
        codepipeline.add_stage(
            stage_name='Source',
            actions=[source_action]
        )
        # ============ source stage end ==============


        # ============ build stage start =============
        # Create build project.
        project = aws_codebuild.PipelineProject(
            self,
            id='sample_build_project',
            project_name='sample_build_project'
        )

        # Add policies to code build role to allow access to the Parameter store.
        project.add_to_role_policy(
            aws_iam.PolicyStatement(
                resources=['*'],
                actions=['ssm:GetParameters']
            )
        )
        
        # Add build stage to my pipeline.
        build_output = aws_codepipeline.Artifact('build_output')
        codepipeline.add_stage(
            stage_name='Build',
            actions=[
                aws_codepipeline_actions.CodeBuildAction(
                    action_name='CodeBuild',
                    project=project,
                    input=source_output,
                    outputs=[build_output]
                )
           ]
        )
        # ============ build stage end ===============

        # ============ deploy stage start ============
        # Add deploy stage to pipeline.
        codepipeline.add_stage(
            stage_name='Deploy',
            actions=[
                app_delivery.PipelineDeployStackAction(
                    stack=deploy_stack,
                    input=build_output,
                    admin_permissions=True,
                    change_set_name='sample-change-set'
                )
            ]
        )
        # ============ deploy stage end ==============


def get_parameters(param_key):
    """
    Get parameter encrypted from parameter store.
    """
    ssm = boto3.client('ssm', region_name='ap-northeast-1')
    response = ssm.get_parameters(
        Names=[
            param_key,
        ],
        WithDecryption=True
    )
    return response['Parameters'][0]['Value']
```

一つずつ順番に追っていっていきましょう。

### 1. CodePipeline用のStackとCodePipelineリソースを定義

```python:continuous_delivery.py

from aws_cdk import core
from aws_cdk import aws_iam
from aws_cdk import app_delivery
from aws_cdk import aws_codebuild
from aws_cdk import aws_codecommit
from aws_cdk import aws_codepipeline
from aws_cdk import aws_codepipeline_actions
import boto3

class ContinuousDeliveryStack(core.Stack):

    # deploy_stackはこのPipelineでデプロイするCDKのStack
    def __init__(self, scope: core.Construct, id: str, deploy_stack: core.Stack, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        
        # ========================================
        # CodePipeline
        # ========================================
        codepipeline = aws_codepipeline.Pipeline(
            self,
            id='sample_pipeline',
            pipeline_name='sample_pipeline',
        )

```
ContinuousDeliveryStackのコンストラクタにdeploy_stackを追加しました。
このPipelineを使用してデプロイするスタックを、外(app.py)から渡す想定です。

aws_codepipeline.Pipelineには最低限の引数を渡してあげます。

### 2. Source Stageを定義
1. GitHubTokenを取得
今回はGitHubをリポジトリに使用しているので[こちら](https://help.github.com/ja/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line)を参考にGitHubのOauthトークンを取得します。

1. Tokenの格納
取得したトークンはParameterStoreに格納しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/4e56fe8d-21f1-0e41-b538-e0f2482a83cf.png)

1. コーディング

```python:continuous_delivery.py        
        # Source StageのOutputのArtifact
        source_output = aws_codepipeline.Artifact('source_output')

        # 自身の設定に変更すること
        owner = 'joe-king-sh'
        repo = 'aws-cdk-fargate-batch'
        branch = 'master']

        # GitHub oauth_tokenをSSMから取得する
        oauth_token = get_parameters('GITHUB_OAUTH_TOKEN')
        
        # GitHubのSourceActionを定義
        source_action = aws_codepipeline_actions.GitHubSourceAction(
            action_name='source_collect_action_from_github',
            owner=owner,
            repo=repo,
            trigger=aws_codepipeline_actions.GitHubTrigger.POLL,
            oauth_token=core.SecretValue.plain_text(oauth_token),
            output=source_output
        )
        # PipelineにSource Stageを追加し、GitHubからソースを取得するActionを追加
        codepipeline.add_stage(
            stage_name='Source',
            actions=[source_action]
        )
```
get_parametersは後の方に定義したSSMを扱うヘルパー関数です。

##### ※リポジトリにCodeCommitを使用する場合
CodeCommitを使用したい場合はActionを下記にように書けば良いです。

```python:CodeCommit使用時
        # ARNからCodeCommitのリポジトリを取得
        repository = codecommit.Repository.from_repository_arn(self, 'sample-repository', 'arn:XXXXX')

        # 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
        )
```

### 3. Build Stageを定義
```python:continuous_delivery.py
        # CodeBuildProjectの定義
        project = aws_codebuild.PipelineProject(
            self,
            id='sample_build_project',
            project_name='sample_build_project_name'
        )

        # CodeBuild実行ロールでParameterStoreにアクセスできるようにPolicyを追加
        project.add_to_role_policy(
            aws_iam.PolicyStatement(
                resources=['*'],
                actions=['ssm:GetParameters']
            )
        )
        
        # PipelineにBuild Stageを追加
        build_output = aws_codepipeline.Artifact('build_output')
        codepipeline.add_stage(
            stage_name='Build',
            actions=[
                aws_codepipeline_actions.CodeBuildAction(
                    action_name='CodeBuild',
                    project=project,
                    input=source_output,
                    outputs=[build_output]
                )
           ]
        )
```
ssm:GetParametersをCodeBuildロールに付与しないと、
Buildステージで権限不足でビルド失敗するので注意。


### 4. Deploy Stageを追加
```python:continuous_delivery.py
        # Add deploy stage to pipeline.
        codepipeline.add_stage(
            stage_name='Deploy',
            actions=[
                app_delivery.PipelineDeployStackAction(
                    stack=deploy_stack,
                    input=build_output,
                    admin_permissions=True,
                    change_set_name='sample-change-set'
                )
            ]
        )
```
app_delivery.PipelineDeployStackActionのstackに、デプロイするstackを指定します。

## 実装 - app.py

```python:app.py
#!/usr/bin/env python3
from aws_cdk import core
from aws_cdk_fargate_batch.aws_cdk_fargate_batch_stack import AwsCdkFargateBatchStack
from continuous_delivery.continuous_delivery_stack import ContinuousDeliveryStack
app = core.App()

fargate_batch_stack = AwsCdkFargateBatchStack(app, "aws-cdk-fargate-batch")

ContinuousDeliveryStack(app, id="continuous-delivery", deploy_stack=fargate_batch_stack)

app.synth()

```
fargate_batch_stackを今回新たに作成したContinuousDeliveryStackに渡します。

## 実装 - buildspec.yml
```yml:buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.7
    commands:
      - |
        # AWS-CDKとPython実行環境をインストール
        echo Instaling packages started..

        python3 -m pip install pipenv
        npm install -g aws-cdk
        pipenv install

  build:
    commands:
      - |
        # Python仮想環境を立ち上げ
        echo Launching virtual environments started..
        export VENV_HOME_DIR=$(pipenv --venv)
        . $VENV_HOME_DIR/bin/activate

        # CDKからCloudformationに変換
        echo Translating into cloudformation started..
        cdk synth aws-cdk-fargate-batch

artifacts:
  base-directory: cdk.out
  files: '**/*'
```

# いざデプロイ
下記コマンドでCodePipelineのスタックのみを手動でデプロイします。

```bash
$ cdk deploy continuous-delivery
```

1. continuous-delivery スタック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/63c0c7c5-0380-e825-cfe9-7800f9b0f45e.png)
continuous-deliveryスタックのデプロイが始まりましたー！:zap:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/382b8802-1b4f-6fe9-d3af-77281c713113.png)
continuous-deliveryスタックのデプロイが終わるとCodePipeLineが構築されています。

2. aws-cdk-fargate-batch スタック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/c073730f-b513-3f16-8ccf-b5f406162094.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541733/e9dfc360-7f1c-8e32-ba5c-5af940e6887d.png)

　先程のPipelineを通して、無事AwsCdkFargateBatchStack達がデプロイされました:zap:


# お掃除
下記cdkコマンドでStackを削除します。

```bash
$ cdk ls
aws-cdk-fargate-batch
continuous-delivery
$ cdk destroy aws-cdk-fargate-batch
Are you sure you want to delete: aws-cdk-fargate-batch (y/n)? y
$ cdk destroy continuous-delivery
Are you sure you want to delete: continuous-delivery (y/n)? y
```
**必ずaws-cdk-fargate-batchから消すこと。**
先にcodepipelineを消すと色々と面倒になる可能性があります。

※前回同様ECRとCloudWatchLogGroupに加え、
　Codepipelineで使用するArtifact格納用のS3は自動では消えません。手動で削除してください。

# まとめ
通常のCfnでCfnをデプロイするCodePipelineを作成する場合と違い、
CDKでは、Codebuildの中で「cdk synth」してCloudformationを出力する必要があります。

今回は、例として前回構築したFargateバッチを使用しましたが、
PipelineStackに渡すStackを変更すれば、色々と応用が効くと思います。

次回は、CDKでSAMがデプロイできると聞いたので、調べて書いてみようと思います。
それではまた:raised_hands:

# 参考にさせていただきました
[[AWS CDK] CodePipelineのソース元を色々指定してみました（CodeCommitとか、Githubとか、S3 Bucketとか、BacklogのGitとか)](https://dev.classmethod.jp/server-side/serverless/aws-cdk-codepipeline/)
[CloudFormationの全てを味わいつくせ！「AWSの全てをコードで管理する方法〜その理想と現実〜」 #cmdevio](https://dev.classmethod.jp/cloud/aws/aws-all-iac/)
[Continuous Integration / Continuous Delivery for CDK Applications](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.app_delivery.README.html)