Webアプリケーションで、画像のアップロード機能を実装している際に遭遇したエラーについて記録しておきたいと思います。
前提として、今回私が触っていたアプリケーションは以下のような構成で画像のアップロードを行っています。
APIサーバ (Go) よりS3の署名付きURLを生成し、クライアントではそのURLを使用してアップロードするようなよくある構成かと思います

AWS SDK Go v2 から LocalStack の S3 にアクセスした際、DNS 解決エラーが発生しました。
dial tcp: lookup dev-bucket.localstack on 127.0.0.11:53: no such host
原因としては、AWS SDK のデフォルト動作である「仮想ホスト形式」と Docker ネットワーク内の DNS 解決の仕組みにあるようでした。
元々は以下のような設定を compose.yaml に定義して、開発環境を動作させていました。
services:
localstack:
image: localstack/localstack:latest
// 省略
networks:
default:
aliases:
- localhost.localstack.cloud
そもそも理解のところで、S3のアドレッシングスタイルとしては、大きく以下2つのURL形式があります。
| 形式 | URL 例 |
|---|---|
| 仮想ホスト形式 | {bucket}.s3.{region}.amazonaws.com |
| パス形式 | s3.{region}.amazonaws.com/{bucket} |
現在、AWSは以下のような理由で仮想ホスト形式を推奨しているようです。
この辺りの経緯があるため、APIサーバ側では AWS SDK Go v2 を利用しているのですが、こちらのライブラリはデフォルトで 仮想ホスト形式 の方を使用しているようです。
localstack 側のドキュメントにも解説されている箇所があるようです。
参考: Path-Style and Virtual Hosted-Style Requests
type Options struct {
....
// Allows you to enable the client to use path-style addressing, i.e.,
// https://s3.amazonaws.com/BUCKET/KEY . By default, the S3 client will use virtual
// hosted bucket addressing when possible( https://BUCKET.s3.amazonaws.com/KEY ).
UsePathStyle bool
...
おそらく以下の関数で仮想ホスト形式に変換されているのだと認識しています。※ UsePathStyle を true にしている場合は何もせずそのまま返しているようですので
func (u updateEndpoint) updateEndpointFromConfig(req *smithyhttp.Request, bucket string, region string) error {
// do nothing if path style is enforced
if u.usePathStyle {
return nil
}
...
// move bucket to follow virtual host style
moveBucketNameToHost(req.URL, bucket)
return nil
}
// updates endpoint to use virtual host styling
func moveBucketNameToHost(u *url.URL, bucket string) {
u.Host = bucket + "." + u.Host
removeBucketFromPath(u, bucket)
}
ポイントとして、SDK の挙動としては localstack というホスト名の前にバケット名を付加するだけ。ということだと認識しています。
BaseEndpoint: http://localstack:4566
↓
http://dev-bucket.localstack:4566/key # バケット名 + "." + localstack の形式になっている
Docker の DNS 明示的に定義されたコンテナ名やサービス名などは解決してくれるため、
localstack に関しては解決できるものの、*.localstack のようなパターンマッチングによる解決はデフォルトではしてくれないので、こちらが原因で処理が失敗していたようでした。
調べてみると諸々解法はあるようなのですが、より手間もお金もかけずにやるとなると compose.yaml でネットワークエイリアスを設定することで解決できます。
services:
localstack:
image: localstack/localstack:latest
...
networks:
default:
aliases:
- localhost.localstack.cloud
# S3 仮想ホスト形式用エイリアスを使用するバケット毎に追加
- dev-bucket.s3.localhost.localstack.cloud
const localstackS3Endpoint = "http://s3.localhost.localstack.cloud:4566"
func InitStorageClient(ctx context.Context, cfg *cfg.Config) *s3.Client {
opts := []func(*s3.Options){}
if cfg.IsDevelopment() {
opts = append(opts, func(o *s3.Options) {
o.BaseEndpoint = aws.String(localstackS3Endpoint)
})
}
return s3.NewFromConfig(cfg.AWSConfig, opts...)
}
このように、エンドポイントを s3.localhost.localstack.cloud 形式にすることで、SDK が生成する URL が {bucket}.s3.localhost.localstack.cloud となるため、通信できるかたちになります。
エラーが出た場合、AI等にヒアリングすることで即時解決ができるかもしれませんが、 知識の定着を意識する場合は、SDKの実装等追ってみるとなんとなく実態がつかめるので、おすすめです。
今回の件についてまとめると、
UsePathStyle を明示的に設定しない限り、バケット名がサブドメインとして付加される点に注意することs3.localhost.localstack.cloud 形式のエンドポイントを使用することで、SDK が期待する URL 形式と Docker DNS の解決を両立できるBedrock cross-region inference profileの動的ルーティングにより、大阪リージョンでモデルアクセスが未設定だったため、リージョン間で交互に成功・失敗するエラーが発生。大阪でのモデルアクセス申請とLambda IAM権限追加で解決した。
AWS Lambdaのエラー通知をSlackに送信する際の設計ポイントを解説。ログ永続化、Subscription Filter上限対策、コスト最適化、通知遅延を考慮し、CloudWatch Logs→Lambda→Slack通知とFirehose→S3保存の構成を採用した実装例を紹介。
Grafana × Loki を使用してログ検索・表示方法を解説。LogQL でのログ検索、ログ詳細表示、Trace との相互遷移機能を紹介。Log と Trace を組み合わせることで問題切り分けが効率化される利点を説明。
OpenTelemetryのローカル検証環境をdocker-composeで構築。Go APIサーバーからOTLP経由でGrafana Alloyにテレメトリを送信し、Tempo/Prometheus/Lokiで収集・保存、Grafanaで可視化する構成を実装。各サービスの起動確認まで完了。
AWS AIPの知見を活かし、論文PDFを構造化JSONに変換するパイプラインを構築。S3、Lambda、Step Functions、Textract、Bedrockを組み合わせ、2つの抽出経路で日本語・英語PDFを処理し、メタデータ付きの構造化データをRAGのソースとして活用する仕組みを実装。