いよいよ本書の心臓部であるTraefikの説明です。
Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience.
It receives requests on behalf of your system and finds out which components are responsible for handling them.
(Translated by DeepL)
Traefikはオープンソースのエッジルーターでサービスの公開を楽しく簡単にすることができます。
システムに代わってリクエストを受け取り、どのコンポーネントがそのリクエストを処理する責任を負っているかを見つけます。
本書ではTraefikをリバースプロキシとして利用し、ホスト名から対象のサービスを切り替える目的で使用しています。
便利な点として次のものがあります。
- docker composeでlabelsを設定するだけで動く
- なんならコンテナ起動するだけでいい
- リバースプロキシ側での設定が不要なので手軽
- ポート番号をいちいち考えなくてもいい
- リバースプロキシがなければ、サービスごとにポート番号を割り当てる必要がありたいへん
- ダッシュボードで設定が確認できる
- どのサービスが起動しているかブラウザで一覧できる
コンテナをたくさん使うなら便利なのでお勧めのソフトです。
こちらのサイトを参考にさせていただきました。
本章はarkbig/devbaseリポジトリのtraefikフォルダの説明になります。
📂 devbase/
├── 📄.env(必要なら作る)
├── 📄compose.yaml
└── 📂traefik/
├── 📂dynamic/ # 動的設定ファイル置き場
│ ├── 📄dashboard.yaml # ダッシュボード画面設定
│ └── 📄tls.yaml # TLS設定
└── 📄traefik.yaml # Traefik設定
関連図
全体構成の説明で書いた図の再掲です。
このTraefikから出ている「Port 53」や「Port 443」などの経路部分を説明します。
traefik.yaml
TraefikはDocker公式イメージtraefikを利用するため、これまでと違ってDockerfileでイメージは作りません。
Traefikの設定は私が使うYAML以外にもTOMLやコマンドライン引数でも指定可能です。
コマンドライン引数が手軽ではありますが、設定が多くなるとファイル設定の方がわかりやすいです。
ここではcompose.yamlがYAMLなので合わせてtraefik.yamlで設定しています。
(TOMLがトムさんのObvious, Minimal Languageといい名前の付け方とシンプルで良さそうなので前から気になってますが、まだ使ったことがありません。)
#---------------(1)--------------#
global:
checknewversion: false
sendanonymoususage: false
#---------------(2)--------------#
# 公開ポート指定
entrypoints:
smtp:
address: :25
dns-tcp:
address: :53/tcp
dns-udp:
address: :53/udp
http:
address: :80
# httpsにリダイレクト
http:
redirections:
entrypoint:
to: https
scheme: https
https:
address: :443
http:
tls: true # TraefikでTLS終端
smtps:
address: :587
mongodb:
address: :27017
#---------------(3)--------------#
log:
level: DEBUG
format: json
accesslog:
format: json
#---------------(4)--------------#
# ダッシュボード有効化
api:
# insecure: true # デフォルトルーターを利用する場合は指定
dashboard: true
#---------------(5)--------------#
# 設定読み込み箇所
providers:
file:
directory: /etc/traefik/dynamic
docker:
# labels:に traefik.enalbe: trueがあるものだけ対象にするならtrueを指定
# exposedbydefault: false
# compose起動ならNameは"services:に指定した名前"-${COMPOSE_PROJECT_NAME}
defaultrule: Host(`{{ normalize .Name }}{{ env "DOMAIN" }}`)
#---------------(6)--------------#
# Prometheusを使う場合
# metrics:
# prometheus:
# entrypoint: https
大きく6つのパートに分かれています。
- グローバル設定
- 公開ポート設定
- ログ設定
- ダッシュボード設定
- 動的設定場所
- メトリクス設定(コメントアウト)
指定できるものはStatic Configuration: CLIをみて確認しています。大文字・小文字の区別はないのでyaml記載時はPascalCaseやcamelCaseで書いてもいいです。(entrypointsの任意に付ける階層名は大文字・小文字が区別される)
global:
)
1.グローバル設定(Traefik自身の挙動について設定しています。
checknewversion
で最新バージョンのチェックをしない、sendanonymoususage
で使用状況の送信をしない、に設定しています。
entrypoints:
)
2.公開ポート設定(Traefikが待ち受けるポート番号を列挙しています。smtp
やdns-tcp
の階層名は任意につけることができます(大文字・小文字は区別される)。
私はプロトコル名(http
://やhttps
://の部分)でつけるようにして、同じポートで別用途に振り分ける場合はハイフン区切り(dns-tcp
やdns-udp
)を使ってます。
基本的にaddress:
でポート番号の指定となります。:
コロンもつけることにご注意ください。/tcp
、/udp
でどちらを使用するかを指定できます。未指定だと/tcp
として扱われます。
http
とhttps
は特別な設定にしています。http
の場合、すべてhttps
へリダイレクトする設定です。(階層名をhttp、httpsにしているので少し混乱しますが)
https
はTLSを使用します。TraefikがTLS終端となり、各サービスへは通常のhttp通信がされます。
ほかにもhttpのオプションにはmiddlewaresも指定できたりします。
log:
、accesslog:
)
3.ログ設定(log:
でログのフォーマットをなんとなくjson
にしています。
accesslog:
はデフォルト無効ですが、このように何かを設定するかtrueとすると有効になります。ときどき見たくなるので有効にしています。
api:
)
4.ダッシュボード設定(ダッシュボードでどのポート番号が使われているか?どういう条件でサービスを切り替えるか?が確認できて便利です。
簡単に済ますなら、insecureを指定するだけで:8080ポートでアクセス可能になります。(api:を有効にすると、dashboard:はデフォルトでtrueになる)
api:
insecure: true
今回はinsecureを有効にしないで、自前でルートルールを書くことでhttpsアクセスさせるようにしてみました。
後述のdynamic/dashboard.yamlの節で説明します。
providers:
)
5.動的設定場所(Traefik用語ではproviderという動的設定を読み込む場所を有効にしています。(これをDynamic Configurationといい、traefik.yamlはStatic Configurationというらしいです)
file:
でファイルからルールを読み込むようにしています。filename:
ではなくdirectory:
指定することで、フォルダないの複数ファイルに分けて設定が可能です。
デフォルトでwatch: true
になっており、ファイルの変更を監視して自動的に新しい設定が反映されます。
ただしMacのColima環境だとうまく動かないので、いつかwatchexec
を試してみたい。
docker:
でDockerのコンテナに設定したlabelからルールを読み込むようにしています。
デフォルトでexposedbydefault: true
になっており、コンテナで公開されているポートへ自動的にルートを作成します。これが不要ならfalseを指定します。
またdefaultrule:
をアクセスしやすいように設定しています。(デフォルトは{{ normalize .Name }}
でName
にはコンテナ名が入ります)
このように設定ファイル内でGo template、sprig template functionsが使用できます。
ここで設定しているprovider以外に、KubernatesやECSなどもあるようです。
metrics:
)
6.メトリクス設定(必要があれば、metrics:
で統計情報が取れます。
設定できるのはDatadog、InfluxDB、Prometheus、StatsDの4つのようです。
dynamic/tls.yaml
TraefikのTLS設定は動的設定(Dynamic Configuration)に書きます。
ここではファイルプロバイダを利用して設定しています。
tls:
# 特定ドメイン名のサーバー証明書を指定(証明書のドメインと一致するものが使われる)
certificates:
- certfile: /certs/ssl-dev.test.cer
keyfile: /certs/ssl-dev.test.key
# stores:
# default:
# # 上記サーバー証明書に一致しないドメインはこちらが使われる(未設定だとTraefikが自動で作る)
# defaultCertificate:
# certfile: /certs/ssl-dev.test.cer
# keyfile: /certs/ssl-dev.test.key
ここのcertfile:
とkeyfile:
にオレオレ証明書の作成とOSへの登録で作成したサーバー証明書と秘密鍵を指定します。
複数形になているように、証明書を複数設定できます。証明書に設定されたドメインを見て、アクセスされたものと一致する証明書が使われます。
log.levelをDEBUGにしていれば、Traefikのログでどのドメインの証明書が設定されたか確認できます。
docker compose logs traefik
# devbase-traefik-1 | {"level":"debug","msg":"Adding certificate for domain(s) dev.test,*.dev.localhost,*.dev.test,dev.localhost,localhost,test,127.0.0.1","time":"2022-05-01T02:38:24Z"}
このほかの設定は公式サイトを参照ください(英語もTLSの知識も少なく丸投げ)
dynamic/dashboard.yaml
api.insecureをfalseのままにしているので、ダッシュボードへのアクセスを自前で書きます。
http:
routers:
traefik:
entrypoints: https
rule: Host(`traefik{{ env "DOMAIN" }}`) && PathPrefix(`/`)
service: dashboard@internal
traefik-api:
entrypoints: https
rule: Host(`traefik{{ env "DOMAIN" }}`) && PathPrefix(`/api`)
service: api@internal
ここでのtraefik
、traefik-api
は任意の名前です。
entrypoints:
に対応させたいポート番号(traefik.yamlで決めた名前)を指定します。(配列指定も可能)
紛らわしいけど、トップレベルのhttpは任意の名前じゃないほうです。(説明がわかりづらくなるから公式とかでは80番ポートをweb、443番ポートをwebsecureってしているのかな)
entrypoints
で指定したポート番号に通信がきたとき、rule
に一致すれば、service
へ転送するという流れになります。
service
へ渡す前にmiddlewares
というパスの変換や認証、HEADER改ざんをする中間処理を挟むことも可能です。
今回はTraefikの用意しているサービスを利用するため、名前に@internal
と接尾子がついています。
metrics
を有効にした場合はprometheus@internal
とかも登録することになるでしょう。
compose.yaml(抜粋)
最後に、dockerで使用するcomoseファイルです。
services:
dnsmasq:
image: arkbig/dnsmasq
init: true
build:
context: ./dnsmasq
args:
no_proxy: ${no_proxy-}
http_proxy: ${http_proxy-}
https_proxy: ${https_proxy-}
restart: unless-stopped
environment:
# This is your host or wsl2 eth0 ip address.
DNSMASQ_ADDR: ${DNSMASQ_ADDR:?Please set your ip address}
# This is your DNS server.
DNSMASQ_SERVER: ${DNSMASQ_SERVER-1.1.1.1}
ports:
- 127.0.0.1::53
- 127.0.0.1::53/udp
labels:
- traefik.enable=true
# TCP (non-tls)
- traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-tcp
- traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
- traefik.tcp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
# UDP
- traefik.udp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-udp
- traefik.udp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
traefik:
image: traefik
restart: unless-stopped
environment:
DOMAIN: ${DOMAIN:-.dev.test}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/etc/traefik:ro
- ./sslcert/.certs/ssl-dev.test.cer:/certs/ssl-dev.test.cer:ro
- ./sslcert/.certs/ssl-dev.test.key:/certs/ssl-dev.test.key:ro
network_mode: host
udptunnel:
image: arkbig/udptunnel
restart: unless-stopped
build:
context: ./udptunnel
args:
no_proxy: ${no_proxy-}
http_proxy: ${http_proxy-}
https_proxy: ${https_proxy-}
environment:
# These are your id.
CONTAINER_UID: ${CONTAINER_UID:?Please set your uid}
CONTAINER_GID: ${CONTAINER_GID:?Please set your gid}
volumes:
- ./udptunnel/udp_forwarding.conf:/home/udpstaff/udp_forwarding.conf
# 他のcompose(network)にも送るかもしれないので、hostにしておく
# (hostじゃなくて、networks:に指定すれば複数ネットワークに入ることもできるけど)
network_mode: host
profiles:
- udptunnel
traefikのvolumes:
でこれまで作成したファイルたちをバインドマウントしています。
あとDocerのコンテナ起動を検知してルールを自動追加するため、docker.sock
もバインドマウントしています。
またTraefikコンテナからほかのコンテナに通信する必要があるので、network_mode
はhost
モードとしています。
手動でnetworks
を指定しコンテナ間通信させることもできます。(すべて同一ネットワークにしたり、Traefikを複数ネットワークに所属させたり)
Git管理外の.envに個人環境設定を書きます。ついでにCOMPOSE_PROJECT_NAME
も使いたいので設定します。
COMPOSE_PROJECT_NAME=devbase
#udptunnel必要な環境ならコメント解除
#COMPOSE_PROFILES=udptunnel
# UID/GIDは`id`コマンドで確認できる
CONTAINER_UID=1000
CONTAINER_GID=1000
# DNSMASQ_ADDRはホストのIPアドレスかWSL2なら割り当てた固定IPアドレス
DNSMASQ_ADDR=192.168.100.100
# DNSMASQ_SERVERは自分の正規DNSサーバー
DNSMASQ_SERVER=1.1.1.1
# ワイルドカード対応ドメイン
DOMAIN=.dev.test
私はローカル環境ではフルの"hoge.dev.test"とFQDN指定しますが、本番(社内環境)ではDNSのserchを利用して"hoge-projectX"と省略してアクセスさせています。
このためDOMAIN環境変数には.
や-
始まりを設定しています。
COMPOSE_PROJECT_NAME
は未指定だとフォルダ名になりますが、環境変数として参照したいので明示しています。
前の章で設定したDnsmasqですが、最終系に合わせて、environment:
とports:
とlabels:
が変わっています。
dnsmasq:
+ environment:
+ # This is your host or wsl2 eth0 ip address.
+ DNSMASQ_ADDR: ${DNSMASQ_ADDR:?Please set your ip address}
+ # This is your DNS server.
+ DNSMASQ_SERVER: ${DNSMASQ_SERVER-1.1.1.1}
ports:
- - 53:53
- - 53:53/udp
+ - 127.0.0.1::53
+ - 127.0.0.1::53/udp
+ labels:
+ - traefik.enable=true
+ # TCP (non-tls)
+ - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-tcp
+ - traefik.tcp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
+ - traefik.tcp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
+ # UDP
+ - traefik.udp.routers.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=dns-udp
+ - traefik.udp.services.dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=53
environment:
は最低限のものを.envファイルで設定できるように登録してあります。
ports:
の指定が見慣れない指定になってます。これは127.0.0.1
のアドレスに対して自動設定されたポート番号(49155とか大きい数字でwikipediaいわく「動的・私用ポート番号」)をコンテナの53番ポートにバインドしています。
こうすることで、ホスト側のポート番号被りを避けられ、Traefik経由以外でのアクセスが難しくなります。(ローカルでは49155とかでアクセスできるけど、127.0.0.1じゃないeth0にはバインドされてない)
実際使用されるポート番号はdocker compose ps
で確認できます。
labels:
でTraefikの動的設定(Dynamic Configuration)を行っています。
dnsmasq-${COMPOSE_PROJECT_NAME:-devbase}
の部分は任意の名前で、ほかのサービスと被らなければ大丈夫です。
(tcp
、udp
、http
)×(routes
、services
、middlewares
)の組み合わせごとにネームスペースがあるようです。
ここでのtcp
とudp
で同じ名前を指定してますが、きちんと棲み分けられてました。
tcp
はTLS接続すればホスト名でのサービス振り分けが可能ですが、通常はHostSNI(*
)となります。(Configuring TCP Routers)
udp
は一般的なしくみがないのでルールは指定できません。(Configuring UDP Routers)
私はこれで満足してしまって調べていませんが、最近はDNSの名前解決もセキュアにするしくみがあるようです。それを使えばホスト名で切り替えができる可能性もあります。
Traefikを起動する
それではTraefikを起動してみましょう。(udptunnelが必要ならコメントアウト解除)
docker compose up -d traefik dnsmasq #udptunnel
起動ができたら、ブラウザでhttps://traefik.dev.test/にアクセスしてみます。
Traefikのダッシュボードが表示されれば成功です🎉。
Traefikダッシュボード
Routers画面
使用例
Traefikの使用例をいくつか示します。
docker runで起動するだけの例
Static Configurationでデフォルトのexposedbydefault: true
のままなら、docker run
で起動するだけでTraefikからアクセスできます。
試しにcaddyで試してみましょう。
docker run --rm -d --name caddyTest caddy
caddyが何かはよく知らないけど、どれかのサンプルで使われて、「おめでとう!」と言ってくれてできた感がうれしい!
cadyTestルートの追加を確認
ダッシュボードを見ると、caddyTest.dev.test
が追加されているのが確認できます。
このホスト名はdefaultrule: Host(`{{ normalize .Name }}{{ env "DOMAIN" }}`)
により決まります。
https://caddyTest.dev.test/にアクセスしてみましょう。
「おめでとう!」と見えましたか?
ホスト名は大文字・小文字の区別はないのでhttps://caddytest.dev.test/でもOK。
動作が確認できたらcaddyさんには退場してもらいましょう。docker stop caddyTest
でコンテナが停止&削除されます。(--rmオプション指定してないと削除はされない)
docker composeでホスト名以外のルールとmiddlewaresの例
コンテナの監視ツールcAdvisorを例にします。(ただしMacのColima環境だと動かなかった)
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.39.3
restart: unless-stopped
command:
- -url_base_prefix=/casvisor
- -global_housekeeping_interval=1m0s
- -housekeeping_interval=30s
- -max_housekeeping_interval=6m0s
environment:
# サブディレクトリ運用するので、ヘルスチェック先を変更
CADVISOR_HEALTHCHECK_URL: http://localhost:8080/cadvisor/healthz
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
- /etc/machine-id:/etc/machine-id:ro
# ホストの情報取得など権限が必要
privileged: true
devices:
- /dev/kmsg
ports:
- 127.0.0.1::8080
labels:
traefik.enable: true
traefik.http.routes.cadvisor.entrypoints: https
traefik.http.routes.cadvisor.rule: Host(`metrics.dev.test`) && PathPrefix(`/cadvisor/`)
# faviconがサブディレクトリ対応してないので無理やり転送
# ただし取得はできない(誤ったトップ階層のを取ってこないくらいの気持ち)
traefik.http.routes.cadvisor-favicon.entrypoints: https
traefik.http.routes.cadvisor-favicon.rule: Host(`metrics.dev.test`) && PathPrefix(`/favicon.ico`) && HeadersRegexp(`Referer`,`://metrics.dev.test/cadvisor/`)
traefik.http.routes.cadvisor-favicon.middlewares: cadvisor-favicon-1
traefik.http.middlewares.cadvisor-favicon-1.addprefix: /cadvisor/
cAdvisorをサブディレクトリ/cadvisor/
で動かす例です。Traefikに関係あるところを説明していきます。
CADVISOR_HEALTHCHECK_URL: http://localhost:8080/cadvisor/healthz
これはコンテナ内でのアクセス先を記載します。そのため、http
でlocalhost
の8080
ポートとコンテナ内の情報になっています。
traefik.http.routes.cadvisor.rule: Host(`metrics.dev.test`) && PathPrefix(`/cadvisor/`)
これはルールを&&
を使って複数指定をand条件でつなげています。(PathPrefixは指定パスで始まる場合の条件)
ちなみにHost(`metrics.dev.test`,`alias.dev.test`)
とホスト名を複数指定もできます。
traefik.http.routes.cadvisor-favicon.rule: Host(`metrics.dev.test`) && PathPrefix(`/favicon.ico`) && HeadersRegexp(`Referer`,`://metrics.dev.test/cadvisor/`)
これはさらにHTTPヘッダの内容も条件に加えています。(HeadersRegexpはHTTPヘッダの正規表現判定)
そのほかメソッドやクエリパラメータも条件にできます。HTTPルールはこちらのドキュメントに記載があります。
traefik.http.routes.cadvisor-favicon.middlewares: cadvisor-favicon-1
traefik.http.middlewares.cadvisor-favicon-1.addprefix: /cadvisor/
これはサービスへ渡す前にリクエストを加工するmiddlewares
を指定する例です。
routesで使用するミドルウェアを指定し、middlewaresで動作を定義しています。
middlewaresを複数使う場合なら配列形式で指定します。
今回のように1つならroute名(cadvisor-favicon)とmiddleware名(cadvisor-favicon-1)と別名にする必要はなく同名でも可能です。
routesとmiddlewaresで別の名前空間を持っていると考えられます。
また共通のmiddlewaresをFile Providerで定義しておいて、hoge@file
で参照もできます。(認証系で使うと便利かも)
そのほかBasicAuth、IPWhiteList、RedirectRegex、StripPrefixなどいろいろ設定できます。一覧はこちらのドキュメントに記載があります。
cadvisorを実行した場合は、docker compose rm -s cadvisor
で削除しておきましょう。
コンテナが複数のポートをEXPOSEしている例
次の章で説明するMailHogの例です。
services:
mailhog:
image: mailhog/mailhog
restart: unless-stopped
ports:
- 127.0.0.1::1025
- 127.0.0.1::8025
labels:
- traefik.enable=true
# SMTP側
- traefik.tcp.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=smtp
- traefik.tcp.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
- traefik.tcp.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=1025
# HTTPS側
- traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=https
- traefik.http.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=8025
このコンテナは1025番ポートと8025番ポートをEXPOSEしています。
# HTTPS側
- traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=https
+ - traefik.http.routers.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.service=mailhog-${COMPOSE_PROJECT_NAME:-devbase}
- traefik.http.services.mailhog-${COMPOSE_PROJECT_NAME:-devbase}.loadbalancer.server.port=8025
compose.yamlのほうではtraefik.http.routes.<name>.service
の指定を省略していますが、説明のため追加した状態にしました。
ルールを記載したrouterの転送先をtraefik.http.routes.<name>.service
で指定します。
そしてサービスがtraefik.http.services.<name>.loadbalancer.server.port
で対応するコンテナのポート番号を指定します。
compose.yamlのようにtraefik.http.routes.<name>.service
を省略すると同名のserviceが自動的に使われるようです。