リバースプロキシとしてnginx-proxyの代わりにtraefikを導入

リバースプロキシとしてtraefikを使うためのメモ。(Docker使用)

目的

今まで使っていたjwilder/nginx-proxyは有用だが、ログ出力にバグがあるようでログが若干扱いにくかった。 具体的にはこのようなログになってしまっていた:

Aug 15 05:12:31 <hostname> <tag>: #033[0m#033[0;36;1mdockergen.1 | #033[0;31;1m2020/08/15 05:12:31 Running 'nginx -s reload'

……これは残念。修正済みのforkedなイメージもあるようだが(参考に記載)、traefikが気になっていたので、これを機に導入してみることにした。

traefikLet's Encryptも扱えて、ワイルドカード証明書も発行できるので…と思ったらこの設定を忘れていることに気づいた。この設定はまた今度にしよう。とりあえず、ワイルドカードでなくてもDNS-01 challengeで使えている。

環境

dockertraefikイメージを使った。

  • traefik v2.3

traefik側で設定すること

traefik.ymlを作成して、コンテナ内の/etc/traefik/に配置する。

  • accesslogの記録
  • httpをhttpsへリダイレクト
  • Let's Encryptの静的設定(certresolverを定義)
  • ログ出力はすべてjson形式に
  • サービスの自動発見を無効化

基本的にはこれらの設定を行う。

webapp側で設定すること

webapptraefikで言う所のサービス。traefikdockerのポートがEXPOSEされているコンテナを自動発見し、サービスとみなすが、DBなどはtraefikで制御される必要がないので前項で述べたように無効にする。

webappではdocker-compose.ymlなどのファイルにlabels:に設定を行う。

  • ホスト名(FQDN)の設定
  • traefikに発見されるようにする
  • tlsを有効化
  • httpsのみルーティング
  • certresolverを設定

今回はサブドメインで区別するが、パスでルーティングを行うこともできる。||&&を使うことで色々できる。

また、サブドメインなしでのURL指定をサブドメインを付加してリダイレクトするようにする。これは動的設定として行うので、対象のwebappについては追加の設定が必要になる。

traefikの実装

一通りの説明が終わったので具体的な構成・中身に入る。まずはtraefik側の設定から。

フォルダ構造

.
├── docker-compose.yml
└── my-traefik
    ├── Dockerfile
    └── traefik.yml

my-traefikディレクトリを作り、その中にDockerfiletraefik.ymlを置く。 ルートディレクトリの直下にはdocker-compose.ymlを置く。

それぞれの中身については以降に示す。

my-traefik/Dockerfile

traefikはイメージサイズが小さい。やることはtraefik.ymlを移すことだけ。

FROM traefik:v2.3

COPY ./traefik.yml /etc/traefik/

my-traefik/traefik.yml

accesslog:
  format: json

providers:
  docker:
    exposedByDefault: false

entrypoints:
  web:
    address: ":80"
    http:
      # httpをhttpsにリダイレクトする。
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesresolvers:
  myresolver:
    acme:
      dnschallenge:
        provider: "conoha"
      # ステージング用設定。本番では使わない。
      caserver: "https://acme-staging-v02.api.letsencrypt.org/directory"
      email: "<email@address.here>"
      storage: "/letsencrypt/acme.json"

# DON'T use webui in production environment.
# api:
#     insecure: true

log:
  level: INFO
  format: json

DNS-01 challengeを利用する。providerconohaを使用なのでそれに。環境変数も必要になる。providerについては参考にて。 caserver: "https://acme-staging-v02.api.letsencrypt.org/directory"はテスト用のCAサーバ。証明書発行制限が別枠だったと思う。

docker-compose.yml

version: "3.8"

services:
  reverse-proxy:
    build: ./my-traefik
    ports:
      - "80:80"
      - "443:443"
    environment:
      - "CONOHA_TENANT_ID=<here>"
      - "CONOHA_API_USERNAME=<here>"
      - "CONOHA_API_PASSWORD=<here>"
    volumes:
      - "certs:/letsencrypt"
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - traefik_network

networks:
  traefik_network:
    external: true

volumes:
  certs:

特筆すべきなのはprovider用の環境変数と証明書格納用のボリュームを作成するぐらい。conohaの場合は、conohaの管理ページのAPIのところから値を確認できる。

あとは、networkstraefik_networkexternalにしていることか。外部のcomposeなどのコンテナを発見・ルーティングをできるようにするため。

webappの実装

次はwebapp側の設定。

docker-compose.ymlの設定

主にlabelsに追記するだけでいい。サブドメインなしをリダイレクトするかどうかで少し違う設定になる。nginxの使用を想定している。

version: "3"

services:
  nginx:
    build: ./nginx
    environment:
      # templateでnginxのconfファイルに埋め込む
      NGINX_HOST: aaa.example.com
    labels:
      # - "traefik.http.routers.aaa-service.rule=Host(`aaa.example.com`)"
      # サブドメインなしをリダイレクトするなら下のruleを使う。
      - "traefik.http.routers.aaa-service.rule=Host(`aaa.example.com`)||Host(`example.com`)"
      - "traefik.enable=true"
      - "traefik.docker.network=traefik_network"
      - "traefik.http.routers.aaa-service.tls=true"
      - "traefik.http.routers.aaa-service.entrypoints=websecure"
      - "traefik.http.routers.aaa-service.tls.certresolver=myresolver"
      # サブドメインなしをリダイレクトするなら下の3行が必要。
      - "traefik.http.middlewares.subdomain-redirect.redirectRegex.regex=^https://example.com/(.*)"
      - "traefik.http.middlewares.subdomain-redirect.redirectRegex.replacement=https://aaa.example.com/$${1}"
      - "traefik.http.routers.aaa-service.middlewares=subdomain-redirect@docker"

networks:
  traefik_network:
    external: true

traefik.docker.networkに指定したnetworkに属するコンテナをサービスとしてくれる。なのでここでは先のtraefik_networkを指定しておく。

routerrule||を指定すればorマッチできる。これを利用して、サブドメインなしのトラフィックを導き、traefikmiddlewareの仕組みを使ってリダイレクトを行なっている。

一時的リダイレクトとして扱うつもりなので、traefikの動的設定としてサービス側で定義することにした。恒久的なリダイレクトにするなら、traefikの静的設定としてtraefik側でymlファイルに記述した方がいいだろう。

nginxの設定

nginx-proxyと違って、nginxのconfファイルが必要になる。Djangoなどはnginxもcomposeでupなどしないと機能が果たせない。

新しめなnginxイメージならテンプレートを利用して環境変数をconfファイルに埋め込むことができる。そのDockerfileは次のようにするだけ:

FROM nginx:alpine

COPY nginx.conf.template /etc/nginx/templates/

gunicornなら次のようなnginxのconfファイルでOK。

upstream app_server {
    ; webappコンテナがEXPOSEしているポートを指定
    server webapp:8000;
}

server {
    listen      80;
    ; ${variable}で埋め込まれる
    server_name ${NGINX_HOSTNAME};
    charset     utf-8;
    root        /var/www;

    location / {
        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        # we don't want nginx trying to do something clever with
        # redirects, we set the Host: header above already.
        proxy_redirect off;
        proxy_pass http://app_server;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

server_tokens off;

tlsはtraefikで終端されるので、80ポートだけ見ればいい。

nginxイメージが古い場合はテンプレートに埋め込む機能がないので、イメージをpullしておくのを忘れないようにする必要がある。

また。nginx起動時にwebappを見つけられないと落ちてしまうので、restart: alwaysなどをつける必要がある。(webappの起動よりnginxの起動が早いのが原因だと思う。他のオプションでもいいはず。)

確認

traefikwebappをそれぞれupして確認する。ブラウザでも確認できるし、curlでもできる。curlだと手軽なのでそちらでの確認をしてみる。

$ curl -I -L -k example.com
HTTP/1.1 308 Permanent Redirect
Location: https://example.com/
Date: Sun, 08 Nov 2020 17:05:00 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

HTTP/2 307 
location: https://aaa.example.com/
content-type: text/plain; charset=utf-8
content-length: 18
date: Sun, 08 Nov 2020 17:05:00 GMT

HTTP/2 200 
content-type: text/html; charset=UTF-8
date: Sun, 08 Nov 2020 17:05:00 GMT
...

オプションは、-I-L-kの3つでいいだろう。それぞれ順にHEADメソッドのリクエスト、リダイレクトにしたがって再リクエスト、証明書を信用する(caserverをステージングのものにしている場合必要)、の3つ。

リダイレクトとステータスコードが想定通りであることを確認できればOK。 これで完了。

nginx-proxyとの比較

traefikに置換したことで、役割の明確化ができたと思う。nginx-proxyでは静的ファイルの配信は兼用で行っていたため、静的ファイルの配信用にexternalなvolumeを繋いでいたが不要にすることができ、サービス内部のnginxに任せることができる。 ログがjson形式で出力できるのもメリット。

nginxの分だけサイズが大きくなるのは欠点。nginx-proxyも軽量だった。 nginxのconfファイルを作成する手間が増えたのもデメリットだが、構成が似ていれば流用が効くのであまり気にはならない。

参考

おわり

ログがずっと気になっていたので修正できてめでたしめでたし。それぞれのコンテナの管理範囲も明確になったし見渡しやすくなった。

実はまだexternalなvolumeのままなので、そちらは早めに修正しておきたい…

traefikの日本語資料が少ない気がする。公式ドキュメントを見れば大体わかるのもあるが、具体例が少なく感じた。 とりあえず実装は完了したのでよしとする。

以上です。


Docker入門者向け(広告)

Bitly
タイトルとURLをコピーしました