AWS CDKへのコントリビュートことはじめ (特にintegration test)
はじめに
年末に息子が産まれ、4ヶ月の育休に突入しました。
運よく寝ている間のフリータイムがそこそこ生まれたので、大変お世話になっているOSSのAWS CDKにコントリビュートしてみようと思い立ちました。
結果として大いにハマり、現在Star Contributorまでなれています。
ただし、最初の1PRのハードルが高かったので、同様の境遇の方に向けて具体的な手順などお伝えできればと思い、記事にしてみました。
こちらの記事(大変お世話になりました)などのN番煎じ感が否めませんが、
統合テスト周りの情報があまりなかったので、そのあたりを重点的に補完できればと思っています。
基本的にはcontributiong guideを参考にしてください。
(といいつつ、これが分かりづらく辛かったのが記事作成のモチベーションです)
前提知識
L1はCloudformationから自動生成されるため、我々の方でいじることはありません。
CDKでの実装内容としてはL2上でのL1操作が基本になります。
L2の新規作成も出来ますが、RFCに則る必要があるなどハードルが高いので割愛します。 (私もやったことはありません)
手順
環境構築
開発環境を構築します。私はlocalで実施しますが、gitpodやらcode catalystでやれたりもします。お好みで。
まずはclone & install
git clone https://github.com/{your-account}/aws-cdk.git
cd aws-cdk
yarn install
その後、aws-cdk-libをビルドします。結構時間かかるので気長に待ちましょう。
npx lerna run build --scope=aws-cdk-lib
issue探索
まずは取り組む課題の明確化です。どうにか自分で対応出来そうなissueを見つけてみます。
effort/small
or effort/medium
ラベルでフィルタしつつ、自分の詳しいサービスに関するものを眺めて見るのが良いと思います。
ラベリングは割と適当なので、smallでも難しかったりmediumでも簡単だったりします。
取り組みやすいパターン
- エンジンやインスタンスのバージョン追加
enumに新バージョンを追加するだけです。一番カンタン。後述する統合テストも多分不要です。
- Cloudformation(L1)で渡せるPropertyをL2の引数に追加
L1はCloudformationから自動生成されるため全てのpropsに対応していますが、L2は都度都度手動で追加していく必要があるため、結構多くのpropsが未対応だったりします。特に、新規でL1に追加されたものが狙い目です。
追記
L2未対応の引数一覧をまとめたページを作りました。誤検知もありますがスクリーニングには使えると思います
日時で内容を更新するGitHub Actionsも動いています。ぜひ参考にしてみてください!
- deploy時にエラーとなるパラメータをsynth時にチェック
Cloudformationのdocumentに許容されるpropsの条件が明記されています。これを満たしていないとdeploy時にエラーとなりますが、synthの段階でthrow ErrorしてあげるとUXが良くなります。
こちらも統合テストを書かなくてもOKなので、取り組みやすさ◎です。
もちろん、自分で新規issueを発行してもOKです。既存issueに取り組むと「あれっ?ここの実装、修正したほうが良さそう..??」と気づくことが多々あります。そんなときはissue発行して取り組んでみましょう
特にissueへのコメントは不要です。ガンガン進めちゃいましょう。
今回は例として以下のissueを題材に進めてみたいと思います。
設計
整理
まずはissue内容を整理します。
L1コンストラクトであるCfnTopic
にはArchivePolicy
という引数がありますが、これはL2コンストラクトであるTopic
からは設定できないようです。そこで、新しくこれを設定できるような引数を追加すれば良さそうです。
これはパターン2の「Cloudformation(L1)で渡せるPropertyをL2の引数に追加」ですね。
仕様調査
Cloudformationのドキュメントには、ArchivePolicy
について以下のように記述されていました。
パラメータはJsonを受け取れるとしか書いてありません。。。不親切ですね。(あるある)
Json形式の情報が見当たらないので、マネコンでSNSトピックを作りArchivePolicyを設定してみます。すると、以下のように丁寧にJSON形式まで表示されていました。
これに則り、以下のようにパラメータを渡してあげれば良さそうです。
// L1コンストラクタでの設定例
const topic = new CfnTopic(this, 'Topic', {
archivePolicy: {
MessageRetentionPeriod: 10,
},
}
また、archivePolicy
はFIFOトピックのみで有効なようです。
これは後ほどバリデーションします。
L2仕様検討
Cfnに沿って愚直に作ると、以下のようなInterfaceを定義しTopicProps
に追加してあげれば良さそうです。
interface ArchivePolicy {
MessageRetentionPeriod: number
}
export interface TopicProps {
... // 既存の引数群
archivePolicy?: ArchivePolicy // 新しいL2の引数として追加
}
const resource = new CfnTopic(this, 'Resource', {
... // 既存のパラメータ
archivePolicy: props.archivePolicy, // CfnTopicへ渡すパラメータを追加
});
しかしこの場合、ユーザは以下のようなオブジェクトを自作してL2を呼び出す必要があり、あまり直感的とは言えません。
// これを自作する
const archivePolicy = {
messageRetentionPeriod: 10 // day単位
}
const myTopic = sns.Topic(this, 'MyTopic', {
archivePolicy,
})
そこで、よりユーザフレンドリーとなるように、messageRetentionPeriodInDays
という引数を受け取るようにしてみましょう。
export interface TopicProps {
... // 既存の引数群
messageRetentionPeriodInDays?: 10 // 新しいL2の引数として追加
}
const period = props.messageRetentionPeriodInDays
const resource = new CfnTopic(this, 'Resource', {
... // 既存のパラメータ
// CfnTopicへ渡すパラメータをL2内で作成
archivePolicy: period !== undefined ? {
messageRetentionPeriod: period,
} : undefined
});
これにより、ユーザは日数をL2へ引数として渡すだけで設定が完了します。さっきより使い勝手が良さそうですね。
実装
コードを実装していきます。上記に沿って、aws-cdk/packages/aws-cdk-lib/aws-sns/lib/topic.ts
をどんどん修正していきます。
バリデーション
ついでに、パラメータのバリデーションも追加します。
今回はmessageRetentionPeriodInDaysが以下の条件を満たすかチェックします。
- FIFOトピックに適用されているか
- 1~365の整数
// FIFOトピックに適用されているか
if (props.messageRetentionPeriodInDays && !props.fifo) {
throw new Error('`messageRetentionPeriodInDays` is only valid for FIFO SNS topics.');
}
// 1-365の整数か?
if (
props.messageRetentionPeriodInDays !== undefined
&& !Token.isUnresolved(props.messageRetentionPeriodInDays)
&& (!Number.isInteger(props.messageRetentionPeriodInDays) || props.messageRetentionPeriodInDays > 365 || props.messageRetentionPeriodInDays < 1)
) {
throw new Error('`messageRetentionPeriodInDays` must be an integer between 1 and 365');
}
Token
CDKではデプロイ後まで確定しない値(ex. ARN)をTokenという形で扱います。
引数としてstring or numberを受け取るとき、念のためTokenの可能性も考慮します。
といいつつ、Tokenに対して値のチェックは実行できないため、あくまでTokenでないことを確認すればOKです
// before
if (props.someValue > 10) { ... }
// after
import { Token } from '../../core';
if (!Token.isUnresolved(props.someValue) && props.someValue > 10) { ... }
テスト
テストには単体テストと結合テストの2種類が存在します。基本的にはどちらも作成してからPRを提出します。
単体テスト
cdk synth
により生成されるCloudformationテンプレートが望み通りのものとなっているかをチェックします。
aws-cdk/packages/aws-sns/test/topic.test.ts
に以下のようなテストを追記していきます。
test('specify message retention period in days', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app);
// WHEN
new sns.Topic(stack, 'MyTopic', {
fifo: true,
messageRetentionPeriodInDays: 10,
});
// THEN
Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', {
ArchivePolicy: {
MessageRetentionPeriod: 10,
},
FifoTopic: true,
});
});
直感的でわかりやすいですね!
同様に、バリデーションについてもテストを追加していきます。
少しバリエーションを加えて、test.eachを活用してみます。これは、配列の要素を順番にtestしていくものです。
全ての要素でちゃんとErrorがthrowされればOKです。
test.each([0, 366, 12.3, NaN])('throw error if message retention period is invalid value "%s"', (days) => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app);
// THEN
expect(() => new sns.Topic(stack, 'MyTopic', {
fifo: true,
messageRetentionPeriodInDays: days,
})).toThrow(/`messageRetentionPeriodInDays` must be an integer between 1 and 365/);
});
test実行は以下のコマンドで行えます。
yarn test topic.test.ts
しっかりテスト通過すればOKです!
結合テスト
実際にCDKテンプレートを自アカウントにデプロイできるかをチェックします。
公式の手順はこちら。
-
CDKの環境設定
自分のアカウントへCDK deployできるようにします。AWS CLIの設定及びCDK bootstrapを行えばOKです。 -
結合テストファイルの作成
packages/@aws-cdk-testing/framework-integ/test/aws-sns/test
に新規の結合テストファイルを作成します。
今回はinteg.sns-fifo-archive-policy.ts
を作成します。
import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Topic } from 'aws-cdk-lib/aws-sns';
import * as integ from '@aws-cdk/integ-tests-alpha';
// デプロイするスタックの定義
class SNSFifoArchivePolicyStack extends Stack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
// 早速、作成した引数を使ってみます
new Topic(this, 'MyTopic', {
fifo: true,
messageRetentionPeriodInDays: 12,
});
}
}
const app = new App();
const stack = new SNSFifoArchivePolicyStack(app, 'SNSFifoArchivePolicyStack');
// おまじない
new integ.IntegTest(app, 'SqsTest', {
testCases: [stack],
});
- @aws-cdk-testing/framework-integ をビルド
npx lerna run build --scope=@aws-cdk-testing/framework-integ
- 生成されたJSファイルを使って
yarn integ
を実行
TSファイルではないので注意してください。
これにより、cdk deploy → スタックデプロイ完了 → snapshotファイル生成 → スタック削除 → 完了 を自動で実行してくれます。
$ yarn integ ./packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js --update-on-failed
yarn run v1.22.21
integ-runner --language javascript test/aws-sns/test/integ.sns-fifo-archive-policy.js --update-on-failed
Verifying integration test snapshots...
NEW aws-sns/test/integ.sns-fifo-archive-policy 1.95s
Snapshot Results:
Tests: 1 failed, 1 total
Failed: /Users/kazuhoshinozuka/git/aws-cdk/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js
Running integration tests for failed tests...
Running in parallel across regions: us-east-1, us-east-2, us-west-2
Running test /Users/kazuhoshinozuka/git/aws-cdk/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js in us-east-1
SUCCESS aws-sns/test/integ.sns-fifo-archive-policy.js/DefaultTest 165.908s
NO ASSERTIONS
Test Results:
Tests: 1 passed, 1 total
✨ Done in 168.56s.
これで生成されたpackages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js.snapshot
フォルダをまるっとgit add し、ブランチに追加します。
README作成
feature実装の際はREADMEの作成が必須です。
既存のREADMEに追記し、ブランチにaddします
PR作成
一般的な流れでPRを作成すればOKです。テンプレが設定されているので、いい感じに埋めればOKです。
レビュー
以上を満たしてPRを作成すると、自動的にpr/needs-community-review
ラベルが付与されます。いつかcommunity reviewerがレビューしてくれますので、レビュー内容に適宜対応していきましょう。
community reviewerがapproveすると、今度は自動的にpr/needs-maintainer-review
ラベルが付与されるので、maintainerのレビューを待ちます。
maintainerにもapproveされれば自動的にマージされます!お疲れ様でした。
概ね1週間くらいはレビュー待ちの期間があります。
簡易な修正なら即日マージされますし、大きなものだと数ヶ月単位でかかると思います。
気長に待ちましょう。
最後に
困ったときはdistinguish contributorの@go-to-kさんがとても優しく質問に対応していただけると思います。(勝手に宣伝)
ハードルが高ければ私でも大丈夫です (@nixieminton)
CDKのissueは2000個ほど積まれていますし、issue化されていない問題は星の数ほどあると思います!
ぜひみんなで良いものにしていきましょう!
Discussion