【CDK】 色んな言語でCRUD API作る【Python】

# この記事について
[AWS CDKv2](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html)でCRUD操作ができるAPIを作る
今回はPythonを使う

# プロジェクト初期化
## CLIのインストール
CLI自体はnodeのものを使う(ので導入済みならスキップ)

```bash
npm install -g aws-cdk
cdk --version
```

バージョンが表示されればOK

## CDK Toolkitのデプロイ
初回のみCDKを利用する際に必要となるファイル群のデプロイが必要
→これもやったことがあるならスキップ

```bash
cdk bootstrap aws://${ACCOUNT}/${REGION}
```

以下のように表示されればOK

```bash
✅  Environment aws://${ACCOUNT}/${REGION} bootstrapped.
```

※異なるアカウント、リージョンでCDKを利用する場合はまたこの作業が必要になる

## プロジェクト初期化
適当なディレクトリで↓を実行

```bash
cdk init app --language python
```

こんな感じでプロジェクトが初期化される

![スクリーンショット 2022-07-04 174309.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/2cfd7d0b-e1fd-e8ca-d1fe-e5ae47c5c11d.jpeg)

## モジュールのインストール
[venv](https://docs.python.org/ja/3/library/venv.html)を使って仮想環境にモジュールをインストールする

```bash
python3 -m venv .venv
source .venv/bin/activate
```

これで仮想環境が立ち上がる
次にインストールするモジュールのリスト(requirements.txt)を更新

```requirements.txt
aws-cdk-lib==2.29.1
constructs>=10.0.0,<11.0.0
aws_cdk.aws_lambda_python_alpha
```

そうしたらpipインストールする

```bash
pip install -r requirements.txt
```

# 実装
## CDKのコードを書く

```/todoapp_py/todoapp_py_stack.py
from aws_cdk import (
    RemovalPolicy,
    Stack,
    aws_dynamodb as dynamodb,
    aws_iam as iam,
    aws_lambda,
    aws_lambda_python_alpha,
    aws_logs as logs,
    aws_apigateway as apigateway
)
from constructs import Construct


class TodoappPyStack(Stack):

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

        # DynamoDB
        todos_table = dynamodb.Table(
            self,
            'TodosTable',
            table_name='todoapp-py-todos-table',
            partition_key=dynamodb.Attribute(
                name='id',
                type=dynamodb.AttributeType.STRING
            ),
            removal_policy=RemovalPolicy.DESTROY)

        # Lambda
        todo_function = aws_lambda_python_alpha.PythonFunction(
            self,
            'TodoFunction',
            runtime=aws_lambda.Runtime.PYTHON_3_9,
            entry='src',
            index='main.py',
            handler='handler',
            function_name='todoapp-py-function',
            environment={
                'TODOS_TABLE_NAME': todos_table.table_name
            },
        )
        # LambdaにDynamoDBのCRUD操作権限を付与
        todo_function.add_to_role_policy(iam.PolicyStatement(
            effect=iam.Effect.ALLOW,
            actions=[
                'dynamodb:Scan',
                'dynamodb:PutItem',
                'dynamodb:UpdateItem',
                'dynamodb:DeleteItem',
            ],
            resources=[todos_table.table_arn]
        ))

        # LogGroup
        logs.LogGroup(self,
                      'TodoFunctionLogs',
                      log_group_name='/aws/lambda/' + todo_function.function_name,
                      removal_policy=RemovalPolicy.DESTROY)

        # API Gateway
        api = apigateway.RestApi(self,
                                 'TodoAPI',
                                 rest_api_name='todoapp-py-api',
                                 default_cors_preflight_options=apigateway.CorsOptions(
                                     allow_origins=apigateway.Cors.ALL_ORIGINS,
                                     allow_methods=apigateway.Cors.ALL_METHODS,
                                     allow_headers=apigateway.Cors.DEFAULT_HEADERS,
                                     status_code=200
                                 ))
        integration = apigateway.LambdaIntegration(todo_function)

        # /todos
        todos_resource = api.root.add_resource('todos')
        todos_resource.add_method('GET', integration=integration)
        todos_resource.add_method('POST', integration=integration)

        # /todos/{id}
        todo_id_resource = todos_resource.add_resource('{id}')
        todo_id_resource.add_method('PUT', integration=integration)
        todo_id_resource.add_method('DELETE', integration=integration)
```

前回TypeScriptで実装した時とほぼいっしょ
Lambdaを定義している箇所は少々ややこしいので↓に抜き出して補足する

### aws_lambda_python_alphaについて

```python
todo_function = aws_lambda_python_alpha.PythonFunction(
    self,
    'TodoFunction',
    runtime=aws_lambda.Runtime.PYTHON_3_9,
    entry='src',
    index='main.py',
    handler='handler',
    function_name='todoapp-py-function',
    environment={
        'TODOS_TABLE_NAME': todos_table.table_name
    },
)
```

- **entry**
  - Lambdaのソースコードがある**ディレクトリ**を指定する
- **index**
  - Lambdaのエントリーポイントとなる**ファイル名**を指定する
- **handler**
  - Lambdaのエントリーポイントとなる**関数名**を指定する

端的に言うと、↑のように書いた場合は[ここ](https://github.com/tetsu0000/todoapp-py/blob/main/src/main.py#L47)をLambdaのエントリーポイントとして、
[ここ](https://github.com/tetsu0000/todoapp-py/blob/main/src/requirements.txt)を参照してモジュールをインストールしアップロードしてくれる

※ドキュメントも参照

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html

## Lambdaのコード実装
[こちら](https://github.com/tetsu0000/todoapp-py)のサンプルコードを参照
ルーティングを[Flask](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/index.html)で、API Gatewayとの結合を[awsgi](https://github.com/slank/awsgi)で実装した

# デプロイ

↓を実行

```bash
cdk deploy TodoappPyStack
```

# 動作確認

↓を実行

```bash
export ENDPOINT="API Gatewayのエンドポイント"
bash tests/test.bash
```

# 感想
CDKのコードは概ねTypeScriptで実装した時と同じ感覚で書けた
唯一Lambdaは割と勝手が違っていたので少々躓いた

※aws_lambda_nodejsとaws_lambda_python_alphaの差分

- エントリーポイントの書き方
- Lambdaのコードで使うモジュールの指定
  - nodejs...CDKのプロジェクトのpackage.jsonに書けばOK
  - python...別途Lambdaのコードのディレクトリにrequirements.txtを書く必要がある

また、TypeScriptに比べて資料が少ないように感じたので、CDK全く触ったことない人が始めるには公式ドキュメントとにらめっこしないといけないので大変そう
※技術ブログではなく何事も常に一次資料にあたるのが本来あるべき姿というのは置いておくとして

とはいえ、(至極当然のことではあるけど)PythonのCDK固有の難点みたいなものは特に感じられなかったので、Pythonをよく使っているチームなら採用しても全く問題ないと思う

次はC#かGolangでやってみる予定

# 参考リンク

サンプルコード
https://github.com/tetsu0000/todoapp-py