私が歌川です

私が歌川です

@utgwkk が書いている

utgw.netをCloudFront + AWS Lambda Function URLに移行した

移行しました。ペライチなのでそんなに難しいことはないだろうと思ったけど、細々とハマりどころがありました。

utgw.net

前回の移行はこちら。

blog.utgw.net

前提

モチベーション

今回の移行のモチベーションは、「おもしろ全部」が8割、Next.js製WebアプリケーションをLambda Function URLで配信する練習をそろそろやっておくか、という気持ちが2割です。

最近は、Next.js製のBFFサーバーをいかにして無限にスケールさせるか、への関心が高まっています。Lambda関数としてデプロイできればスケーラビリティが得られるんじゃないか、ということはよく思うけど試せていなかったので、自分の個人サイトというおもちゃで試してみるか、と思った次第です。

やり方 & ハマりポイント

先に結論から言うと、Next.js製アプリケーションをLambda Function URLにデプロイする方法については以下の記事でだいたい解説されているので、そっちをまず読むのがいいと思います。

serverless.co.jp

ここから先は細々としたハマりどころを紹介するコーナーになります。

CloudFront経由でLambda Function URLを実行すると {"Message": null} というエラーが返る

以下のStackOverflowと同じことが発生していました。

stackoverflow.com

確かオリジンリクエストポリシーを AllViewer ではなく AllViewerExceptHostHeader に設定することで直ったはず。HostヘッダがCloudFrontのものに上書きされた状態でリクエストがLambda Function URLに到達しておかしくなっていたのかな。

S3にあるはずのオブジェクトパス指定してリクエストを送っても404が返ってくる

これも先述したのと同じでオリジンリクエストポリシーを AllViewerExceptHostHeader に変えたら直った気がする。このあたりはコンソールをポチポチいじって試行錯誤しまくったので少し曖昧です。

静的ファイルがないとき404ではなく403が返る

CloudFrontからS3のオブジェクトを参照できるようにするポリシーを設定するとき、コンソールで指示されるJSONをコピペすると、存在しないオブジェクトを参照したときにHTTP 404ではなく403が返ってきます。

既にピンと来ている人もいると思いますが、GetObject APIを叩くクライアントがListBucket APIを叩く権限がないと404が返ってこない例のアレです。

docs.aws.amazon.com

これはAPIのドキュメントにも書いてあるし、いろいろ思うところはあるけど変えづらいのでしょう……ということでバケットに対してListObject APIを実行する権限を足してやりましょう。

If you have the s3:ListBucket permission on the bucket, Amazon S3 returns an HTTP status code 404 Not Found error.

If you don’t have the s3:ListBucket permission, Amazon S3 returns an HTTP status code 403 Access Denied error.

ACMの証明書はus-east-1リージョンで発行する

1敗

Next.jsのレスポンスキャッシュ

App Routerのキャッシュ機構についてまだ把握しきれていなくて、ドキュメントをつまみ食いしながら設定していました。

nextjs.org

ページコンポーネントと同じモジュールから revalidate: number という変数をexportすることでキャッシュの有効期限を設定できるようです。たとえば以下のような変数を定義すると、レスポンスが10分間キャッシュされます。

export const revalidate = 1200

この設定でNext.js 13.4.9を使っていたときは Cache-Control: s-maxage=1200, stale-while-revalidate というレスポンスヘッダが付与されており、stale-while-revalidateって秒数の指定が要るのでは? と思っていたけど、Next.js 15.0.3に上げたら Cache-Control: s-maxage=1200, stale-while-revalidate=31534800 に変わりました。

CloudFrontはstale-while-revalidateに対応しているので、これでキャッシュの期限が切れたときのレイテンシが隠蔽しやすくなりそう。

aws.amazon.com

Next.jsレイヤでのリダイレクトをキャッシュさせる

CloudFrontで3xx系のステータスコードのレスポンスをキャッシュさせるには、オリジンから Cache-Control レスポンスヘッダを付与してやる必要があります。

Next.jsには next.config.js にリダイレクト設定を書く機能がありますが、これだけだと Cache-Control レスポンスヘッダが付与されません。

nextjs.org

いったん以下のようなミドルウェアを書いてしのいでいます。

import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/labs")) {
    const redirectUrl = new URL(
      request.nextUrl.pathname,
      "https://sugarheart.utgw.net"
    );
    return NextResponse.redirect(redirectUrl, {
      status: 308,
      headers: {
        "Cache-Control": "public, max-age=31536000, s-maxage=31536000",
      },
    });
  }
}

おわりに

aws-lambda-web-adapterや先人のブログ記事のおかげで、そんなに大きくハマることもなくNext.js製WebアプリケーションをCloudFront + Lambda Function URLの構成に移行できました。もっと複雑な構成になっている場合はいろいろあるかもしれません。

Lambda関数自体のデプロイはlambrollでやっていたのですが、こちらは全くハマることがなく使えました。便利ですね。

クラウド破産が見えてきたらまたお知らせします。

Duolingoの韓国語コースが一区切りついた

Duolingoの韓国語コースのセクション2が完了した結果、精神と時の部屋みたいなのが開始した。クリア後の世界かもしれない。


2024/11/18 時点で438日連続記録になっているので、1年以上ほぼ毎日韓国語に触れていたということになる。簡単な文はふつうに読めるようになっている。語彙をもうちょっと増やすといいのだろう、という段階だと理解している。

blog.utgw.net

TSKaigi Kansai 2024に参加した #TSKaigiKansai

参加しました。みやこめっせは非常に近いので気軽に行けて便利ですね。みやこめっせに同人イベント以外で行ったのは大学の卒業式以来なので、実に5~6年ぶりとかになるんじゃないか、マジか~。

kansai.tskaigi.org

トーク

docs.google.com

canalunさんのキーノート。いきなり宇宙人に連れ去られる寸劇から始まってなんなんだという感じでした。クワインを構成可能であることの証明は何気に初めて目にしたかも。スマートコントラクトの世界では意図的に言語をチューリング完全じゃなくしている、という話は知らなかったです。

tocomiさんの「型チェック 速度改善 奮闘記」は、まさに明日から役に立つTypeScriptの型チェック高速化のレクチャーという感じでよかったです。Naming Complex Typesパターンにはキャッシュが効きやすくなるという側面もあるのか~。tscのコマンドライン引数でプロファイリング用の情報を出力できるのは知りませんでした。

speakerdeck.com

ホリちゃんさんの「TypeScript Graph でコードレビューの心理的障壁を乗り越える」はコードの依存関係・変更箇所を可視化することでコードの構造をとらえやすくする、という話でした。これは依存関係の表現方法を差し替えられるようになっていたら任意のプログラミング言語に対して横展開できるようになって夢が広がるのでは!?

speakerdeck.com

sadnessOjisanさんの「TypeScript、上達の瞬間」は、個人的にベストトークを選ぶならこれになるでしょう。どのようにTypeScriptの体系に馴染んでいったのか、がすらすらと話されていたと思います。また、「自分はTypeScript自体に興味があるわけではない」というようなことを仰っていたと思うのですが、この言葉にも勇気づけられるものがありました。何も全てを突き詰めなければいけないのではなく、必要に応じて手を伸ばして回るならそれでいい、という態度もまた重要だと思います。

speakerdeck.com

izumin5210さんの「よくできたテンプレート言語として TypeScript + JSX を利用する試み」の話は、日頃からけっこう思っていることがまさに実践されているな〜と思いながら聞いていました。我々が欲しいのは静的解析が効くテンプレートエンジンなのではないか、というのをまさに体現していました。JSONを生で書くのはつらいよ〜という場面ではよく効きそう。

交流とか

会場に行ったら id:myfinder さんがいたのでいろいろ話していました。ここだけ切り出すと急にYAPCっぽくなりますね。あとはベースマキナさんのブースで id:f_syumai さんや id:yebis0942 さんなど普段はGoのほうで見かける人たちもいて、TypeScriptのイベントに初めて来てみたけど意外と知ってる人いるな? という感じでした。

ほかにもスポンサーブースをいろいろ回ってみて、TypeScriptバックエンドを採用するようになった理由を聞いてみたり、TypeScriptクイズ*1を解いたり、脱リモートワークの話をしたりなどしていました。知ってる人はそこそこいたけど会社の人がぜんぜんいなかったような。

懇親会では、Next.jsのApp RouterやServer Actionsは人類には早いのでは、とか、Node.jsの実行モデルを考慮した上でスケーリングすることを考えて実装できているのか? とかそういう話をずっとしていたような気がします。

二次会は行きつけのビアバーに行きましょう、ということで知ってる人から順番に声をかけて人数を増やし、GOでタクシーを3台手配したら1台ロストしてしまいウケました。生きてるとこういうこともあるんですね。

そして人数を増やしまくった結果全員は入店できないということだったので、急遽マネーフォワードさんのオフィスに入れていただきました。いろいろ雑にやりすぎてすみません、なんとかなってよかった~~。キャパが無限*2って最高ですね。id:luccafort さんありがとうございました。

マネーフォワードさんのオフィスに入る前に何人かで飲みに行っていたのですが、長濱浪漫ビールの店舗が三条にできていたのをそのとき初めて知りました。

www.romanbeer.com

次回はもうちょっとキャパシティを考えて店を手配しようと思います。あとは人数が増えすぎたら適宜グループを分割するとか……。

おわりに

普段はKyoto.js以外にはフロントエンドっぽいイベントには行かないのですが、近場で開催されるということで顔を出してみました。けっこう面白い話を聞けたし、懇親会や二次会でもいろいろ話せたしよかったと思います。次回も関西で開催されるならまた行きます。

そういえば、ホームページのURLに年号 (2024) が入っていないのがずっと気になっています*3

*1:BitKeyさんのクロスワードパズルの難易度が一番高かった

*2:実際には無限ではない

*3:アンケートで送信済

AWS Step FunctionsのHTTP TaskでMackerelにメトリックを投稿する

やってみた記事ってやつです。

AWS Step Functionsから任意のHTTP APIを叩けるようになった*1ので、これを使ってMackerelに投稿してみましょう。

投稿するメトリックを用意する

サービスメトリックを投稿するときのリクエストボディの形式に沿ったJSONを組み立てるために、↓こういうしょぼいLambda関数を用意してみます。

export const handler = async (event) => {
  const time = Date.now() / 1000
  const value = Math.random() * 10000000
  return [
    { name: "custom.example_metrics.metric1", time, value }
  ]
};

Node.jsランタイムで実装する場合、非同期処理がなくてもハンドラは非同期関数として定義しておかないと、ステートの実行結果が常に null になってしまうので注意してください (1敗)。

EventBridge Connectionを用意する

  • 認証タイプ: API Key
  • APIキー名: X-Api-Key

でMackerelのAPIキーをセットしたらよいです。

ステートマシンを用意する

エラーハンドリングやリトライ処理を全部省略したステートマシンをつくりおきします。ARNとかservice nameって書いてあるところは必要なリソースのARNとかを書いてください。固定値でいいならLambda関数をはさむ必要すらないかも。

{
  "StartAt": "Construct request body",
  "States": {
    "Construct request body": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "<function ARN>"
      },
      "ResultSelector": {
        "mackerelRequestBody.$": "$.Payload"
      },
      "Next": "Post service metrics to Mackerel"
    },
    "Post service metrics to Mackerel": {
      "Type": "Task",
      "Resource": "arn:aws:states:::http:invoke",
      "Parameters": {
        "ApiEndpoint": "https://api.mackerelio.com/api/v0/services/<service name>/tsdb",
        "Method": "POST",
        "Authentication": {
          "ConnectionArn": "<EventBridge Connection ARN>"
        },
        "Headers": {
          "Content-Type": "application/json"
        },
        "RequestBody.$": "$.mackerelRequestBody"
      },
      "End": true
    }
  }
}

図示するとこう。図示するまでもなかったですね。

gyazo.com

おわりに

Step FunctionsからMackerelに投稿するにあたって、追加のLambda関数なしでHTTPリクエストを投げられるようになり、いい時代になりました。

HTTP TaskがVPCに対応するようになったら、もう1つLambda関数を減らせるな~と思いながら開発中のプロダクトのほうを見ているので、機能拡充されるとよいですね。

*1:VPCには対応していないので、パブリックネットワークから到達可能なHTTP APIにのみリクエストできる

MySQLの生成カラムをMODIFY (CHANGE COLUMN) できるかどうかはあんまり自明じゃない

この記事のタイトルにあることの理由は MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.1.9.2 ALTER TABLE および生成されるカラム を読んだら分かります。


生成カラムの式を変えてsqldefでマイグレーションを反映したら、DROP COLUMNしてADD COLUMNするDDLが実行されたことで、表題の件に気づいた。

ものすごく単純化すると、たとえば以下のようなDDLが実行されることがある。

ALTER TABLE `users` DROP COLUMN `id_mod`;
ALTER TABLE `users` ADD COLUMN `id_mod` int GENERATED ALWAYS AS (id % 5) VIRTUAL AFTER `id`;

参照しているカラムをDROPされると、稼動中のアプリケーションでエラーが発生してしまう。これは都合がよくない。以下のように生成カラムに対してもCHANGE COLUMNで安全にカラム定義を変更できるはず。

ALTER TABLE `users` CHANGE COLUMN `id_mod` `id_mod` int GENERATED ALWAYS AS (id % 5) VIRTUAL;

ということで、以下のissueを起票した。

github.com

ところで (個別のDBMSの実装にもよると思うけど) sqldefが何故このようなDDLを出力するのか。この記事を書いた時点ではちゃんと実装は追っていないけど、MySQLのドキュメントを読みにいったらだいたい分かった。

dev.mysql.com

生成カラムの定義は常にin-placeに変更 (MODIFYやCHANGE COLUMN) できるわけではなく、条件によっては一度DROP COLUMNを経由する必要がある。今回適用したいマイグレーションはこの条件に当てはまらない。DROP COLUMNしてADD COLUMNするのは (一部のケースを除いて) 常に可能である。

「CHANGE COLUMNできるスキーマ変更かどうか」がうまく (sqldefの枠組みの中で) 判定できる方法が分かったらPRを出したいですね。

3年ぶり! 川見てるアドベントカレンダー2024やります

川見てるアドベントカレンダー2024をやります。

adventar.org

前回開催は2021年、実に3年ぶりの川見てるアドベントカレンダーということになります。

blog.utgw.net

川をはじめとした、海、湖、滝、流れなどについて語ったり、写真を貼ったりしませんか。前に取り上げたことがある川などであっても構いません。どうぞよろしくお願いします。

Kaigi on Rails 2024に参加した #kaigionrails

参加しました。

kaigionrails.org

普段の仕事では主にGo/TypeScript/Reactあたりを書いています。Rails周辺のぼんやりとした世界観は知ってるかも、ぐらいの立場です。

よかったトーク

speakerdeck.com

推しVTuberのために匿名質問サービスを高速に作り、仲間を集める、という流れが非常に美しく、懐しさすら感じさせる発表でした。こういう勢いをたぶん失っているんだろうな、とも……。

speakerdeck.com

どこのプロダクトでも長時間の非同期ジョブに悩まされているのだ、と知れて勇気づけられました。非同期ジョブの実装側でlong-runningなジョブを中断・再開できるようにする仕組みは面白かったです。一方で、ジョブをじゅうぶん小さな単位に分割して並列実行したのち結果をマージする、といった手法も考えられないか? と思い、そのあたりを懇親会で議論できたのでよかったです。

speakerdeck.com

スループットを上げるために1行1在庫で管理する、逆に決済システムのレートリミットを考慮してスループットをあえて下げるなど、正しくパフォーマンスと向き合っている、と感じました。1行1在庫だと行数が爆発しないか? と思ったけど、一時的なイベントなら不要になった行は消せるし、200万在庫をさばいた実績がある、とのこと。

speakerdeck.com

ActiveJobやSidekiq、そしてSolid Queueができるまでの歴史の話が面白く、TheSchwartzFireworqを使っているプロダクトが身近にあるのでなかなか遠くないところの話のようにも感じました。最近だとAWS SQSを使った仕組みを手作りするなどの事例があるのですが、「非同期処理」と一口に言ってもいろいろあるし、Sidekiqのwikiを一度読んでおくといいんだろうな、と思いました。

speakerdeck.com

データマイグレーションの決定解みたいなの、Rails界ならあるでしょって勝手に思っていたけど意外とまだないんですねえ。手元からスクリプトを打ってデータを修正した経験は自分もあるけどいまいちだし、かといって最強の仕組みを作るほど手をかけづらい場合もあって悩ましい。サーバーレスかつ安全にonetimeスクリプトを実行する最強の環境セットがあるといいのか??

speakerdeck.com

最終日の基調講演です。修復とは元に戻すことではなく変化に適応しつづけること、という言葉が目から鱗でした。これはフレームワークもそうだし、長寿プロダクトとかにも言えることになりそう。

Railsエンジニアじゃない立場から見た「Rails way」

冒頭で述べたように、普段はサーバーサイドの実装言語としてGoを使っています。昔はPerlも書いていました。

Railsに対して、やっぱり道具がひと通り揃っているのは強いだろう、ということをよく思います。重厚よりは簡素なフレームワークを選び、足りない道具があれば自分で実装する、というのを仕事でよくやっているのですが、一方でRailsの方を見て、こういうのは数行ぐらいで完成するんだろうな*1……と思うことは少なくありません。密結合で高速に走れる、という言葉を耳にしたことがあります。

一方で、Rails wayにうまく乗れないと気持ちのいい開発体験が損われがちで、そのあたりのノウハウが模索されている部分もあるんだろうな、とも感じました。モデル設計やHotwireなど、うまくハマるといいけどちゃんと考えないと苦しくなる面はありそうで、Railsだから思考停止でいい、ということはなさそう。「隣の芝は青い」みたいな感じで、どこにいてもちゃんと考えてプロダクトを作っていかないといけない、ということですね。

今の仕事にRails wayをそのまま取り込めるかは分からないけど、ある種のヒントは得られたかもしれません。とくに、最後の基調講演での話はRailsと関係なく普段から意識できるとよいのだろうな、と思いながら聞いていました。

おわりに

Railsは普段書いてない、という立場で参加しましたが、けっこう学びになるトークが多かったと思います。見れていないトークも興味深そうなものがいくつかあるので、資料を見返しておきます。一方、もっと深掘りできるとよさそうだけど15分トークだとギリギリすぎるか、ということも思ったので悩ましいですね。

来年も何もなければ参加します。会場が東京駅直結になるっぽくてすごい。

写真コーナー

*1:実際には数行では済まないかもしれないけど