echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【JAWS】JAWS FESTA 2024 in 広島に参加しました

今回のお題

10/11(金)~10/13(日)に開催された「JAWS FESTA 2024 in 広島」に参加してきましたので、その感想です。

公式サイトはこちら

jawsfesta2024.jaws-ug.jp

JAWS FESTAって何?

JAWS-UG(Japan AWS User Group)が開催する、大きなカンファレンスイベントの一つ。

JAWS-UGが開催する大きなカンファレンスイベントとして

  • JAWS DAYS(春頃に開催)
  • JAWS FESTA(秋頃に開催) ※今回はこっち

の2つがあり、今回のJAWS FESTAは広島で開催されました。(去年は福岡)

前夜祭

前夜祭はEight SupperClubという豪華なクラブで立食パーティー形式で開催され、LT大会で盛り上がりました。

個人的には アイレット株式会社のこまきちさん (@komakichidev) の 「実家の娘情シス」の発表 が印象的でした(アーキテクチャが完全にガチ勢のそれ)

その後(非公式の)2次会では、ジャニさん(@beajourneyman)、三戸さん(@mito_tetsuya)、市野さん(@kazzpapa3)と一緒に(人生初の)コンカフェに行ったり、汁なし担担麺を食べたりしました。(こういう時じゃないと行かないからなあ)



本編

会場は広島大学で、敷地が広い&自然豊かで、非常にキャンパスライフを過ごしやすい大学だなあと思いました。

キーノートでは現職の広島県知事である湯崎 英彦氏が登場し、広島県における AI 活用の事例を紹介してくれました。
「失敗を許す」「失敗を活かす」など、いわゆる最新のIT系の考えをバリバリの公官庁(しかも中国地方の中核である広島県で)で積極的に取り入れているのはすごいなあと思いました。

なお本編に関しては、僕自身は当日ボランティアとしてDトラックの午後セッションの司会を担当していたため、正直あまりセッションは聞けませんでした(ここは今回残念な部分)

ただ、たまたまDトラックの午後セッションがCloudFormationやAmplify Gen2(内部的にAWS CDKを使用)といった、個人的に大好きなInfrastructure as Codeに関係する内容だったため、結果的には非常に有意義な時間を過ごせました。

あとこの日の夜、ホテルのエレベータで参加者の方に「本日は司会、お疲れさまでした」と言われたのがとてもうれしかったです。


懇親会

懇親会は1次会は近くの西条HAKUWAホテル、2次会はたまたま同じタイミングで開催されていた西条酒まつりでした。(3次会もあったんですが、こちらは店員オーバーで参加できず)

正直、JAWS FESTA開催前は「さすがに懇親会多すぎ&長すぎじゃね?」と思っていたんですが、いざ参加したら「まあ、これはこれでこういうのもありか」と思いました。

特に(後で知ったんですが)広島県西条市は「日本三大酒どころ」の一つだけあって、西条酒まつりってめちゃくちゃ有名なんですね。
それを知って「そりゃあえて酒まつりをイベントに組み込むわな」と納得。

あと、1次会でのAWS HERO三浦さん(@miu_crescent)のLTが、自分にとって(足りなかった部分&反省する部分があるという意味で)すごい刺さる内容であり、とても心に残りました。(あと北海道の酪農の歴史も学べて、すごく良かったです)

その後、広島市で知人と個人的に3次会を...と思ったんですが、タイミングが合わなかった&前日かなり寝不足だったのもあり、この日は早めに就寝。


おとなの遠足

最終日は参加希望者のみで、「おとなの遠足」という(健全な)イベントに参加。

午前に厳島神社参拝、午後はOKOSTA(オタフクソースさん直営の施設)でお好み焼きハンズオン(=自分で作る体験)をしてきました。

厳島神社は半年前のYAPC::Hiroshima で来て以来でしたが、半年前と違って今回は鳥居まで歩いて行けたので、また違った体験ができました。(前回は満潮で眺めるしかできなかった)

そしてOKOSTAのお好み焼き体験では、正直「どうせ失敗するだろうし、帰りに別の場所で締めのお好み焼き食べていくか...」くらいな感覚でしたが、(講師の方の教えもあり)やってみたら意外とうまくできたので(作るのも味も)、思いがけずいい体験ができましたし、結果的にこれが締めのお好み焼きとなって、いい思い出になりました。



その他

そしてその後すぐに帰宅...しようと思いましたが、少しだけ時間があったので、電車で呉市へ。
あまり時間はなかったのですが、大和ミュージアムに行ってきました。(たまたま艦これのイベントもやってましたが、僕は全く分からないのでスルー)

そして広島駅に戻り、そのまま岐路につきました。

まとめ

昨年同様、今回もとても有意義な時間を過ごせました。

正直最初は先述の通り「懇親会多すぎ&長すぎじゃね?」と思っていたんですが、これはこれでありかもな...と思いましたし、広島のことをよく知るきっかけにもなったので「地方のITでの発展に貢献する」というのをここ数年で真剣に考えるようになった自分にとって、結果的に非常に有意義な機会となりました。

正直メンタル不調など諸々の理由で参加を結構悩んでいたんですが、今は参加して本当に良かったと思っています。

ただ残念ながら、今回はCFP落選してしまったので、次回はぜひ登壇者として参加出来たらなと思いました。

また「地方のITでの発展に貢献する」ためにも、こういう地方での大きいイベントには積極的に何かしらの形で貢献しようと思います。

それでは、少々長くなりましたが、今回はこの辺で。

【JAWS】JAWS Pankration 2024で登壇しました

はじめに

めちゃくちゃお久しぶりです。
体調不良(特にメンタル不調。自律神経失調症?)がひどく、数か月間まともにブログすら書けませんでした。

久々のブログとなる今回は、まさに本日「JAWS Pankration 2024」で登壇したので、その話です。

JAWS Pankration 2024って何?

JAWS-UG(Japan AWS User Group)による、地球規模の交流イベント。
2024/8/24 12:00~2024/8/25 12:00まで24時間ぶっ通しで行われる、完全オンラインイベントです。

最近は嬉しいことにオフラインイベントが増えてきたのですが、このJAWS Pankration 2024は完全オンラインイベントです。

また「地球規模の」というだけあり、日本のみならず、世界各地の方が発表者です。(例えばアメリカ、イギリス、ドイツ、インドネシアなど)

前回は3年前の2021年に開催されましたが、それ以来3年ぶりの開催になります。(ちなみに、私は前回も登壇しています)

jawspankration2024.jaws-ug.jp

発表内容は?

8/25(日) AM8:00より「What is Lambdaless Serverless」という、AWSの淡路さんがAWS Summit Japanで発表されていた「Lambdaレスなサーバーレス」に関する私なりの考察を発表しました。

speakerdeck.com

内容的には、JAWS-UG Okayama 2024の懇親会LTで発表した内容をベースに(このあたりはまたブログに書きます) 、アドバイスを頂いた点などを加筆修正したもので、概要としては以下になります。

  • Lambdaがなくてもできる処理ではLambdaを無くすことで、構成をシンプルに出来るよ
  • ただしそれ相応のデメリットもあるから、うまく使い分けようね(お約束の「銀の弾丸はない」)
  • 「Lambdaを使うのがアンチパターン」なんてことは100%ありえないよ

JAWS Pankration 2024ですごいなあと思ったこと

修正対応が早い

「ライブ動画見てるとブラックアウトする」「前の人の翻訳がずっと残ってる」「翻訳の増加に合わせて、自動スクロールしてほしい」など、開催中に色々不都合や要望が出たのですが、それへの対応が早くてびっくりしました。
早急に実装して本番デプロイするのもそうなのですが、 「ライブ動画見てるとブラックアウトする」について、原因を早急に見つけられることもすごいなあと思いました。

ダウンタイム0

これだけ世界中から数多くの方が視聴し、リアルタイム翻訳も実施する中、1回も遅延らしい遅延やサイトダウンが発生しませんでした。
これは本当にすごいと思いました。

タイムキーピングが完璧

今回のイベント、もちろんタイムテーブルが決められていたのですが、完全に時間通りで、遅れが全く発生しませんでした。
もちろん(私含め)登壇者が気を付けていたのもあるのですが、運営の方のタイムキーピングが完璧だなあと思いました。

ちなみに今回は「15分経過したらたとえ発表途中でも強制終了」というスタンスだったのですが、個人的にこのスタンスはメリハリがついて、とても良いなと思いました。(ちなみにNGK(名古屋合同懇親会)もこのスタンスです。なんならNGKは持ち時間より早く終わるのもNGで、その場合は時間まで何とかして間をつなぐ必要があります)

個人的に良かったこと

モチベーションアップ

「はじめに」に書いた通り、ここ半年以上体調不良(特にメンタル不調)がひどく、なかなか気力もわかなかったのですが、今回久々にこういう大きなイベントで登壇させて頂いて、元気・気力をもらった感じがしました。
またモチベーションアップにもつながりました。

やりたいことが明確になった

今年に入ってから、自分がやりたいことや進むべき方向についてかなり悩んでおり(転職もしてしまったほどですし)、まだそれが続いていたのですが、今回色々な方のセッションを聞くことで「なんだかんだで自分は『アプリ』(のバックエンドやアーキテクチャ、IaC、CI/CDなど)がやりたかったんだ」ということが明確に分かりました。

AWS Summit Japan同様、こういう大きいイベントに参加したからこそ分かったことだと思います。

個人的な反省点

集合時間に間に合わなかった

登壇者は発表時間の30分前に「VoicePing」という控室みたいな場所にログインする必要があったのですが、直前にバタバタしてしまったこと、そして最初なぜかログインできなかったことがあり(Chromeを再起動したら直った)、集合時間にログインできず、しかも運営の方からの「ログインお願いします」という連絡にも気づいていませんでした。
この点は反省し、次回はちゃんと時間厳守で行動しないといけないと感じました。

なお補足すると、AM8:00という早い時間なので「実は寝坊してる?」的なことを思われた方もいるかもしれませんが、ちゃんと朝6:30に起床してます。

セッションが短かったかも

前述の通り持ち時間は15分で「時間オーバーは絶対NG」だったので、それだけはしないようにと意識しすぎたのか、10分もかからず終了する結果になり、さすがに短すぎたかもと個人的には感じました。

一応、前日にリハをやった際に時間が短かったので内容を追加したのですが、もう少し長くても(せめて10分)良かったかなと思いました。(NGKなら勝手に質疑応答タイムしてたかも)

まとめ

今回は「発表資料はすべて英語」ということで大変だった部分もありますが、なんだかんだで非常に充実した時間を過ごせました。
やはりこういうコミュニティイベントに参加したり、自分の知見をアウトプットするのは勉強になるし、充実するし、なにより気力アップやモチベーションアップにつながるので、これからも(時間とお金が許す限り)継続して行いたいと思いました。

最後に(朝早いにもかかわらず)私の発表を聴いてくれた方、ありがとうございました。
そして参加者の皆さん、お疲れさまでした。

そして何より運営の皆さん、「JAWS Pankration 2024」という素晴らしい機会を作って頂きまして、本当にありがとうございました&24時間お疲れさまでした。
とりあえず今日は無理せず、ゆっくり休んでください。(明日は有休を取っているはずだと信じてますが...)

という訳で、今回はこの辺で。

【AWS CDK】IAMロールにIAMポリシーをアタッチする際の挙動について調べてみた

今回の記事の概要

AWS CDK(以下「CDK」)でIAMロールにIAMポリシーをアタッチする際のアタッチ方法による挙動の違いを検証する

はじめに

IAMロール(以下「ロール」)を使用する際、「ロールにIAMポリシー(以下「ポリシー」)をアタッチして権限を設定する」ということを行います。(=ロールを割り当てたAWSリソースに適用する権限を設定する)

もちろんCDKでもそうなのですが、CDKではロールにポリシーをアタッチする方法が複数用意されています。

そこで、今回はこれらの方法による挙動の違いを検証したいと思います。

検証内容

具体的に、CDKでロールにポリシーをアタッチする手段として、下表のものがあります。(「説明」「備考」は AWS CDK Reference Documentation の説明を和訳したものです)

方法 説明 備考
inlinePolicies(props) このロールにインライン化する名前付きポリシーのリスト これでポリシーを設定した場合、CloudFormation(以下「CFn」)テンプレートで AWS::IAM::Policy の定義は作成されず、AWS::IAM::Role の1プロパティとして設定される
addToPolicy(メソッド) このプリンシパル(=ロール)のポリシーを追加する
addToPrincipalPolicy(メソッド) ロールのデフォルトのポリシーDocumentに権限を追加 デフォルトポリシーが未設定の場合、自動作成する
attachInlinePolicy(メソッド) このロールにポリシーをアタッチする

なお managedPolicies props、及びaddManagedPolicy メソッドなどの「『AWS管理ポリシー』をアタッチする処理」は下記の挙動が明確なため、今回は除外します。

  • ポリシーが新規に作成されない
  • AWS管理」としてポリシー単位で個別にアタッチされる

また applyRemovalPolicy メソッドは、(「Policy」とあるけど)「下記のケースにおいて、そのロールを残すかどうか」の設定であり、IAMポリシーとは無関係です。

  • このロールを定義しているスタックが削除された
  • CDKの定義からこのロールが削除された
  • 上書き不可の変更が発生したため、リソースを削除→再作成する必要がある

CDKコード

検証用に、下記CDK定義を作成してデプロイします。(関係部分のみ抜粋)

import { Role, Effect, ServicePrincipal, PolicyStatement, PolicyDocument, Policy, ManagedPolicy } from "aws-cdk-lib/aws-iam";  
  
// inlinePoliciesで適用するポリシー
const inlinePolicy = new PolicyDocument({
  statements: [new PolicyStatement({
    actions: ['dynamodb:Scan'],
    effect: Effect.ALLOW,
    resources: ['*']
  })]
});
  
// addToPolicyで適用するポリシー
const policy = new PolicyStatement({
  actions: ['dynamodb:Query'],
  effect: Effect.ALLOW,
  resources: ['*']
});
      
// addToPrincipalPolicyで適用するポリシー
const principalPolicy = new PolicyStatement({
  actions: ['dynamodb:GetItem'],
  effect: Effect.ALLOW,
  resources: ['*']
});
      
// attachInlinePolicyで適用するポリシー(ドキュメント)
const attachInlinePolicyDocument = new PolicyDocument({
  statements: [new PolicyStatement({
    actions: ['dynamodb:DescribeTable'],
    effect: Effect.ALLOW,
    resources: ['*'],
  })]
});
    
const attachInlinePolicy = new Policy(this, 'RoleAttachInlinePolicyId', {
  document: attachInlinePolicyDocument,
  policyName: 'RoleAttachInlinePolicy',
});
    
// ロール&ポリシーの設定
// managedPoliciesは個人的検証で使用しただけなので、スルーしてください
const role = new Role(this, 'LambdaRole', {
  assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
  roleName: 'RoleForPolicyAttachmentTest',
  managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBReadOnlyAccess')],
  inlinePolicies: {
    'RolePropsInitialPolicy': inlinePolicy,
  },
});
    
role.addToPolicy(policy);
role.addToPrincipalPolicy(principalPolicy);
role.attachInlinePolicy(attachInlinePolicy);

検証結果

上記コードをデプロイした結果は、下図の通りで、概要としては以下の通りです。(下2つは画像はないですが、目視で確認)

  1. すべて「カスタマーインライン」として扱われる(「カスタマー管理」ではない)
  2. addToPrincipalPolicyaddToPolicy のポリシーは1つにまとめられる
  3. 逆に inlinePolicies propsとattachInlinePolicy のポリシーはそれぞれ個別に扱われる
  4. inlinePolicies props のポリシーは、CFnテンプレートで AWS::IAM::Policy の定義は作成されない(「検証内容」の表に記載した通り)
  5. 他のポリシーはAWS::IAM::Policy の定義はあるが、実際に「ポリシー」として作成はされない(「カスタマーインライン」だから?)

個人的に2と3は逆の感覚だったので、ちょっと意外でした。

まとめ

上記を踏まえ、CDKでロールにポリシーをアタッチする際は、下記のようにするのがいいのではないかと思いました。(あくまで個人の見解です)

  • InlinePolicy 系は、1つの手段にまとめる。
    • CFnのクオータ(最大500個)を考えると、テンプレートが作成されない inlinePolicies props にまとめるのが良い?
  • それ以外は、コードの可視性とか管理のしやすさなどで決めると良い
    • 正直、あまり深く考える必要はなさそう(CFnテンプレートでも1定義に集約されるので)

告知

今週土曜日の2024/06/08(土)に石川県金沢市で「JAWS-UG金沢 #99 CDKワークショップやってみよう」というAWS CDKのイベントが開催されます。

jawsug-kanazawa.doorkeeper.jp

こちらのイベントにゲスト(講師?)として参加させて頂くことになりましたので、よろしくお願いします。(ちなみにワークショップの内容も私が作成しています)

まだ数名ほど参加可能のようなので、興味がある方はぜひ参加してみてください。

それでは、今回はこの辺で。

【Bun.js】Bun がメジャーリリースされたけど、本当にBun はNode.js に取って代わるのか?の最新状況

今回のお題

Bun.js v1.0.12時点で出来なかった一部機能について、Bun.jsの最新版ではどうなったか...の検証記事

はじめに&宣伝

いきなり宣伝になってしまうのですが、本日(2024/05/19(金))発売の「Software Design 2024年6月号」誌において、「第2特集:[実証]Bun 次世代JavaScriptランタイムの実体に迫る」の記事を担当させて頂きました。

※ちなみに、担当したのは第1章と第3章です。

gihyo.jp

また記事内に記載の通り、「第3章:BunとNode.jsの徹底比較」の内容は、私が2023/11/19(日) に開催された、JSConf JP 2023 において発表した「Bun がメジャーリリースされたけど、本当にBun はNode.js に取って代わるのか?をAWS Lambda で検証してみた」の内容がベースになっています。(資料は以下)

speakerdeck.com

なおこの資料の中で「困った点」として、当時のBun.jsでは出来なかった点を挙げています。(当時のバージョンはv1.0.12)

しかしあれからBun.jsは頻繁にアップデートが行われ、日本時間で今年の4/1(月)に(マイナーアップデートバージョンである)v1.1.0がリリースされました。*1 *2

そこで今回は「Software Design記事のおまけ」的な感じで、上記の「困った点」について『最新のBun.jsではどうなっているか』の検証結果を記事にしようと思います。

アジェンダ

上記「困った点」の3つについての検証です。

前提

Bun.jsはv1.1.2で検証しています。(最新版でも同じ結果になるはず)

最新版が使えない(packages/bun-lambdaが動かない)

どんな現象?

Bun.js公式リポジトリ には、packages/bun-lambdaという、Bun.jsをAWS Lambdaで動かす際に必要なパッケージがあるのですが*3、これがエラーになってしまい動かない、という現象です。

最新版では?

最新版では上記現象は改修されており、問題なく動作します。(なお権限系のエラーは別途対応が必要です。Software Designの記事にも対応方法を記載しています)

ビルドファイルが動かない

どんな現象?

ビルド時(bun build) にnpmモジュールをバンドルすると、ビルド後のjsファイル実行時にエラーが発生してしまい、動かないというものです。

正常に動かすためには、ビルド後のjsファイルの先頭に以下2行を明示的に追加する必要がありました。

outputFile.write('import { createRequire as createImportMetaRequire } from "module";   
import.meta.require ||= (id) => createImportMetaRequire(import.meta.url)(id);\n\n');  

最新版では?

こちらも最新版では上記現象は改修されており、上記2行を追加しなくても問題なく動作します。

モジュールモック未対応

どんな現象?

Bun.jsのテスト用モジュール(bun:test)で、npmモジュールなどのモジュールモックは未対応だった。

最新版では?

最新版ではモジュールモックにも対応しており、npmモジュールのモックも可能です。(詳しくは公式ドキュメント を参照。またサンプルソースを末尾に記載しておきます)

またマッチャーのJest互換性について、かなりの数のマッチャーが対応済ですが、まだ未対応のマッチャーも存在します。

ちなみに、bun:test でLambda関数のテストを実施する際の注意点について、以下に記載しておきます。

レスポンスをtoEqual() するだけではダメ

単体テスト時のレスポンスについて、await expect(response).toEqual(expected) みたいに、単に戻り値をtoEqual()しただけでは正しく判定できません。(おそらく、レスポンスがResponseクラス(のインスタンス)であることに関係していると思われます。) *4

とりあえずの回避策として、APIGatewayProxyResultなど、別の形式に変換することで対応できます。(末尾のサンプルソースを参照)

aws-sdk-client-mock-jest のマッチャーが使えない

Lambda関数内でAWS SDKを使用している場合、単体テストaws-sdk-client-mock-jest を使用するケースも多いと思いますが、aws-sdk-client-mock-jest の一部マッチャーが使えません。*5

これはBunとJestで、expect関数の戻り値の型が違うのが原因です。

  • Bun:Expect<T>型
  • Jest:JestMatchers<T>型

また、そもそもBun読み込んだ時点でJestのexpect(declare const expect)が上書きされてしまい、aws-sdk-client-mock-jestとの整合性が取れなくなってしまうようです。

これについては今のところ有効な対策がなさそうで、強いて言えば「bun:test を使わない(Jestを使う)」くらいしかありません。(もしわかる人がいましたら教えてください)

まとめ

以上、最新のBun.jsによる検証結果でした。
基本的に「困ったこと」の現象は全て改修されており、最新版では問題なく使えることが確認できました。

実際Bun.jsは今でも頻繁にアップデートが実施されているので、これからもどんどん使い勝手が良くなっていくでしょうね。
今後のBun.jsの進化に期待です。

最後に繰り返しになりますが、Software Design 2024年6月号、よろしくお願いいたします。

それでは、今回はこの辺で。

参考:npmモジュールモック&レスポンス変換のサンプルソース

import { expect, test, describe, mock, jest as bun_jest } from 'bun:test';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
import { mockClient } from 'aws-sdk-client-mock';
import { handler } from '../lambda/index_bun';
import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda";
  
// npmモジュールをモックする例。  
// ここでは「uuid」をモックしています。
mock.module('uuid', () => {
  return {
    v4: bun_jest.fn(() => '1111-2222-3333-4444'),
  };
});
  
const dynamoDbOutput = {
  Type: 'treasure',
  Floor: '100'
};
  
// aws-sdk-client-mock やRequest、Responseなどで必要になる設定  
const testUrl = 'https://dummy.example.com';
const ddbMock = mockClient(DynamoDBDocumentClient);
const dynamodb = new DynamoDBClient({});
DynamoDBDocumentClient.from(dynamodb);
  
const eventData: APIGatewayEvent = {
  queryStringParameters: {
    "floor": "3"
  } 
} as unknown as APIGatewayEvent;
  
const headers = new Headers({
  'Content-Type': 'application/json'
});
  
const responseOptions = {
  status: 200,
  headers
};
  
const responseString = JSON.stringify({
    uuid: '1111-2222-3333-4444',
    item: dynamoDbOutput,
});
  
const expectedResponse = new Response(responseString, responseOptions);
  
describe('index_bun.handlerのテスト', () => {
  test('レスポンスが正しい事', async () => {
    ddbMock.on(GetCommand).resolves({
      Item: dynamoDbOutput,
    });
    
    const res = await handler(new Request(testUrl));
    
    // ResponseインスタンスをAPIGatewayProxyResult型に変換
    const [resResult, expectedResult] = await Promise.all([createApiGatewayProxyResultResponse(res), createApiGatewayProxyResultResponse(expectedResponse)]);
    
    expect(resResult).toEqual(expectedResult);
  });
});
  
// ResponseインスタンスをAPIGatewayProxyResultに変換する関数
async function createApiGatewayProxyResultResponse(res: Response): Promise<APIGatewayProxyResult> {
  const contextType = res.headers.get('Content-Type');
  const body = await res.json();
  const result = {
    statusCode: res.status,
    headers: {
      "Content-Type": contextType,
    },
    body: JSON.stringify(body),
  } as APIGatewayProxyResult;
  
  return result;
}

*1:なお執筆時点での最新版はv1.1.8です。

*2:4/1という日付から「エイプリルフールネタなんじゃね?」という噂も流れました

*3:正確には「Bun.jsをAWS Lambdaで動かす際に必要になるLambda Layerを作成する」処理です

*4:レスポンスの内容が異なっていてもtoEqual()の結果がtrueになる。

*5:toHaveReceivedCommandWith()で確認

【AWS CDK】S3サーバーアクセスログを有効にすると、「The bucket does not allow ACLs」エラーが発生する

本日のお題

AWS CDK(以下「CDK」と記載)において、既存のS3バケットを別バケットの「サーバーアクセスログ」の送信先に設定すると、CloudFormation(以下「CFn」と記載)にデプロイした際にエラーになる

詳細

(Webホスティングを行っているS3バケットなど)Public Readに設定しているS3バケットにおいて、サーバーアクセスログを有効にしてアクセス監視・解析をするということは、運用上よくあると思います。(定義については「参考情報:AWS CDKでアクセスログを有効にする方法」を参照)

ところが、サーバーアクセスログ送信先に既存のバケットを指定した場合、これをデプロイすると、CloudFormationでなぜか以下のエラーが発生する場合があります。

Resource handler returned message: "The bucket does not allow ACLs (Service: S3, Status Code: 400, Request ID:..(中略)..., HandlerErrorCode: InvalidRequest)

本日はこの問題についてのお話です。

いきなり結論

先に結論から言ってしまうと、「サーバーアクセスログを有効にした場合、CDKが『ACLを有効にするCFnテンプレート』を出力する」ことがエラーの原因です。

サーバーアクセスログを有効にした場合、CDKは送信先バケットのプロパティとして、下記CFnテンプレートを出力します。

ただ、このテンプレートは、AccessControlObjectOwnership(=オブジェクト所有者) が ObjectWriter だったりすることからも分かる通り「ACLAccess Control List)が有効」の時のテンプレートになっています。

 {
   "AccessControl": "LogDeliveryWrite",
   "BucketName": "fortune-tmm-auth0-logo-bucket-dev-accesslog",
   "OwnershipControls": {
     "Rules": [
       {
         "ObjectOwnership": "ObjectWriter"
       }
     ]
   },
}

しかしS3のデフォルトは「ACL無効」なので*1、既存バケット(=ACLが無効)に対してこの設定を適用しようとすると、「ACL無効のバケットACL有効時のプロパティを設定しようとしている」となり、エラーになってしまいます。

なお、これはあくまで「既存バケット」の場合であり、新規作成するバケットならエラーは発生しないかもしれませんが、現在はS3バケットのアクセス制御はバケットポリシーを使用することが推奨されているため、「ACL有効」のプロパティを出力するのはあまりよろしくないです。

プロパティを(強引に)書き換える

上記の現象は「ACL有効の設定を強制的に書き換える」ことで対応できます。

具体的には下記の対応をします。

  • AccessControl を削除する
  • ObjectOwnershipBucketOwnerEnforced (=バケット所有者の強制)にする、または OwnershipControls を削除する*2

ただしL2 Constructでは上記を実行できないので、node.defaultChild を使用してL1 Constructに変換した後で実行します。

具体的には、先述のソースの末尾に下記ソースを追加します。

const cfnLogBucket = logBucket.node.defaultChild as s3.CfnBucket;
  
// ObjectOwnershipの上書き。
// もちろんaddPropertyDeletionOverride('OwnershipControls') でもOK  
cfnLogBucket.addPropertyOverride('OwnershipControls.Rules.0.ObjectOwnership', 'BucketOwnerEnforced');
  
// AccessControlの削除
cfnLogBucket.addPropertyDeletionOverride('AccessControl');

なお、addProperty系メソッドでプロパティに配列のキーを指定する方法は、以下のCDK公式ドキュメントを参照してください。
addOverride(path, value)

参考情報:AWS CDKでアクセスログを有効にする方法

AWS CDKでサーバーアクセスログを有効にする場合、下記コードを記載します。

import * as cdk from 'aws-cdk-lib';
import { aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib';  
  
// アクセスログを有効にするバケットのバケット名
const departureBucketName = `departure`;
  
// アクセスログの送信先Bucket
const logBucket = new s3.Bucket(this, `LogBucket`, {
  bucketName: 'destinationLogBucket' ,
  removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
});
  
// アクセスログの送信先Bucketのバケットポリシー
logBucket.addToResourcePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [new iam.ServicePrincipal('logging.s3.amazonaws.com')],
    actions: ['s3:PutObject'],
    resources: [`arn:aws:s3:::destinationLogBucket/*`],
    conditions: {
      ArnLike: {
        'aws:SourceArn': `arn:aws:s3:::${departureBucketName}`,
      },
      StringEquals: {
         'aws:SourceAccount': <アカウント番号>,
      },
    },
  }),
);
  
// アクセスログを有効にするBucket
const departureBucket = new s3.Bucket(this, `DepartureBucket`, {
  // blockPublicAccessは無くてもいいかも
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
  bucketName: departureBucketName,
  publicReadAccess: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
  serverAccessLogsBucket: logBucket,
  serverAccessLogsPrefix: 'logs/',
});
   
// アクセスログを有効にするBucketのバケットポリシー
// iam.StarPrincipal()は「Principal: '*'」という定義を作成するメソッド
departureBucket .addToResourcePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [new iam.StarPrincipal()],
    actions: ['s3:GetObject'],
    resources: [`arn:aws:s3:::${departureBucketName}/*`],
  }),
);

なお、サーバーアクセスログ送信先バケットに必要なバケットポリシーについては、下記のAWS公式ドキュメントを参考にしてください。
docs.aws.amazon.com

それでは、今回はこの辺で

*1:2023年4月より、ACLはデフォルトで「無効」となります

*2:ObjectOwnershipのデフォルト値がBucketOwnerEnforcedなので、OwnershipControlsを削除することでもObjectOwnershipをBucketOwnerEnforcedに出来ます

【Node.js】Node.js 22の新機能を確認する

※このブログは、4/30(火) 19:00~開催の「JAWS-UG札幌 オンラインもくもく会 #95」にて記載した内容になっております。

jawsug-sapporo.connpass.com

はじめに

4/24(水)に、Node.jsの最新版であるNode.js 22がリリースされました。

そこで今回は、さっそくNode.js 22の新機能についてチェックしようと思います。(ちなみに、Node.js 22はおそらく10月ごろからLTSになるものと思われます)

参考サイト

新機能一覧

新機能の概要はこちら。

概要 説明 experimental 備考
V8 Update to 12.4 V8エンジンが12.4にアップデートし、それに伴いWASMのガベージコレクションや各種メソッドが追加された × 各種メソッドについては「Pick Up」で説明
Maglev Maglevコンパイラがデフォルトで有効になった ×
Support require()ing synchronous ESM graphs CommonJSの「require」で、ES Moduleのソースを読み込めるようになった ただし条件あり。詳しくは「Pick Up」で説明
Running package.json scripts node.jsから直接package.jsonのscriptを実行できるようになった 詳しくは「Pick Up」で説明
Stream default High Water Mark High Water Mark(stream処理における内部バッファの閾値)の初期値が16KiBから64KiBに増加し、パフォーマンスが向上 ×
Watch Mode Watchモード(node --watch)がexperimentalからstableに ×
WebSocket ブラウザ互換のWebSocketがexperimentalからstableになり、デフォルトで有効に ×
glob and globSync ファイルの検索時に使うglobパターン指定用関数が追加 詳しくは「Pick Up」で説明
Improve performance of AbortSignal creation AbortSignalインスタンス生成時のパフォーマンスが向上 ×

Pick Up

ここからは、一部新機能について紹介します。

V8 Update to 12.4

ブラウザではサポートされていた下記の機能が使用可能になりました。

機能 説明 備考
Array.fromAsync 反復可能(=itelable)オブジェクトから値のみの(=シャローコピーされた)配列を返します
Setオブジェクト 型やプリミティブ/オブジェクトを問わず、いろいろな一意の値を格納できる 「一意の値」なので、重複した値は格納できない
Iterator helpers 反復可能オブジェクト(=Iterator)の各種インスタンスメソッドを使用可能に filter, find, mapなど、Array.prototypeで使用可能なメソッドと同じものが多い

なおこれらについて、ソースコードや説明に関してはMDN Web Docsにかなり詳しく書いてあるので(特にSetクラス)、そちらを参照して下さい。(上表の「機能」にリンクを貼っておきます)

Setオブジェクトのメソッド紹介
  • いずれのメソッドも、戻り値としてSetオブジェクトを返します
  • A, BはどちらもSetオブジェクト
メソッド 戻り値のSetオブジェクトに含まれる値 備考
A.intersection(B) A, B両方に含まれている値 積集合
A.union(B) A, Bいずれかに含まれている値 和集合。なおA. B両方に含まれる値は1つにまとめられます
A.difference(B) AにあってBにない値 差集合
A.symmetricDifference(B) AかBのいずれか一方にしかない値 対象差集合。AとB両方が持つ値は除去されます
Iterator helpersの一部メソッドの紹介

Iterator helpersのメソッドのうち、Array.prototypeが持っていないものを紹介します。

メソッド 説明 備考
drop(limit) 元のIteratorから、先頭limit個分の要素を除去したIterator helperを返却します Arrayで言うArray.slice(limit)
take(limit) 元のIteratorから、先頭limit個分の要素のみを持つIterator helperを返却します Arrayで言うArray.slice(0, limit)
toArray() Iteratorから通常の配列(=Arrayクラスの配列)を返します Array.from(iterator)と同じ
小ネタ:Array.fromAsyncでファイルを読み込む

MDN Web Docsにも記載がある通り、ReadableStreamは非同期反復可能オブジェクトです。
つまり「fs.createReadStream*1で読み込んだストリーム(≒ファイルの内容)をArray.fromAsyncで取得する」という事が可能です。(実用的かどうかは別として)

以下がサンプルソースになります。

const fs = require('fs');  
  
// sample.txtには「I am Node.js 22.」というテキストが書いてある
async function getAsyncIterable() {
  const fileName = 'sample.txt';
  const stream = fs.createReadStream(fileName);
  stream.setEncoding('utf8');
  
  const result = await Array.fromAsync(stream);
  
  // 戻り値は配列なので、配列の要素を取得する
  return result[0];
}
  
(async () => {
  const result = await getAsyncIterable();
  console.log(result);
})();

上記ソースを実行すると、以下の結果が返ってきます。

$ node index.js
I am Node.js 22.

Support require()ing synchronous ESM graphs(Experimental)

CommonJSの「require」にて、ES Module形式のモジュール(関数なりクラスなり)を読み込むことが可能になりました。

ただし読み込み元のES Module形式のファイルは、以下を両方満たしている必要があります。

  • 対象のpackage.jsonに「"type": "module"」の定義がある、または拡張子が「*.mjs」である。(=ES Module形式で書く際のルール)
  • すべての処理が同期処理である(=awaitなど、非同期処理を含んでいない)

また現段階ではこの機能は「Experimental(=実験的)」であり、実行時に --experimental-require-module オプションをつける必要があります。

サンプルソースは以下になります。

// point.mjs
export function myPow(a, b) { return Math.pow(a, b); }
function mySqrt(a) { return Math.floor(Math.sqrt(a)) };
  
export default mySqrt;
// index.js
const required = require('./index.mjs');
  
async function requiredSample() {
  console.log(`mySqrt is ${required.default(9)}`);
  console.log(`myPow is ${required.myPow(2, 3)}`);
  
  const imported = await import('./index.mjs');
  console.log(imported === required);
  console.log(`mySqrt is ${imported.default(25)}`);
  console.log(`myPow is ${imported.myPow(3, 4)}`);
}

(async () => {
  await requiredSample();
})();

上記index.jsは、下記コマンドで実行し、その結果は以下の通りです。

$ node --experimental-require-module index.js
mySqrt is 3
myPow is 8
true
mySqrt is 3
myPow is 8

importedとの比較結果がtrueで、各関数の実行結果も同じなので、requireとimportで全く同じである事が分かると思います。

Running package.json scripts(Experimental)

package.jsonの「scripts」について、いままではnpm/yarnなどパッケージマネージャー経由で実施していましたが、nodeから直接実行できるようになりました。

なお、こちらも現段階ではExperimentalとなっています。

具体的なソース&実行例は以下の通りです。

// package.json
{
  "scripts": {
    "hoge": "echo hogehoge"
  }
}
$ node --run hoge
hogehoge

glob and globSync(Experimental)

何かの処理対象ファイルを指定する際などに使用可能なglobパターンについて、下記関数が追加され、Node.jsだけで完結できるようになりました。

  • fs.glob(pattern[, options], callback)
  • fs.globSync(pattern[, options])
  • fsPromises.glob(pattern[, options])

サンプルソースを下記に示します。(なお、カレントフォルダには以下ファイル&フォルダが存在するものとします)

  • node_modules
  • .gitignore
  • index.js
  • index.mjs
  • package-lock.json
  • package.json
  • sample.txt
// index,js
const fs = require('fs');
const fsp = require('node:fs/promises');
  
// いずれの関数も「package.json」と「package-lock.json」が
// ヒットすることを確認しています。
  
// fs.globとfs.globSyncの戻り値はファイル名の配列。  
// 違いは同期関数かどうかだけです
function getAsyncGlobPathFs() {
  fs.glob('./*kag*.js*', (err, matched) => {
    if (err) throw err;
    console.log(matched);
  });
}
  
function getSyncGlobPath() {
  const matched = fs.globSync('./*kag*.js*')
  console.log(matched);
}
  
// fsPromises.glob戻り値は非同期反復可能オブジェクト(AsyncIterator)なので、  
// ファイル名はそこからさらにArray.fromAsyncなどで取得する必要があります。
async function getAsyncGlobPathFsPromise() {
  const matched  = await Array.fromAsync(fsp.glob('./*kag*.js*'));
  console.log(matched);
}
  
(async () => {
  getAsyncGlobPathFs();
  getSyncGlobPath();
  await getAsyncGlobPathFsPromise();
})();
$ node index.js
[ 'package-lock.json', 'package.json' ]
[ 'package-lock.json', 'package.json' ]
[ 'package-lock.json', 'package.json' ]

個人的には、これが一番実用的かなと思いました。

まとめ

以上、Node.js 22の新機能でした。

今回も機能面・性能面で色々追加されており、Node.jsでの開発がもっと楽になるといいですね。

それでは、今回はこの辺で。

*1:fs.createReadStreamの戻り値fs.ReadStreamは、stream.Readableを継承しています。

【退職エントリー】株式会社DeNAを退職しました

※2024/04/28:諸事情により、一部内容を修正しました。

はじめに

2022年9月から約1年半勤めた株式会社DeNA(以下DeNA)を4/26(金)で退職しましたので、その退職エントリーを書こうと思います。

※正式には5/31(金)退職で、4/26(金)最終出社となりました。(ちなみに私のゴールデンウィークは37連休です)

DeNAで何をやってたの?

肩書き上は「クラウドアーキテクト」として、

  • モバゲーのアプリ開発部署にてAWSの管理や関連サービスのアーキテクチャ設計、
  • IaC(Infrastructure as Code, 業務ではServerless FrameworkやAWS CDK)を用いたインフラ構築、運用管理、
  • 一部開発・テスト(TypeScript/Jest/Node.js)

なんかを担当していました。

その他、DeNAがプラチナスポンサーをつとめるYAPC:2023, YAPC:2024での支援、及び各種イベントでの登壇なんかをやりました

自分はDeNAに就職する前は6年間フリーランスだったのですが、久しぶりに正社員に戻って、色々経験することができました。

特にモバゲーという大規模サービスの運用に携わることができたこと、そしてYAPC:2023, YAPC:2024といった大きな技術カンファレンスにスポンサーとしてブース出展などに携わることができたのは、とても良い経験になりました。

何で辞めようと思ったの?

私がJAWS-UG CDK支部(と名古屋支部)に深く携わっていることもあり、IaC(特にAWS CDK)を用いたインフラ構築・組織でのAWS運用やオブザーバビリティ・SREに非常に興味を持ち、それを深く追求していきたいと考えるようになったからです。

もちろんDeNAでもそれは考えており、昨年の秋くらいから色々それができるよう、自分なりに模索したんですが、色々なタイミングなどがうまくかみ合わず、残念ながら思うような結果が出ませんでした。(そりゃアプリ「開発」部署ですからね...)

とはいえ、同じグループメンバーとの仲は良好でしたし、そこまで「今すぐ辞めてやる」というよりは「どうするか葛藤している」という感じだったので、かなり悩みました。) *1

ただそれでも、今になって振り返ると「ああいうアクションを取ればよかったんじゃないか」「こういう視点が足りなかったんじゃないか」と自分に足りなかった点があると感じる部分は多々あります。(自分なりに結論が出て、いろいろ落ち着いた状態になったから色々アイデアが出てくるのかもしれません)

次は何するの?

(4月~5月は有休消化で)6月からは某アプリにおいて、AWS環境のIaC(AWS CDKやTerraform)を用いたインフラ構築・組織でのAWS運用・オブザーバビリティ・SREといった業務にガッツリ携わります。

業務でソースコード(TypeScriptなど)を書く機会は減ると思いますが、全く書かないことはないでしょうし、個人では引き続きバックエンドアプリエンジニアとして、主にLambdaのソースコードを書くことと思います。

Special Thanks!

株式会社DeNAの関係者の皆さん、1年半お世話になりました。 6年間のフリーランス生活から会社員に戻って、色々新しい経験することができました。ありがとうございました!

それでは、今回はこの辺で。

*1:実際、今でも自分の選択肢が正しかったのか、と色々考えますからね...