LoginSignup
2
1

More than 1 year has passed since last update.

traefikでwebsocketを細かく制御する

Posted at

前置き

元が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を公開しておきます。

traefik_example

解説

上記のgithubを見ながら読んでください。

treafik (reverse-proxy)

dynamic_conf.yml
# 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は無理)。
なお、このファイルが変更されると即時反映されます。

私のダッシュボードはこんなんです。image.png
ダッシュボードはルートに/dashboard/で飛べます。

traefik.yml
# Providers config
# log:
#   level: DEBUG

これでログのレベルを変更したりします。

traefik.yml
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に取り込まれないようにします。

traefik.yml
# API/Dashboard config
api:
  insecure: true # Enable the Web UI

ダッシュボードを有効に

traefik.yml
experimental:
  http3: true

http3を有効に

traefik.yml
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ごとに個別に設定します(確か)。

traefik.yml
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にします。自動更新です。独自の設定するときは頑張ってください。公式

acme.json

証明書の情報とかが入っています。githubのは#で黒塗りしてあります。

docker-compose.yml
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は説明することはないです。

index.js
//ボタン登録 ---------------------------------------------------------
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のログが表示されます。

default.conf
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

Dockerfile
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.txt
###### Requirements without Version Specifiers ######`

# asyncio
# websockets

###### Requirements with Version Specifiers ######`

websockets==10.4
asyncio==3.4.3

よくあるpythonのdockerfileとrequirements.txtです。pip freezeしてあります。

main2.py
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のサーバをたてて、受け取ったメッセージを全員に送り返すだけのプログラムです。

main.py
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

docker-compose.yml
networks:
  traefik-net:
    external: true

でネットワークを引き込みます。

nginx
docker-compose.yml
    networks:
      - traefik-net

でネットワークを設定

docker-compose.yml
      - traefik.enable=true

でtraefikの対象に設定

docker-compose.yml
      - 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準拠です。

docker-compose.yml
      - 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
docker-compose.yml
    build:
      context: ./wss/

でDockerfileを指定します。

docker-compose.yml
    expose:
      - 8082

で、traefik-netに8082ポートを解放します。

docker-compose.yml
    ports:
      - 53005:8081

で、8081ポートをtraefikを介さずに外部に出します。wssでないとサイトは弾くということをわかりやすくするためです。

docker-compose.yml
    volumes:
      - ./wss:/home/wss:ro

で、書き換え不可でpythonのファイルをマウントします。

docker-compose.yml
    command: python main.py & && python main2.py

で、最後にmain.pyをバックグラウンドで、main2.pyをデーモンで実行します。

docker-compose.yml
      - traefik.http.routers.wss2.rule=Host(`oligami.ml`)&&(Path(`/wss/traefik_example2/`))

で、パスを正確に指定し、これ以外を全てシャットアウトします。

docker-compose.yml
      - traefik.http.routers.wss2.entrypoints=tls8082

セキュアにしたい8082ポートに繋がっているtls8082というエントリポイントを指定します。

docker-compose.yml
      - traefik.http.routers.wss2.tls=true
      - traefik.http.routers.wss2.tls.certresolver=myresolver

でセキュアにします。

wss
docker-compose.yml
    expose:
      - 8081
      - 8082

でtraefik-netに8081と8082を公開します。

docker-compose.yml
      - traefik.http.routers.wss.entrypoints=tls8081,tls8082

で、8081と8082のエントリポイントを指定します。

最後に

これで全部だと思います。
質問や修正がある方はコメントに。

traefikはまだ全然広まっていませんが、とても便利です。皆さんも使っていきましょう。
困ったときは公式ドキュメントです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1