📘

CloudFormationからCDKへ移行する

に公開

外部に委託していたプロダクトの内製化の際に、サーバーサイド開発を担当するエンジニアがインフラリソースの管理も担うことになりました。しかしながら、引き受けたインフラリソースは CloudFormation で管理されており、チーム内でインフラ選任のメンバーがいない開発チームにとって知見に乏しく認識コストが重いという課題がありました。
そこでプログラミング言語で管理可能な IaC である CDK への移行をしました。その手順についてまとめます。

AWS CDK とは

AWS のインフラリソース管理のための IaC です。CDK で記述したコードは cdk synth コマンドで CloudFormation テンプレートに合成され、cdk deploy コマンドで CloudFormation を通じてデプロイされます。CloudFormation をプログラミング言語によって抽象化し、少ない記述量でインフラを管理できること、なによりプログラミング経験者からしてみると CloudFormation のような yaml と比べて認識コストが低いことが魅力的です。

対応するプログラミング言語は以下。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/languages.html

今回 CDK の言語は TypeScript を選択しました。我々のプロダクトで採用している言語が Go なのでこちらでも問題はないのですが、CDK のデファクトスタンダードが TypeScript でありコミュニティも TypeScript が中心で情報量が多いこと、また毎度 CDK の情報を収集する度に都度言語を Go に読み替えるコストが高いと判断しました。

CloudFormation から CDK 移行への大まかな流れ

  1. cfn-flip による CloudFormation の整形

  2. cdk migrate の実行

  3. L1 コンストラクトを L2 コンストラクトに変換

cfn-flip の実行

cfn-flip とは

AWS の公式で提供されている CloudFormation テンプレートを json や yaml に相互変換してくれるツールです。
https://github.com/awslabs/aws-cfn-template-flip

CloudFormation テンプレートを正規化された形式に変換することで、cdk migrate コマンドの実行をより確実にします。必須ではありませんが、テンプレートに複雑な記法が含まれる場合に有用です。

利用には python と pip が必要です。環境の用意が面倒だったので今回はコンテナを用意して実行。

Dockerfile
FROM python:latest

RUN pip install --upgrade pip
RUN pip install cfn-flip

ENTRYPOINT [ "cfn-flip" ]

イメージをビルドします。

docker image build --tag cfn-pip:latest .

コンテナを用意できたら cfn-flip を実行。

docker container run --rm -v .:/workspace -w /workspace cfn-pip:latest -i yaml -o yaml <target.yaml> <new-target.yaml> -l

cdk migrate の実行

cdk migrate コマンドを実行していきます。CDK の言語は TypeScript を選択。

cdk migrate --stack-name <target-stack-name> --language typescript --from-path <target.yaml>

変換されたコードは TypeScript の型チェックで引っかかる箇所があるため、手動で修正していきます。

L1 コンストラクトを L2 コンストラクトに変換

cdk migrate によって L1 コンストラクトが生成された時点で CDK への移行自体は完了していますが、CDK の真価を発揮させるため、L1 コンストラクトから L2 コンストラクトに書き換えていきます。

L1 コンストラクト: CloudFormation リソースをそのままプログラミング言語で表したもの(Cfn というプリフィックスで始まる)。CloudFormation の仕様と 1 対 1 で対応しており、すべてのプロパティを明示的に設定する必要があります。

L2 コンストラクト: L1 コンストラクトを抽象化し、デフォルト値や便利なメソッドを提供するもの。必要な IAM ロールなどを自動的に作成・設定してくれるため、より少ないコードで安全なリソースを構築できます。

L1 コンストラクトであればリソースに必要な IAM ロールなどを都度自分で作成・定義してやる必要があります。それに対し、L2 コンストラクトであれば、必要な権限などを自動的に作成・設定してくれます。

この自動で作ってくれているという部分を忘れると知らぬうちにリソースを立てて痛い目を見ます。便利ですが自分が何を操作しているのかはしっかり認識しましょう。

cdk migrate で生成された L1 コンストラクトを、そのまま L2 コンストラクトの定義に書き換えてデプロイしようとすると、リソースが replace されてしまいます。L2 へ置き換える場合には overrideLogicalId を使って論理 ID を既存のリソースと一致させる必要があります。

例: S3 バケットの L1 を L2 に書き換える場合

// L1コンストラクト (cdk migrate 直後)
// new CfnBucket(this, 'MyBucketLogicalId', { ... });

// L2コンストラクト (書き換え後)
const bucket = new s3.Bucket(this, "MyBucket");

// このままでは論理IDが変わり replace されてしまうため、
// overrideLogicalId で元の論理ID ('MyBucketLogicalId') を指定する
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
cfnBucket.overrideLogicalId("MyBucketLogicalId");

したがって、cdk diff --verbose で差分を確認しながら replacemay be replace と表記されたリソースに対し overrideLogicalId を使って論理 ID を統一したまま L2 コンストラクトに変換していきます。

なお、全てのリソースを必ずしも L2 に置き換える必要はありません。リソースによっては安全に L1 から L2 の変換が難しいものなども存在しますので、状況に応じて対応していきます。

参考

GitHubで編集を提案
リンクチャネル

Discussion