この記事は Docker Advent Calendar 2020 の23日目のエントリーです。
ちなみにエントリーしたのは 12/24 でして、カレンダーの23日だけが空いていたので滑り込みました。
さて、今どき Webサイトは HTTPS が当たり前の時代ですよね。
でも HTTPS 対応するのって結構手間がかかるんですよね。お金も。
なんとか簡単に出来ないのかなって、ずっと考えていました。
WebサービスのHTTPS対応にあたっての困りごと
- SSL証明証を発行してもらい、定期的に更新していかなくてはならない(この時点でもう面倒くさい)
- WebアプリケーションフレームワークにHTTPSの設定をしないといけない(普段やらないので不慣れでよくわからない)
- もっとアプリケーションの構築作業に集中したい(それが本業だもんね)
- サーバ負荷が高まれば、アプリケーションサーバを複数台にして負荷分散させたい(夢は大きく)
- でも詳しい人がいない(インフラ周りはよく分かんないよね)
こういった課題に対応するための Docker コンテナ型サービスを作ってみました。
EzGate とは
EzGate を使えば、HTTPS に対応したリバースプロキシを簡単に構築できます。
いわゆる SSL アクセラレータってやつです。
特長
- nginx でリバースプロキシ機能を提供
- HTTPS のためのSSL証明書には無料の Let'sEncrypt を利用
- 証明書の更新期限が近づくと自動的に更新
- HTTP/2 にも対応
- すでにSSL証明書がある場合や、開発環境のための証明書を個別に指定することも可能
- 中継先サーバが複数台の場合にも対応可能(負荷分散構成)
- ドメインが複数の場合にも対応可能(マルチテナント構成)
単純な例
まずは Webサーバが1台だけの最もシンプルな構成で、リバースプロキシを立てる手順を見てみましょう。
Webサーバの IP アドレスは 172.17.0.4
であり、80番ポートで待ち受けしているとしましょう。
この場合は、EzGate コンテナに環境変数を指定するだけでリバースプロキシのコンフィグが完了します。
ここからは Google Compute Engine の無料枠を使って、実際に動かしてみたので手順を紹介します。
なお仮想マシンには事前に docker をインストールしておく必要があります。
OSは Ubuntuです。Cent OS でも同じ手順で試行可能と思います。
# 疎通確認用のWebサーバとして wordpress コンテナを起動
$ sudo docker run -d --rm --name server1 wordpress:5.6.0-apache
# コンテナの起動確認
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c0f0d145d86d wordpress:5.6.0-apache "docker-entrypoint..." 18 seconds ago Up 16 seconds 80/tcp server1
# コンテナのIPアドレスを確認、変数にセット
$ SERVER1_IP=`sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server1|tee /dev/stderr`
172.17.0.4
# 残りの設定情報
$ DOMAIN=test.35.197.56.116.sslip.io # このサーバのドメイン。35.197.56.116 の部分は実際のグローバルIDを指定。
$ MAIL=your@email.com # SSL証明書取得に使用するメールアドレス。自分のものに変えてください。
# リバースプロキシを起動
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
-e PROXY_TO=$DOMAIN,$SERVER1_IP:80 \
-e CERT_EMAIL=$MAIL \
neogenia/ez-gate:latest
# コンテナログを確認
$ sudo docker logs -f ez-gate
コンテナのログがずらずらっと出てきて、以下のようになれば起動成功です。
----- finish all setups successfully -----
--- START MONIT -----
* Starting daemon monitor monit [ OK ]
==> /var/log/monit.log <==
[UTC Dec 24 09:56:08] info : New Monit id: f439d27cdf8d377e03482d877b043d2e
Stored in '/var/lib/monit/id'
[UTC Dec 24 09:56:08] info : Starting Monit 5.25.1 daemon
[UTC Dec 24 09:56:08] info : 'c91b02ea822e' Monit 5.25.1 started
[UTC Dec 24 09:56:08] error : 'crond' process is not running
[UTC Dec 24 09:56:08] info : 'crond' trying to restart
[UTC Dec 24 09:56:08] info : 'crond' start: '/etc/init.d/cron start'
==> /var/log/nginx/error.log <==
==> /var/log/nginx/error_test_35_197_56_116_sslip_io.log <==
Webブラウザを開き、 $DOMAIN
で設定したドメインにアクセスしてください。
Wordpress の初期設定画面が表示されたら成功です。
このように、ドメインと中継先を設定するだけであとは 自動的に SSL 証明書を取得してくれて リバースプロキシとして稼働します。
中継先は Dockerコンテナでなくとも、別のサーバでも、TCPでアクセス可能であれば何でも構いません。
環境変数の説明
コンテナ起動コマンドをもう一度見てみましょう。
# リバースプロキシを起動
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
-e PROXY_TO=$DOMAIN,$SERVER1_IP:80 \
-e CERT_EMAIL=$MAIL \
neogenia/ez-gate:latest
PROXY_TO
には、中継するドメインと、中継先サーバをカンマ区切りで設定します。
中継先には :80
というようにポート番号を指定することも可能です(省略時は 80
)。
CERT_EMAIL
には、SSL証明書の取得に使用するメールアドレスを設定します。
不正なメールアドレスだと、エラーになる可能性があります。
設定情報として必要なのはたったこれだけです。
エラーになる場合
1: 以下のようなエラーになる場合
Traceback (most recent call last):
7: from /var/scripts/reload_config.rb:227:in `<main>'
6: from /var/scripts/reload_config.rb:216:in `backup_dir'
5: from /var/scripts/reload_config.rb:236:in `block in <main>'
4: from /var/scripts/reload_config.rb:236:in `each'
3: from /var/scripts/reload_config.rb:238:in `block (2 levels) in <main>'
2: from /var/scripts/reload_config.rb:96:in `setup_ssl'
1: from /var/scripts/reload_config.rb:28:in `setup'
/var/scripts/reload_config.rb:19:in `shell_exec': ## ERROR ## exit status: 1 command_line: 'APP_DOMAIN=test.35.197.56.116.sslip.io LETS_ENCRYPT_CERT_MAIL=your@email.com /var/scripts/setup_letsencrypt.sh' (RuntimeError)
設定に誤りがあります。
例えば CERT_EMAIL
環境変数に正しいメールアドレスが設定されていない可能性があります。
2: 以下のようなエラーになる場合
Failed authorization procedure. test.35.197.56.116.sslip.io (http-01): urn:ietf:params:acme:error:dns :: No valid IP addresses found for test.35.197.56.116.sslip.io
Let's Encrypt のサーバ側で名前解決に失敗してる可能性があります。
sslip.io
はダウンしてるときもあるため、時間をおいてリトライしてみるか、別のフリードメインで試してみてください。
また、ドメインを自分で割り当てることが出来る人は、なにか適当なサブドメインを割り当てるのが一番確実です。
3: 以下のようなエラーになる場合
An unexpected error occurred:
There were too many requests of a given type :: Error creating new order :: too many certificates already issued for: nip.io: see https://letsencrypt.org/docs/rate-limits/
Let's Encrypt のエラーです。
そのドメインでの SSL証明書発行回数が上限に達しています。
nip.io
などの人気のあるワイルドカードDNSでは、すでに多くの人がSSL証明書発行を試行しているため、このようなエラーになることが多いです1。
Freenom などのフリードメインを取得して試してみるか、ドメインを自分で割り当てることが出来る人は、なにか適当なサブドメインを割り当てるのが一番確実です。
なお、短時間のうちにあまり何度も証明書発行を繰り返していると Let's Encrypt の回数制限に引っかかってしまってエラーになることもあるので注意が必要です2。
また、ez-gate
コンテナを起動し直す場合は、先にコンテナを停止して rm
しておく必要があります。
$ sudo docker kill ez-gate
$ sudo docker rm ez-gate
開発環境のための証明書を使用する
ローカル開発環境など(グローバルIPで外部からアクセスできない環境)では、Let's Encrypt の証明書発行ができませんので、
mkcert を使って、ローカル開発環境のためのSSL証明書を使用することができます。
もう「自己署名入り証明書」(通称:オレオレ証明書)を使わなくて良いんです!
mkcert のインストール
mkcert のインストールはとても簡単です。
macOS なら Homebrew を使ってインストールできます。
brew install mkcert
# Firefox ユーザなら以下も必要
brew install nss
詳しくは、mkcert 公式の README を参照してください。
mkcert で証明書を作成
開発環境のURLが https://localhost/
の場合で説明します。
他のIPアドレスでアクセスしたい場合は、 localhost
の部分をIPアドレスに読み替えてください。
# 証明書格納用フォルダを作成
$ mkdir certs
# mkcertを使って localhost 向けの証明書ファイルを生成
$ mkcert -install # 初回のみ
$ mkcert -key-file certs/key.pem -cert-file certs/cert.pem localhost
リバースプロキシに証明書を指定して起動
# 証明書格納用フォルダをボリュームマウントし、環境変数でそれらのファイルを指定
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
-e PROXY_TO=localhost,$SERVER1_IP:80 \
-e CERT_FILE=/mnt/cert.pem \
-e KEY_FILE=/mnt/key.pem \
-v `pwd`/certs:/mnt \
neogenia/ez-gate:latest
# コンテナログを確認
$ sudo docker logs -f ezgat
正常に起動できたら、Webブラウザを開き https://localhost/
にアクセスしてください。
SSL証明書のエラーが出なければ成功です。
環境変数の説明
CERT_FILE
には、mkcert で生成された cert-file のパスを指定します(コンテナ内でのパス)。
KEY_FILE
には、mkcert で生成された key-file のパスを指定します(コンテナ内でのパス)。
上記の環境変数を指定することで、Let's Encrypt のSSL証明書取得は行われなくなり、指定された証明書を使って HTTPS リバースプロキシが稼働します。
サーバ複数台構成の場合(負荷分散)
中継先サーバが複数台の場合は、環境変数で指定する方法がないため、設定ファイルを書く必要があります。
では再び、GCEで実際に試してみた例を紹介します。
# Webサーバとして wordpress コンテナを2つ起動 (本当はセッションやDBを共有させてクラスタ構成にする必要がある)
$ sudo docker run -d --rm --name server1 wordpress:5.6.0-apache
$ sudo docker run -d --rm --name server2 wordpress:5.6.0-apache
# コンテナの起動確認
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a8c41d95a57d wordpress:5.6.0-apache "docker-entrypoint..." 2 minutes ago Up 2 minutes 80/tcp server2
c0f0d145d86d wordpress:5.6.0-apache "docker-entrypoint..." 3 hours ago Up 3 hours 80/tcp server1
# それぞれのコンテナのIPアドレスを確認
$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server1
172.17.0.4
$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server2
172.17.0.5
# 設定ファイルを新規作成しエディタで開く
$ mkdir mnt
$ vi mnt/config
# 以下の内容を書く
domain('test2.35.197.56.116.sslip.io') {
proxy_to "172.17.0.4", "172.17.0.5" # 中継先サーバ。カンマ区切りで複数指定可能
cert_email 'your@email.com' # SSL証明書取得に使用するメールアドレス。自分のものに変えてください。
}
# 書き終わったらエディタを終了
# コンフィグファイルを指定してリバースプロキシを起動
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
-v `pwd`/mnt:/mnt \
-e CONFIG_PATH=/mnt/config \
neogenia/ez-gate:latest
# コンテナログを確認
$ sudo docker logs -f ez-gate
正常に起動できたら、Webブラウザを開いてアクセスしてみてください。
負荷分散されていることを確認するため、Webサーバ側のログを開いておきましょう。
# server1 のログを監視
$ sudo docker logs -f server1
# 別のターミナルを開いて server2 のログも監視
$ sudo docker logs -f server2
この状態でブラウザリロードすると、双方のサーバへアクセスが来ることが確認できます。
複数ドメインでのマルチテナント型
更に複雑な構成として、ドメインを複数割り当てて、それぞれに中継先を設定することが出来ます。
例えば、www.example.com
でアクセスされたら www
というサーバに中継し、
redmine.example.com
でアクセスされたら redmine
というサーバに中継する、
といったようなイメージです。
いわゆるマルチテナント(マルチドメイン)構成です。
設定ファイルの例
以下のようにドメインごとに中継先を記述するだけです。
domain('www.example.com') {
proxy_to "www" # 中継先サーバリスト。名前解決できればIPでなくとも良い
cert_email 'your@email.com' # SSL証明書取得に使用するメールアドレス
}
domain('redmine.example.com') {
proxy_to "redmine" # 中継先サーバリスト。名前解決できればIPでなくとも良い
cert_email 'your@email.com' # SSL証明書取得に使用するメールアドレス
}
設定ファイルの書き方
ここからは 公式README からの引用です。
設定ファイルの基本構文は以下です。
domain('www.example.com') {
proxy_to "webapp1", "webapp2", ...
}
複数の domain()
を記述することが可能です。
さらに、以下のように cert_email
nginx_config
のオプション指定が可能です。
domain('www2.example.com') {
proxy_to "apache1", "apache2"
cert_email 'your@email.com'
nginx_config <<~_CONFIG_
# change upload size max
client_max_body_size 100M;
_CONFIG_
}
cert_email
はコンテナの環境変数 CERT_EMAIL
の指定があればそちらが優先されます。
nginx の設定ファイルに追記したい内容があれば、nginx_config
に指定してください。
この設定ファイルは Ruby の内部DSLであり、プログラムとして解釈されますので、別ファイルの読み込みや環境変数の参照なども可能です。
更に高度な設定
※2022-06-24 追記
アクセス元IPによって中継先を切り替える
複数サーバでの負荷分散のためにリバースプロキシを使用する場合、ある特定のサーバだけを切り離して検証したい場合があります。 そういった利用シーンを想定し、EzGateではある特定のPCからアクセスした場合のみ、切り離したサーバに中継させることが出来ます。
まず、アプリケーションサーバを2台で負荷分散する場合、以下のように proxy_to
にカンマ区切りで中継先を指定します。
domain('myservice.example.com') {
# アプリケーションサーバ2台で負荷分散
proxy_to 'apserver1', 'apserver2'
cert_file '/mnt/cert.pem'
key_file '/mnt/key.pem'
}
ここで、apserver1
をメンテナンスのために切り離し、自社のネットワークのグローバルIPからアクセスした時だけ apserver1
につながるようにしたい場合は、以下のように proxy_to
のオプション引数 from:
を指定します。
domain('myservice.example.com') {
# アクセス元IPアドレスが '11.22.33.44' の時だけ、`apserver1` へ中継
proxy_to 'apserver1', from: '11.22.33.44'
# それ以外は `apserver2` へ中継
proxy_to 'apserver2', from: :all # `from: :all` は省略可能
}
GitHub リポジトリの example3/ ディレクトリ にサンプルが入っています。
リダイレクト
中継させるのではなくリダイレクト応答させることも出来ます3。
proxy_to
の代わりに redirect_to
でリダイレクト先を設定するだけです。
ドメインの移行時などに便利です。
domain('old.example.com') {
redirect_to "new.example.com" # 新しいドメインにリダイレクト
cert_email 'your@email.com'
}
GitHub リポジトリの example4/ ディレクトリ にサンプルが入っています。
ロケーションごとに中継先を切り替える
ある特定のパスにアクセスされた時だけ、中継先を切り替えることが出来ます。
例えば、 通常のアクセスは webapp1
サーバに中継し、 /map_api
にアクセスされた時だけ webapp2
サーバに中継する、といった設定が簡単に出来ます。
ドメイン内でパスごとに複数のサーバで運用することが出来るので、既存サイトに別システムを付け加えたり、システムの五月雨式な移行を行う際に便利です。
SERVER_IP = '192.168.11.22'
domain("#{SERVER_IP}.nip.io") {
# デフォルトの中継先
proxy_to 'webapp1'
# 特定のパスにアクセスされた時だけ、別サーバに中継する
location('/map_api') {
proxy_to 'webapp2'
}
cert_file '/mnt/cert.pem'
key_file '/mnt/key.pem'
}
location() { }
で囲って proxy_to
を指定することにより、特定のパスだけに絞って中継先を上書きすることが出来ます。 また、location() { }
の中では、nginx_config
を指定することも出来ます。
location
で指定可能なロケーションは、 nginx の location ディレクティブ
と同じです。
例えば location('~* \.(gif|jpg|jpeg)$') { }
と書いた場合、nginx
の設定ファイルには以下のように展開されます。
location ~* \.(gif|jpg|jpeg)$ {
}
GitHubリポジトリの example5/ ディレクトリ にサンプルが入っています。
設定ファイルのリロード
設定ファイルを変更した場合、以下のようにリロードコマンドを実行することで リバースプロキシを停止することなく設定ファイルの内容を反映させることが出来ます。
docker exec -ti ez-gate /var/scripts/reload_config.rb
もし設定ファイルにエラーがあった場合は、リロードは失敗し nginx のコンフィグは書き換えられず、動き続けますので安心です。
docker-compose での利用
実際の開発現場では docker 単体ではなく docker-compose などの構成管理ツールを使うケースが多いでしょうから、構成設定ファイルに環境変数を定義しておくことでこれまでのサンプルと同等の設定が出来ます。
docker-compose での YAMLファイルのサンプルは 公式のREADME にあります。
EzGateコンテナの上げ下げのたびに 証明書取得が行われてしまうという問題がありますので、証明書ファイルが保存されるディレクトリを Docker volume に割り当てて永続化する方法があります。
/etc/letsencrypt
を volume に割り当てればオッケーです。
まとめ
今回は自社での開発現場での困りごとから生み出されたインフラ支援サービスとも言えるようなサービスを紹介しました。
実際に弊社では、このようなマルチテナント構成で 会社のWebサイト、Redmine、Mattermost などをコンテナで起動しておき、
それぞれのドメインを割り当て、1つのサーバに集約してホスティングしています。
他にも、弊社の開発現場では汎用性の高いものは機能単位で切り出して流用性を高め、
マイクロサービスとして使えるように整備していくような取り込みをしています。
EzGate はオープンソースとして開発しています。
いくつかのプロジェクトでの稼働実績もあり、とても良いプロダクトなのですが、
ドキュメントが少なく、まだまだ世間一般に広まるには整備しないといけないことが沢山残っていますが、
もし興味がありましたら使ってみてください。issue や pull request も大歓迎です。
現場からは以上です。