Dockerコンテナ内から同一ホストの別コンテナにホストを経由してアクセスできない


世の中、覚えなければ上手く行かないことが多すぎる。だから、全てを覚えてから始めるんじゃなくて、やってみて失敗してから掘り下げるアプローチが好きです。まどかママの「上手な転び方を覚え」るってのは至言だと思う。

そんな訳で、Keycloakで認証を行うWebアプリを開発していて、動作確認も終わり、いざ検証環境で動かそうとしたら、Webアプリの起動が失敗してしまった。

システムの構成と経緯は以下のようなイメージ。

  • Keycloakサーバは、検証環境でdocker-composeを使って構築。
  • Webアプリは、バックエンドをSpring Boot、フロントエンドをAngularで開発。

    • Angularアプリではkeycloak-angularを使用。
    • Spring Bootアプリはリソースサーバとして動作するのでspring-boot-starter-oauth2-resource-serverを使い、Angularアプリから送られてきたトークンを検証。
  • 開発中は、検証環境のKeycloakサーバに接続して動作を確認。

  • Webアプリをコンテナにして、検証環境のKeycloakサーバと同じホストのDocker上で起動すると、Spring BootアプリからKeycloakサーバへの経路がない(no route to host)とのエラーで終了していた。

まず試したこと

そこでKeycloakコンテナとWebアプリコンテナを同じDockerネットワークに参加させ、Spring BootアプリからDockerネットワーク経由で接続するようにしてみた。

  1. docker network create ${ネットワーク名}でネットワークを作成。
  2. KeycloakとWebアプリのdocker-compose.ymlで、上記のネットワークに参加するように設定。
例)Webアプリのdocker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
    container_name: app
    ports:
      - 50010:8080
    networks:
      - ${ネットワーク名}
networks:
  ${ネットワーク名}:
    external: true
SpringBootアプリのapplication.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://${keycloakコンテナ名}.${ネットワーク名}:8080/auth/realms/${レルム名}

これでSpring Bootアプリは、Keycloakサーバと通信が成功して、正常に起動するようにはなった。
が、Angularアプリでログインを行うと、keycloakサーバへのログインに成功するものの、その後でAngularアプリからSpring BootアプリへのAjax通信で 401 Unauthorizedとなってしまった。

ブラウザでレスポンスを見てみると、ヘッダに以下の情報があった。

WWW-Authenticate: Bearer error="invalid_token",
                  error_description="This iss claim is not equal to the configured issuer", 
                  error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

どうやらAngularアプリとSpring BootアプリがアクセスするKeycloakサーバのURLは一致していないとのこと。
なので、どうしてもDockerコンテナ内部から外部ネット経由でアクセスする必要だった。

根本の原因

そこでネットから情報を漁ると、Dockerがiptablesを設定していて、それによって通信ができないらしい。
こちらの記事とか参考にしてiptablesの見方を勉強してみた。
まだ完全に理解できていないが、自ホストからパケットを送信する場合のチェインを辿ってみると、、、

# iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 12 packets, 853 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
 ※あとは割愛

Dockerkコンテナにデフォルトで割り当てられるbridgeネットワークのアドレス172.17.0.0/16からの通信は、docker0ブリッジには戻れない設定だと思えた。

回避策

現状の理解ではiptablesを弄るのもハードルが高いので、WebアプリのDockerコンテナがデフォルトのbridgeドライバを使ったネットワークではなく、hostドライバを使うように指定した。

docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
    container_name: app
    network_mode: host     # 追加
# ポートマッピングは使えなくなるので削除
#    ports:
#      - 50010:8080

これによりコンテナ内部で使うポートが、直接ホストで使われてしまうが、そこはSpring Bootの設定ファイルで簡単にポート番号を変更できるので良しとしました。