aws-cdkをbeta版の0.20から最新版に上げた

この記事は [Opt Technologies Advent Calendar 2020](https://qiita.com/advent-calendar/2020/opt-technologies) 3 日目記事です
さっそく遅刻しましたすいません

2 日目の記事は @peko858 さんの [社内ISUCONの予習の予習としてやったこと](https://qiita.com/peko858/items/538ef6554e36f680e000) です
4 日目の記事は @U1F419 さんの [新卒研修の振り返り記事](https://tech-magazine.opt.ne.jp/entry/2020/12/01/184550) です

---

というわけで上げた。がんばった。
・・・ので、対処した点を書き残しておきます

### 環境

- 対象となったパッケージ
  - @aws-cdk/aws-elasticloadbalancingv2
  - @aws-cdk/aws-cloudfront
  - @aws-cdk/aws-ecr
  - @aws-cdk/aws-ecs
  - @aws-cdk/aws-iam
  - @aws-cdk/aws-rds
  - @aws-cdk/aws-sns
  - @aws-cdk/aws-sns-subscriptions
  - @aws-cdk/aws-sqs
  - @aws-cdk/aws-s3
  - @aws-cdk/core
  - aws-cdk
- 言語: TypeScript

### やった手順

コード量が多くなかったので力技
TypeScriptの実装が全部で700行弱程度だったので、`cdk synth` コマンドを使って吐き出したCloudFormation(以下CFn)テンプレートの内容を差分見て対処で十分やれるだろうと判断してエイッとやりました

- `npm install @aws-cdk/{...}` する
- コンパイルエラーが大量に出るので腕力で直す
- `cdk synth` でCFnテンプレートを出力し、変更前との差分を取り、問題がなさそうかどうか目で確認&調整
- 検証用環境で検証デプロイ
- 本番リリース

aws-cdkは結局の所CFnというサービス向けのjson/yamlを吐き出すだけのライブラリなので、aws-cdkのバージョンが変わってもCFnテンプレートの出力結果が多少変わるだけだと考えたらそんなに身構えなくてもよいという気持ちでした

### 対処した問題

#### クロススタックでのリソース参照の方法が変わった

CFnでStackを跨いでリソースを参照する場合、参照させたいリソースについてOutputsを定義する必要があります（参考: [チュートリアル: 別の AWS CloudFormation スタックのリソース出力を参照する](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html)）

aws-cdkではこのクロススタック参照を、Stackクラスのインスタンスのプロパティなどを介して行えます
以下の例は、FooStackに定義してるS3バケットをBarStackで参照して利用する、というコードになります

```typescript
class FooStack extends cdk.Stack {
  bucket: s3.Bucket;
  constructor(scope: cdk.Construct, id: string) {
    super(scope, id);

    this.bucket = new s3.Bucket(this, 'FooBucket', {
      bucketName: 'foo-bucket',
    });
  }
}

class BarStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, opts: { bucket: s3.Bucket }) {
    super(scope, id);

    // opts.bucketを使ってなにかやる
  }
}

const app = new cdk.App();
const fooStack = new FooStack(app, 'Foo');
new BarStack(app, 'Bar', { bucket: fooStack.bucket });
```

このとき、aws-cdkはFooStackのbucketへの参照をCFn上でOutputsを使って表現してくれます
aws-cdkのbeta版とstable版でOutputsの出力内容が変わってしまったので、単純にバージョンアップした場合に問題が起こります
aws-cdkのアップデートをする場合、Stackごとに新しいバージョンで生成したCFnテンプレートを適用していくことになりますが、stable版で作ったStackとbeta版で作ったStackで相互に互換がないために、クロススタック参照するための仕組みが動かなくなってしまいます

上記例だと、BarStack参照対象のリソースであるFooStackのbucketを読み取れなくなってしまう、ということになります
したがって、互換を保つためにbeta版で出力されるOutputsを自前でCFnテンプレートとして出力させるような実装を書いておく必要があります

```typescript
class FooStack extends cdk.Stack {
  bucket: s3.Bucket;
  constructor(scope: cdk.Construct, id: string) {
    super(scope, id);

    this.bucket = new s3.Bucket(this, 'FooBucket', {
      bucketName: 'foo-bucket',
    });

    // aws-cdkのbeta版で出力したCFnテンプレートから参照した値をベタ書きしたらこんな感じ
    new cdk.CfnOutput(this, 'FooBucketNameXXXXXXXX', {
      value: cdk.objectToCloudFormation({ Ref: 'FooBucketYYYYYYYY' }),
      exportName: 'FooStack:FooBucketNameXXXXXXXX',
    });
  }
}
```

ここに書いた例では `bucket` 変数をクロススタック参照するために `FooBucketNameXXXXXXXX` というリソースIDのOutputsを定義しています
これは、 aws-cdkのbeta版でCFnテンプレート上に出力されていたものを流用して書いたものになります
このように、beta版で出力されていたOutputsを明示的に定義しておくことで、stable版で作ったStackに対してもbeta版で作ったStackからクロススタック参照を出来るように互換性を維持できます

#### 出力されるテンプレートに変更があった

使っていたAPIが消滅したということはなく、出力するリソースの細かい内容が変更されていただけだった（ただし自分が触っていた範囲に限る）が、そこに非互換な変更がないかを一つ一つ調査しました

やったこととしては、差分が出た部分について、CFnのドキュメントを見ながら差分の前後で意味的に変更がないかを調べる、ということを地道に行いました

例えばECRリポジトリのURIをOutputsとして出力する以下のコードについて、

```typescript
    const repository = new ecr.Repository(this, 'Repository', {
      repositoryName: 'foo-repository',
    });
    new cdk.CfnOutput(this, 'RepositoryUri', {
      value: repository.repositoryUri,
    });
```

beta -> stableにアップデートすることでCFnテンプレートのOutputsの項目について以下のような差分が出る


```diff
{
  "Value": {
    "Fn::Join": [
      "",
      [
        {
          "Fn::Select": [4, { "Fn::Split": [":", { "Fn::GetAtt": ["RepositoryABCDE", "Arn"] }] }]
        },
        ".dkr.ecr.",
        {
          "Fn::Select": [3, { "Fn::Split": [":", { "Fn::GetAtt": ["RepositoryABCDE", "Arn"] }] }]
        },
-        ".amazonaws.com/",
+        ".",
+        { "Ref": "AWS::URLSuffix" },
+        "/",

        { "Ref": "RepositoryABCDE" }
      ]
    ]
  }
}
```

この場合なら、 `"Ref": "AWS::URLSuffix"` で取得される値が [このドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html) から同値であると判断出来そう、という具合です
（ドキュメントだとリージョンごとに異なるとか書いてあるので別途検証したりもありましたが）

正直インフラ規模が小さいから出来たことだとは思います

### 感想

思ったより難しくなかったので良かったという気持ちです
aws-cdkがbetaの時点で普通に動くライブラリとして仕上がってたおかげかなというのもありそうですね

といってもバージョンアップ対象となるインフラの規模が大きかったりした場合は、Stackごとに段階的にバージョンを上げる、といった工夫をしないと現実的な工数で出来ないのかもとか思います