私が歌川です

@utgwkk が書いている

docker runでコンテナにホスト側のAWS CLIの認証情報を伝えたい

はじめに

docker run コマンドで実行するDockerコンテナ内でAWSのAPI呼び出しを行うとき、何もしないと認証情報がなくて失敗すると思う。なんとかしてホスト側の認証情報を伝えたい。なお、令和なのでアクセスキーはホストのどこにも (~/.aws/credentials ファイルにも) 永続化されておらず、一時的な認証情報 (SSOやAssumeRole) しかないものとする。

やりかた

結論から言うと、aws configure export-credentials コマンドを使って認証情報をアクセスキー・シークレットキーの形式で取得できるので、これが使える。ホスト側でアクセスキーを使っていない限りは永続性のない (有効期限のある) 認証情報として取り回せる。

aws configure export-credentials --format env-no-export のようにフォーマットを指定してあげることで、以下のような形式で環境変数の定義が取得できる。実用上は AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY があればよくて、残り2つの環境変数は要らない気がするけど、今のところ実害を確認できていないので放置してる。--format env にすると環境変数を export する文字列が出てくる。

AWS_ACCESS_KEY_ID=xxxxxxx
AWS_SECRET_ACCESS_KEY=yyyyyyy
AWS_SESSION_TOKEN=zzzzzzz
AWS_CREDENTIAL_EXPIRATION=YYYY-MM-DDThh:mm:ssZ

あとはこれをなんとかして docker run コマンドで使える形に整形したらいい。docker run コマンドでコンテナに環境変数を渡すには -e オプションを使う必要があるので、ひと工夫したらよい。リージョンだけ個別に渡してあげる必要があるので注意。以下はamazon/aws-cliイメージを使う例。サブシェル内で認証情報を環境変数としてexportしつつコマンドを実行している。

% ( eval $(aws configure export-credentials --format env); docker run --rm -e AWS_REGION=ap-northeast-1 -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN amazon/aws-cli sts get-caller-identity )

あるいは、bashやzshを使っているのであれば --env-file オプションと匿名パイプを組み合わせるともっとシンプルに書ける。<(cmd) のように書くことでコマンドの実行結果をファイルディスクリプタ経由で取得できる。少なくともMac zsh + Rancher Desktopではこれで動作した。

% docker run --rm -e AWS_REGION=ap-northeast-1 --env-file=<(aws configure export-credentials --format env-no-export) amazon/aws-cli sts get-caller-identity

追記

もともとは aws --profile configure export-credentials コマンドの実行結果をワンライナーで加工して -e AWS_ACCESS_KEY_ID=xxxxx -e AWS_SECRET_ACCESS_KEY=yyyyyyy のようなコマンドライン引数を組み立てる方法を紹介していましたが、これだとコマンドライン引数から認証情報の中身が見えてしまう問題があるため、修正しました。

まとめ

aws configure export-credentials コマンドを活用することで、ホストのAWS CLIの認証情報を環境変数経由でコンテナに伝えることができて便利。

参考

ミニPC日記

Windows10のサポートが終了し、ESU (拡張セキュリティアップデート) プログラムに登録していったんしのいでいたが、世界情勢やらAIの台頭やらでいつ新しいWindowsマシンを買うのがよいか分からない状態になっていた。10年前のデスクトップマシンはWindows11にアップデートできない。性能的には困っていないのだけれど、困ったね。

ちょっと考えて、このミニPCを買った。考えてみるとWindowsマシンでヘビーなゲームをするわけじゃないし、インターネットやYouTubeが見れたらじゅうぶんだろう、というところに落ち着いたのであった。仕事でも趣味でも開発にはMacを使うようになっている。

たまにファンが回る音が気になるけど、今のところ快調に動いている。SSDが512GBなので換装してもいいかもしれないけど、困ったら考えることにする。

古いマシンのSSDやGPUはどうしようかな……。

React Testing Libraryでuse関数を使うコンポーネントをテストする (workaround編)

はじめに

PromiseをawaitせずにReactコンポーネントのpropsとして引き回すように修正し、テストコードで単に値を Promise.resolve() でラップするようにしたらテストがじゃんじゃん落ちて困った。ちょっと試行錯誤して直せたので事例共有とします。たぶんworkaroundなのでタイトルもそのようなテンションにしています。

利用しているライブラリのバージョンは以下の通り。

  • react, react-dom 19.2.4
  • @testing-library/react 16.3.2
  • jsdom 29.0.2

やりかた

use 関数*1を使ってPromiseの解決を待つ (suspendする) コンポーネントがあったとして:

import { use } from "react";

type SuspendComponentProps = {
  data: Promise<string>;
};

export function SuspendComponent({ data }: SuspendComponentProps) {
  const str = use(data);
  return <h1>Hello, {str}!</h1>;
}

コンポーネントに渡したPromiseが解決された後の状態をテストするとき、単に render 関数を呼ぶのではなく、render 関数の呼び出しを await act(async () => ...) で囲んでやる必要がある。つまり、以下のテストコードは1つ目のテストケース (rendered with await act(async () => ...)) しか通過しない。2つ目以降のテストケースでは screen.findByText("Hello, foo!") が1秒でタイムアウトする。

import { act, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SuspendComponent } from "./SuspendComponent";
import { Suspense, ReactNode } from "react";

function wrapper({ children }: { children: ReactNode }) {
  return <Suspense fallback={<div>loading...</div>}>{children}</Suspense>;
}

describe("SuspendComponent", () => {
  it("rendered with `await act(async () => ...)`", async () => {
    await act(async () => {
      render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });

  it.fails("rendered with `act(() => ...)`", async () => {
    act(() => {
      render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });

  it.fails("rendered without `act(async () => ...)`", async () => {
    render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });
});

以下のリポジトリで実際に動作確認できる。

github.com

なぜこうなるのか

どうやら以下のissueの事象が起こっていそう。すごくざっくり言うと、React 18→19で挙動が変わったことの影響を受けているらしい。

github.com

12:21 追記: actrender 関数を非同期にする、というPRが用意されているっぽい。非互換変更なのでいろいろありそう。

github.com

React Testing Libraryでuse関数を使ってuse関数を使うコンポーネントをテストする (workaround編) - 私が歌川です

へーと思って調べてみたら <a href="https://github.com/testing-library/react-testing-library/pull/1214" target="_blank" rel="noopener nofollow">https://github.com/testing-library/react-testing-library/pull/1214</a> で render 関数を非同期にして内部で await act(...) 相当のことをするようになるっぽい。これマージされると良いですね。

2026/04/09 10:31
b.hatena.ne.jp

おわりに

React Testing Libraryでuse関数を使ってsuspendするコンポーネントをテストする場合、もうしばらくは render 関数を await act(async () => ...) で囲む必要がありそう。少なくとも、自分が遭遇したパターンでは、この記事に書いたような修正が有効そう。

*1:useで始まる関数だけどフックではないので、コンポーネントやカスタムフック中の条件分岐やループの中で呼び出してもよい

utgw.netをNext.js App RouterからHonoに移行した

はじめに

utgw.netをNext.js App RouterからHonoに移行しました。Claude Codeが一晩でやってくれました。

utgw.net は id:utgwkk のホームページです。前回までのあらすじは以下の記事を読んでください。

blog.utgw.net

blog.utgw.net

なぜ移行したのか

個人サイトなので好きにしたらいい、というのは前提であるとして……。

Next.js App Router (以下、単にApp Routerと書く) のキャッシュ戦略と、自分のホームページでやりたいキャッシュ戦略が噛み合っていないのをいい加減解決しよう、というのが移行のモチベーションです。

utgw.net には、SpeakerDeckのRSSフィードから最新のスライド一覧を取得して表示する面があります。都度SpeakerDeckにHTTPリクエストを送信すると、レイテンシが増大したり、SpeakerDeck側に負荷がかかったりします。スライド一覧はある程度キャッシュしつつ、スライドを公開したら一覧にも反映されてほしくなります。

App Routerには時間ベースのキャッシュ破棄の仕組みがあるため、一見するとこれが使えそうに見えます*1。つまり、page.tsx ファイルに以下のように書いたら10分キャッシュしてくれそうです。

export const revalidate = 1200;

ところが、これだけだとビルド時にデータキャッシュが焼き込まれ、再度ビルド・デプロイしないとSpeakerDeckのスライド一覧が更新されなくなってしまいます。Pages Router時代は getServerSideProps 関数でデータフェッチしつつ Cache-Control レスポンスヘッダをつけたらよかったのですが、同じことをApp Routerで実現する方法が分かりませんでした。完全にキャッシュを無効化することでスライド一覧が更新されるようになりますが、これは都度SpeakerDeckにリクエストを投げていることになり、やりたいことに反します。Next.js middlewareで無理やり Cache-Control レスポンスヘッダをつけようとしたのですが、デプロイしてみるとNext.jsのどこかの層でヘッダごと叩き落とされていたように記憶しています。

長々と書いたけど、要するに「スライド一覧をキャッシュしつつ適度に更新されるようにしたい」というのが最大のモチベーションでした。ここまで書いて思ったけど、今ならAIに聞いたら解決できたのかも。

構成

移行前の構成は以下の通りです。

  • ペライチのWebページとリダイレクト機能がある
  • Next.js App Router
  • AWS Lambda上で動いている
  • 前段にCloudFrontがある

移行後に期待することは「壊れていない」ぐらいしかなくて気楽です。

移行の流れ

テストを書く

移行に先んじて、リダイレクト機能が動いていることや、画像がリンク切れになっていないことなどを確かめるE2Eテストを書きました。テストがあることで、どういう状態が期待されているのか明確になります。

github.com

Hono (hono/jsx) に移行する

期待する状態が明確になったので、あとは移行するだけです。

まずはおもむろに CLAUDE.md ファイルを用意します。AIコーディングエージェントに移行してもらおう、という算段です。

github.com

あとはClaude Codeを開くだけです。「Next.jsからHono (hono/jsx) に移行してください」というプロンプトでPlanしてもらったあと作業に取りかかってもらったらだいたい完了したと思います。

github.com

デプロイ時に必要なスクリプトのコピーを忘れていてLambda関数がコケるようになったのですが、そこを直したら無事に動くようになりました。

github.com

ちなみに、HonoはネイティブでLambda上で動作する*2のでLambda Web Adapterは外せます。

github.com

ESLint flat configに移行する

ESLintを更新しきれていなかったのですが、重い腰を上げて (プロンプトを投げて) flat configに移行しました。

github.com

oxlint, oxfmtに移行する

欲が出たので、そのままoxlint, oxfmtに移行してもらいました。lintは速ければ速いほどいいですからね。

github.com

github.com

おわりに

App RouterからHonoに移行することで、狙い通り Cache-Control レスポンスヘッダでCDNキャッシュの制御をしやすくなりました。よかったですね。

自分の代わりに動いてくれる頭と手が増えるだけで物事が一気に進みはじめる、という事例でした。今年度からClaudeのProプランに課金しました。AI時代になっても個人サイトを技術の砂場にする流れは変わらず、むしろ加速していくでしょう。

あわせてよみたい

HTTPレベルのキャッシュについてはMDNの記事がよくまとまっているので、まずはそちらを読むとよいでしょう。

キャッシュやCDNについては以下の本が詳しいです*3

DynamoDB localのDescribeTable APIのレスポンスを補完するプロキシを書いた

blog.utgw.net

DynamoDB localのDescribeTable APIのレスポンスを補完することで、Terraform経由でDynamoDB localのテーブルを作成できるようにパッチするためのプロキシを書きました。

github.com

使い方

Docker Compose経由で使う

↓こういう感じで、お手元の docker-compose.yml*1に書き足しつつ、endpoint-urlhttp://localhost:8888 に向けるだけで使えます。

services:
  dynamodb-local:
    command: -jar DynamoDBLocal.jar -sharedDb -dbPath ./data
    image: amazon/dynamodb-local
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
    networks:
      - dynamodb
  dynamodb-local-proxy:
    image: ghcr.io/utgwkk/dynamodb-local-proxy
    environment:
      - DYNAMODB_LOCAL_ADDR=dynamodb-local:8000
      - HOST=0.0.0.0
      - PORT=8888
    ports:
      - "8888:8888"
    networks:
      - dynamodb

networks:
  dynamodb:
    driver: bridge

GitHub Actionsで使う

GitHub Actions runner上でDocker Composeの bridge ネットワークモードを使うと、DynamoDB localに対するAPIコールが詰まってタイムアウトする問題があります。GitHub Actions workflowの services フィールドを使って起動することをおすすめします。

on:
  push:
    branches:
      - main
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      dynamodb-local:
        image: amazon/dynamodb-local
      dynamodb-local-proxy:
        image: ghcr.io/utgwkk/dynamodb-local-proxy
        env:
          DYNAMODB_LOCAL_ADDR: dynamodb-local:8000
          HOST: 0.0.0.0
          PORT: 8888
        ports:
          - 8888:8888
    steps:
      - run: aws --endpoint-url=http://localhost:8888 dynamodb list-tables

おわりに

どうぞご利用ください。こういうプロキシを書く経験はそんなにないので変なコードがあったらどしどし直してください。

DynamoDB localが修正されたらめでたくお役御免になります。

余談 (実装テクニック)

せっかくなので実装テクニックを書いておきます。

HTTPレスポンスをシリアライズし、モックサーバーのレスポンスとして再利用

net/http/httputilパッケージの DumpResponse 関数を使うことで、以下のようなテキストが得られます。まさにHTTP/1.1のレスポンスという感じで都合がいいですね。

HTTP/1.1 400 Bad Request
Content-Length: 128
Content-Type: application/x-amz-json-1.0
Date: Mon, 30 Mar 2026 11:45:44 GMT
Server: Jetty(12.0.31)
X-Amzn-Requestid: 3b2e4c07-1565-4350-9017-8c67b57dc82d

{"__type":"com.amazonaws.dynamodb.v20120810#ResourceNotFoundException","Message":"Cannot do operations on a non-existent table"}

さて、これをデシリアライズする (*http.Response に復元する) にはどうすればいいか? これはnet/httpパッケージの ReadResponse 関数を使うことで実現できます。あとは得られたレスポンスを返すようにnet/http/httptestServer を作ってやればよいです。簡単ですね。

……ということがテストコードに全部書いてあります。

github.com

graceful shutdown

github.com/ne-sachirou/go-gracefulを使って実現しています。ありものを使うことで「正しいgraceful shutdownができているか」の議論を回避することを狙っています。

c4se.hatenablog.com

ログにリクエストIDを入れる

リクエストを一気通貫してログを眺めたいとき、ログにリクエストを一意に識別するIDが入っていると何かと便利です。

リクエストIDは適当に生成したらよいでしょう。UUID v6とかにしておきます。

現代ではlog/slogを使って構造化ログを吐いたらいいので、あとはcontextの中身をログに出せそうなライブラリを探します。ちょうどそのような記事を読んでおり、github.com/PumpkinSeed/slog-contextを採用できました。

blog.arthur1.dev

github.com

リクエストをcopy as curlできるようにする

github.com/thinkgos/httpcurlを使うと *http.Request をcurlコマンド形式の文字列に変換できます。デバッグなど、何かとcurl経由で気軽に叩きたいことが多いので便利に使っています。

https://github.com/utgwkk/dynamodb-local-proxy/blob/c01a878031937002da00de5a1bc0c92fd70230b9/internal/handler/handler.go#L59-L66

スタックトレースのあるerror

エラーにスタックトレースがないと暮らしていけないと思っているので、そういうライブラリを探しておきます。github.com/cockroachdb/errorsには WithStack 関数があり、エラーにスタックトレースを乗せられるのでちょうどよいでしょう。

環境変数から設定値を注入する

github.com/caarlos0/envを使っています。いつの間にかジェネリクス対応しているようでした。

github.com

JSONの一部のフィールドだけ書き換えてシリアライズする

2026/3/31 1:03 追記: 急に github.com/itchyny/gojq のことを思い出したので書き直しました。jqでフィールドの書き換えができるのでGoで書くよりは取り回しがよさそう、可読性がいいかどうかは諸説ある??

github.com

ここだけ実装テクというよりは泥臭い仕草って感じなんですが、JSONの構造をふわっと定めつつ関心のある一部のフィールドだけ書き換えるために map[string]any 型を使っています。配列は any[] 型にデシリアライズされるのでキャストしてアクセスしたらよい、ということをついさっき知りました。forループに渡すスライスのキャストがノーガードなので、変なレスポンスが返ってきたら壊れると思います。

https://github.com/utgwkk/dynamodb-local-proxy/blob/c01a878031937002da00de5a1bc0c92fd70230b9/internal/handler/handler.go#L93-L129

E2Eテスト

実際にterraform-provider-awsを使ってDynamoDB localにテーブルを作成できるE2Eテストを用意しました。

……ぐらいで終わったらよかったんですが、GitHub Actions上で terraform plan がタイムアウトして一生終わらないのでひたすらデバッグとか、GSIがない場合のハンドリングが漏れていたので直すとか、それはもう色々なことがありました。GSIの配列を返すフィールドがあったら空配列が返ってくると思うじゃん……。

何はともあれ真に実用できるようになってきたので、E2Eテスト書いてよかった〜

github.com

*1:最近は docker-compose.yml じゃなくて compose.yaml が使われていそう

TerraformでDynamoDB localのテーブルを作成しようとするとタイムアウトする現象

2026/3/30 追記: 表題の現象を回避するためのプロキシを書きました。

blog.utgw.net

github.com


表題のことが以下のバージョンで発生している。

  • DynamoDB local 3.3.0
  • terraform-provider-aws 6.38.0

起こっていること

まっさらな状態のDynamoDB localに対してテーブルを作成する変更をapplyしようとすると、2分半ほど待たされてタイムアウトしてしまう。

% terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.test will be created
  + resource "aws_dynamodb_table" "test" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "pk"
      + id               = (known after apply)
      + name             = "test"
      + range_key        = "sk"
      + read_capacity    = (known after apply)
      + region           = "ap-northeast-1"
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "pk"
          + type = "S"
        }
      + attribute {
          + name = "sk"
          + type = "S"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
aws_dynamodb_table.test: Creating...
aws_dynamodb_table.test: Still creating... [10s elapsed]
aws_dynamodb_table.test: Still creating... [20s elapsed]
(中略)
aws_dynamodb_table.test: Still creating... [2m20s elapsed]
aws_dynamodb_table.test: Still creating... [2m30s elapsed]
╷
│ Error: waiting for update AWS DynamoDB Table (test): couldn't find resource (21 retries)
│ 
│   with aws_dynamodb_table.test,
│   on main.tf line 25, in resource "aws_dynamodb_table" "test":
│   25: resource "aws_dynamodb_table" "test" {
│ 
╵

原因

terraform-provider-aws v6.13.0でaws_dynamodb_tableリソースに対するWarmThroughputのサポートが入った。

github.com

この変更によって、DynamoDBのテーブルに対して DescribeTable APIを呼び出して得られたレスポンスの Tables.WarmThroughput フィールドの存在を確認が行われるようになった。

一方で、DynamoDB localはWarmThroughputをサポートしていない。実際に DescribeTable APIを呼び出してみると、Table.WarmThroughput フィールドがないことが分かる。

{
  "Table": {
    "AttributeDefinitions": [
      { "AttributeName": "sk", "AttributeType": "S" },
      { "AttributeName": "pk", "AttributeType": "S" }
    ],
    "TableName": "test",
    "KeySchema": [
      { "AttributeName": "pk", "KeyType": "HASH" },
      { "AttributeName": "sk", "KeyType": "RANGE" }
    ],
    "TableStatus": "ACTIVE",
    "CreationDateTime": 1774787505.241,
    "ProvisionedThroughput": {
      "LastIncreaseDateTime": 0.0,
      "LastDecreaseDateTime": 0.0,
      "NumberOfDecreasesToday": 0,
      "ReadCapacityUnits": 0,
      "WriteCapacityUnits": 0
    },
    "TableSizeBytes": 0,
    "ItemCount": 0,
    "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/test",
    "BillingModeSummary": {
      "BillingMode": "PAY_PER_REQUEST",
      "LastUpdateToPayPerRequestDateTime": 1774787505.241
    },
    "DeletionProtectionEnabled": false
  }
}

これらの組み合わせによって、terraform-provider-aws v6.13.0以降を使ってDynamoDB localのテーブルを作成しようとするとタイムアウトするようになったと考えられる。

対策

DynamoDB localの実装が修正されるのを待つのが最も素直だと思う。筆者のほうでAWSのサポートケースを起票したので、問題は認識してもらえているとは思う。

今すぐに不具合を回避したいなら、terraform-provider-aws v6.12.0にバージョンを固定するのが早い。DynamoDB localだけなら多少Terraform providerのバージョンが古くてもやっていけるかもしれない。ただし、マルチキーGSI*1など新しめの機能を使っている場合はダウングレードするのが難しい場合もありそう。

LocalStackを使うことでも問題を回避できるかもしれないが、筆者は検証していない。

検証

「原因」の節では、以下の組み合わせによってタイムアウトするようになった、という仮説を述べた。

  • 新しいterraform-provider-awsが WarmThroughput フィールドの存在確認を行うようになった
  • DynamoDB localはWarmThroughput機能をサポートしておらず、当該フィールドを DescribeTable APIで返さない

この仮説は正しいのか? つまり、DynamoDB Localが WarmThroughput フィールドを返すようになったら terraform apply が正常終了するようになるのだろうか。

……ということを以下のリポジトリで検証した。そしてどうやら仮説は正しそうである。

github.com

main.go はDynamoDB localに対するリクエストのプロキシとして機能する。DynamoDB localに対する DescribeTable APIの呼び出しが正常終了したとき、レスポンスボディの Table.WarmThroughput フィールドにダミーの値を詰めて返すようにしている。どのAPIを呼び出しているかは X-Amz-Target リクエストヘッダを見ると分かるので、この値によって分岐を入れている。バイブコーディングなしで手書きしまくっているし、デバッグの跡が残っているのでコードの見た目はあんまりよくない。

おわりに

TerraformでDynamoDB localのテーブルを作成しようとするとタイムアウトする現象と、その対策について述べた。原因と考えられる仮説 (DescribeTable APIのレスポンスボディのフィールドが足りないこと) に対して、実際に DescribeTable APIのレスポンスボディを変形させることで検証を行った。

Terraformを使って手元の開発環境のDynamoDB localのテーブルを管理できるようにすると、マイグレーションスクリプトなどが不要になって便利だろう、と考えて手を動かしているうちに、この記事に書いたような不具合を踏み抜いた。

この不具合の原因はClaude Opusが発見し、人間が検証を行うことで確定した。デバッグの泥沼にハマったら強いモデルを使うことで解決できるかもしれない、という体験になったのであった。よかったですね。

時間のないサイト運営者リング