AWS CDK Python を使って簡単な AWS Lambda 関数をデプロイする

# はじめに

AWS AppSync を使うと GraphQL エンドポイントを作成できる。 この時、簡単な設定でデータの保存には DynamoDB を、Resolver の実装には DynamoDB Table からテンプレートに従ってデータを読み出すための簡易設定だけではなく、AWS Lambda を使った実装も可能である。
ウェブコンソールから GraphQL の Schema や Resolver の設定も可能だが、できれば何らかの方法でコードベースでのデプロイを行うようにしたい。 Amplify を使おうかと思ったが、今回は AWS CDK を使ったデプロイを学ぶ意味でも、こちらを使うことにした。

本記事では、AppSync を AWS CDK Python で構築する前準備として、AWS CDK Python のインストールと簡単な AWS Lambda 関数のデプロイまでを行う。 AppSync については別記事で記載予定。

# AWS CDK とは

[AWS Cloud Development Kit (CDK)](https://aws.amazon.com/jp/cdk/) は様々な言語でAWSリソースの定義を行い、その定義を元に CloudFormation ファイルを生成してくれるツールである。 ただし、単なるトランスパイラではなく、CloudFormation の存在を隠蔽してデプロイを実施するコマンドラインツールとしての側面も持つ。

AWS CDK で利用可能な言語には JavaScript, TypeScript, Python, Java, C#, F#, GO がある (記事執筆現在) が、AWS CDK を利用する場合にはこれらの言語だけではなく、AWS CDK そのものの実行環境として、nodejs および aws-cdk をインストールする必要がある。

CloudFormation のファイルは json や yaml であり、記述が非常に長くなるだけでなく、プログラム的な処理（例えば、繰り返し）を記載する方法が限定的であった。 しかし、CDK を利用するとプログラムによる記載が可能になるため、より柔軟な Infrastructure as Code が実現できる。 また、CloudFormation の書式を学ばなくても、より開発チームが慣れ親しんでいる言語でインフラを記載できるということも嬉しい。

# AWS CDK のインストール

## 前提条件

- Python の仮想環境管理として `pipenv` を利用する
- Node.js と npm は環境にインストールされている
- aws-cdk を npm でグローバルインストール可能なユーザーである
- AWS リソースをデプロイ可能なプロファイルを `--profile` で指定可能である (※以下の記述では profile 記載は省略し、デフォルトプロファイルを利用する)

## インストールと初期設定

以下のページに従ってインストールする。

https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_install

```bash
$ npm install -g aws-cdk
$ cdk --version
1.124.0 (build 65761fe)
```

新しいプロジェクトを作成するにはここでインストールされる `cdk init` コマンドを使う。 今回は Python で AWS CDK の記載を行いたいため、`--language=python` を記載する。

```bash
# AWS CDK Python プロジェクトの作成
$ cdk init app --language=python
# ... ここにインストール状況が記載
✅ All done!
```

コマンドが実行されると、カレントディレクトリに以下のようなコンテンツが生成される。

```bash
# インストール後のディレクトリの状況
# これは cdk_python という名前のディレクトリで cdk init を行った場合
$ ls
README.md  app.py  cdk.json  cdk_python  requirements.txt  setup.py  source.bat
```

Pythonの仮想環境管理に `pipenv` を使いたいため、`pipenv install` を行う。 AWS Lambda で利用可能な最新の Python バージョンは 3.9 (記事執筆現在) なので、ライブラリの都合などを考慮して 3.8 か 3.9 かのどちらかを選んでインストールすることをオススメする。 以下は Python 3.8 の例。

```bash
# Pipfile を作成して仮想環境を作成
$ pipenv install --python=3.8

# 利用する Python の aws-cdk をインストール
$ pipenv install -d aws-cdk.core
```

以降、`cdk` コマンドを使う場合は Python での仮想環境が読み込まれている前提になっていることがあるので `pipenv shell` で仮想環境を読み込んだ状態にするか `pipenv run cdk` としてコマンドを利用する。 この記事では後者の方法を用いる。

Python の仮想環境が構築できたら `cdk bootstrap` による初期化を行う。 
この初期化は init でプロジェクトを生成するのではなく、AWS CDK を利用するための初期化である。 というのも、デプロイ時のコンテンツがソースコード内だけで完結すれば良いが、実際のところはそれ以外のリソースをデプロイに利用する場合がある。 具体例を挙げると、今回のデプロイ対象である AWS Lambda は関数ソースコードの実体がデプロイスクリプトとは別に必要になる。 
こういったリソースを (CloudFormationで) デプロイする場合にコンテンツを配置しておく場所として、S3バケットが利用されるが、このバケットを作成する必要がある。 これを準備するコマンドが `cdk bootstrap` である。

```bash
$ pipenv run cdk bootstrap
```

実体としては、CDKToolkit という CloudFormation を実行しているだけの模様。

補足として、初期化順序的には `cdk init` する前に bootstrap を実施することもできる。 しかし、この場合に上記のコマンドを打つと `Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json'.` といったメッセージが表示され、AWSアカウント情報の指定が `aws://123456789012/ap-northeast-1` のような形式で必要になる。 しかし、一度 `cdk init` したところで実施すると `cdk.json` が存在するので、アカウント情報を入力しなくてもよくなるため、この順序で説明した。

# AWS CDK Python を用いた Lambda 関数のデプロイ

## リソースの準備

まずは AWS CDK で Lambda リソースを取り扱うため、`pipenv` で追加モジュールをインストールする。

```bash
$ pipenv install -d aws-cdk.aws-lambda
```

今回は以下の簡単な Lambda 関数をデプロイする。 `lambda` ディレクトリを作って、 `fixed_string.py` という名前で保存する。

```python:lambda/fixed_string.py
def handler(event, context):
    return {
        'statusCode': 200,
        'body': 'This is a foo!'
    }
```

その後、`cdk_python/cdk_python_stack.py` (※ `cdk init` するディレクトリ名によって `cdk_python` 部分は異なるのでそこは読み替えること) を以下の通り書き換える。

```python:cdk_python/cdk_python_stack.py
from aws_cdk import (
    core as cdk,
    aws_lambda as cdk_lambda
)


class CdkPythonStack(cdk.Stack):

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

        cdk_lambda.Function(
            self,
            'FixedStringLambda',    # リソース名
            runtime=cdk_lambda.Runtime.PYTHON_3_8,    # 利用ランタイム
            code=cdk_lambda.Code.asset('lambda'),     # ディレクトリ指定
            handler='fixed_string.handler',           # 実行handler (プログラム.関数)
            environment={},                           # 環境変数
        )
```


## AWS CDK コマンドの動作確認

以下のコマンドで現在のディレクトリコンテンツのデプロイを行う。

```bash:コンテンツのデプロイ
$ pipenv run cdk deploy
his deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                             │ Effect │ Action         │ Principal                    │ Condition │
├───┼──────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${FixedStringLambda/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴──────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                         │ Managed Policy ARN                                                             │
├───┼──────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${FixedStringLambda/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴──────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
CdkPythonStack: deploying...
[0%] start: Publishing 94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23:current
[100%] success: Published 94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23:current
CdkPythonStack: creating CloudFormation changeset...






 ✅  CdkPythonStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:************:stack/CdkPythonStack/32a31760-1c85-11ec-8e47-0e2869a7825f
```

コマンド実行の結果 CloudFormation の Stack が生成され、AWS Lambda リソースがデプロイされていることが確認できる。

![SnapCrab_NoName_2021-9-24_0-51-7_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/1cb77387-5428-f002-2465-8782a9196f71.png)


何らかの変更があれば、再度デプロイすれば良いし、これを破棄するのであれば `cdk destroy` を実行すれば良い。

```bash:コンテンツの削除
$ pipenv run cdk destroy
Are you sure you want to delete: CdkPythonStack (y/n)? y
CdkPythonStack: destroying...




 ✅  CdkPythonStack: destroyed

```

ちなみに `pipenv run cdk synth` で出力した CloudFormation 設定が以下の通り。 自動生成部分があるとはいえ、AWS CDK がこれと比べるとどれだけ簡単に記載できるかが分かる。

```yml:出力されたCloudFormation用の設定
Resources:
  FixedStringLambdaServiceRole60E6AA1A:
    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: CdkPythonStack/FixedStringLambda/ServiceRole/Resource
  FixedStringLambdaC8DBA9F7:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Ref: AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23S3Bucket0E475F3C
        S3Key:
          Fn::Join:
            - ""
            - - Fn::Select:
                  - 0
                  - Fn::Split:
                      - "||"
                      - Ref: AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23S3VersionKey4A7F6AD5
              - Fn::Select:
                  - 1
                  - Fn::Split:
                      - "||"
                      - Ref: AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23S3VersionKey4A7F6AD5
      Role:
        Fn::GetAtt:
          - FixedStringLambdaServiceRole60E6AA1A
          - Arn
      Handler: fixed_string.handler
      Runtime: python3.8
    DependsOn:
      - FixedStringLambdaServiceRole60E6AA1A
    Metadata:
      aws:cdk:path: CdkPythonStack/FixedStringLambda/Resource
      aws:asset:path: asset.94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23
      aws:asset:property: Code
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:*************************************************************************************************************************************
    Metadata:
      aws:cdk:path: CdkPythonStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Parameters:
  AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23S3Bucket0E475F3C:
    Type: String
    Description: S3 bucket for asset "94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23"
  AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23S3VersionKey4A7F6AD5:
    Type: String
    Description: S3 key for asset version "94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23"
  AssetParameters94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23ArtifactHashB4C31672:
    Type: String
    Description: Artifact hash for asset "94b43bba4a4a072af5eeefe2502b6fbce7f805ae6b21cc7303836f4716d5ab23"
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - af-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2
```

それ以外にも、利用可能なコマンドがあるが、詳しくは `cdk --help` で確認のこと。


# まとめ

AWS CDK Python を使って簡単な Python Lambda 関数をデプロイする流れを実施した。 個人的には pip で `aws-cdk.*` をインストールさえすれば利用できると思っていたため、別途 cdk コマンドをインストールする必要があることに少し戸惑ったが、そこさえクリアできれば CloudFormation を直接書くよりも非常に楽になると感じた。

今回 AWS CDK Python を使うにあたって、TypeScript 版と両方を使って比較してみたが、ほぼインタフェースは変わらなかった。 TypeScript の場合、option パラメータの指定が１オブジェクトで行われていて、Python 版では可変長引数 (**kwargs) で行われているが、言語の文化に併せた形にしている印象で、そこが違うぐらい。 そのため、特定の言語での書き方を学べば、他の言語の AWS CDK を読み書きするのは比較的容易に思える。

```typescript:lib/cdk_typescript-stack.ts
import * as cdk from '@aws-cdk/core';
import * as cdk_lambda from '@aws-cdk/aws-lambda';

export class CdkTypescriptStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new cdk_lambda.Function(
      this, 'FixedStringLambda', {
        runtime: cdk_lambda.Runtime.PYTHON_3_8,
        code: cdk_lambda.Code.fromAsset('lambda'),
        handler: 'fixed_string.handler',
        environment: {}
      }
    );
  }
}
```