maybe daily dev notes

私の開発日誌

CDK Tips: ID=Defaultの使い方

AWS CDK Tipsシリーズです。

昨日のCDKカンファレンスで触れた ID=Default の話に思わぬ反響があった (嬉しい!) ので、詳しくまとめます。

ID=Default とは

この記事では、AWS CDKのコンストラクトの第2引数のID (Identifier) に Default という文字を指定することを指します。

// この第2引数に注目
new Bucket(this, "Default");

ID=Default にしたコンストラクトは、CloudFormationの論理IDを計算する際に特殊な処理が適用されます。以下で挙動を見てみましょう。

挙動

CDKのコンストラクト群はコンストラクトツリーというデータ構造で管理され、論理IDはそのコンストラクトのパスから計算されます。 パスとは、合成されたテンプレートのMetadataにも書かれている、コンストラクトツリー上にコンストラクトがある場所を示すものです。

   "Metadata": {
    "aws:cdk:path": "Stack/Logger/Bucket/Resource"
   }

ID=Default のコンスラクトは、論理IDを計算する際に、パスから除外されます。 このため、以下の挙動が観測できます。

ネストしないコンストラクトと、 ID=Default でネストしたコンストラクトとの論理IDが同じになる

次のCDKコード

{
  const stack = new Stack(app, "Stack");
  new CfnBucket(stack, "Bucket");
}

{
  const stack = new Stack(app, "StackNest");
  const parent = new Construct(stack, "Bucket");
  new CfnBucket(parent, "Default");
}

はそれぞれ以下のテンプレートに合成されます:

 "Resources": {
  "Bucket": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "Stack/Bucket"
   }
  },
 "Resources": {
  "Bucket": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackNest/Bucket/Default"
   }
  },

StackNest のコードでは、CfnBucket があるコンストラクトの中にネストされていることに注目してください。 2つのスタックでコンストラクトツリーの構造は異なりますが、CloudFormation テンプレート内の論理IDは2つのスタックで同一 (Bucket) になります。

このように、ID=Default にすることで論理IDの計算方法が変わることがポイントです。

ちなみに、CDKで生成される論理IDは通常 BucketDC7D6F65 のようにランダムなハッシュ値が末尾に付きます。 一方上のテンプレートでそれが付いてない (Bucket) のは、スタック直下のコンストラクトのみハッシュ値を付けないためのようです。 これは ID=Default の話とは無関係な挙動ですが、念の為補足しておきます。(これはこれで興味深い例外処理ですね!)

Defaultを複数回ネストさせたとき

ID=Default のコンストラクトを複数ネストさせることもできます。次のコード

  const stack = new Stack(app, "StackMoreNest");
  const parent = new Construct(stack, "Bucket");
  const child = new Construct(parent, "Default");
  new CfnBucket(child, "Default");

は、以下のテンプレートに合成されます。

 "Resources": {
  "Bucket": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackMoreNest/Bucket/Default/Default"
   }
  },

Default はすべて切り詰められることがわかります。

Defaultにより論理IDが重複してしまうとき

また、Default が切り詰められた結果、意図せず論理IDが重複し、エラーになることもありえます。

  const stack = new Stack(app, "StackNest");
  const con2 = new Construct(stack, "C2");
  new CfnBucket(con2, "Bucket");  // C2/Bucket
  const child = new Construct(con2, "Default");
  new CfnBucket(child, "Bucket");  // C2/Default/Bucket

// Error: section 'Resources' already contains 'C2BucketAB4DC557'

これは2つ目の CfnBucket のパスが実質的に C2/Bucket と等しくみなされるためです。 コンストラクト外に影響を及ぼすものではないためこの挙動で問題が起きる場合は少ないと思われますが、注意すべき挙動でしょう。

node.defaultChild

また、ID=Default のリソースは、 node.defaultChild メソッドで取得可能です。

  const stack = new Stack(app, "StackNest");
  const parent = new Construct(stack, "Bucket");
  new CfnBucket(parent, "Default");
  console.log(parent.node.defaultChild);

// <ref *1> CfnBucket {
// ...

Escape hatchを使う時に若干便利かもしれません。

なお他のIDと同じく、一つのscopeには一つの ID=Default のみが存在できます。

実装を見てみる

挙動は分かったので、どうしてそうなるのか実装を見ていきます。

まずはリソースの論理IDを計算するコードを見てみましょう:

  protected allocateLogicalId(cfnElement: CfnElement): string {
    const scopes = cfnElement.node.scopes;
    const stackIndex = scopes.indexOf(cfnElement.stack);
    const pathComponents = scopes.slice(stackIndex + 1).map(x => x.node.id);
    return makeUniqueId(pathComponents);
  }

変数名で大体分かりますが、あるリソースのスタック以下のパスを配列にし、 makeUniqueId 関数を呼んでいます。 makeUniqueId 関数は uniqueid.ts に定義されています。

/**
 * Resources with this ID are complete hidden from the 論理ID calculation.
 */
const HIDDEN_ID = 'Default';

// 中略

export function makeUniqueId(components: string[]) {
  components = components.filter(x => x !== HIDDEN_ID);

この関数ではあるコンストラクトのパス (を / で分割した配列) からUnique IDを計算します。 関数の1行目で、計算の対象となる配列 components から ID=Default が除外されていることが分かります。

その後、 components からハッシュ値を計算し、またハッシュの前の文字列も計算します。 いずれも先程フィルター処理された components から計算されるので、 ID=Default はこの関数全体を通じて存在しないものとしてみなされています。

  const hash = pathHash(components);
  const human = removeDupes(components)
    .filter(x => x !== HIDDEN_FROM_HUMAN_ID)
    .map(removeNonAlphanumeric)
    .join('')
    .slice(0, MAX_HUMAN_LEN);

今回の挙動を理解するためにはこの程度で十分でしょう。コード内のコメントを読むと他の豆知識も拾えるので、興味のある方は深掘りしてみてください。 なぜ末尾にハッシュ値が必要なのか、論理IDが上限長を超える場合はどうするのか、などを理解できます。

ちなみに少し上に書いた、スタック直下のリソースではハッシュ値が末尾に付加されない例外処理も、ここで行われていました。

これらはundocumentedな実装の詳細ですが、おそらく今後も変更が入ることはないでしょう。 論理IDの計算ロジックを変更すると、既存のCDKアプリに強烈な影響を与える破壊的変更になるためです。 このため、以下のようなIssueも見つかります。CDKの産みの親自身、ロジックの一部に後悔があるようですね。

github.com

findDefaultChild メソッドは、 constructs ライブラリの方に実装されています。単純に tryFindChild を呼んでいるだけです。

    const defaultChild = this.tryFindChild('Default');

ここには ID=Resource に関する処理も書かれていますが、こちらは後ほど触れます。

活用方法

ID=Default を活用する方法をいくつか紹介します。 と言っても、こちらの記事でも紹介したとおりの内容です。他になにか思いついた方は教えてください。

tmokmss.hatenablog.com

1. CloudFormationの自動命名を見やすくする

ネストが深いリソースについても論理IDを短縮できる可能性があります。 論理IDの長さは通常気にする必要がないですが、CloudFormationがリソース (S3やIAM Roleなど) の自動命名をする際に利用されることに注意してください。 論理IDを短くすることで、こうした自動命名をわかりやすくすることができます。

2. コンストラクトツリーをリファクタする

あるコンストラクトを他のコンストラクトの子に入れる場合、通常は対応するリソースの論理IDが変化するため、置き換えが発生します。

しかしながら、以下2つのいずれかの方法を採れば、論理IDを保持できます。

  1. 親コンストラクトのIDを Default にし、子コンストラクトのIDをそのままにする
  2. 親コンストラクトのIDをそのままに、子コンストラクトのIDを Default にする

1の方法は複数のコンストラクトを移動することができます。しかしそのスコープですでに Default が使われている場合は、使えません。 2は単一のコンストラクトを移動できます。通常はこちらのほうが使いやすいと思われます。

  // これをリファクタしたい
  const stack = new Stack(app, "StackRefactor");
  new CfnBucket(stack, "Bucket1");
  new CfnBucket(stack, "Bucket2");
  
  // 方法1
  const parent = new Construct(stack, "Default");
  new CfnBucket(parent, "Bucket1");
  new CfnBucket(parent, "Bucket2");

  // 方法2
  const parent = new Construct(stack, "Bucket1");
  new CfnBucket(parent, "Default");

これらにより、リソースの置き換えを防ぎながら、コンストラクトの構造をリファクタすることができます。

ちなみに、論理IDを保持するもう一つの方法として、 overrideLogicalId メソッドがあります。

  const parent = new Construct(stack, "AnyIdParent");
  const child = new Construct(parent, "AnyIdChild");
  const bucket = new CfnBucket(child, "Bucket1");
  bucket.overrideLogicalId("Bucket1");

を合成後のテンプレート:

 "Resources": {
  "Bucket1": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackRefactor/AnyIdParent/AnyIdChild/Bucket1"
   }
  },

こちらは自動生成の論理IDを無視して任意文字列でオーバーライドする、非常に強力な機能です。 コード内にある種の負債として残り続けてはしまいますが、他にどうしようもないときはこちらも利用できるでしょう。

さらにちなみに、CDKのRFCで新たなリファクタ向け機能が提案されています。 overrideLogicalId メソッドよりも柔軟に論理IDをオーバーライドするAPIが含まれるようです。こちらもいつか実現されることを期待したいですね!

github.com

豆知識: ID=Resource との違い

実は上のコードにも登場したのですが、 IDを Resource に設定した場合も、似た挙動を示すようになっています。

このコード

  const stack = new Stack(app, "StackResource");
  const parent = new Construct(stack, "Bucket");
  new CfnBucket(parent, "Resource");

は、以下のテンプレートに合成されます:

 "Resources": {
  "Bucket83908E77": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackResource/Bucket/Resource"
   }
  },

論理IDから Resource という文字列が消えていることに注目してください。 これにより、Default と同じく論理IDの短縮に利用できます。

Default との違いは、ハッシュの計算時に無視されないことです。このため Default とは異なり、意図せぬ論理IDの競合は発生しません。 パスが違えば論理IDも異なるという、分かりやすいモデルが維持されます。

例えば、以下の2つの CfnBucket はそれぞれ別のリソースとして見なされます。Default ではエラーになっていたことに注意してください。

  const stack = new Stack(app, "StackResource");
  const con2 = new Construct(stack, "C2");
  new CfnBucket(con2, "Bucket");  // C2/Bucket

  // このIDが "Default" だとエラーになっていた
  const child = new Construct(con2, "Resource");
  new CfnBucket(child, "Bucket");  // C2/Resource/Bucket
 "Resources": {
  "C2BucketAB4DC557": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackResource/C2/Bucket"
   }
  },
  "C2BucketC6A4BB8A": {
   "Type": "AWS::S3::Bucket",
   "Metadata": {
    "aws:cdk:path": "StackResource/C2/Resource/Bucket"
   }
  },

一方この挙動のために、Default とは異なりリファクタの用途では使えません。あくまでも論理IDの短縮が使い道になるでしょう。

多くのL2コンストラクトでも、そのコンストラクトのうちメインのリソースのIDが Resource に設定されています (例: Bucket に対する CfnBucket)。 いたずらに論理IDを長くするのを防ぐ配慮だと思われ、コンストラクトライブラリの開発者は意識すると良いでしょう。

また上でも触れた通り、IDが Default or Resource のコンストラクトは node.defaultChild で取得可能です。 同じスコープ内に DefaultResource のコンストラクトをそれぞれ配置して合成することは可能ですが、 defaultChild メソッドを呼ぶとエラーが発生するので、避けるのが無難でしょう。

  const stack = new Stack(app, "StackNest");
  const parent = new Construct(stack, "Bucket");
  new CfnBucket(parent, "Default");
  new CfnBucket(parent, "Resource");
  // ここまでなら合成できる

  // defaultChildを呼ぶとエラー
  parent.node.defaultChild;
  // Error: Cannot determine default child for StackNest/Bucket. There is both a child with id "Resource" and id "Default"

まとめ

コンストラクトのIDを Default にしたときの挙動やその活用方法をまとめました。 知っているとなにか役立つときがあるかも。 活用方法が他にあれば・見つけたら教えてください!

AWS Summit Tokyo 2023に登壇しました

2023/4/20-21に開催されたAWS Summit Tokyo 2023に、AWS社員として登壇してきました! 大規模イベントでのオフライン登壇は初めてだったので、完走した感想をまとめます。

きっかけ

もともとAWS Summitを知ったのは前職で2020年開催の時でした。当時のAWS営業チームから登壇を促され渾身のCfPを提出するも不採用となり、高い壁を感じた記憶です。その年はコロナで結局オンライン開催にフォールバックした回ですね、懐かしい。

その後AWSに入社し、今度は社員側で登壇できるようになりました。しかしCfP採択のバーは明らかに高く、2021・2022はそれに見合うネタもないので応募せず終い。 2023で色々と現職での知見が積もり、伝えたいことがクリアになってきたので、応募した次第です。

実際のところ周りの同僚がほとんど応募してたので、ビッグウェーブに乗る勢いでCfPを提出したというところもあるのですが。3年ぶりのオフライン開催ですしね!

社内での選考プロセスの詳細は不明なので想像ですが、割と多めの候補から選ばれるので、一定の運要素もあると思います (自分の伝えたいことが運営側の意向とマッチするかという意味で)。そういう意味では2021・2022もとりあえず応募しておけばよかったかも?とにかくラッキーでした。

準備

1月末ごろに採択が決まったので、準備期間は3ヶ月です。色々と準備することが多く、長いようで短い四半期でした。 内容のレビューやリハーサルは社内でかなり協力いただいて非常に感謝しています。

また、この機会に登壇に関連するノウハウ (話の構成、立ち居振る舞い、話し方、etc...) も色々と学びました。この辺りは社内によくできた教材があるのでそれらを見つつ、YouTubeだと以下もとても良かったです (タイトルがシンプル!)

youtu.be

  • 繰り返し主題に立ち返る
  • 印象的なイメージ・シンボルがあると良い
  • 小道具の重要さ (記憶に刻みこまれる)
  • つかみはオーディエンスにもたらす変化を約束する。ジョークはやめたほうが良い
  • 指し棒・レーザーポインタは使うな

などなど、様々な知見が説得力を持って紹介されています (一部、好みが分かれる部分はあると思います)。

あとは他の登壇動画を見て参考にするなど。例えば以下の発表は全体的に感じが良く、特に締めの部分 (ケーキ作りで失敗するくだり) はかなり真似しましたw

youtu.be

また今回のステージは発表時間の超過に厳しいので、時間管理が重要になります。そのためには練習が不可欠なのですが、40分通しで練習するのは割と大変です。 このため、セクションごとに時間を配分 (下図) し、それぞれのセクションごとに所定の時間で話す練習をするのが効果的でした。分割統治!

これまで数回の発表経験から、少なくとも準備の進め方については良さそうな方法論がまとまってきたので、そちらもいずれ記事にしたいですね!

当日

当日は気合を入れて3時間前に会場入りしました。発表会場を事前に客席から見ておくことで、本番の緊張感が抑制されるらしいのです。

しかし着いてみると存外やることもなく、孤独に会場をさまようことになりました。心細さとロンリネスで意気消沈していましたが、その後偶然3人の同僚と遭遇・会話ができて気持ちがかなりアガりました (あのときは本当に感謝!!)。 さすがに社内に顔見知りが少なすぎることに気づけた (リモートワークの罪!) ので、今年の残りは友達作りを目標にしたいと思います。

TGS 2018ぶりの幕張

発表は意外と緊張せずに (足がガクガクに震えていたのは除く) 進行でき、練習の偉大さを感じました。 お客さんも500席が埋まるくらい着席されており、とても嬉しかったです。

とはいえ、言いたかった小話をいくつか飛ばしてしまうミスもありました。私は基本的に原稿は書かずにスライド上の文字から話す言葉を再構成する方法が好きなのですが、これだとやはりスライドに書いてないことは忘れがちですね。

同僚の ymhiroki が撮ってくれた写真。締めのウィニングランの様子です。

実際どういう話をしたのかは、後日動画と資料を公開予定なので、そちらに譲ります! // TODO: リンクを貼る

ちなみに今回はサイレントセッションという、お客さんとの距離が遠く、イヤホン越しに声が届けられる方式でした。 このためお客さんと高度なインタラクションをとることは難しいという通説があるのですが、発表慣れしていない自分にとっては、取りうる行動の選択肢が減るので逆に良かったと思います。(とはいえ他の発表を聞いていると、中にはお客さんに挙手してもらう高等テクをやってのけた猛者もいました、すごい。)

発表後の質疑も盛り上がり、全体としてかなりスピーカー体験が良かったです。集客や準備、当日オペレーションされた運営メンバーにも大感謝です!

その後

その後前職の同期と食事に行ったり、同僚・元同僚と飲みに行けたりしました。リアル開催イベントさまさまです。

またクラスメソッドさんのブログに2件もレポートが掲載されていました (エゴサの鬼)!

dev.classmethod.jp dev.classmethod.jp

各記事末尾の感想を拝読すると、どちらも伝えたい内容をまさに汲み取っていただいており、安心できました。ありがとうございます! 他にもこの発表に言及している記事やツイートをいくつも拝見しました、嬉しい!

一方登壇の動画を妻に見せたところ、次の辛口評価をもらいました:

  • 思想的な話が多い。哲学の講義か?
  • たまに息遣いがトイレで気張っている人みたいになっている

未だに自分の動画を見ることに慣れず、とはいえ公開前の確認が必要なので代わりに見てもらったのですが、最も遠慮のないであろう意見が聞けて面白いです。 やや慢心気味になっていましたが、良い具合に引きずり下ろしてくれました、感謝。

まとめ

数百人規模相手のオフライン登壇は初めてだったので、とても良い経験になりました。 同時に、発表を聞いていただいた方の行動や気持ちが少しでも良い方向に変わると嬉しいです。 私も「伝える」力を改善するため精進していきたいと思います。

もなちゃん(文鳥)が初めて卵を産みました

先日桜文鳥のもなちゃんが初めて無事に卵 (無精) を産みました 🎉 顛末をまとめるので、メス文鳥を飼っている方は参考にしていただければ幸いです!

産卵日の前から時系列順に様子などをまとめます。

〜2ヶ月前

この時期は卵を安全に産める場所を探すようです。 もなちゃんも放鳥するたびに巣ごもりの場所を探していました。

お気に入りは洗面台の三面鏡の中で、産卵の1ヶ月前頃からほぼ常時籠もるようになり、人が手を出すとキュルキュル怒っていました。

三面鏡の中で怒るもなちゃん

ただ、洗面台の中だとさすがに人の生活と干渉してお互いに不便なので、心を鬼にして出禁にしました。放鳥中は、常に三面鏡の扉を閉鎖したのです。 もなちゃんも最初は開けろ〜と騒いでましたが、我慢して3日間くらい開けないでいると諦めてくれました。聞き分けが良い。

結局最終的にはカゴの中のツボ巣に落ち着いてくれました (たしか産卵3日前くらい?)

ツボ巣でチルのもなちゃん

今思うとツボ巣に落ち着いてから産むまでが妙に早かったので、落ち着ける場所を見つけたら産み始めるというフローなのかもしれません。 そういう意味では三面鏡にいるときは産む様子がなかったので、取り上げて正解だったのかも。

産卵・抱卵時は、文字通り四六時中そこに文鳥がとどまることになります。 人の生活と干渉する場所はお互いにストレスになるので、良い具合の場所を用意してあげる必要があると思いました。 画像のような抱卵専用の巣箱も売ってますが、抱卵中に文鳥の様子が見えたほうが安心なので、個人的にツボ巣はオススメです。

こういう巣箱も良いが、ツボ巣は中が見えて安心

ツボ巣は配置時に傾けてしまうと文鳥が落ち着かないようなので、地面と水平に置くように注意してください。(多分モナちゃんが最初ツボ巣に入らなかったのもそのせい。) 傾いていると卵が転がってしまうからだと思います。

一日前

産卵一日前(このときはまだ卵を産むと知らなかったのですが) は、水を飲む量・回数が顕著に増えました。 それに伴って、フンも水分の非常に多い状態で出てくるようになります。平常時とは明らかに異なるので、不安になります。

これは下痢ではなく、多尿という症状らしいです。下痢はフンの固形成分がない状態、多尿は固形成分はあるが水分が多い状態。

www.torinobyouki.com

下痢だと病気の可能性が高いらしいですが、多尿は直ちに問題があるわけではないということなので、この日は様子見にしました。 産卵の兆候だとする情報もあったため、そろそろ産むのかな?と妻と話したりもしてました。

こういうときは体重を測れば (卵が体内にあるなら) 一発で分かると思います。 しかしもなちゃんは日頃体重を測る習慣がないためうまく測れませんでした (秤に載せてもすぐに怖がって逃げる)。 教訓としては、ひな鳥のときから怖がらないよう体重測定を習慣づけておけば良かったです。

産卵初日

前日の様子が様子だったので、朝から見守っていました。相変わらずフンは多尿気味。 普段はカゴのドアを開けるとすぐに出てくるのですが、この日はずっとツボ巣に籠もっていました。

朝10時くらいにいつもより呼吸が激しいことに気づき、注視します。もう明らかに産みそうな感じです。 ここまで来ると怖いのは卵詰まりなのですが、寒い中遠くの獣医に連れて行くとそれはそれでストレスになるだろうということで、もう少し様子見をすることにしました。

その後1時間後くらいに無事産卵していることがわかりました! もなちゃんがツボ巣から出てきた時に初めて卵を確認できたので、正確にいつ産んでたのかは謎です。

産んだ卵 (2日目) きれいな形でした

産んだあとは呼吸も落ち着きケロッとしていたので安心した記憶があります。 調べたら明日以降もまだまだ産卵は続くようだったので、いつもより気持ちカゴの中が暖かくなるようにしておきました。

初産が無事に済み、きゅうりに舌鼓を打つもなちゃん。ドーナツ状に食べる。

2日目〜5日目

その後4日続けて、計5つの卵を産みました。

なお、産んだ卵は毎日すぐに擬卵に交換しました。 今回は無精卵なので孵化の可能性はないですし、もし割れたら色々と都合が悪いだろうためです。 あるいは擬卵を使わず単純に卵を撤去すると、いつまでも産み続けてしまうという話も聞きました。産卵は文鳥には負担でしょうから、これも避けたいところです。

もなちゃんは擬卵に気づく様子もなく温め続けています。

5つの卵。このときは擬卵が足りなかったので1個だけ本物。

擬卵はAmazonで売っていたやつです。 私は4個セットのものを飼ったのですが、5個以上産む文鳥も多いとのことだったので、6個セットのほうを買うことを強くおすすめします。

www.amazon.co.jp

卵を産む期間の特徴的な変化として、もなちゃんの場合は以下が見られました:

  • 水を普段よりよく飲み、餌もよく食べる
    • 産卵直前時の行動と一緒です
  • 翼が少し下がる (下図)
    • 理由はよくわからないのですが、明らかに普段とフォルムが変わりました

いつもと違う翼の様子。背中でぴっちりと閉じていません。

5つ産み終わった後のもなちゃん。翼が背中の上でぴったり閉じています

  • 排泄口が常にやや開いている
    • お尻の穴の様子が変わりました

これは平常時の排泄口。産卵時期は穴が常に見えているような状態でした

これらは個体により異なるとは思いますが、ご参考までに。 最後の卵を産んだあとは、これらの変化がパタリと止み通常の様子に戻りました。

産卵後

産卵後は、文鳥自身がひたすら卵を温めるようになります。 本来はつがいで交互に温めるのかもしれないですが、もなちゃんはシングルのためワンオペを強いられています。

一人で無精卵を健気に抱えている様子は少し気の毒にすらなりますが、抱卵自体はあまり体力の消耗もないようなので、飽きるまで見守っていることにしています。 なおこの記事を書いている時点で5つ目の卵を生んでから1週間が経過しましたが、全く飽きる様子はありません。

擬卵で置き換えた本物の卵たちは捨てるのも忍びなく目玉焼きにして食べてみました。 見かけはとてもきれいな色でしたし、味もニワトリの卵とほぼ同じで美味しかったです! 卵を食べると文鳥と一体になれたような感覚を得られるので、興味ある方はお試しください。

目玉焼き

別の変化として、カゴの中では一切フンをしなくなり、外で大きなフンをするようになりました。 カゴの扉を開けているとたまに出てきてフンをしてまた卵の元へ帰るという様子です。 寝かせている深夜の間もほとんどフンをせず、朝にまとめて出しているようです。

これは抱卵時としては正常な状態らしい (参考) ので、不安になる必要はないようです。 ただ、大きなフン (本当に大きいです!) を本や服の上にされると少し悲しいので、カゴから出てきた直後はしばらく一緒にゴミ箱の上で待機するようにしています。

まとめ

以上、もなちゃんの産卵記でした。とりあえず無事に産み終わって安心した一方、抱卵が終わるまでは気が抜けないです。

また、世間にはそもそも卵を産ませないという方針もあるようです。産卵には卵詰まりのリスクもありますし、抱卵中は全く人間と遊んでくれなくなるので、産ませずに済ませられるのならそうするのがベストではありそうです。 とはいえかなりの注意が必要 (発情を避けるため、2歳までは文鳥にほぼ触らない) とのことなので、並の覚悟では難しそうだなとも思ってます。

突然母の顔になったもなちゃん、卵の世話で精一杯のようで、人間には目もくれず抱卵していますw これからはもなさんと呼ぼうかと思います。

AWS CDK Tips 記事まとめ

AWS CDKを日々使う中で溜まった知見を書き出しています。記事を追加するごとにここも更新していきます。

以下はベストプラクティス的な話です:

上記のプラクティスをまとめたスライドもあります: AWS CDKのあるあるお悩みに答えたい

以下はより発展した使い方の考察です:

以下はもう少し細かいユースケースごとの話です:

以下はコンストラクトライブラリ開発者向けです:

AWS CDK Tips: クロスリージョンのデプロイ

AWS CDK TIpsシリーズの記事です。

AWSでサービスを構築する際、単一リージョンで提供するサービスであっても、クロスリージョンのデプロイが必要になる場合がまれにあります。AWS CDKでは、そのような構成も簡単に実装可能です。今回はCDKを使ったクロスリージョンアプリのデプロイ方法をまとめます。

クロスリージョンの必要な状況

まず、クロスリージョンのデプロイが必要になるのはどのような場合でしょうか?

DRやレイテンシー低減を考慮したマルチリージョンのアーキテクチャでは、もちろん必要でしょう。しかし実はそうでない場合、つまり単一リージョンで提供するサービスであっても、次の場合などにクロスリージョンのデプロイが必要になります:

1. CloudFrontでカスタムドメインを使いたい

CloudFront Distributionはデフォルトで cloudfront.net ドメインのURLを発行します。これを独自ドメインに設定するためには、AWS Certificate Manager (ACM) の証明書をDistributionに紐付ける必要があります。このとき、ACMの証明書は必ず us-east-1 リージョンで作成されたものでなければなりません (ドキュメント)。

このため、すべてのリソースをus-east-1にデプロイする場合を除いて、複数のリージョンを跨いでリソースを作成する必要が生じます。

2. AWS WAFを使いたい

AWS WAFを使う場合、Web ACLを作成する必要があります。Web ACLはWAFの挙動を定義するためのリソースで、us-east-1 リージョンでのみ作成可能です (ドキュメント)。

CloudFront DistributionやAmazon API Gateway APIなどを作成する際に、Web ACLのARNを追加指定することで、アクセスがWAFを経由するようになります。Web ACLを参照するリソースがus-east-1以外にあれば、クロスリージョンの参照が必要になります。

3. 他にも、色々

他にも上記のようなケースは考えられます。 例えばLambda@Edgeも、関数はus-east-1のみにデプロイ可能です (ドキュメント)。 こういった制約を網羅的に知るのは難しいので、サービスを使う時に適宜調べると良いでしょう。

また、特定のリージョンでは提供されていないサービスや、逆に特定のリージョンでのみ提供されるサービスもあります (リージョンごとの対応サービスリスト)。そうした場合も、その制約を回避するためにクロスリージョンのデプロイが必要になることがあるでしょう。

CDKでクロスリージョンデプロイする方法

前置きが長くなりましたが、ここから本題です。上記のような場合に使えるいくつかの方法を紹介します。

1. CDKネイティブのクロスリージョン参照

CDKでは2.50.0から簡単にクロスリージョンのデプロイができるようになりました (PR)。以下のコードは、東京リージョンからヴァージニアリージョンのリソースを参照する例です。クロスリージョンのリソースの受け渡し (スタック間参照) が実現できています。

const app = new cdk.App();

const virginia = new VirginiaStack(app, 'VirginiaStack', {
  env: {
    region: 'us-east-1',
  },
  crossRegionReferences: true,
});

new TokyoStack(app, 'TokyoStack', {
  env: {
    region: 'ap-northeast-1',
  },
  crossRegionReferences: true,
  certificate: virginia.certificate,
});

class VirginiaStack extends cdk.Stack {
  readonly public certificate: Certificate;

  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    this.certificate = new Certificate(this, 'Certificate', { /* 略 */ });
  }
}

関連する2つのスタック両方に crossRegionReferences: true のpropを渡すことに注意してください。 今はpreview機能としての提供であり、crossRegionReferences がFeature Flagとして働きます。おそらくGAされればこのフラグは不要になり、完全に透過的にクロスリージョン参照を実装できるようになるでしょう。

2. カスタムリソースを使う

1の方法ではスタックをリージョンごとに作る必要があるため、やや面倒に感じることもあるでしょう。代わりに、CloudFormationのカスタムリソース機能を利用することができます。例えば ap-northeast-1のスタックにデプロイしたカスタムリソースから、us-east-1のAPIを叩いて必要なリソースを作成するのです。

このアプローチが実現されたものはいくつかあり、例えば DnsValidatedCertificateACM Certificateを作成・検証するカスタムリソースですが、作成するリージョンを任意に指定可能です。ただしこちらは既にDeprecatedであり、非推奨です。 また別の例として、AWS Prototyping SDK (PDK)*1では、AWS WAFのWeb ACLをリージョンまたぎで作成できます(参照: cloudfront-web-acl.ts)。

この方法はシングルスタックに保てるという利点はあるものの、大抵カスタムリソースのハンドラ実装がCFnの再発明になり諸々の困難が予測されます。それらを覚悟の上でなら、こちらの方法もアリでしょう。

3. cdk-remote-stack (昔の方法)

従来CDKでクロスリージョン参照といえば、cdk-remote-stack を使う方法が主流でした (更にその前は同じような機能を各自で実装していました)。ググって出てくる記事もこの方法が多いと思います。私もよくお世話になったライブラリです。

2023年では1の方法が利用できるため、この方法をあえて使う理由は無くなったと考えて良いでしょう。ただしこちらは弱い参照 (後述)なので、人によってはもうしばらく出番があるかもしれません。

1の仕組み

せっかくなので1 (CDKネイティブのクロスリージョン参照) の仕組みもまとめます。意外と手の込んだ仕様で面白いです。

基本的な思想は、CloudFormationのクロススタック参照の挙動を模擬する方針のようです。このため、例えば他のスタックに参照されているパラメータは更新や削除ができません。使用者はダウンタイムを回避するため、複数段階に分けて安全にデプロイする必要があります。(もし使用中のパラメータを書き換える事ができたら、参照している側はデプロイされるまで古いパラメータを利用し続けることになります。これでは動作の保証ができず、危険です。)

図にするとこのようなものです。Lambda関数は、CDKが自動的に作成するカスタムリソースのハンドラです。参照の更新時や削除時の挙動は、従来のクロススタック参照と似たものになります。

クロススタック参照に似た強い参照の挙動は、検証時など頻繁にリソースを更新・削除する用途では不便だという声もあります。これを受けて、弱い参照の実装も検討されているようです (RFC)。元のPR にも strong-ref というキーワードが頻出しますし、また「この仕組みはそのまま弱参照の実装に利用可能だ」という旨も記載されています。期待しておきましょう。

まとめ

AWS CDKを使えば、クロスリージョンのシステムも簡単にデプロイできます。便利に使っていきましょう!

*1:これはAWS Prototypingのオーストラリアチームが主に開発したコンストラクトライブラリです。日本ではあまり見ない方法もあるので面白いです。AWS Prototyping SDK

AWS SDK JavaScript v3でS3のファイル操作 チートシート

ワタミチートシート以来、久々のカンペ記事。

AWS SDK JavaScript v3がリリースされて久しいが、移行は進んでいるだろうか? LambdaのNode.js v18ランタイムではv3 SDKのみビルトインされているなど、そろそろ移行を進めないとまずい状況も増えてきている。

私自身は未だにv3のSDKでS3のファイルをダウンロード/アップロードする操作に慣れないので、この記事にそれらのサンプルコードをまとめる。

事前準備

以降のコードに必要なライブラリは、基本的に以下の一つだけでOK。

npm install @aws-sdk/client-s3

コード

S3頻出のパターンとして、以下5つがあるだろう:

  1. メモリ上のデータをS3にアップロード
  2. ファイルシステムのデータをS3にアップロード
  3. メモリ上にS3のデータをダウンロード
  4. ファイルシステムにS3のデータをダウンロード
  5. S3上のファイルを他のS3バケットコピー/移動

それぞれのサンプルコードを以下に示す。

1. メモリ上からアップロード

以下は、some data という文字列を test.txt としてアップロードする例。文字列以外にもBufferなどが使える。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// Bodyのデータをtest.txtとしてアップロード
await s3.send(
  new PutObjectCommand({
    Body: 'some data',  // Bufferなども指定可
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);

2. ファイルシステム上からアップロード

以下は、test.txt というファイルをアップロードする例。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createReadStream } from 'fs';

const s3 = new S3Client({});

// ローカル上のtest.txtをアップロード
await s3.send(
  new PutObjectCommand({
    Body: createReadStream('test.txt'),
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);

ファイルを一度メモリ上に展開する方法も考えられるが、ReadStreamを使うほうが省メモリで済む。

備考: 対象のファイルサイズが大きな場合

対象のファイルサイズが大きな場合は、Multipart機能を使うことでより高速にアップロードできる場合がある。これを便利に使うための機能がSDK v3にはあるので、追加でインストールする。

npm i @aws-sdk/lib-storage

コードは以下:

import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from 'fs';

const s3 = new S3Client({});
const upload = new Upload({
  client: s3,
  params: {
    Body: createReadStream('sample.bin'),
    Bucket: process.env.BUCKET_NAME,
    Key: 'sample.bin',
  },
  // 性能改善用の細かなパラメータ
  queueSize: 10,  // アップロードの並列数
  partSize: 1024 * 1024 * 5,  // 1パート当たりのサイズ
});

await upload.done();

手元で500MB程度のファイルをアップロードしたところ、およそ40%速くなった。色々な条件にも依ると思うため、参考までに。こちらも参照

3. メモリ上にダウンロード

/test.txt というオブジェクトをメモリ上に展開する例:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// S3上のtest.txtをダウンロード
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);
const str = await s3Object.Body?.transformToString();
// バイト列として取得したいときはこちら
// const bytes = await s3Object.Body?.transformToByteArray();

ちなみに、最近この操作が上記のように楽になった (以前はStreamを意識したコードが必要だった) 件は、ここにも書いた。

TIL: AWS SDK for JavaScript v3 で s3.GetObject する最新の方法 - maybe daily dev notes

4. ファイルシステム上にダウンロード

/test.txt というオブジェクトをファイルシステム上に test.txt というファイルとして保存する例:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import {createWriteStream } from 'fs';
import { Readable } from 'stream';

const s3 = new S3Client({});

// S3上のtest.txtをダウンロードし./test.txtに保存
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);
await new Promise((resolve, reject) => {
  if (s3Object.Body instanceof Readable) {
    s3Object.Body.pipe(createWriteStream('test.txt'))
      .on('error', (err) => reject(err))
      .on('close', () => resolve(0));
  }
});

メモリからディスクに都度書き出す、省メモリな実装。この場合は依然としてStreamを繰る必要がある。

5. S3からS3にコピー/移動

あるバケットtest.txt を 別バケットtest_copy.txt としてコピー・移動する例:

import { S3Client, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// test.txtをtest_copy.txtにコピー 
await s3.send(
  new CopyObjectCommand({
    Bucket: process.env.TARGET_BUCKET_NAME,
    Key: 'test_copy.txt',
    CopySource: `${process.env.SOURCE_BUCKET_NAME}/test.txt`
  })
);

// 移動の際は元ファイルを削除
await s3.send(
  new DeleteObjectCommand({
    Bucket: process.env.SOURCE_BUCKET_NAME,
    Key: 'test.txt',
  })
)

コピー元であるCopySourceの指定方法がすこし特殊 ( s3:// などではない)。詳細はこちら

ちなみにオブジェクトを移動したい場合は、コピー後に元オブジェクトを削除する。2つの操作をアトミックに実行するS3 APIは今のところ存在しないので、整合性が重要な場合は要注意 (あまりないと思うが。)

なお、CopyObject APIを使えるのはファイルサイズが5GBまで。5GBを超えるファイルは、コピー元オブジェクトをダウンロードしながらコピー先にアップロードする必要がある。Streamのおかげでコードは簡単かつメモリ上で完結する。

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';

const s3 = new S3Client({});

// 5GBを超えるファイルをコピーする
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.SOURCE_BUCKET_NAME,
    Key: 'sample.bin',
  })
);

// 大きなファイルで高効率なMultipartアップロードを利用
const upload = new Upload({
  client: s3,
  params: {
    // streamを直接渡せる
    Body: s3Object.Body,
    Bucket: process.env.TARGET_BUCKET_NAME,
    Key: 'sample_copy.bin',
  },
});

await upload.done();

注意

いくつかの疑問が生じたので、ついでに調べた:

  • @aws-sdk/client-s3S3S3Client の2つあるが、どっち使えば良い?

このドキュメントに詳しく書かれている。S3 はv2と似た体験を実現するために用意されたもの。これを使うと、s3.putObject のように xxCommand クラスを使わずにAPIを呼べる。

一方でTree shaking観点ではこの古い方法はイマイチらしく、フロントエンドなどバンドルサイズの要求がシビアな場面では S3Client を使う方法が好まれる。個人的には書き方を使い分けるのも面倒なので S3Client の方で統一するのが良いと思うが、バックエンドでの利用などバンドルサイズが多少大きくても問題ない場合は書きやすい昔の記法もアリだろう。

なおこれはS3に限らず、DynamoDBやEC2など他のすべてのサービス用SDKで共通の話。

  • new S3({}){}、必要?

必要。型定義上この引数を省略することはできない。理由は不明だが、オプショナルにするとnullチェックが追加で必要になるので、多少気持ちはわかる。このIssueで提案はされたが、特に対応されなかった模様。

以上、AWS SDK for JS v3でS3のファイルを扱うときのコードをまとめた。

TypeScriptのcode-firstなGraphQL開発ツール比較: TypeGraphQL vs Nexus vs Pothos

GraphQLサーバーを開発する際は、まず schema-first か code-first かを決めることになるでしょう。前者はまず graphql.schema を手書きし、そこから言語固有のコードを生成する方法です。後者は言語固有のコードを書いてから、 graphql.schema を生成する方法です。

今回2つのどちらが良いかは議論しませんが、後者の code-first & TypeScriptでGraphQL開発をする場合、ライブラリの選択肢がいくつかあります。この記事では、それらのライブラリの特徴をまとめます。特に以下の観点で比較します:

  • Prismaとの連携: 私がTypeScriptでウェブ開発するときはORMのPrismaをよく使います。GraphQLと組み合わせる上では、型定義の重複やN+1問題を回避するため、ライブラリ間の連携は重要です。
  • 開発の活発さ: 将来的にライブラリがDeprecateされるリスクはできるだけ避けたいものです。今のメンテナンスの活発具合でこのリスクを占います。

特徴

ということで見ていきましょう。今メジャーなcode-first GraphQLライブラリは3つあります。

1. TypeGraphQL

github.com

Nest.jsTypeORMと同様に、TypeScriptのデコレータ記法をベースにしたフレームワークです。デコレータ記法は今流行りのフロントエンドライブラリではほとんど使わないので、好みが分かれるところかもしれません。

Prismaとの連携は TypeGraphQL Prisma というインテグレーションが用意されています。結構大胆で、何も設定しなくても、Prismaで定義したすべてのテーブルのCRUDゾルバーを勝手に実装してくれます。微調整も可能な模様。ただし、ORMとしては同じデコレータ記法を使うTypeORMのほうが相性良さそうに思います。

開発の活発さについて、GitHub上では一見現状の最終リリースが2020/11/5と停滞しているように見えます。しかし、実は去年v1.2.0 RCをプレリリースし、また今はv2のベータの開発を進めているようです。ほぼ作者一人の個人プロジェクトなのでやや今後に不安を覚えますが、少なくともスポンサーは複数付いています。

Contribution数はほぼ一人が支えている

正直私自身はこれをあまり使ってないので深いことは書けません。

2. Nexus

github.com

Nexus organizationメンバー3人中2人がPrismaの人という、Prismaと密接なつながりを持っているフレームワークです。TypeGraphQLとは異なりデコレータは一切使わず、コードの雰囲気もどことなくPrismaと似ています。

Nexusは実行時にTypeScriptの型定義ファイルを生成します。このため、開発時は以下のようにNexusサーバーを常駐させることが推奨されています。この仕様のため、エディタでの型チェックの反映がワンテンポ遅いという欠点もあります。

# ts-node-devで常駐させる
ts-node-dev --transpile-only --respawn nexus-server.ts

Prismaと連携するにはnexus-prisma ライブラリを利用します。前身の nexus-plugin-prisma ライブラリもあるのですが、こちらは既に開発終了で deprecated です。

ただし nexus-prisma も若干雲行きが怪しく、2022半ば頃は開発が停滞していたようです。そんな中、2022/10からメンテナンスの主体がPrismaからコミュニティに移管されつつあります。移管先は現状開発者一人のため開発速度が大きくブーストされるかは不透明ですが、今は過渡期のため当面状況を見守る必要があるでしょう。

Nexus自体も生存確認のIssueが立つ程度には開発が活発ではありません。後述のPothosに移行するためのツールを作る開発者もいるなど、穏やかではない状況です。また、メインのメンテナであるtgriesserさんが後述のPothosに感心し、NexusからPothosへの漸進的な移行方法を提供したいとも発言しています。

上記を考慮すると、今のままではNexusがユーザー数を維持・拡大する可能性は低いと推測されます。ひとまずは公式声明を待ちたいですが、当面は新規採用を控えるのが無難かもしれません。

3. Pothos

github.com

初版のリリースが2020/7と3つの中では最も若いライブラリです。元はGiraphQLという名前でしたが、視認性や検索性の問題で改名したようです。

記法はNexusとよく似ており、TypeGraphQLのようなデコレータ記法は用いません。このため、上記の通りNexusからPothosに移行する動きもあるようです。 また、謳い文句によればランタイムのオーバーヘッドがなく軽量で高速なことが売りのようです。コード生成にも頼らないため、Nexusよりも型チェックが俊敏です。

Prismaとの連携も容易で、プラグインが用意されています。密接にPrismaと統合されており、リゾルバーの中で宣言的にPrismaのクエリを書くことも可能です。

ただし、個人的にはPrisma連携で一つつらみがありました: こちらで議論されているのですが、Prismaの型を持ってくる時にデータ型やnullabilityを改めて指定する必要があり、Prisma側とコードの重複が生じる点です。ここはNexusやTypeGraphQLのほうがスマートに書けます。

// Prismaの型定義
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
}

// PothosのObject定義
 builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    email: t.exposeString('email'),
    name: t.exposeString('name', { nullable: true }),
  }),
});

開発は明らかに活発で、他の追随を許さない勢いでバージョンアップが重ねられています。今はhayesさんの個人開発なのがやや不安ですが、後述の通りユーザー数がここ1年で激増しているので、モチベーション高く開発されているようです。今のところは楽観視できるでしょう。

npmダウンロード数も比べてみましょう。緑がTypeGraphQL、オレンジがNexus、青がPothosです。全体の数はTypeGraphQL > Nexus > Pothosですが、ここ1年の伸び率は真逆でPothos > Nexus > TypeGraphQLです。

@pothos/core vs nexus vs type-graphql | npm trends

Pothosはまだユーザー数は少ないものの指数関数的な成長です。性能の良さや開発の活発さが評価されているのでしょう。Nexusの動向やTypeGraphQL v2の出来次第で、今後更に伸びるのではないでしょうか。

Nexusも2021の後半勢いづいていますが、こちらの理由は不明です。今後はPothosへの置き換えが進んでいく可能性も高いと思われます。

TypeGraphQLは2020/11以降大きなアップデートもないため、線形な成長率です。今後v2がリリースされれば、また変化が見られるでしょう。

考察

ということで、どれを選ぶべきでしょうか。

私個人の意見としては、デコレータ記法がなじまないのでTypeGraphQLはNG、またNexusは先行きが不安のためNGで、より将来性を感じるPothosを選びます。ここはまだ正解がないので、各々が考える必要があるでしょう。答え合わせは半年後になるでしょうか。

また、この選択をする上では開発体験も重要なファクターです。ここは主観にもよるところがあるので、3つのライブラリで簡単なGraphQL APIを実際に作ってみて、体験を評価することをおすすめします。

まだ私自身もGraphQLを使い始めたばかりなので、もう少しPothos or Nexusをやり込んだら再び記事を書きたいと思います。以上、TypeScript x GraphQL x code-firstなライブラリ3つの比較でした。