【AWS CDK (Python)】 CDKスタックでS3にファイルをアップロードする方法

## はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
今回は、AWS CDKを実行する際にスタックの中でS3にファイルをアップロードする方法を確認しています。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。

## CDKでS3にファイルをアップロードしたい状況
主にzipにまとめたソース、ライブラリからLambda Functionを作成する際ですが、CDKスタックの実行でファイルをS3にアップロードしたくなることがあります。また、S3で静的webページをホスティングする際もCDKで直接S3にファイルをアップロードしたくなる状況かもしれませんね。
通常のリソースタイプでは、S3へのアップロードはできませんので、こういった場合にアップロードする方法を確認します。

## 環境
本記事は以下の環境を使用して記載しています。
* AWS Cloud9
* AWS CDK：2.80.0
* Python： 3.10.11
* Node.js： 16.20.0

また、以下の記事に基づいてAWS CDKの環境を作成しています。
* [【AWS CDK/Python】 AWS CDKの始め方](https://qiita.com/takataka987/items/4448dbf06d8c70173305)

## CDKスタックでS3にファイルをアップロードする方法
では、本題のS3にファイルをアップロードする方法を確認します。今回は、S3バケットを作成し、そのS3バケットにCloud9のローカルファイルをアップロードするCDKスタックを作成したいと思います。
アップロードするためには、`aws_cdk.aws_s3_deployment`を使用します。
まず、準備として、アップロードするファイルを以下のように用意します。
```console
(.venv) user_name:~/environment/cdk-app (master) $ tree ./upload-test/
./upload-test/
└── upload-dir
    └── upload-file.txt

1 directory, 1 file
(.venv) user_name:~/environment/cdk-app (master) $ cat ./upload-test/upload-dir/upload-file.txt 
test file.
```

アップロードするファイルの準備ができたので、スタックのプログラムを作成します。S3バケットの作成とそのS3バケットにアップロードするaws_s3_deploymentの処理を以下のように記述しました。
1点注意点として、file_pathに指定可能なのは、ディレクトリかzipファイルになります。zipでないファイルを直接指定はできません。ここでは、ディレクトリを指定しました。
```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3_deployment as s3deploy,
    aws_s3 as s3,
    RemovalPolicy
)
from constructs import Construct
import random, string

class CdkAppStack(Stack):

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

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-yyyymmdd",
            versioned=True,
            removal_policy=RemovalPolicy.DESTROY
        )

        file_path = "./upload-test/upload-dir/"

        s3_deploy = s3deploy.BucketDeployment(
            self,
            "FileUploadS3",
            sources=[s3deploy.Source.asset(file_path)],
            destination_bucket=s3bucket,
        )
```

この状態でデプロイします。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy

✨  Synthesis time: 18.38s

(中略)
This 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 │
├───┼───────────────────────────────────────────────────────┼────────┼───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────┼───────────┤
│ + │ ${Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC │ Allow  │ sts:AssumeRole                                        │ Service:lambda.amazonaws.com                           │           │
│   │ 9EB8756C/ServiceRole.Arn}                             │        │                                                       │                                                        │           │
├───┼───────────────────────────────────────────────────────┼────────┼───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────┼───────────┤
│ + │ ${MyS3Bucket.Arn}                                     │ Allow  │ s3:Abort*                                             │ AWS:${Custom::CDKBucketDeployment8693BB64968944B69AAFB │           │
│   │ ${MyS3Bucket.Arn}/*                                   │        │ s3:DeleteObject*                                      │ 0CC9EB8756C/ServiceRole}                               │           │
│   │                                                       │        │ s3:GetBucket*                                         │                                                        │           │
│   │                                                       │        │ s3:GetObject*                                         │                                                        │           │
│   │                                                       │        │ s3:List*                                              │                                                        │           │
│   │                                                       │        │ s3:PutObject                                          │                                                        │           │
│   │                                                       │        │ s3:PutObjectLegalHold                                 │                                                        │           │
│   │                                                       │        │ s3:PutObjectRetention                                 │                                                        │           │
│   │                                                       │        │ s3:PutObjectTagging                                   │                                                        │           │
│   │                                                       │        │ s3:PutObjectVersionTagging                            │                                                        │           │
├───┼───────────────────────────────────────────────────────┼────────┼───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────┼───────────┤
│ + │ arn:${AWS::Partition}:s3:::{"Fn::Sub":"cdk-hnb659fds- │ Allow  │ s3:GetBucket*                                         │ AWS:${Custom::CDKBucketDeployment8693BB64968944B69AAFB │           │
│   │ assets-${AWS::AccountId}-${AWS::Region}"}             │        │ s3:GetObject*                                         │ 0CC9EB8756C/ServiceRole}                               │           │
│   │ arn:${AWS::Partition}:s3:::{"Fn::Sub":"cdk-hnb659fds- │        │ s3:List*                                              │                                                        │           │
│   │ assets-${AWS::AccountId}-${AWS::Region}"}/*           │        │                                                       │                                                        │           │
└───┴───────────────────────────────────────────────────────┴────────┴───────────────────────────────────────────────────────┴────────────────────────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                                                                   │ Managed Policy ARN                                                             │
├───┼────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(中略)

Do you wish to deploy these changes (y/n)? y
CdkAppStack: deploying... [1/1]
CdkAppStack: creating CloudFormation changeset...

 ✅  CdkAppStack

✨  Deployment time: 127.91s

Stack ARN:
(中略)

✨  Total time: 146.29s
```

`cdk deploy`するとなぜか記載してないIAMロールを作成して良いか確認されます。合成されたCloudFormation Templateを確認すると解りますが、IAMロールだけでなく、Lambda Functionも作成されます。aws_s3_deploymentは、実体としてはLambda Functionのようですね。
では、S3バケットを確認してみます。
アップロードされていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/8d306a05-d17e-fb77-5588-70daed141907.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/0598aeb0-9eeb-00cf-cb84-db6cff183c3e.png)

次にzipファイルのアップロードを確認してみます。（一度`cdk destroy`してます。）
以下のようにzipファイルを準備しました。
```console
(.venv) user_name:~/environment/cdk-app (master) $ tree ./upload-test/
./upload-test/
└── upload-dir
    └── upload-file.zip

1 directory, 1 file
(.venv) user_name:~/environment/cdk-app (master) $ unzip -l ./upload-test/upload-dir/upload-file.zip 
Archive:  ./upload-test/upload-dir/upload-file.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       11  06-06-2023 12:26   upload-file.txt
---------                     -------
       11                     1 file
```

この状態でデプロイします。（コードは変更してません。＝file_pathの指定はディレクトリ）
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
```

S3バケットを確認すると、zipファイルがアップロードされていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/ffd0016a-4885-7432-28b2-6825c4e8c973.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/826740e9-f828-e6b5-cc63-9981b5f43389.png)

今度は、コードは変更してfile_pathの指定をディレクトリではなく、zipファイルを直接指定してみます。
```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3_deployment as s3deploy,
    aws_s3 as s3,
    RemovalPolicy
)
from constructs import Construct
import random, string

class CdkAppStack(Stack):

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

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-yyyymmdd",
            versioned=True,
            removal_policy=RemovalPolicy.DESTROY
        )

        # ↓↓ zipファイルを直接指定するように変更
        file_path = "./upload-test/upload-dir/upload-file.zip"

        s3_deploy = s3deploy.BucketDeployment(
            self,
            "FileUploadS3",
            sources=[s3deploy.Source.asset(file_path)],
            destination_bucket=s3bucket,
        )
```

デプロイしてS3バケットを確認します。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/ec947990-3022-8dde-5d8b-bb4346d1b820.png)

zipファイルが展開されてS3バケットにアップロードされていることが分かります。このようにzipファイルを直接指定する場合とディレクトリを指定する場合で、S3へのアップロードのされ方が異なるので、注意が必要ですね。Lambda Functionを作成する場合はディレクトリを指定するなど、用途に応じて制御する必要がありそうです。

## まとめ
AWS CDKでデプロイの際にS3にファイルをアップロードする方法を確認しました。手動操作を挟むことなく一連の流れとしてファイルアップロード→Lambda Functionの作成などが行えるのは助かりますね。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。
#### 参考文献
* [AWS Cloud Development Kit (AWS CDK) v2デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)
* [AWS CDK Python Reference](https://docs.aws.amazon.com/cdk/api/v2/python/index.html)