AWS CDKに入門してみた。

# はじめに
この記事は[NEアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/ne)(2)の2日目の記事です。
AWS CDKに入門してみたよと言う記事です。

日頃の業務でもAWSに関わる機会が増えてきたので、個人的にAWSには触れるようにしています。
ただ、LambdaとEventBridgeで定期的に何かをするくらいのことしかやったことがなかったので、
今回は最近気になっていたAWS CDKを触ってみました。

# [AWS CDK (AWS Cloud Development Kit)](https://aws.amazon.com/jp/cdk/)
AWSが提供している、 AWS CloudFormationというサービスを我々になじみがあるプログラミング言語で使えるようにしたものという認識でいいと思います。
もっと具体的に説明するとAWSの提供するリソースである、LambdaやVPC、S3などと言ったリソースを、Pythonなどで定義し、デプロイまでできるツールキットです。

AWS CDKのワークショップをやるとなんとなく雰囲気を掴めるのでおすすめです。
[CDKワークショップ](https://cdkworkshop.com/ja/)

最初の準備などはここも参考にしています。
[AWS CDK の使用を開始する
入門ガイド](https://aws.amazon.com/jp/getting-started/guides/setup-cdk/)

# Pythonで AWS CDKを使ってみる
業務では主にPHPを使用していますが、学生時代からお世話になっているPythonで書いてみようと思います。
(PHPのCDKはないとPHP Conference Japan 2022の発表で聞きました)
[AWS CDK に魅入られた PHPer がオススメする IaC から入るインフラの話](https://fortee.jp/phpcon-2022/proposal/66d8d2f7-0c5d-4df0-9fe1-cca5501be520)

## 準備
ここにあるように npm で aws-cdkのCLIをインストールする。
https://cdkworkshop.com/ja/15-prerequisites/500-toolkit.html

CLIのインストールが済んだら、CDKを使うための準備としてbootstrapコマンドを実行する。
https://aws.amazon.com/jp/getting-started/guides/setup-cdk/module-two/

以下のコマンドで account IDを確認する
```
aws sts get-caller-identity
```

XXXXXXはaccount ID、東京リージョンで実行
```
cdk bootstrap aws://XXXXXXX/ap-northeast-1
```

これを実行することで、[CloudFormation](https://aws.amazon.com/jp/cloudformation/)に CDKToolKitというスタックが追加され、AWSのリソースを取り扱えるようになる。

Pythonで開発するため、開発ディレクトリに移動し、以下のように入力すると雛形のファイルが生成される。
```
cdk init --language python
```

ここから先はPythonで作業するので、init中のREADMEにもある通りPythonの仮想環境に入って必要なライブラリをインストールする。私は個人的に[pipenv](https://pipenv-ja.readthedocs.io/ja/translate-ja/basics.html)が好きなのでpipenvで作業した。

```
pipenv shell
pipenv install -r aws-cdk/requirements.txt
```

## AWS CDKでLambdaとAPIを定義してみる。
この内容をなぞってみる。
https://cdkworkshop.com/ja/30-python/30-hello-cdk.html

### lambdaのコードを定義する。
今回の作業ディレクトリは以下のようになっている。

```
README.md               cdk.json                source.bat
app.py                  cdk.out                 requirements-dev.txt    tests
aws_cdk_python          cdk_api_test            requirements.txt
```

ここに新しく lambdaなるディレクトリを切り、その中にlambdaのコードを書いていく。
まったく同じようにやるのは面白くないので日付を表示するLambdaを書いている。


```python:lambda/hello.py

import json
from datetime import datetime,timezone,timedelta

def handler(event, context):
    return {
        'statusCode': 200,
        'body': f'Hello, AWS CDK! {datetime.now(timezone(timedelta(hours=9)))}'
    }
```

### LambdaとApiGatewayを定義する。
ここではStackと呼ばれるAWSのリソース群(LambdaとAPIGateway)を定義している。


AWSのリソースはConstructと呼ばれるもの(Construct　LibraryにS３とかLambdaは定義されている)で、スタックはConstructが複数集まったものという認識で良さそう。
スタックがCloudFormationでの管理単位になる。

[AWS CDK の3種類の Construct を使ってデプロイしてみた](https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/)

ここでは、ApiStackに、LambdaとApiGatewayを定義している。


```python:cdk_api_test/api_stack.py
from constructs import Construct
from aws_cdk import (
    Stack,
    aws_lambda as _lambda,
    aws_apigateway as apigateway,
)
class ApiStack(Stack):
    def __init__(self, scope:Construct, id:str, **kwargs):
        super().__init__(scope, id, **kwargs)

        # APIGatewayで呼ばれるlambda
        lambda_function = _lambda.Function(
            self, 'HelloHandler',
            runtime = _lambda.Runtime.PYTHON_3_8,
            function_name = 'Hello',
            code = _lambda.Code.from_asset('lambda'),
            handler = 'hello.handler',
        )

        # apiGatewayを用意する
        api = apigateway.LambdaRestApi(
            self, 'hello-api',
            handler = lambda_function
        )
```

### デプロイの流れと便利なコマンドまとめ 
ここまで定義してきたリソースをdeployするためには、CloudFormationのテンプレートファイルに変換する必要がある。
そのため、cdk initをすると以下のようなファイルが生成される。(テンプレートファイルの合成というらしい)

ここで先ほど作ったApiStackを呼び出してあげるようにすると、cdk deployというコマンド叩くことで、
app.pyが実行され、それがAWSに反映されるという仕組みになっている。

```python:app.py
#!/usr/bin/env python3

import aws_cdk as cdk
from cdk_api_test.api_stack import ApiStack

app = cdk.App()
ApiStack(app, "cdk-api-test")
app.synth()

```

cdk synthというコマンドを実行するとテンプレートファイルの合成ができる。
実際に実行してみるとこんな感じになった。

```
Resources:
  HelloHandlerServiceRole11EF7C63:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Metadata:
      aws:cdk:path: cdk-api-test/HelloHandler/ServiceRole/Resource
  HelloHandler2E4FBA4D:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
        S3Key: 858be1ac788ecac49277e3b302cd59f1680dedd75249e49a23aa3b70b40f2e4a.zip
      Role:
        Fn::GetAtt:
          - HelloHandlerServiceRole11EF7C63
          - Arn
      FunctionName: Hello
      Handler: hello.handler
      Runtime: python3.8
    DependsOn:
      - HelloHandlerServiceRole11EF7C63
    Metadata:
      aws:cdk:path: cdk-api-test/HelloHandler/Resource
      aws:asset:path: asset.858be1ac788ecac49277e3b302cd59f1680dedd75249e49a23aa3b70b40f2e4a
      aws:asset:is-bundled: false
      aws:asset:property: Code
  helloapi4446A35B:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: hello-api
    Metadata:
      aws:cdk:path: cdk-api-test/hello-api/Resource
  helloapiDeploymentFA89AEEC67438e664a52adc5b41dc10017360ad4:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: helloapi4446A35B
      Description: Automatically created by the RestApi construct
    DependsOn:
      - helloapiproxyANY9220DBE4
      - helloapiproxy705C7382
      - helloapiANY67B044D3
      - helloapitestGET1C16631F
      - helloapitestDFB1842D
    Metadata:
      aws:cdk:path: cdk-api-test/hello-api/Deployment/Resource
  helloapiDeploymentStageprod677E2C4F:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId:
        Ref: helloapi4446A35B
      DeploymentId:
        Ref: helloapiDeploymentFA89AEEC67438e664a52adc5b41dc10017360ad4
      StageName: prod
    Metadata:
      aws:cdk:path: cdk-api-test/hello-api/DeploymentStage.prod/Resource


```

cdk deployというコマンドを実行するとAWS上に実際にリソースを展開して文字通りAPIがデプロイされる。
実装したAPIを実行すると以下のように実行した日時とメッセージが表示される。

12/2公開の記事を12/2の深夜に執筆しているとか、余計なことには気づかないで欲しい。
```
Hello, AWS CDK! 2022-12-02 02:04:51.518130+09:00
```

このほかにも、Constructを使う例とか、DynamoDBを使う例などがあるので、このワークショップにチャレンジしてみるのもいいと思う。

修正していくうちにどう変化するのかをみたくなることがあると思う、そうgitでdiffを確認するかのように。
そんなときは前回との差分を出してくれるdiffコマンドが便利だ。
```
cdk diff 
```

これは一部抜粋だが、stack名をcdk-api-test2に変えたときにどこが更新されるのかを出してくれている。
```
Stack cdk-api-test2
IAM Statement Changes
┌───┬─────────────────────────────────────────┬────────┬───────────────────────┬─────────────────────────────────────────┬──────────────────────────────────────────┐
│   │ Resource                                │ Effect │ Action                │ Principal                               │ Condition                                │
├───┼─────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────┼──────────────────────────────────────────┤
│ + │ ${HelloHandler.Arn}                     │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com        │ "ArnLike": {                             │
│   │                                         │        │                       │                                         │   "AWS:SourceArn": "arn:${AWS::Partition │
│   │                                         │        │                       │                                         │ }:execute-api:${AWS::Region}:${AWS::Acco │
│   │                                         │        │                       │                                         │ untId}:${helloapi4446A35B}/${hello-api/D │
│   │                                         │        │                       │                                         │ eploymentStage.prod}/*/*"                │
│   │                                         │        │                       │                                         │ }                                        │
│ + │ ${HelloHandler.Arn}                     │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com        │ "ArnLike": {                             │
│   │                                         │        │                       │                                         │   "AWS:SourceArn": "arn:${AWS::Partition │
│   │                                         │        │                       │                                         │ }:execute-api:${AWS::Region}:${AWS::Acco │
│   │                                         │        │                       │                                         │ untId}:${helloapi4446A35B}/test-invoke-s │
│   │                                         │        │                       │                                         │ tage/*/*"                                │
│   │                                         │        │                       │                                         │ }                                        │
│ + │ ${HelloHandler.Arn}                     │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com        │ "ArnLike": {                             │
│   │                                         │        │                       │                                         │   "AWS:SourceArn": "arn:${AWS::Partition │
│   │                                         │        │                       │                                         │ }:execute-api:${AWS::Region}:${AWS::Acco │
│   │                                         │        │                       │                                         │ untId}:${helloapi4446A35B}/${hello-api/D │
│   │                                         │        │                       │                                         │ eploymentStage.prod}/*/"                 │
│   │                                         │        │                       │                                         │ }                         
```

また開発中は、ローカルで修正しては反映して、という作業を繰り返すことになるのだが、その時の心強い見方が、cdk watchである。
watchという名の通り、変更のあったファイルを見つけ次第、cdk deployの簡易版を実行してAWSに変更を反映してくれる。

実際の動作時の様子
```

'watch' is observing the file 'app.py' for changes
'watch' is observing directory 'aws_cdk_python' for changes
'watch' is observing directory 'cdk_api_test' for changes
'watch' is observing directory 'lambda' for changes
'watch' is observing the file 'lambda/hello.py' for changes
'watch' is observing the file 'lambda/store_data.py' for changes

'watch' is observing the file 'cdk_api_test/api_caller.py' for changes
'watch' is observing the file 'cdk_api_test/api_stack.py' for changes
'watch' is observing the file 'aws_cdk_python/api_stack.py' for changes
'watch' is observing the file 'aws_cdk_python/fitbit_api_caller.py' for changes
Triggering initial 'cdk deploy'

✨  Synthesis time: 8.74s

⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments
⚠️ It should only be used for development - never use it for your production Stacks!
cdk-api-test2: building assets...


```

最後に不要になったスタックは cdk destroyで削除することができる。（AWSのリソースは使えば使うほどお金がかかるので、こまめに不要なものは削除すべし）

```
Are you sure you want to delete: cdk-api-test (y/n)? y
cdk-api-test: destroying...
2:19:19 AM | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack  | cdk-api-test
2:19:35 AM | DELETE_IN_PROGRESS   | AWS::ApiGateway::Deployment | hello-api/Deployment

```

# まとめ
AWSのリソースをコードベースで取り扱える AWS CDK に入門してみた。チュートリアルなどが豊富で概念の理解は比較的すぐにできた。何よりも画面ぽちぽちか、Terraformなどの学習コストの高いものを使わなくてもある程度のレベルのインフラを使い慣れた言語で構築できるのはありがたい。

一方で AWS CDK の問題なのか、AWSの理解が甘いからなのか、エラーが発生した時の対処には正直困った。
本当は、AWS CDKで、外部のAPIをコールする実装済みのLambdaを呼び出してきて、その結果をDynamoDBに保存してAPIの無駄なアクセスを減らすということをやりたかったのだが、LambdaからDynamoDBを呼び出す部分で権限エラーが発生してしまい、先に進めなかった。

コンソールからロールを確認するとちゃんとwrite & read 権限がついているんだけどな... 
```
ApiStoreData is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:ap-northeast-1:xxxxxxxx:table/fitbit-api-result because no identity-based policy allows the dynamodb:GetItem action"
```

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:BatchWriteItem",
                "dynamodb:ConditionCheckItem",
                "dynamodb:DeleteItem",
                "dynamodb:DescribeTable",
                "dynamodb:GetItem",
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:xxxxxxxxx:table/fitbit-api-store"
            ],
            "Effect": "Allow"
        }
    ]
}
```

というわけで、本来やりたかったところまでは行けなかったが、 CDKというものをじっくり体験できたと思う。


## 追記
Lambdaで呼んでいるDynamoDBのテーブル名とStackなどで定義したDynamoDBのテーブル名が違っていたのが問題だっただけでした。お騒がせしました。