🪣

LocalStackのS3を本気で使いこなす:DNS設定からURL形式まで

に公開

はじめに

この記事は何

LocalStackは、AWSの各種サービスをローカルで模倣できる便利なツールです。特にS3などの基本的なサービスは、AWS SDKのエンドポイントを切り替えるだけで簡単に扱えます。しかし、LocalStackをDockerコンテナ上で動かし、さらにアプリケーションも別のコンテナで動かす場合、「アプリケーションコンテナとホストPCの双方からS3にアクセスする」ことは、思いのほか難易度が高くなります。

本記事では、LocalStackとアプリケーションコンテナを連携させる際の具体的な課題や設定(エンドポイントの指定、DNSによる名前解決の工夫)について、サンプルアプリケーションを通じて丁寧に解説します。

最終的なサンプルアプリケーション構成は下図の通りです。下図構成はWebアプリケーションがS3にオブジェクトを保存した上で署名付きURLを発行し、ブラウザがそのURLを使ってファイルをダウンロードする仕組みを実現しています。

architecture.png

図中には既にキーポイントを明示していますが、本記事ではそれぞれのポイントをステップバイステップで説明していきます。LocalStackの使いこなしという点に限らず、AWS(S3)の仕様や、ネットワークの面白い点についても触れていきます。 図を見て不明な点がある方はぜひご一読頂ければと思います。

本記事の対象読者

  • LocalStackが好きな人、使い始めたい人
  • ローカル開発環境構築の手札を増やしたい人

本記事で扱わないこと

  • AWS SDKやAWS CLIの基本的な使用方法は説明しません
  • LocalStackのインストール方法や基本的な使用方法は説明しません

前提条件

  • LocalStackはDockerコンテナとして実行する。
    • イメージはlocalstack/localstack:3.7.2
    • S3サービスを実行する。(詳細はサンプルアプリケーションの章を参照)

結び

次からはいよいよ本題に入っていきます。以下の順番で説明を進めていきます。

  1. LocalStackを使うためのAWS SDK設定
    1. AWS SDK設定の概要
    2. S3のURL形式
  2. サンプルアプリケーション
    1. URL形式をパス形式にする場合
    2. 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)
  })
  // ...
}

https://docs.localstack.cloud/user-guide/integrations/sdks/go/

この例では、エンドポイントとして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>

https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html

<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
  # 他の項目は記載省略
)

https://docs.localstack.cloud/user-guide/integrations/sdks/ruby/

このパターンではUsePathStyle[1]を使ってURLをパス形式にする必要はありません。突然現れたs3.localhost.localstack.cloudとは何者なのでしょうか?実はこれが名前解決や仮想ホスト形式への対応のためにLocalStackが用意している工夫です。 この点は次章のサンプルアプリケーションの説明を通じて紐解いていきましょう。

サンプルアプリケーション

それでは、いよいよサンプルアプリケーションに取りかかりましょう。おさらいになりますが、期待する動作を次のように定めます。

  1. WebアプリケーションはS3にオブジェクトを保存する。
  2. ユーザーはホストPCのブラウザでhttp://localhost:8080にアクセスする。
  3. WebアプリケーションはS3のオブジェクトをダウンロードする署名付きURLを発行し、リンクとしてユーザーに表示する。
  4. ユーザーは署名付きURLを用いてS3バケット内のオブジェクトをダウンロードする。

本記事冒頭の完成図からいくつかの設定を省略した図は以下の通りです。LocalStackコンテナ、Appコンテナを動作させ、http://localhost:8080にユーザーがアクセスするとAppコンテナの8080番ポートで待ち受けているWebアプリケーションが表示されます。

architecture-plain.png

現在の図中には、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にしてみます。これで動くでしょうか?

architecture-plain.png

S3へのオブジェクトの保存と署名付きURLの発行には成功しますが、ホストPCのブラウザからオブジェクトにアクセスできません。[2] AWS SDKのエンドポイントに設定するURLは、AWS SDKが発行する署名付きURL等にそのまま使用されるため、Webアプリケーションはhttp://localstack:4566/test-bucket/key-nameというURLをブラウザに表示します。ホストPCからするとhttp://localstackを名前解決できないため、LocalStackコンテナにアクセスできないですね。

Step3. s3.localhost.localstack.cloud形式を使う

LocalStackドキュメントの中で見かけたhttp://s3.localhost.localstack.cloud:4566というエンドポイント設定を試してみましょう。いよいよこれで動くでしょう!

architecture-plain.png

いいえ、動きません。S3にオブジェクトを保存できません。

おまじないのように使用したs3.localhost.localstack.cloudというホストが何なのかを確認してみると、この設定がうまく働かない理由がわかります。s3.localhost.localstack.cloudlocalhost.localstack.cloudのCNAMEで、localhost.localstack.cloud127.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.と同じ結果になります。

architecture-plain.png

Step1.と同じくホストPCを意識した仕組みであると理解することが必要ですね。

Step4. AppコンテナのDNSサーバーを指定する

LocalStackのドキュメントを彷徨うと、いよいよ求めていた答えに辿り着けます。

https://docs.localstack.cloud/references/network-troubleshooting/endpoint-url/#from-your-container

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として扱われ、それぞれ適切に動作するようになるという絡繰りです。おもしろいですね!

architecture-plain.png

ついに動きました!

URL形式を仮想ホスト形式にする場合

パス形式の場合とほとんど同じ議論です。Step4.の構成が必要です。

もしかしたら「エンドポイントにバケット名を含めるべきかどうか」で迷われるかもしれませんが、バケット名はエンドポイントの設定に含めません。バケット名はAWS SDKが自動で付与します。 Step4.の構成において、次のように処理が進められます。

  1. WebアプリケーションのAWS SDK設定でhttp://s3.localhost.localstack.cloud:4566をエンドポイントに、UsePathStylefalseに設定する。(デフォルトがfalseのため何も設定しなければOK)
  2. AWS SDKはhttp://test-bucket.s3.localhost.localstack.cloud:4566という仮想ホスト形式のURLを使用してサービス(LocalStack)へのリクエストを行う。
  3. LocalStackコンテナのDNS機能は、test-bucket.s3.localhost.localstack.cloudをLocalStackコンテナのIPアドレスに名前解決する。
  4. LocalStackコンテナはs3.というプレフィックスを含むリクエストをS3の仮想ホスト形式のURLとして解釈し適切に処置する。
  5. Webアプリケーションはhttp://test-bucket.s3.localhost.localstack.cloud:4566/key-nameという仮想ホスト形式の署名付きURLを発行する。
  6. ブラウザがDNSサーバーにtest-bucket.s3.localhost.localstack.cloudを問い合わせると127.0.0.1と名前解決される。
  7. ブラウザはLocalStackコンテナからオブジェクトを取得する。

LocalStackコンテナのDNS機能が仮想ホスト形式URLの名前解決を担う点と、ホスト名にs3.を含めておくことでLocalStackコンテナが仮想ホスト形式URLを適切に処理する点がポイントです。

これにて本記事冒頭で宣言したサンプルアプリケーションが完成しました!

おわりに

LocalStackを使ったS3の仮想ホスト形式URLの外部公開について、設定の落とし穴やネットワーク/DNSの仕組みを交えながら解説しました。LocalStackが用意しているDNSレコードやLocalStackコンテナのDNS機能を活用することで、DockerコンテナとホストPCの双方がLocalStackコンテナにアクセスするようなアプリケーションのローカル開発を進められることがご理解いただけたかと思います。

本記事で紹介した構成や知識は、S3に限らないLocalStackの他のAWSサービスや、それ以外にもローカル開発環境の設計に応用できます。LocalStackの進化やAWSの仕様変更にも注意しつつ、より快適な開発環境を構築していきましょう。

もし質問やご意見があれば、ぜひコメント等でお知らせください。

脚注
  1. オプションの名前は各言語ごとに異なります ↩︎

  2. ただしホストPCにlocalstack 127.0.0.1のようなホスト設定をしてあればアクセスできます。 ↩︎

GitHubで編集を提案

Discussion