Docker Swarm + Traefikで手軽にリバースプロキシ＆ロードバランシングを導入する方法

先日、WEBサイト制作者向けのウェブサービスをリリースしたのですが、その制作過程で得た知見をシリーズで発信していく記事の第2弾です。
個人開発でウェブサービスにトライしてみたいと考えている方の参考になりましたら嬉しいです。

さて、「個人開発でウェブサービスをやろう！」となりますと、バックエンドも自分で整えていくことになります。

## Dockerを利用する

これまでに僕は、個人開発でいくつかのウェブサービスをVPSサーバを使って運用したことがあるのですが、OSのバージョンアップや使用しているライブラリのバージョンアップの際に発生する依存関係の問題に悩まされることが何度かありました。例えば、CentOS6上のcertbotをアップデートしようとしたら、python2からpython3へのアップデートが必要だけどyumが使えなくなるので困るとか、phpをバージョンアップしたいけど問題が発生した場合にうまく巻き戻せるかしらとか、フットワーク軽く動けないことにストレスを感じていました。

こうした悩みを解消すべくDockerに手を伸ばしたのですが、いやぁすばらしいですね、ほぼ悩みが解消してしまいました。
（もちろん、Dockerゆえの悩みもありますが、メリットの方が大きいと感じています）

今回は、Docker Swarmによるオーケストレーションを利用しつつ、リバースプロキシ＆ロードバランシングする具体的な方法について解説していこうと思います。

※ここではDockerについての解説は行いません。格安VPSをレンタルして、Dockerを使い始めるまでの手順については、個人ブログに書いた記事がありますので、興味がある方は以下のリンクを参照ください。
→　[Linode(格安VPS)でDocker用ホストを準備する手順メモ](https://blog.bagooon.com/?p=1806)

## Docker Swarm で気軽にコンテナオーケストレーションを実現する

ウェブサービスを運用する場合、ある程度の規模に成長した場合に備えて、スケール出来るようにしておかなければなりません。負荷が高くなったらホストやコンテナを増やして並列に処理するなど、状況に応じてコンテナを増減できるようコンテナオーケストレーションツールを利用します。

コンテナオーケストレーションツールといえば、Kubernetesがデファクトスタンダードですが、ここではDockerが標準で搭載しているDocker Swarmを利用して、手軽にコンテナオーケストレーションしていこうと思います。

## Traefik(v1.7)をリバースプロキシ＆ロードバランシンサーとして使用する

リバースプロキシ＆ロードバランシンサーといえば、まず思いつくのがNGINXではないかと思いますが、ここではTraefikを使用します。まさに「そうそう、こういうのが欲しかった！」という優れもので、マイクロサービス立ち上げの強い味方といえるでしょう。

※Traefikのバージョンについては、2.xではなく、1.7を採用しています。
（理由は、開発中に2.x系が正式リリースされたのですが、設定の方法に互換性が全くないため、1.7をそのまま使用しています。いずれ2.x系のノウハウが蓄積できたら差し替えようと思っています。）

Traefikには以下のような特徴があります。

- ロードバランシング（ラウンドロビン、リーストコネクション、スティッキーセッション）
- 構成のホットリロード
- HTTP/2サポート
- Websocketのサポート
- SSLバックエンドのサポート
- SSLフロントエンドのサポート
- Let'sEncryptによるSSL証明書の自動取得＆更新

以下に、whoamiというHello World的なコンテナを使ったdocker-compose.ymlの例を提示します。

```yaml:docker-compose.yml
version: '3.4'

services:

  traefik:
    image: traefik:1.7-alpine
    command:
      - "--logLevel=error"
      - "--entryPoints=Name:http Address::80 Redirect.EntryPoint:https"
      - "--entryPoints=Name:https Address::443 TLS"
      - "--defaultentrypoints=http,https"
      - "--acme"
      - "--acme.storage=certs.json"
      - "--acme.entrypoint=https"
      - "--acme.httpchallenge.entrypoint=http"
      - "--acme.onHostRule=true"
      - "--acme.email=your-email-address@sample.com"  // あたなのEmailアドレス
      - "--docker"
      - "--docker.endpoint=unix:///var/run/docker.sock"
      - "--docker.swarmMode"
      - "--docker.watch"
    ports:
      - 80:80
      - 443:443
    networks:
      - internal
      - overlay
    volumes:
      - ./certs/certs.json:/certs.json
    deploy:
      placement:
        constraints:
          - node.role == manager
      restart_policy:
        condition: on-failure

  whoami:
    image: containous/whoami
    networks:
      - internal
    deploy:
      replicas: 1  // コンテナの数を指定する
      restart_policy:
        condition: on-failure
      labels:
        - "traefik.enable=true"
        - "traefik.backend=whoami"
        - "traefik.frontend.rule=Host:hogehoge.com"  // ★ドメインをここで指定する
        - "traefik.frontend.entryPoints=https"
        - "traefik.docker.network=overlay"
        - "traefik.protocol=http"
        - "traefik.port=80"

networks:
  overlay:
    external: true
  internal:
    internal: true
```
このdocker-compose.ymlを走らせる前提条件は以下のとおりです。

- Docker Swarmを開始しておく  
マスターノード上で、以下のコマンドを実行しておく。  
docker swarm init --advertise-addr xxx.xxx.xxx.xxx  // マスターノードのプライベートIPアドレス
- overlayという名称でoverlayネットワークを作成しておく（ややこしい名称でスマン）  
docker network create --driver overlay overlay
- certs.json ファイルを準備しておく  
./certs/certs.json をマウントして永続化する  
ファイルは空でOK。chmod 500する必要あり（はまりポイント！）

※Traefikの設定については、traefik.tomlに記述してマウントする方法が一般的（？）ですが、ここではdocker-compose.ymlのcommandの引数として渡す方法を採用しています。（単純に僕の好みです。）

> service/
　├ certs/
　│　└ certs.json (500)
　└ docker-compose.yml

以下のコマンドでスタックを立ちあげれば完了！

```
docker stack deploy -c docker-compose.yml test
```

Dockerを使い始めた頃は、なっげーコマンドだなぁー、などと思ってましたが、慣れると何とも思わなくなるものですね。

さて、注目して欲しいのは、docker-compose.yml内の★の部分です。
`- "traefik.frontend.rule=Host:hogehoge.com"`とドメインに関する記述があります。
Traefikは起動すると、各コンテナのモニタリングを開始します。そして、各コンテナのラベルの記述に応じて、ドメインとコンテナの紐づけや、SSL証明書の取得から更新までを、すべて自動的に処理します。

しかもホットリロードにも対応していますので、後から別ドメインのコンテナを立ち上げても、やはり同様に処理してくれます。

ロードバランシングについては、「replicas: 1」の数値を書き換えて複数のコンテナを立ち上げれば、Traefikが勝手に各コンテナにラウンドロビンでアクセスを振り分けてくれます。

もしコンテナ内でセッションを使っていて、ユーザー別に前回と同じコンテナに振り分けたい場合にも、スティッキーセッションの設定をラベルに１行追加するだけでOKです。
`- "traefik.backend.loadbalancer.stickiness=true"`

ちょっと話がうますぎて不安になるレベルです。

### パスによる振り分け

パスによるコンテナの振り分けも簡単にできます。
パスによってAPIサーバを切り分けたい場合など、非常に重宝する機能のひとつです。

先程とは別のコンテナを追加して、そのコンテナに以下のように記述します。
`traefik.frontend.rule=Host:hogehoge.com;PathPrefix:/api/v1/`

すると`https://hogehoge.com/api/v1/～`にマッチするアクセスのみが、新しいコンテナに振り分けられ、それ以外のアクセスは最初のコンテナに振り分けられます。

### パスの書き換え
また、パスの書き換えも簡単にできます。
先程の記述を以下のように変更します。

`traefik.frontend.rule=Host:hogehoge.com;PathPrefix:/api/v1/`
　↓
`traefik.frontend.rule=Host:hogehoge.com;PathPrefixStrip:/api/v1/`

すると、以下のようにパスを書き換えた状態でコンテナにアクセスが振り分けられます。

`https://hogehoge.com/api/v1/fugafuga.html`
　↓
`https://hogehoge.com/fugafufa.html`

dockrt-compose.ymlはこんな感じになります。

```yaml:docker-compose.yml
version: '3.4'

services:

  traefik:
    image: traefik:1.7-alpine
    command:
      - "--logLevel=error"
      - "--entryPoints=Name:http Address::80 Redirect.EntryPoint:https"
      - "--entryPoints=Name:https Address::443 TLS"
      - "--defaultentrypoints=http,https"
      - "--acme"
      - "--acme.storage=certs.json"
      - "--acme.entrypoint=https"
      - "--acme.httpchallenge.entrypoint=http"
      - "--acme.onHostRule=true"
      - "--acme.email=your-email-address@sample.com"  // あたなのEmailアドレス
      - "--docker"
      - "--docker.endpoint=unix:///var/run/docker.sock"
      - "--docker.swarmMode"
      - "--docker.watch"
    ports:
      - 80:80
      - 443:443
    networks:
      - internal
      - overlay
    volumes:
      - ./certs/certs.json:/certs.json
    deploy:
      placement:
        constraints:
          - node.role == manager
      restart_policy:
        condition: on-failure

  whoami:
    image: containous/whoami
    networks:
      - internal
    deploy:
      replicas: 1  // コンテナの数を指定する
      restart_policy:
        condition: on-failure
      labels:
        - "traefik.enable=true"
        - "traefik.backend=whoami"
        - "traefik.frontend.rule=Host:hogehoge.com"
        - "traefik.frontend.entryPoints=https"
        - "traefik.docker.network=overlay"
        - "traefik.protocol=http"
        - "traefik.port=80"

  whoami-api-v1:
    image: containous/whoami
    networks:
      - internal
    deploy:
      replicas: 1  // コンテナの数を指定する
      restart_policy:
        condition: on-failure
      labels:
        - "traefik.enable=true"
        - "traefik.backend=whoami-api-v1"
        - "traefik.frontend.rule=Host:hogehoge.com;PathPrefixStrip:/api/v1/"
        - "traefik.frontend.entryPoints=https"
        - "traefik.docker.network=overlay"
        - "traefik.protocol=http"
        - "traefik.port=80"

networks:
  overlay:
    external: true
  internal:
    internal: true
```

docker-compose.ymlのラベルを書き換えるだけで、Treafikが自動的に条件に応じて各コンテナにアクセスを振り分けてくれます。

その他に、正規表現を使った振り分けやパスの柔軟な書き換えなども可能です。

## 最後に

今回は、Docker Swarm + Traefikで手軽にリバースプロキシ＆ロードバランシングを導入する方法について書いてみました。Dockerの学習コストはそれなりにありますが、Dockerの活用により、サーバ周りの準備や管理が格段に楽になりスピードアップすると思いますので、ぜひチャレンジして頂きたいと思います。

宣伝になりますが、冒頭で触れたウェブサービスは以下のとおりです。
WEBサイト制作者さん向けのサービスで、お気軽にお試しいただけると幸いです。（無料で使えます）
元々は自分たち向けに作ったシステムで、WEBサイト再作時に大量の情報やファイルを管理・共有するのに疲れ果て、それを解消するために作りました。なかなか評判が良かったので、一般向けに作り直してリリースした次第です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ウェブサイト制作に携わる人達に使っていただきたいウェブサービスをリリースしました。<br><br>ウェブサイトを作る際、大量の情報やデータを管理する必要がありますが、それをスッキリ整理整頓できて、簡単にチームで共有できます。<br><br>昨年の夏から作り始めて、やっと公開です。<a href="https://t.co/kFOCip3eUD">https://t.co/kFOCip3eUD</a></p>&mdash; 橋本技研@売れるネットショップ研究所 WEB技術×販促 (@hashimotosubaru) <a href="https://twitter.com/hashimotosubaru/status/1246611675379191808?ref_src=twsrc%5Etfw">April 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>