S3 BucketへのファイルのデプロイまでAWS CDKでやりたかった話

# 前談

本記事は [エムティーアイ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mti) の 9日目の記事です。

業務ではAWSをよく触る立場ですが、AWS CDKに触れてからは「ちょっと検証したい」といったときに手作業で作成することに忌避感を覚え、CDKで環境を作成、掃除することが多くなりました。
そんな中S3を用いて検証したい、というときに「CDKの中でS3にファイルまで置いてくれるしくみがあればなー」なんて思っていたら…[ある](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-deployment-readme.html)じゃないですか、と見つけたのでちょっといじってみた記録になります。

なお注意として、「Status: Experimental」と警告されていることに注意です。
今後変更、削除も考えられるので、自分みたいな「検証環境作るの楽したい」みたいなケースでないと安易な利用は危険かもしれません。

# 今回の設計

```
    ┌[AWS]───────────────────────────┐
    │ ┌[CloudFront]┐    ┌[S3]──────┐ │
 ───┼─┤            ├────┤ [html]   │ │
    │ └────────────┘    └──────────┘ │
    └────────────────────────────────┘
```

特に変なことはせず、「S3のリソースへの直接アクセスは禁止」「CloudFrontを経由してS3上のhtmlを表示する」程度の構成。（これ要る？）

# 手順

## 準備

```shell-session
$ cdk init sample-app --language typescript
```

とりあえず `cdk init` を実行、自分はよく `typescript` で作成します。

```shell-session
$ npm i -S @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-iam @aws-cdk/aws-cloudfront
```

今回の主役は `@aws-cdk/aws-s3-deployment` 。 `@aws-cdk/aws-s3` ではないので注意。

## cdkのコード全景（抜粋）

```ts
    // Bucketの作成
    const bucket = new s3.Bucket(this, `${PREFIX}Bucket`, {
        bucketName: 'sample-bucket-name', // なんか適当に決める
        removalPolicy: cdk.RemovalPolicy.DESTROY, // 検証用なのでformation削除時に一緒に掃除したい
    });

    // S3とCloudFrontとの連携用oai
    const oai = new cloudfront.OriginAccessIdentity(this, `${PREFIX}Oai`);

    // CloudFrontからGetObjectできるようにポリシーを追加
    const myBucketPolicy = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ['s3:GetObject'],
        principals: [
            new iam.CanonicalUserPrincipal(
                oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
            ),
        ],
        resources: [`${bucket.bucketArn}/*`],
    });
    bucket.addToResourcePolicy(myBucketPolicy);

    // htmlをS3にデプロイ
    new s3deploy.BucketDeployment(this, `${PREFIX}BucketDeployment`, {
        sources: [s3deploy.Source.asset('./assets')], // npmコマンド実行階層から見てのパス
        destinationBucket: bucket,
    });

    // CloudFrontの作成
    new cloudfront.CloudFrontWebDistribution(this, `${PREFIX}CFWebDistribution`, {
        viewerCertificate: {
            aliases: [],
            props: {
                cloudFrontDefaultCertificate: true,
            },
        },
        priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
        originConfigs: [
          {
            s3OriginSource: {
                s3BucketSource: bucket,
                originAccessIdentity: oai,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                pathPattern: "*",
                defaultTtl: cdk.Duration.days(1)
              }
            ]
          },
        ],
    });
```

全景なのか抜粋なのか…（´・ω・｀）
設定は急ごしらえなので適当です。サービスとして利用するつもりは無いので最低限検証したい部分が動作すればいいかな、位のものです（というか以下の主役以外はかなり適当です）。

## 今回の主役

```ts
import * as s3deploy from '@aws-cdk/aws-s3-deployment';

:
(途中省略)
:

    // htmlをS3にデプロイ
    new s3deploy.BucketDeployment(this, `${PREFIX}BucketDeployment`, {
        sources: [s3deploy.Source.asset('./assets')], // npmコマンド実行階層から見てのパス
        destinationBucket: bucket,
    });
```

今回のメインディッシュです。内容はシンプルで、「どのソースをS3に入れるのか」「どのバケットに入れるのか」の設定くらいです。
コメントにも書いたとおり、 `s3deploy.Source.asset` の引数はnpmコマンド実行階層（= `package.json` のある階層）から見てのパスになることに気づかないと首を捻ることになります（数敗）。なおこの辺り「正しいasset指定できてないよ！」はbuildの時点で指摘してくれるのがありがたいです。
あとは `destinationKeyPrefix` オプションがありますが、要するにデプロイするファイルのプレフィックスを指定できるので、Bucket上でrootフォルダを作成してデプロイしたい、というときには使うといいかもしれません。ただ `sources` の指定しているフォルダ内はフォルダ構成ごとデプロイしてくれるので、予め `assets` 配下にフォルダ整理した状態で入れておくと使わないかもしれません。

### deploy実行前の注意

ドキュメントにちらっと書かれていますが、どうやら `sources` で指定した `Source.asset` は、直接作成したバケットに送られるわけではなく、アーカイブされて「一時的なバケット」に保存された後、cdkで作成したバケットに展開される、という流れの様です。
そうなると「一時的なバケット」が必要になるわけですが、当然このStackでは作成していません。
この状態でdeployを実行すると、以下の様なエラーが表示されて処理が止まります。(一部マスク済み)

```
 :
 ❌  StaticHtmlFormationStack failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://<aws-account-id>/<region>")
 :
```

先に `cdk bootstrap` なるコマンドを実行ししてね、と言われるので素直に従うと、 `CDKToolkit` なるFormationが実行されます。リソースを確認すると `AWS::S3::Bucket` が作成されており、どうやらこれが「一時的なバケット」となってくれる様です。
コマンド実行後、再度 `deploy` コマンドを実行すると、 今度はすんなり「こんなIAMとか作るけどいいよね？」的なことを聞かれ、承諾すればFormationの実行が開始されます。


### ちなみに

今回は使いませんでしたが、公式ドキュメント曰くローカルのディレクトリの他、zipアーカイブの指定や別バケットのコピーもできる様です。ただし（特に別バケットのコピー？）時には「そのソースの安全性はちゃんと確認しなさいね、でないとなんか悪さされて予期しないファイルの書き換えされるかもよ？」との注意書きがあります。
こうした点が「Status: Experimental」が外れない理由かもしれません。やはり検証用等にとどめ、サービスに利用するのはまだまだ危険かもしれません。

# おわり

これにより 「cdkデプロイしてからせこせこhtmlをS3に入れる」みたいな手順を踏むことなく、一発で環境の用意ができるようになりました。
果たして「Status: Experimental」が今後どう転ぶのかは不明ですが、とりあえず検証で使う程度であれば便利そうではあるので用法用量を守って使いましょう。