こんにちは。マネージドサービス部、ソフトウェアエンジニア(自称)の橋本 (@hassaku_63)です。
タイトルの通りですが、CDK の L2 を作ろうとして最終的に断念しました。当時と今とでは状況が違う部分もありますが、せっかく色々と調べたしな・・・と思ったのでその供養がてら当時の経験を振り返ってみます。OSS コントリビュートチャレンジを狙っている方、あるいは OSS としての CDK に興味を持つ皆さんへのなにかしらの参考になれば。
おことわり
この記事に書かれているいる内容は、この記事の執筆時点 (2024/07/23) の約1年前、およそ 2023年7月前後の話です。
技術的な仕様や実装、Contribution guide などの観点は当時の私の記憶・記録をベースとしていますので、正確性に欠ける場合があることをご承知ください。
この記事はただの読み物です。「ふーん、そういうきっかけで OSS コントリビューションにチャレンジした人がいたのか」とか、「CDK へのコントリビューションをするのに、こういう知識が必要(になる場合もある)のか」などといった情報を与太話程度に見ていただければよいと思います。
何を作ろうとしていた?
IAM の OIDC Identity Provider の L2 Construct です。
コードや RFC を書いていたころの調査の名残りがこちらです。
馴染みのない読者もそれなりにいらっしゃると思いますので、まず OIDC Identity Provider について説明します。
OIDC Identity Provider は、外部の IdP サービスからその IdP 自身のアイデンティティを用いて AWS に連携するための仕組みを構築する際に用いるものです。最も典型的であろうと思われるユースケースは GitHub Actions です。GitHub の IdP を使った認証情報から AssumeRole して、クレデンシャルを取得して AWS 環境へのデプロイを実行する、といったケースです。 当社のブログにもこの用途で OIDC Identity Provider を使う紹介記事がありますので、具体例に興味のある方は以下の記事をご覧ください。
結論
根本的には私自身のモチベーションが保てなくなった...という話なのですが、AWS のアップデートによってやる意義が薄れた(ように感じた)のと、該当の L2 Construct 自体も CDK 本家の実装としてはやらない方針が決まったため断念しました。
後者の方針については、RFC レポジトリの以下のコメントで議論されている通りです。
https://github.com/aws/aws-cdk-rfcs/issues/512#issuecomment-1856862465
とはいえ、今でも OIDC Identity Provider の L2 はカスタムリソース実装のままですので、これを CloudFormation ネイティブなリソースタイプを使えるように置き換える改善提案はアリだと思います。本家の機能には入らないというだけなので、例えば今なら open-constructs/aws-cdk-library で公開する、ということもできると思います。
なぜこれをやろうと思った?
当時の案件で、ちょうど前述のような GitHub Actions -> AWS 連携をする用事があり、連携先となる AWS アカウントの初期セットアップのタスクの一環としてこのへんを CDK App として実装する案件をやっていました。
当時からこのリソースの実装には L1, L2 が存在していました。CloudFormation には AWS::IAM::OIDCProvider というリソースタイプが存在し、L1 はこのリソースタイプを使っています。
AWS::IAM::OIDCProvider - AWS CloudFormation
一方、L2 の方は内部実装に AWS::IAM::OIDCProvider のリソースタイプを使わず、カスタムリソースを使う実装になっていました。これは本来 CloudFormation 標準のリソースタイプだけで記述できるはずの場所で IaC (CDK) だけの都合でカスタムリソースを増やす選択のように映り、個人的には大変違和感がありました。結局案件では L2 をそのまま使いましたが、ないのなら自分で作ろうと思いました。
ちなみに、この件は CDK 公開後のかなり初期の段階で issue として指摘されています。
これは、まだ CDK が v1 のころの issue です。v1 時代の会話ログも混じっているし、参照しているプルリクも v1 時代のものが混じっていたので追いかけるのが少々大変でした。
何をしていた?
だいたい以下のような順番でした。
- Contribution guide を読む
- RFC 用のレポジトリを読む
- issue の議論をざっくり読む
- CDK の関連する実装を読む
- 自分でプロトタイプ実装を組んでみる
- RFC の議論に参加する
- AWS から本件の仕様に関連するアップデートが発表され、L2 を作るモチベーションが半減
- 挫折
ほったらかしにしていた時期もありましたが、それも含めるとトータルで 3〜5 ヶ月ほどこの件に着手していました。多分。
調べたこと・思ったこと
色々ありますが、できるだけざっくりめに説明します。
コード書く際に調べたことや、詰まったことなど、特に分類せずに雑多に記載しますので読みづらかったらすいません。
Contribution Guide と RFC を読んだ
(注意)当時と今は承認までのフローが変わっているのでご注意ください
まずは CDK レポジトリのドキュメントを見ました。 "Getting started", "Pull Requests", "Breaking Changes" あたりを重点的に読みました。
aws-cdk/CONTRIBUTING.md at main · aws/aws-cdk · GitHub
ドキュメントを見ると、L2 の挙動を変更する場合は特別な考慮事項や進め方があるとのことでした。具体的には以下のようなことが書いてありました。
- Feature Flag を用いるなどして既存ユーザーに影響させないようにしてね
- 大きな変更はいきなり CDK レポジトリへの PR として提案するのではなく、まずは RFC を提案して、合意してから進めてね
ここで CDK 専用の RFC があることを知りました。RFC は本体のコードベースとは別の、独立したレポジトリとして管理されています。
GitHub - aws/aws-cdk-rfcs: RFCs for the AWS CDK
Contribution Guide の次に、ここのレポジトリの README を読みました。だいたいどういった流れで RFC が承認され、実装として取り込まれるのかがわかります。
お作法が見えていなかったので、翻訳しながら調べた情報を Zenn scrap にまとめていました。その残骸が以下のリンクです。本記事の執筆時点と当時では中身が違うため、参考程度で。
個人的には "API Bar Raiser" というロールを設けているのが Amazon 流だな〜、と思いました。"API Bar Raiser" というのは、簡単に言えば RFC を提案する人についてくれるメンターあるいはサポーターのような役割の人です。提案する RFC を Bar Raiser が承認しないと先に進めないともありましたので、RFC の承認者としての役目も一部担っているようです。
"Bar Raiser" という単語ですが、「何らかのプロジェクト・成果物に関する水準を引き上げる人」という意味合いで Amazon 社内でも普通に使われている用語らしいです。カルチャー出てるなぁと感心しました(小並感)
そもそも、なぜ OIDC Identity Provider の L2 Construct はカスタムリソース実装だったのか?
私の推察も交えた説明になりますが、おそらく「対象のリソースタイプを L2 で抽象化するにあたって、CloudFormation で完結しない独自ロジックを動かす仕様が必要だったから」です。
※これから書く内容は 2023/07 ごろの当時の事情を前提としています。執筆時点 (2024/07) では関連仕様のアップデートがあったため、当時とは事情が異なります。
CloudFormation の AWS::IAM::OIDCProvider リソースには ThumbprintList というプロパティがあります。ここの値は CloudFormation 的には必須ではありませんが、連携相手である IdP 側が持っているサーバー証明書の有効な thumbprint を与える必要があります。少なくとも GitHub との連携であれば(CloudFormation の仕様に反して)実質的には有効値の指定が必須となります*1。
もし GitHub と連携するのであれば、以下のような記事を参照し、IdP (GitHub) 側が公開しているサーバー証明書の thumbprint を調べあげて、それを CloudFormation の ThumbprintList プロパティで指定する必要があった、ということです。
GitHub はサーバー証明書の thumbprint を公開してくれていましたが、本来であれば openssl を使うなどして IdP のサーバー証明書の thumbprint を導出する必要があります。そして、AWS のユーザーは連携先 IdP のサーバー証明書がローテした場合はこれに追随し ThumbprintList を更新する必要がありました。面倒くさいです。
ということで、CDK の L2 であるOpenIdConnectProvider クラスでは、ここの手間を省力化する実装が組み込まれていました。Url パラメータで指定された FQDN からサーバー証明書をダウンロードして、thumbprint を計算して、必要があれば証明書のチェーンをたどってルートまで同じ計算をすることで ThumbprintList に放り込むべき値を導出していました。これは CloudFormation 単体では不可能なので、独自ロジックとして実装する必要がありました。
まとめると、2023年7月当時の OIDC Identity Provider, CloudFormation の仕様および CDK の実装では、OpenIdConnectProvider クラス (L2) は ThumbprintList パラメータの計算を自動化するためのロジックを内包しており、その実現のためにカスタムリソースが必要だった、という事情です。thumbprint の計算ができなければ当然 AWS::IAM::OIDCProvider リソースも作れないので、同じカスタムリソースでついでに OIDC Identity Provider リソースも API 越しで作成してしまう実装になっていました。なお、これは 2023/07 当時の仕様ですが、2024/07/23 現在も変わらないようです。
ここからは余談になります。OIDC Identity Provider および CloudFormation リソースタイプの仕様にアップデートがあり、このあたりの事情は変化しています。2024/07 現在では、Auth0 や GitHub といった一部の IdP では AWS::IAM::OIDCProvider リソースの ThumbprintList 属性の指定が不要になりました。AWS さんの神アプデによって、相手方の証明書を取得して thumbprint を計算するまでの計算は裏で吸収してくれるようになっています。詳しくは以下の記事を参照ください。
Feature Flag について調べた
OpenIdConnectProvider クラスの内部実装をカスタムリソースから AWS::IAM::OIDCProvider に変える必要があるわけですが、そのまま愚直にやると既存の CDK ユーザーに影響が出ます。
今回の話はあくまで CDK の内部的な改善にあたるので、CDK のバージョンを最新にした結果 synth の結果が変わってしまうというのは好ましくありません。
CDK の内部実装には Feature Flag の仕組みが導入されています。これを使うことで、既存の CDK ユーザーがバージョンを更新した際に、OpenIdConnectProvider クラスが内部的にカスタムリソースを使うか、AWS::IAM::OIDCProvider を使うかを選べるようできます。
CDK 利用者としての立場では、あまり Feature Flag の存在を意識することは、それほどないでしょう。ですが、私たちは普段 context.json で Feature Flag に触れています。以下のドキュメントを見てください。
cdk init で初期化したプロジェクトの context.json ファイルに、@aws-cdk/aws-xxx:SomeFlagName のようなキーを見かけたことはありませんか?あれが Feature Flag です。CDK の実装側でこのフラグのデフォルト値を定義しており、Context で指定された場合はそれを上書きできるようになっています。未定義の場合はデフォルト値を使う仕組みが入っており、こうすることで新しい実装が Feature Flag に正しく対応していれば、旧実装を使っていたユーザーにも影響が出ないように改修を入れることができます。CDK のバージョンを上げた際に新しい機能を試したくなったら、対応する Feature Flag の設定を context.json で切り替えればよいのです。CDK のバージョンアップに追随する際には、時にこうした対応が必要になります。
このあたりの Context を絡めた Feature Flag 仕様を読み解き、内部実装としてどのように定義する必要があるのかを調べました。GitHub では以下のリンクから掘っていくとよいと思います。
それまではオレオレ流儀で Feature Flag を使っていたので、CDK のような大きなプロダクトで実際に Feature Flag が使われている様子を知れたのは大変良い勉強になりました。開発側にとっても、CDK のユーザーサイドの視点でも使いやすいものになっていて、さすがだなと思いました。
Feature Flag を使って内部的に利用するリソースタイプを切り替える実装は途中まで着手していたんですが、本家レポジトリの派生ブランチとして作業し、結局 PR を出す前に挫折してしまったのでブログで公開できる実装例が手元に残っていませんでした。とはいえ要領は把握しているので、もうちょっと具体的な実装に踏み込んだネタが補足なり別記事なりで書ければいいなと思います。気が向いたら書きます。
実装方法を練るついでに色々検証した
RFC を書いたり実装案を試したりするのに実験用コードを作りました。単独のレポジトリとして残っているものがありましたので、せっかくなので紹介します。
GitHub - hassaku63/cdk-oidc-provider-thumbprint-construct github.com
このレポジトリでは、既存の OpenIdConnectProvider クラスの実装に含まれていた (IssueUrl から) ThumbprintList を計算する処理だけを個別の Construct として切り出せないか試行錯誤したものです。ここで作った Construct を、内部的に AWS::IAM::OIDCProvider リソースを用いる OpenIdConnectProvider クラスの新実装と組み合わせれば私の目的が達成できるであろうと思い、コンセプトを実装してみました。コンセプト実装とは言いましたが、一応ちゃんと動きます。
この検証コードを実装する過程で、ついでに X509 証明書を触るコードの実例を見ることができました。
RFC を書いてみて AWS らしさを再発見した
RFC レポジトリにすでに issue はあったので、あとはレポジトリの流儀に従って RFC を書いてみました。手元に書きかけのブランチが残っていたので、サルベージして Gist に転記しました。書きかけです。
AWS CDK aws-cdk-rfcs issue#512 に関する書きかけ RFC の供養 · GitHub
テンプレートをコピーして、とりあえず書けるところを埋めて、英語で表現しづらかったらいったん日本語で書く、というスタイルで途中まで進めました。
当時はテンプレートの内容全部埋めなきゃという意識で書いていましたが、他の RFC を見てみると必ずしも全部のセクションを記載しているものばかりではないようでした。もうちょい雑に書いて、さっさと PR で提案してしまえばよかったと思います。
この RFC を書くにあたって興味深かったのは、"Working Backwards" のセクションです。Amazon の「顧客ファースト」の哲学は有名ですが、それを体現する仕組みとしてよく取り上げられる要素のひとつが "Working Backwards" です。雑に言えば、これは「それをリリースしたら、顧客はどういう(有益な)体験が得られるのか?」を説明するドキュメントです。
Amazon の社内カルチャーを表すエピソードとして "Working Backwards" や "Bar Raiser" といった言葉を見聞きすることはありましたが、CDK のコントリビューションにまでこうした用語が出てくるのを目の当たりにして「こんなところまでカルチャーが浸透してるんやな・・・」と驚きました。いくら CDK といえど AWS からすれば山ほどあるプロダクトの1つに過ぎないわけで。こんなところにまで Amazon 流の価値観を体現した言葉が使われているというのは、実は相当にすごいことなんじゃないでしょうか。自社開発のレポジトリで会社の価値観を体現した言葉を自然に使えるか?と問われて、自信持って「できてる」と言える人は少ないのではないでしょうか。少なくとも私は自社に関して同じことは言えないです。
調べ物ついでにドキュメントの修正提案をした
Contribution Guide を読んでいたらリンク切れした箇所を見つけました。v1 時代に書かれていたドキュメントが、v2 移行の際にメンテされずそのままになっていたようです。本命の L2 実装はまだ時間が掛かりそうでしたし、せっかく見つけたならということでサクッと PR を出して、サクッとマージされました。
この PR を作ったのは私の元々の予定にまったくなかったものでしたが、個人的には普段利用する OSS に対する関わり方を再考するきっかけになりました。どういうことかと言うと、実務で発生した OSS ツールの不都合を、google 検索だけで終わらせずに issue 検索して調べてみたり、ドキュメントだけではなくソースコードを読んで調べてみたり、もっとソースコードを身近なものとして扱ってみたらいいのでは?という話です*2。
私はいちエンジニアとして長らく OSS へのコントリビューションに対する憧れ感情を持っていましたが、具体的に実績が出る気配のないまま何年も過ごしてきてしまいました。でも、実務で困ったことを解消するためにレポジトリのソースコードや issue を眺めていたら、すぐに自分でも改善提案できそうなネタが見つかってしまいました。この PR を出す体験を通して「自分が使うツールのソースコードをもっと普段から見てたら、もっと早くコントリビューションの機会に巡り会えていたのではないか。また、その方がいち利用者としても、エンジニアとしても健康的なのでは?」と考えるようになりました。
それまでの私は「OSS へのコントリビュートってなんかかっこいいよね」とか「どうせならコード書いて貢献した実績を作ってみたいな」のような漠然とした意識がありました。OSS へのコントリビュートをしてみたい、という欲が先行していたと思います。そのへんの考え方を改めることになりました*3。今は CDK に限らず、利用機会があるものについてはできるだけソースコードを読むようにしています。最近だと X-Ray SDK for Python のドキュメントを直す PR を出しました。
OIDC Identity Provider のリソースタイプの仕様を深堀りした
L2 を実装するなら、L1 (=CloudFormation) をより使いやすくした形で抽象化したものを用意してあげる必要があります。まあ当然ですね。
使い方あるいはパラメータにバリエーションがあるサービスの場合はそれらを潰さないようにうまいことラップしてあげる必要があります。それを考える過程である程度 CloudFormation や該当のリソースタイプの詳細を調べることになりました。そもそもパラメータの意味がよくわからなかったので、それを調べるところから始める必要がありました。その過程で、サーバー証明書そのものの知識をおさらいしましたし、証明書から thumbprint を計算する方法を知ることができました。ついでに、Node.js でこのへんの領域を扱うライブラリである Crypto や tls の使い方を部分的に学ぶことができました。
L1 リソースの抽象化ということに関しては、OIDC Identity Provider に関しては割と簡単な方だと思います。ですが、それでも自分が実務で遭遇した以外のユースケースを追いかける必要はあったので、そのへんの調べ物でモチベーションを保つのが大変でした。
ちなみに、私が知る限りで CDK L2 Construct としての抽象化が極めて難しい部類のサービス(のひとつ)は Kinesis Firehose だと思います。配送先として複数種類のサービスをサポートしており、その種類ごとで記述できる Config の仕様が全く違います。それぞれの種類ごとのパラメータバリエーションも割とあるので、うまいこと L2 に落としこむのはかなり難しいと思います。ちなみに、2024/07 現在も Stable バージョンとしての公式の Firehose L2 Construct は出ていません*4。
aws-cdk-lib.aws_kinesisfirehose module · AWS CDK
やれなかったこと
PR 出してレビューを受ける段階に至るまでに対応が必要だったが、やりきれなかったものを紹介します。
- integ, snapshot テストの実装
- RFC を通す
テストコードの書き方は、例えばディレクトリのどこにどういうテストを書く必要があるのか、それはどうすれば実行できるのか?・・・といったようなことです。このあたりを調べきる前に挫折してしまいました。
当時は挫折しましたが、既存のソースコードからいくらでもヒントは拾えるので、それを見ていけばなんとかなると思います。例えば Construct の単体テストであれば、モジュールごとにテストコードの置き場は簡単に推測できます。例えば Lambda Function であれば packages/aws-cdk-lib/aws-lambda/test/ あたりを見に行って、そのコードを読むなり、git blame でその実装が入ったコミットを追いかけてみたりすれば良いです。
他には integ レベルのテストとして実際に synth の結果を吐き出すようなテストも存在します。Lambda であれば packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/ 以下のディレクトリにあるディレクトリ・ファイル群がそうで、ここのファイル郡を作成する方法は通常の cdk synth でいいのか、それともビルドコマンド的な手段が提供されているのか、そういった部分はまだ明らかにできないまま挫折してしまいました。ここは未だによくわかっていないです。
他の人の feat 実装を見てみるというのもアリです。勝手にご紹介しちゃうのですが、最近 CDK への PR にデビューした日本人の方がいらっしゃいます。
実装内容諸々めちゃくちゃ不安ですが、CDKへの初プルリク提出できました!https://t.co/dpA4UrXhhs
— 山梨 蓮 (@yama_ren_tw) 2024年7月22日
コントリビュートワークショップのおかげです・・・@nixieminton @365_step_tech @WinterYukky ありがとうございました!!
ちょうど上述したようなテストコードにも対応されているので、CDK L2 へのコントリビューション方法を学ぶ教材としてうってつけだと思います。
さいごに
最終的に目に見える成果には結びつきませんでしたが、色々調べたりコードを書いたりする経験を通して色々と得るものがありました。その多くは自分が事前に予想すらしていなかったことでした。やってみてよかったと思います。
OSS はもっと身近なものだと思えるようになりました。また、開発業務を本業のひとつとする人間として、OSS の存在を自ら遠ざけるような考え方をしていたのではないか?と思い直すきっかけを得ました。このへんの意識の変化が自分の中では一番大きな収穫だったように思います。せっかくなら最後までやり通したうえでこのような台詞を述べたかったのですが、まあ仕方ないですね。
ここで調べた Feature Flag の話あたりを抽出して AWS CDK Conference Japan 2024 の登壇ネタに持っていく予定だったんですが、本業が立て込んでいたり日程を放置しすぎてしまったりで見送ってしまいました。どこかしらで供養したいなと、思ってはいます。
*1:2024/07 現在はアップデートにより仕様が変わりましたので、事情が異なります。そのあたりは本文中で後述しています
*2:普段からこうした行動をやっているソフトウェアエンジニアの方からすれば「今さらそんな当たり前なことを?」と思われるかもしれませんね。。
*3:私個人としては、最終的に公衆の利益に繋がるのなら入口のモチベはなんでも良いと思っています
*4:α版では @aws-cdk/aws-kinesisfirehose-alpha と @aws-cdk/aws-kinesisfirehose-destinations-alpha というモジュールの開発が行われているようです。