初めてのCDK

# 初めに

IaCとしてCDKが気になっていたので、資格学習の際にお世話になった[JP Contents Hub](https://aws-samples.github.io/jp-contents-hub/)から、
[AWS CDK Immersion Day ワークショップ](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/ja-JP)で学んでいきたいと思います。
本ハンズオンの概要については、以下のとおりです。

>本ワークショップは、AWS CDK を手を動かしながら学習できるコンテンツです。
>開発環境をセットアップし、CDK Toolkit を用いて AWS 環境へアプリケーションをデプロイする手順について体験できます。
>AWS CDK Workshop の後継ワークショップです。

以下、ワークショップを開いていることを前提に、
その項立てに従って躓いた点・気づいた点等を感想を交えながら記述していきます。

# 前提条件
## CDK bootstrap
下記コマンドで、CDKで使用されるAWSリソースがプロビジョニングされました。
その中で、気になったリソースが2つありました。
```powershell
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
```



 **1\. S3バケット** 
バケット作成に併せてSSLでのアクセス以外を拒否するバケットポリシーも作成されており、
AWSのベストプラクティスに沿ったS3バケットが作成されていました。
事前学習で、CDKはAWSリソースをよしなに作成できる(L2?L3?grantが便利?)と知り、
どのようなもの気になっていましたが、早速ベストプラクティスに沿ったS3バケットが
作成されていることに気づき、CDKへの期待が高まりました。

 **2\. SSMパラメーターストア** 
CdkBootstrapVersionが保存されていましたが、どのように使用されるか想像できない ~~ので、注視したいと思います。~~ 何に使用しているかわからなかったので、別の機会に調べたいと思います。

参考までに、このセクションで構築した環境について、記載します。(2024/07/13時点)

```powershell
>node -v
v20.15.1

>aws --version
aws-cli/2.9.23 Python/3.9.11 Windows/10 exe/AMD64 prompt/off

>cdk --version
2.149.0 (build c8e5924)

>python3 --version
Python 3.12.3
```

# Python ワークショップ
今回は、使用経験のあるPythonを選択しました。

:::note info
[CDK Community Survey - 2023](https://matthewbonig.com/posts/community-survey-2023/)によると、CDKで使用されている言語は、
Typescriptが74%で最も多く、Pythonが次点で23.2%でした。
:::

## 最初の CDK プロジェクトの作成
自分でコーディングしていないので実感が湧きませんが、
約30行のコードから約150行のCloudFormationテンプレートが
生成・デプロイされ便利だと感じました。
次項で、コーディングするようなので楽しみです。

## Hello, CDK!
### サンプルのクリーンアップ
コンストラクタを削除して、`cdk diff`を実行しただけですが、
`cdk deploy`を実行したときに何が起こるかわかりやすく記述されており、
間違いが減るとともに、作業エビデンス作成が簡単になると感じました。
 ~~コンソールのキャプチャを取得し、Excelにペタペタ貼る作業から解放されるとかなりありがたいです。~~ 

### Hello Lambda
>このワークショップでは、コピー&ペーストするのではなく、CDK のコードを入力することを強くおすすめします(入力する量はあまり多くありません)。 この方法で、CDK を使用する感覚を完全に体験できます。 自分の IDE を使用している場合は、自動補完、インラインドキュメント、型安全性などの支援を目の当たりにできます。

とのことなので、コピペせずに手打ちで入力したのですが、
添付画像のとおり、自動補完がかなり強力で驚きました。
（python3.7はリタイア済みなので、そこだけ惜しいですね。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/58738c79-a40b-155d-9260-906138f935af.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/620d9874-8909-eafd-c17f-d88fb9058f23.png)

:::note warn
ワークショップはpython3.8でしたが、2024年度末でサポート期限が切れます。
そのため今回は、python3.12に変更して実施します。
:::

`cdk diff`での確認で、明示的に定義していないLambda用のサービスロールが
作成されることがわかり、改めて便利さを実感しました。
<details open><summary>cdk diff出力</summary>

```powershell
>cdk diff
Stack CdkWorkshopStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                        │ Effect │ Action         │ Principal                    │ Condition │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                    │ Managed Policy ARN                                                             │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler/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)

Resources
[+] AWS::IAM::Role HelloHandler/ServiceRole HelloHandlerServiceRole11EF7C63
[+] AWS::Lambda::Function HelloHandler HelloHandler2E4FBA4D


✨  Number of stacks with differences: 1
```

</details>


### CDK Watch
`cdk watch`とともに、`cdk deploy --hotswap`も実施。
その際に気になることが3点ありました。

 **1\.Lambdaのバージョン管理** 
`cdk watch`(`cdk deploy --hotswap`)でのデプロイ後にコンソールを確認すると、
新しいバージョンが発行されずコードが上書きされていました。

 [`my_lambda.current_version`](https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda/Version.html)を 使用すると、CDKが自動的に
 Lambda関数のコードの変更を検出し、必要に応じて新しいバージョンを作成します。
 (エイリアスも、[エイリアスのコンストラクタ](https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda/Alias.html)を定義すれば出来ました。)
```python:cdk_workshop_stack.py
from constructs import Construct
from aws_cdk import (
    Stack,
    aws_lambda as _lambda,
)

class CdkWorkshopStack(Stack):

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

        # Defines an AWS Lambda resource
        my_lambda = _lambda.Function(
            self, 'HelloHandler',
            runtime=_lambda.Runtime.PYTHON_3_12,
            code=_lambda.Code.from_asset('lambda'),
            handler='hello.handler',
        )

        # Lambda version
        version = my_lambda.current_version
        
        #Lambda Alias
        alias = _lambda.Alias(
            self, 'HelloAlias',
            alias_name='prod',
            version=version,
        )

```

**2\.Lambda関数の更新**
CloudFormationだと、Lambda関数を更新したくてもテンプレートの中身は変わらないため、
Lambda関数を更新する際はひと手間必要だった覚えがあります。
CDKの場合は、コードを変更し`cdk deploy`するだけで、
Lambda関数を更新できたので便利だと思いました。

 **3\.CI/CD** 
 GitHubや、Codeシリーズと統合して、CI/CDパイプラインを構築したい場合、
 どうすればいいのか不明。
  ~~このままハンズオンでCI/CDについて特に何もなければ、またの機会に調査したい。~~ 
ハンズオンの一番最後にCI/CDのセクションがあり、実施しました。
 

 ### API Gateway
たった数行のLambdaRestApiコンストラクタを追加しただけで、
10個も新しいリソースが追加されました。
コンソールからAPIGatewayを設定するのは(UIも変更されたばかりで)手間なので、
CDKの強力さに驚かされました。(n回目)
また、`cdk deploy`でCloudFormationのOutputが標準出力されるため、
APIGatewayのURLをすぐに確認出来地味に助かりました。

## コンストラクトの作成
### アクセス許可の付与
事前学習で、grantが便利だということは知っていましたが、ついにgrantが登場しました。
ワークショップでは、下記の手順でしたが、補完が強力で手順を飛ばしてしまいました。
 1\.DynamoDBへの読み書きのみを付与
 2\.Lambdaの呼び出し権限不足のエラー
 3\.Lambdaの呼び出し権限も付与
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/9f7e9e09-9a52-6614-f484-61784dccf3af.png)

## コンストラクトライブラリの使用
### 追加課題
現状だと、特にソートされずにテーブルビューが表示されます。
追加課題は、テーブルビューアを設定して、「hits」で降順にテーブルをソートすることが求められています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/41087f27-619b-ccf2-bf5c-02a1e98b97b8.png)
<br></br>
VScodeのシグネチャヘルプで簡単にソート方法を確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/dbdc7f09-a271-85b9-40ad-3320f2ffb350.png)
<details><summary>念のため、インタラクティブシェルでもヘルプを確認</summary>

```python
>>> from cdk_dynamo_table_view import TableViewer
>>> help(TableViewer)
Help on class TableViewer in module cdk_dynamo_table_view:

class TableViewer(constructs.Construct)
 |  TableViewer(*args: Any, **kwargs) -> ~M
 |
 |  (experimental) Installs an endpoint in your stack that allows users to view the contents of a DynamoDB table through their browser.
 |
 |  :stability: experimental
 |
 |  Method resolution order:
 |      TableViewer
 |      constructs.Construct
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  __init__(self, parent: constructs.Construct, id: str, *, table: aws_cdk.aws_dynamodb.ITable, endpoint_type: Optional[aws_cdk.aws_apigateway.EndpointType] = None, sort_by: Optional[str] = None, title: Optional[str] = None) -> None
 |      :param parent: -
 |      :param id: -
 |      :param table: (experimental) The DynamoDB table to view. Note that all contents of this table will be visible to the public.
 |      :param endpoint_type: (experimental) The endpoint type of the `LambdaRestApi <https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.LambdaRestApi.html>`_ that will be created. Default: - EDGE
 |      :param sort_by: (experimental) Name of the column to sort by, prefix with "-" for descending order. Default: - No sort
 |      :param title: (experimental) The web page title. Default: - No title
 |
 |      :stability: experimental
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |
 |  endpoint
 |      :stability: experimental
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __jsii_declared_type__ = 'cdk-dynamo-table-viewer.TableViewer'
 |
 |  __jsii_type__ = 'cdk-dynamo-table-viewer.TableViewer'
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from constructs.Construct:
 |
 |  to_string(self) -> str
 |      Returns a string representation of this construct.
 |
 |  ----------------------------------------------------------------------
 |  Class methods inherited from constructs.Construct:
 |
 |  is_construct(x: Any) -> bool
 |      Checks if ``x`` is a construct.
 |
 |      Use this method instead of ``instanceof`` to properly detect ``Construct``
 |      instances, even when the construct library is symlinked.
 |
 |      Explanation: in JavaScript, multiple copies of the ``constructs`` library on
 |      disk are seen as independent, completely different libraries. As a
 |      consequence, the class ``Construct`` in each copy of the ``constructs`` library
 |      is seen as a different class, and an instance of one class will not test as
 |      ``instanceof`` the other class. ``npm install`` will not create installations
 |      like this, but users may manually symlink construct libraries together or
 |      use a monorepo tool: in those cases, multiple copies of the ``constructs``
 |      library can be accidentally installed, and ``instanceof`` will behave
 |      unpredictably. It is safest to avoid using ``instanceof``, and using
 |      this type-testing method instead.
 |
 |      :param x: Any object.
 |
 |      :return: true if ``x`` is an object created from a class which extends ``Construct``.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from constructs.Construct:
 |
 |  node
 |      The tree node.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from constructs.Construct:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from constructs.Construct:
 |
 |  __jsii_ifaces__ = [<class 'constructs.IConstruct'>]
 ```
</details>

ヘルプで取得した情報をもとに、TableViewerのコンストラクタを下記のように変更
```python:cdk_workshop_stack.py
        TableViewer(
            self, 'ViewHitCounter',
            title='Hello Hits',
            table=hello_with_counter.table,
            sort_by='-hits'
        )
```
無事、「hits」で降順にソートできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/9371ec90-be2e-b03f-389d-fda461e52b7f.png)

## 高度なトピック (オプション)
### コンストラクトのテスト

ハンズオンのとおりに`pytest`を実行すると、Pythonの将来のバージョン(3.14)に向けた非推奨警告が出ます。
<details><summary>warn(一部抜粋)</summary>

```powershell
>pytest
============================================================ test session starts =============================================================
platform win32 -- Python 3.12.3, pytest-6.2.5, py-1.11.0, pluggy-1.5.0
rootdir: C:...\cdk_workshop
plugins: typeguard-2.13.3
collected 3 items

tests\unit\test_cdk_workshop.py ...                                                                                                     [100%]

============================================================== warnings summary ==============================================================
.venv\Lib\site-packages\_pytest\assertion\rewrite.py:958
.venv\Lib\site-packages\_pytest\assertion\rewrite.py:958
packages\_pytest\assertion\rewrite.py:958: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
    inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])

-- Docs: https://docs.pytest.org/en/stable/warnings.html
======================================================= 3 passed, 14 warnings in 6.67s =======================================================
```
</details>

pytestのバージョンが下記のとおり少し古いので、`pip install --upgrade pytest`を実行し再度`pytest`を行います。
```text:requirements.txt
pytest==6.2.5
```

<details open><summary>再テスト結果</summary>

```powershell
>pytest
================================================================================================== test session starts ==================================================================================================
platform win32 -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0
rootdir: C:...\cdk_workshop
plugins: typeguard-2.13.3
collected 3 items

tests\unit\test_cdk_workshop.py ...                                                                                                                                                                                [100%]

=================================================================================================== 3 passed in 8.29s ===================================================================================================
```
</details>

### CDK Pipelines
:::note warn
CDK Pipelines開始前に、一度クリーンアップの項を実施することを推奨します。
CDK Pipelines内でapp.pyを書き換えるのですが、
おそらくその影響で環境の削除に苦労しました。
:::

ワークショップどおりにコマンドを実行したところ、エラーが出ました。
(あとで、ワークショップにも注意書きがあったことに気が付きました。。。)
```powershell
>git push --set-upstream origin main
error: src refspec main does not match any
error: failed to push some refs to 'https://～'
```
ローカルのGitの設定が古く、デフォルトブランチがmasterだったので、これを機にmainに変更し、既に作成済みのブランチ名を変更しました。
<details><summary>ローカルのGitのデフォルトブランチ変更</summary>

```powershell
>git branch
* master
>git config --global init.defaultBranch main
>git branch -m master main
```
</details>
<br></br>

 **CDKの感想** 

Codeシリーズは業務では全く触れたことがなく、資格勉強の際にコンソールを眺めていたことがあるのみでしたが、CI/CDも問題なくできて安心しました。
typoのせいでパイプラインの途中で失敗し、ログを読んで修正するのに苦労しました。

また、Codeシリーズの知識が足りず、
いまひとつわからないまま進めてしまっている部分がありました。
![苦労の跡](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3837488/5024c414-ed62-430d-bb16-91b23e76228e.png)

CodeBuildのBuildspecを見ると、明示的に設定していないコマンド( `cdk -a . deploy WorkshopPipelineStack --require-approval=never --verbose: ` )が設定されており、
詳しくないサービスでも強力なツールであるCDKに任せれば
うまくできてしまう部分があり便利だと感じました。

`git push`だと`cdk deploy`の時のように、デプロイの進捗状況をコマンドプロンプト上で
見ることが出来ないので少し不便でした。
CodePiplineでSNS通知を作成する方法があると思いますが、
もっと簡単に進捗を確認できたら嬉しいです。

## クリーンアップ
`cdk destory`だとCodeシリーズから作成されたスタックが削除されませんでした。
`-all`オプションを付けても削除されず、`cdk ls`から残っているスタックを確認し、
個別に削除する必要があり、躓きました。

```powershell
>cdk ls
WorkshopPipelineStack
WorkshopPipelineStack/Deploy/WebService (Deploy-WebService)

>cdk destroy WorkshopPipelineStack/Deploy/WebService
```
さらに、これでも`CdkWorkshopStack`が残り続けていたので、
`app.py`のコンストラクタの論理IDの部分を編集し、`CdkWorkshopStack`を認識させました。
そして、再度`cdk destory`を実施し削除しました。
(正しい削除方法をご存じの方がいたら教えてください。)
<details><summary>編集したapp.py</summary>

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

import aws_cdk as cdk
from cdk_workshop.cdk_workshop_stack import CdkWorkshopStack

app = cdk.App()
CdkWorkshopStack(app, "CdkWorkshopStack") 

app.synth()
```

</details>


# まとめ

AWS CDK Immersion Day ワークショップを通じて、
CDKの基本的な使用方法から発展的なトピックまでを以下の通り学びました。

1\. 強力な自動補完機能により、開発効率が向上することを実感しました。

2\. わずか数行のコードで、複雑なAWSリソースを作成できる CDK の力強さに驚きました。
　特に、API Gateway の設定の簡素さは印象的でした。

3\. grantメソッドを使用した権限付与の簡単さを体験できました。

4\. コンストラクトライブラリの使用方法を学び、
　DynamoDBのテーブルビューアの実装を通じてその便利さを実感しました。

5\. CDK Pipelines を使用した CI/CD パイプラインの構築方法を学び、
　自動デプロイの便利さを体験しました。

AWSのベストプラクティスに沿ったリソース作成や、
複雑な設定を簡単に行える点が魅力的だったので、理解を深めていきたいと思います。
また、圧倒的なシェアを誇るTypeScript版のCDKにも挑戦し、
CDKを活用していきたいと考えています。