【AWS】CDKでECRを構築しよう

みなさんAWS CDK使っていますか？
私はCFnを使用していたのですがYAMLで書くのがいろいろ辛くなってきたので、最近AWS CDKに乗り換えました

という訳で今回は、AWS CDKを使ったECRの構築方法を書いていきたいと思います

# 環境

![language](https://img.shields.io/badge/language-python-blue.svg) ![python3.8](https://img.shields.io/badge/python-^3.8-blue.svg) ![pipenv](https://img.shields.io/badge/package_manager-pipenv-blue.svg)

# 前提

以下のコマンドがインストール済み

- pipenv
- aws-cdk

# ディレクトリ構成

```
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── app.py # CDKのPython用メインスクリプト
├── cdk.json # CDKのコンフィグ
├── cdk.out # CDKから出力されたCFn templateなどが格納される
├── mypy.ini
├── src
│   ├── __init__.py
│   ├── entity # リソースの実体を格納
│   ├── props # リソースのパラメータ定義クラス
│   └── stack # CFn stackを格納
└── tox.ini
```

## ディレクトリやスクリプトの関係性

```
props/ -> entity/ -> stack/ -> app.py
```

上記のような関係性を保つことで変更に対して強くなりますし、環境ごとにパラメータを変えて管理したい場合にも対応します

# インストール

必要なaws_cdkのライブラリをインストールしましょう

```bash
pipenv install aws_cdk.core aws_cdk.aws_ecr
```

# 実装

## props/ecr.py

Python用のaws_cdkライブラリにも各リソースのPropsクラスがあるのですが、個人的に使い勝手がよくない印象なので、独自に定義しています
ここには必要なパラメータしか設定せずに、用途によってデフォルト値も公式のものから変更しています

CFnの場合、ライフサイクルルールをJSONテキストで書かなければなりませんが、CDKだとプロパディに値を渡すだけなので、かなり楽です

```python
from dataclasses import dataclass
from typing import Optional

from aws_cdk.aws_ecr import TagStatus
from aws_cdk.core import RemovalPolicy

from src.props.base import Base


@dataclass(frozen=True)
class LifecycleRule(Base):
    description: Optional[str] = None
    max_image_count: int = 5
    rule_priority: int = 1
    tag_status: TagStatus = TagStatus.ANY


@dataclass(frozen=True)
class Repository(Base):
    id: str
    repository_name: str
    image_scan_on_push: Optional[bool] = None
    removal_policy: RemovalPolicy = RemovalPolicy.RETAIN
```

## entity/ecr.py

作成したいリソースを定義を書いていきます
今回はクラスで定義していますが、インスタンス化させても良いと思います
クラスの方が個人的に体裁がよく見えるのでそうしているだけです

```python
from typing import List, Optional

from src.entity.base import Base
from src.props.ecr import LifecycleRule, Repository


class EcrBase(Base):
    """ECR基底class"""

    repository: Repository
    lifecyle_rules: Optional[List[LifecycleRule]] = None


class SampleEcr(EcrBase):
    """Sample"""

    id = 'SampleEcr'

    repository = Repository(
        id='SampleEcrRepo',
        repository_name='sample'
    )

    lifecyle_rules = [
        LifecycleRule(
            description='Delete more than 10 images',
            max_image_count=10
        )
    ]
```

## stack/ecr.py

ECR用のスタックを定義していきます
スタックのクラスに渡すリソース定義は基本的に`entity`だけになるはずです
あとは必要に応じてapp.pyで全体で共通して設定したいtagなどを引数に渡してあげましょう

```python
from typing import Any, Type

from aws_cdk import core
from aws_cdk.aws_ecr import Repository

from src.entity.ecr import EcrBase


class EcrStack(core.Stack):
    def __init__(
            self,
            scope: core.Construct,
            entity: Type[EcrBase],
            **kwargs: Any) -> None:
        super().__init__(scope, entity.id, **kwargs)

        repo = Repository(self, **entity.repository.to_dict())

        if entity.lifecyle_rules:
            for rule in entity.lifecyle_rules:
                repo.add_lifecycle_rule(**rule.to_dict())
```

## app.py

stack/ecr.pyに書いたスタックをデプロイするためにapp.pyに書いていきましょう

```python
#!/usr/bin/env python3

from aws_cdk import core

from src.entity.ecr import SampleEcr
from src.stack.ecr import EcrStack


app = core.App()

# 全てのリソースに設定するタグ
tags = {'CreatedBy': 'iscream'}

EcrStack(app, entity=SampleEcr, tags=tags)

app.synth(skip_validation=False)
```

# デプロイ

デプロイ方法は以下の2つです

```bash
pipenv run cdk deploy SampleEcr
```

OR

```bash
pipenv shell
cdk deploy SampleEcr
```

デプロイする際に注意して欲しいのはpipenvで仮想環境上でcdkコマンドを実行する点です
それを忘れると以下のようなエラーになります

```
Traceback (most recent call last):
  File "app.py", line 3, in <module>
    from aws_cdk import core
ModuleNotFoundError: No module named 'aws_cdk'
Subprocess exited with error 1
```

# おわりに

CDKの場合はプログラミング言語でリソースを定義できるので、柔軟にリソースを管理することができます。
その反面、コードの設計をしっかりしないとスパゲッティになり、柔軟性がなくなり運用がつらくなります。
今回紹介した方法がベストプラクティスかはわかりませんが、ちゃんと機能ごとにディレクトリやファイルを分けて管理するだけでも運用はよくなると思います。

CDKはかなり良いプロダクトなので、ぜひ使ってみてください!!

# Reference

- https://github.com/aws/aws-cdk
- https://docs.aws.amazon.com/cdk/api/latest/python/modules.html