LocalStackのS3を本気で使いこなす:DNS設定からURL形式まで
はじめに
この記事は何
LocalStackは、AWSの各種サービスをローカルで模倣できる便利なツールです。特にS3などの基本的なサービスは、AWS SDKのエンドポイントを切り替えるだけで簡単に扱えます。しかし、LocalStackをDockerコンテナ上で動かし、さらにアプリケーションも別のコンテナで動かす場合、「アプリケーションコンテナとホストPCの双方からS3にアクセスする」ことは、思いのほか難易度が高くなります。
本記事では、LocalStackとアプリケーションコンテナを連携させる際の具体的な課題や設定(エンドポイントの指定、DNSによる名前解決の工夫)について、サンプルアプリケーションを通じて丁寧に解説します。
最終的なサンプルアプリケーション構成は下図の通りです。下図構成はWebアプリケーションがS3にオブジェクトを保存した上で署名付きURLを発行し、ブラウザがそのURLを使ってファイルをダウンロードする仕組みを実現しています。
図中には既にキーポイントを明示していますが、本記事ではそれぞれのポイントをステップバイステップで説明していきます。LocalStackの使いこなしという点に限らず、AWS(S3)の仕様や、ネットワークの面白い点についても触れていきます。 図を見て不明な点がある方はぜひご一読頂ければと思います。
本記事の対象読者
- LocalStackが好きな人、使い始めたい人
- ローカル開発環境構築の手札を増やしたい人
本記事で扱わないこと
- AWS SDKやAWS CLIの基本的な使用方法は説明しません
- LocalStackのインストール方法や基本的な使用方法は説明しません
前提条件
- LocalStackはDockerコンテナとして実行する。
- イメージは
localstack/localstack:3.7.2
。 - S3サービスを実行する。(詳細はサンプルアプリケーションの章を参照)
- イメージは
結び
次からはいよいよ本題に入っていきます。以下の順番で説明を進めていきます。
- LocalStackを使うためのAWS SDK設定
- AWS SDK設定の概要
- S3のURL形式
- サンプルアプリケーション
- URL形式をパス形式にする場合
- URL形式を仮想ホスト形式にする場合
それでは始めましょう!
LocalStackを使うためのAWS SDK設定
サンプルアプリケーションの説明の要点を理解するために、事前にLocalStackを使う際に必要なAWS SDKの設定をおさらいしましょう。本記事の肝の1つでもあるS3の2つのURL形式についても触れていきます。
AWS SDK設定の概要
AWS SDKやaws cli
はAWSサービスのエンドポイント設定をカスタマイズすることが可能です。エンドポイントにLocalStackを指定することで、他のアプリケーションコードは変更することなく、LocalStackを使って開発することができます。例えばLocalStackのドキュメントには以下のように記載されています。
func main() {
awsEndpoint := "http://localhost:4566"
awsCfg, _ := config.LoadDefaultConfig(context.TODO()) // 紙面都合上エラーハンドリング省略
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = true
o.BaseEndpoint = aws.String(awsEndpoint)
})
// ...
}
この例では、エンドポイントとしてhttp://localhost:4566
を使っています。LocalStackコンテナのサービスポート(4566)は一般的にホストPCの4566番ポートにバインドして使用するため、この設定によりホストPCはLocalStackのサービスにアクセスするようになります。
ところで、先ほどの例にあるUsePathStyle
とは何でしょうか?これはS3のURLが仮想ホスト形式とパス形式のどちらの形式も扱うことから、SDKとしてどちらを使用するかを選択できるようにしているオプションです。2つのURL形式の違いを理解することは意外と重要です。
S3のURL形式
仮想ホスト形式とパス形式は、それぞれ以下のようなURLです。なお、パス形式については廃止予定となっています。
形式 | URL |
---|---|
仮想ホスト | https://<bucket>.s3.<region>.amazonaws.com/<key> |
パス | https://s3.<region>.amazonaws.com/<bucket>/<key> |
<region>
などが入り込んでいますが、もう少し汎用的にすると以下の形式とも読み取れます。(後ほど説明するLocalStackでのエンドポイント設定深掘りのため汎用的にしておきます。)
形式 | URL |
---|---|
仮想ホスト | <endpoint-schema>//<bucket>.<endpoint-host>/<key> |
パス | <endpoint-schema>//<endpoint-host>/<bucket>/<key> |
AWS SDKはデフォルトでは仮想ホスト形式を扱います。どちらのURL形式を使ってもSDKに隠蔽されている限りは大きな違いはありませんが、S3の署名付きURLをSDKを使って発行するような、S3のURLを外部公開するケースにおいては、設定した形式でURLが出力されます。
つまり先ほどの例では、エンドポイントをhttp://localhost:4566
、URL形式をパス形式
としているため、http://localhost:4566/<bucket>/<key>
のようなURLが得られることになりますね。
LocalStackのドキュメントには各プログラミング言語ごとのSDKの設定方法が記載されていますが、多くのケースでパス形式
を扱う例が記載されています。これは推測ですがパス形式
を扱う方が名前解決上はるかにシンプルでトラブルが少ないためと考えられます。仮想ホスト形式ではhttp://<bucket>.localhost:4566/<key>
のようなURLになるため、何らかの設定をしていない限り名前解決ができません。パス形式を使えば、この「何らかの設定」を気にかけなくて良いメリットがあるでしょう。
ところで、LocalStackのドキュメントを読んでいくと、先ほどとは異なるエンドポイントの設定例も見つけられます。
Aws.config.update(
endpoint: 'http://s3.localhost.localstack.cloud:4566', # update with localstack endpoint
# 他の項目は記載省略
)
このパターンではUsePathStyle
[1]を使ってURLをパス形式にする必要はありません。突然現れたs3.localhost.localstack.cloud
とは何者なのでしょうか?実はこれが名前解決や仮想ホスト形式への対応のためにLocalStackが用意している工夫です。 この点は次章のサンプルアプリケーションの説明を通じて紐解いていきましょう。
サンプルアプリケーション
それでは、いよいよサンプルアプリケーションに取りかかりましょう。おさらいになりますが、期待する動作を次のように定めます。
- WebアプリケーションはS3にオブジェクトを保存する。
- ユーザーはホストPCのブラウザで
http://localhost:8080
にアクセスする。 - WebアプリケーションはS3のオブジェクトをダウンロードする署名付きURLを発行し、リンクとしてユーザーに表示する。
- ユーザーは署名付きURLを用いてS3バケット内のオブジェクトをダウンロードする。
本記事冒頭の完成図からいくつかの設定を省略した図は以下の通りです。LocalStackコンテナ、Appコンテナを動作させ、http://localhost:8080
にユーザーがアクセスするとAppコンテナの8080番ポートで待ち受けているWebアプリケーションが表示されます。
現在の図中には、AppコンテナがLocalStackコンテナのS3にオブジェクトを保存し署名付きURLを発行するための設定が欠けています。LocalStackのドキュメント等を見ながらいろいろと設定を変えてみて、どうすれば期待する動作を実現できるか考えてみましょう。
S3のURL形式については、まずパス形式で説明を進めます。一通りの説明の後、仮想ホスト形式を扱う場合を説明します。
URL形式をパス形式にする場合
Step1. 最初に見つけられるドキュメントを真似る
WebアプリケーションのAWS SDKに、先ほど説明した設定を加えてみましょう。これで動くでしょうか?
func main() {
awsEndpoint := "http://localhost:4566"
awsCfg, _ := config.LoadDefaultConfig(context.TODO()) // 紙面都合上エラーハンドリング省略
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = true
o.BaseEndpoint = aws.String(awsEndpoint)
})
// ...
}
いいえ、動きません。S3にオブジェクトを保存できません。 Appコンテナのlocalhost:4566
では何のサービスも動いていませんので、ただただエラーとなります。この例はあくまでホストPC上で動作させるソフト向けの設定であると理解することが必要ですね。
Step2. Dockerネットワークを加味してホスト名を変える
次の実験です。同じDockerネットワークに繋がるコンテナは互いにサービス名をホスト名として通信することができます。そこでエンドポイントをhttp://localstack:4566
にしてみます。これで動くでしょうか?
S3へのオブジェクトの保存と署名付きURLの発行には成功しますが、ホストPCのブラウザからオブジェクトにアクセスできません。[2] AWS SDKのエンドポイントに設定するURLは、AWS SDKが発行する署名付きURL等にそのまま使用されるため、Webアプリケーションはhttp://localstack:4566/test-bucket/key-name
というURLをブラウザに表示します。ホストPCからするとhttp://localstack
を名前解決できないため、LocalStackコンテナにアクセスできないですね。
s3.localhost.localstack.cloud
形式を使う
Step3. LocalStackドキュメントの中で見かけたhttp://s3.localhost.localstack.cloud:4566
というエンドポイント設定を試してみましょう。いよいよこれで動くでしょう!
いいえ、動きません。S3にオブジェクトを保存できません。
おまじないのように使用したs3.localhost.localstack.cloud
というホストが何なのかを確認してみると、この設定がうまく働かない理由がわかります。s3.localhost.localstack.cloud
はlocalhost.localstack.cloud
のCNAMEで、localhost.localstack.cloud
は127.0.0.1
に名前解決されます。(お手元でdig
等でご確認頂けます。)
これはホストPCのブラウザであればhttp://s3.localhost.localstack.cloud:4566/test-bucket/key-name
というURLがhttp://127.0.0.1:4566/test-bucket/key-name
として名前解決され、LocalStackコンテナの4566番ポートに辿りつくためうまく働きます。一方でAppコンテナの中でもエンドポイントをhttp://127.0.0.1:4566
としてしまうため、Step1.と同じ結果になります。
Step1.と同じくホストPCを意識した仕組みであると理解することが必要ですね。
Step4. AppコンテナのDNSサーバーを指定する
LocalStackのドキュメントを彷徨うと、いよいよ求めていた答えに辿り着けます。
LocalStackコンテナが持つDNS機能を使ってlocalhost.localstack.cloud
を名前解決してね! ということです。
services:
localstack:
image: localstack/localstack
# 省略
networks:
ls:
# Set the container IP address in the 10.0.2.0/24 subnet
ipv4_address: 10.0.2.20
app:
image: ghcr.io/localstack/localstack-docker-debug:main
# 省略
dns:
# Set the DNS server to be the LocalStack container
- 10.0.2.20
networks:
- ls
networks:
ls:
ipam:
config:
# Specify the subnet range for IP address allocation
- subnet: 10.0.2.0/24
LocalStackコンテナをDNSサーバーとして使用するため、LocalStackコンテナのIPアドレスを固定し、AppコンテナのDNS設定にそのIPアドレスを指定します。これによりlocalhost.localstack.cloud
というホストは、ホストPCからは127.0.0.1
として、Appコンテナからは10.0.2.20
として扱われ、それぞれ適切に動作するようになるという絡繰りです。おもしろいですね!
ついに動きました!
URL形式を仮想ホスト形式にする場合
パス形式の場合とほとんど同じ議論です。Step4.の構成が必要です。
もしかしたら「エンドポイントにバケット名を含めるべきかどうか」で迷われるかもしれませんが、バケット名はエンドポイントの設定に含めません。バケット名はAWS SDKが自動で付与します。 Step4.の構成において、次のように処理が進められます。
- WebアプリケーションのAWS SDK設定で
http://s3.localhost.localstack.cloud:4566
をエンドポイントに、UsePathStyle
をfalse
に設定する。(デフォルトがfalse
のため何も設定しなければOK) - AWS SDKは
http://test-bucket.s3.localhost.localstack.cloud:4566
という仮想ホスト形式のURLを使用してサービス(LocalStack)へのリクエストを行う。 - LocalStackコンテナのDNS機能は、
test-bucket.s3.localhost.localstack.cloud
をLocalStackコンテナのIPアドレスに名前解決する。 - LocalStackコンテナは
s3.
というプレフィックスを含むリクエストをS3の仮想ホスト形式のURLとして解釈し適切に処置する。 - Webアプリケーションは
http://test-bucket.s3.localhost.localstack.cloud:4566/key-name
という仮想ホスト形式の署名付きURLを発行する。 - ブラウザがDNSサーバーに
test-bucket.s3.localhost.localstack.cloud
を問い合わせると127.0.0.1
と名前解決される。 - ブラウザはLocalStackコンテナからオブジェクトを取得する。
LocalStackコンテナのDNS機能が仮想ホスト形式URLの名前解決を担う点と、ホスト名にs3.
を含めておくことでLocalStackコンテナが仮想ホスト形式URLを適切に処理する点がポイントです。
これにて本記事冒頭で宣言したサンプルアプリケーションが完成しました!
おわりに
LocalStackを使ったS3の仮想ホスト形式URLの外部公開について、設定の落とし穴やネットワーク/DNSの仕組みを交えながら解説しました。LocalStackが用意しているDNSレコードやLocalStackコンテナのDNS機能を活用することで、DockerコンテナとホストPCの双方がLocalStackコンテナにアクセスするようなアプリケーションのローカル開発を進められることがご理解いただけたかと思います。
本記事で紹介した構成や知識は、S3に限らないLocalStackの他のAWSサービスや、それ以外にもローカル開発環境の設計に応用できます。LocalStackの進化やAWSの仕様変更にも注意しつつ、より快適な開発環境を構築していきましょう。
もし質問やご意見があれば、ぜひコメント等でお知らせください。
Discussion