前置き
元がLT用なので布教形式になっています。
traefikの布教
正式にはtraefik proxy
なので注意してください。
メリット
- traefik proxyといい、oss
- dockerやk8sなどにネイティブ対応し、ラベルで設定を書ける
- 一つのポート開放でwebsocketをすべてのdockerに振り分けられる
- tcp・udpをサーバーに振り分けられる(ロードバランサ)
- httpとwebsocketを簡単にhttpsとwssにできる
- ミドルウェアを使うことでパスの書き換えなどができる
- ダッシュボードでデバッグや現在の状況が見れる
- モダン
※tcpやudpはサーバーを一個しか使わないなら普通にdockerから解放でいいと思う。よって、traefikではポートを80,443,8080,wssの四つを解放でいいと思う。
デメリット
- 知名度がないので記事がほぼ公式のみ
- .cert .pemファイルなどができない。acme.jsonにまとめられる。変換してくれるサードパーティソフトがあるので問題はない(必要になったことないけど)解説(別の人)
- http3に対応しているがまだ実験的な機能(なおかなり早くなるので私はオンにしている)
簡単なチュートリアル
自分の丸パクでいいです。コードの解説を下に書きます。
注意点
- auth用のパスなどは
htpasswd
コマンドを使います。
sudo dnf install -y httpd-tools
などでinstallしてください。 - dockerを使えるようにしておいてください
- 先に
docker network create traefik-net
を実行しコンテナを横断するネットを作成します。 - docker compose のタグを更新した際は、
docker compose restart
ではなくdocker compose down && docker compose up -d
としましょう。設定が更新されません。 - https関係の設定を弄るときは本家を読みながらしてください
-
oligami.ml
は私のドメインです。全て自前のドメインに置き換えてください。 - faviconはrootなnginxが担当しています。あなたの環境にはありません。
- しばらくはurlを公開しておきます。
解説
上記のgithubを見ながら読んでください。
treafik (reverse-proxy)
# Dynamic Configuration
http:
routers:
dashboard:
rule: Host(`oligami.ml`)&&(PathPrefix(`/api`)||PathPrefix(`/dashboard`))
tls:
certResolver: myresolver
service: api@internal
middlewares:
- auth
middlewares:
auth:
basicAuth:
usersFile: /etc/traefik/.htpasswd
これはtraefikの状況やエラーを簡単に見れたりする、ダッシュボードの設定です。簡単なログインを通して見られなくしています。ファイルが無ければhtpasswd -c .htpasswd user
で、あれば-c
なしで実行します。このgithubのはtest:testPasswordだったはずです。
こんな感じのファイルで設定せずに全部dockerのラベルで済ませられるのがtraefikのいいところです(dashboardは無理)。
なお、このファイルが変更されると即時反映されます。
私のダッシュボードはこんなんです。
ダッシュボードはルートに/dashboard/で飛べます。
# Providers config
# log:
# level: DEBUG
これでログのレベルを変更したりします。
providers:
docker: # Enable tells treafik to listen to docker
exposedByDefault: false
network: traefik-net
file:
directory: /etc/traefik/dynamic
これでdockerを使いますよ宣言してます(k8sとかも行けるので)。
あと、さっきのファイル(dynamic_conf.yml)があるフォルダ(/etc/traefik/dynamic)を登録します。
あと、exposedByDefault: false
で、dockerは明示的に有効にしないとtraefikに取り込まれないようにします。
# API/Dashboard config
api:
insecure: true # Enable the Web UI
ダッシュボードを有効に
experimental:
http3: true
http3を有効に
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: web-secure
scheme: https
web-secure:
address: :443
http3: {}
tls8081:
address: :8081
tls8082:
address: :8082
エントリポイント(使うポート)を登録します。web(http)はweb-secure(http3)に飛ばすようになっています。httpかhttpsはdockerごとに個別に設定します(確か)。
certificatesResolvers:
myresolver:
acme:
email: hogehoge@gmail.com
storage: /letsencrypt/default/acme.json
tlsChallenge: {}
# caserver: 'https://acme-staging-v02.api.letsencrypt.org/directory'
# httpChallenge:
# entryPoint: web
Let's encryptでてきとーにhttpsにします。自動更新です。独自の設定するときは頑張ってください。公式
証明書の情報とかが入っています。githubのは#
で黒塗りしてあります。
version: "3"
services:
traefik:
image: traefik:latest
container_name: traefik
ports:
- 80:80
- 443:443/tcp
- 443:443/udp
- 8080:8080
- 8081:8081
- 8082:8082
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config:/etc/traefik:ro
- ./letsencrypt:/letsencrypt
networks:
- traefik-net
networks:
traefik-net:
# docker network create traefik-net
external: true
dockerを横断するネットワークであるtraefik-net
は事前に作っておく必要があります。
ウェブ用である80ポート、https用である443ポート、http3用である443/udpポート、ダッシュボード用の8080ポート、websocket用の8081・8082を解放しています。
本来websocket用は一つでいいのですが、今回は一つのドッカーの二つのポートを同時にセキュアにできるという点の証明のため二つあります。これが一つでいいのがtraefikの利点です。
マウント(volumes)では、docker.sockを監視のため内部に取り込みます。あとはコンフィグファイルとacme.jsonの保存場所ですね。
networksではtraefik-net
を指定しています。externalで外部から取り込んでいます。これによりdockerから該当のネットを引き込んでセキュアにします。
見本のページ(traefik_example)
web server
htmlは説明することはないです。
//ボタン登録 ---------------------------------------------------------
document.addEventListener("DOMContentLoaded", function () {
const text_str = document.getElementById("text_str");
const received_text = document.getElementById("received_text");
var send_array = [];
const websocket = new WebSocket("wss://oligami.ml:8081/wss/traefik_example/");
const websocket2 = new WebSocket("wss://oligami.ml:8082/wss/traefik_example/");
const websocket3 = new WebSocket("wss://oligami.ml:8082/wss/traefik_example2/");
websocket.onmessage = (e) => {
message = e.data;
console.log(message);
var li = document.createElement('li');
if (send_array.includes(message))
li.style.color = 'blue';
var text = document.createTextNode(message);
li.appendChild(text);
received_text.appendChild(li);
};
document.getElementById('writeBtn').addEventListener("click", () => {
websocket.send(text_str.value);
send_array.push(text_str.value);
})
const websocket4 = new WebSocket("wss://oligami.ml:8081/wss/");
const websocket5 = new WebSocket("ws://oligami.ml:53005");
})
send_arrayには今まで送信した単語が全て入っています。ここに単語が存在していたら色を変えるようになっています。
const websocket = new WebSocket("wss://oligami.ml:8081/wss/traefik_example/");
ここを見るとわかるのですが、このようにwebsocketにはパスを設定することができます。
そして、traefikではこのパスごとに違うdockerに割り当てることができるので、websocket用のポートを一つしか開放しなくてよくなります。
この周りの三つのnew WebSocketは成功しますが、websocket4はパスがあっていないため何にも接続できず、websocket5はtraefikを介さずに接続するため、wssにならず、ブラウザから拒否られます。
websocket.onmessageでは、websocketで受け取ったメッセージを下にどんどん表示します。送ったことがあったら色を変えます。
logフォルダはそのままnginxのログが表示されます。
server {
server_name oligami.ml;
listen 80;
listen 443;
location /traefik_example/ {
root /var/www/html;
index index.html;
}
location /link/7iMsf0/ {
return 308 https://oligami.ml/traefik_example/;
}
}
パスによって振り分けます。/link/7iMsf0/は短縮リンクです。
wss
FROM python:3.11.2-slim
ENV APP_HOME=/home/wss
WORKDIR $APP_HOME
COPY requirements.txt $APP_HOME/
RUN pip install --upgrade pip \
pip install --no-cache-dir -r $APP_HOME/requirements.txt
###### Requirements without Version Specifiers ######`
# asyncio
# websockets
###### Requirements with Version Specifiers ######`
websockets==10.4
asyncio==3.4.3
よくあるpythonのdockerfileとrequirements.txtです。pip freezeしてあります。
import websockets
import asyncio
CLIENTS = set()
async def received(websocket):
global CLIENTS
CLIENTS.add(websocket)
async for msg in websocket:
websockets.broadcast(CLIENTS, msg)
async def main():
async with websockets.serve(received, "", 8082):
await asyncio.Future() # run forever
asyncio.run(main())
ローカルホストの8082にwebsocketのサーバをたてて、受け取ったメッセージを全員に送り返すだけのプログラムです。
import websockets
import asyncio
CLIENTS = set()
siritori_array = []
async def received(websocket):
global CLIENTS
global siritori_array
CLIENTS.add(websocket)
if len(siritori_array) != 0:
for item in siritori_array:
await websocket.send(item)
async for msg in websocket:
if len(siritori_array) == 0:
websockets.broadcast(CLIENTS, msg)
siritori_array.append(msg)
elif msg in siritori_array:
await websocket.send("It's already")
elif siritori_array[-1][-1] != msg[0]:
await websocket.send("not equal")
else:
websockets.broadcast(CLIENTS, msg)
siritori_array.append(msg)
async def main():
async with websockets.serve(received, "", 8081):
await asyncio.Future() # run forever
asyncio.run(main())
siritori_arrayにしりとりの文字列を保存します。後は接続してきたら今までのを送る。
最初の文字列なら無条件、もう言われてたらIt's already
と返す。
最後の一文字と最初の一文字が同じでなかったらnot equale
と返す。
って感じです。listenするポートは8081。
なお、ん
で終わったら終了する処理はありません。
この下のが最後のオオトリですね。
docker-compose.yml
networks:
traefik-net:
external: true
でネットワークを引き込みます。
nginx
networks:
- traefik-net
でネットワークを設定
- traefik.enable=true
でtraefikの対象に設定
- traefik.http.routers.traefik_example_webpage.entrypoints=web-secure
- traefik.http.routers.traefik_example_webpage.rule=Host(`oligami.ml`)&&(PathPrefix(`/traefik_example/`)||Path(`/link/7iMsf0`))
- traefik.http.routers.traefik_example_webpage.tls=true
- traefik.http.routers.traefik_example_webpage.tls.certresolver=myresolver
traefik_example_webpageと名付けたものを作成し(なかったら勝手に作られる)、エントリポイントをweb-secureに。
名前がかぶると正常に動作しないので注意
Host(`oligami.ml`)&&(PathPrefix(`/traefik_example/`)||Path(`/link/7iMsf0`))
で、Host
でドメインを設定、PathPrefix
で/traefik_example/からはじまるやつと短縮リンク用のやつをこちらに引き込むように設定します。
割り当ての優先度は長さによって決まっているのでとにかく長くするとこちらに来ます。最終的に行きつくnginxはpriorityを明示的に1にしておくといいでしょう。
traefikはgoでできているのでここら辺の書き方はgo準拠です。
- traefik.http.middlewares.pathsetup.replacepathregex.regex=^/(link)/([^/]*)
- traefik.http.middlewares.pathsetup.replacepathregex.replacement=/$${1}/$${2}/
- traefik.http.routers.traefik_example_webpage.middlewares=pathsetup@docker
これでpathsetupと名付けたミドルウェアを作成します。replacepathregexという公式が用意したmiddlewareを用いて、正規表現で、linkのあとのパスの最後が/ではなかったら勝手につけるようにしています。nginxでは最後が/でないと振り分けてくれないので。
そしてそれをtraefik_example_webpageのミドルウェアとして登録します。
ちなみに、dynamic_conf.ymlみたいにファイルで書いた場合の利点としては、ミドルウェアなどの設定をdocker内からリアルタイムで弄ることで、色々できると思います。
wss2
build:
context: ./wss/
でDockerfileを指定します。
expose:
- 8082
で、traefik-net
に8082ポートを解放します。
ports:
- 53005:8081
で、8081ポートをtraefikを介さずに外部に出します。wssでないとサイトは弾くということをわかりやすくするためです。
volumes:
- ./wss:/home/wss:ro
で、書き換え不可でpythonのファイルをマウントします。
command: python main.py & && python main2.py
で、最後にmain.pyをバックグラウンドで、main2.pyをデーモンで実行します。
- traefik.http.routers.wss2.rule=Host(`oligami.ml`)&&(Path(`/wss/traefik_example2/`))
で、パスを正確に指定し、これ以外を全てシャットアウトします。
- traefik.http.routers.wss2.entrypoints=tls8082
セキュアにしたい8082ポートに繋がっているtls8082というエントリポイントを指定します。
- traefik.http.routers.wss2.tls=true
- traefik.http.routers.wss2.tls.certresolver=myresolver
でセキュアにします。
wss
expose:
- 8081
- 8082
でtraefik-netに8081と8082を公開します。
- traefik.http.routers.wss.entrypoints=tls8081,tls8082
で、8081と8082のエントリポイントを指定します。
最後に
これで全部だと思います。
質問や修正がある方はコメントに。
traefikはまだ全然広まっていませんが、とても便利です。皆さんも使っていきましょう。
困ったときは公式ドキュメントです。