JQueryとPythonを勉強しながらGCP上でWebサービスを公開するまでという記事から始めたのだが、このアーキテクチャを作るのもなかなかハードルが高かった。
NginxとFlaskの初期の設定から説明してみたい。
localhostとpublic domainの両方で試せるようにdocker-compose.ymlを分けて作ったものをGitHubに置いている。
https://github.com/legacyworld/nginx-proxy-test
環境
まずはドメインのないローカルで始める。
Docker VM:CentOS 7
docker: 19.03
docker-compose: 1.25.5
HTTPS無しで始める
いきなり全部作り始めるのはあまりにもハードルが高いので、HTTPS無しで始めて見る。
これなら単なるリバースプロキシなので情報はいくらでも転がっている。
やることは単純で
ユーザ => index.html読む => javascript実行 => REST APIでFlaskにデータ(ファイル)を投げる => Pythonで処理して返す => FlaskがJSONで返す => 表示する
これを実現するには以下の事をやればよい。
- http://ip address/api/v1というREST APIにアクセスが来たらFlaskにForward
- それ以外は全てNginxのローカルにあるstatic fileで返す
とりあえずリバースプロキシの設定の確認なので最も単純な状態にする。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<h1>これはindex.html</h1>
</html>
from flask import Flask, jsonify, request, make_response,send_from_directory
app = Flask('test')
@app.route('/api/v1/test/',methods=['GET'])
def test():
return "Flask OK",200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
今Docker VMのIP Addressが172.16.0.21なのでこうなる状態まで持っていく。
nginx.confを作る
追加が必要なのはlocation /api/v1/
の部分だけである。
Dockerのコンテナ間通信はコンテナ名で行われるのでこのような書き方になっている。
events {
worker_connections 1024;
}
http{
server{
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api/v1/ {
proxy_request_buffering off;
proxy_pass http://app:8080;
}
}
}
Dockerfileを作る
まずはNginx用のDockerfile。nginx.confをコピーするだけ
FROM nginx:alpine
COPY ./nginx.conf /etc/nginx/nginx.conf
次にFlask用のDockerfile。
FROM python:slim
USER root
RUN pip3 install flask
WORKDIR /src
ENV PORT=80
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["python", "-u", "server.py"]
docker-compose.ymlを作る
ディレクトリ構造とdocker-compose.yml
.
|-- app
| |-- Dockerfile
| `-- src
| `-- server.py
|-- docker-compose.yml
`-- web
|-- Dockerfile
|-- html
| `-- index.html
`-- nginx.conf
version: '3.7'
services:
web:
build:
context: ./web
dockerfile: Dockerfile
container_name: web
ports:
- 80:80
volumes:
- ./web/html:/usr/share/nginx/html
app:
build:
context: ./app
dockerfile: Dockerfile
container_name: app
volumes:
- ./app/src:/src
Nginx,Flaskのコンテナ両方ともvolumesでhtmlファイルとソースコードファイルをホストから直接読み込むようにしている。
これですべて揃ったので後はいつものdocker-compose up --build -d
で終わり。
無事立ち上がった。
#uwsgiへの対応
NginxとFlask間の通信をuwsgiにする。結構いろんなファイルへの変更が発生する。
まずはDockerfileから。
FROM python:alpine
USER root
RUN apk add gcc build-base linux-headers
RUN pip3 install flask uwsgi
WORKDIR /src
ENV PORT=80
ENV PYTHONUNBUFFERED=1
RUN adduser -S uwsgiusr
USER uwsgiusr
ENTRYPOINT ["uwsgi","--ini","/src/uwsgi.ini"]
ほぼ全部入れ替えになってしまった。
- FROM python:alpine
- slimを使っていたが、apk addが使えなかったのでalpineに変更
- RUN apk add gcc build-base linux-headers
- uwsgiのBuildにgccが必要なため追加インストール。これがないと下記のエラーが出る
Exception: you need a C compiler to build uWSGI
- RUN pip3 install flask uwsgi
- これはuwsgiが増えただけ
- RUN adduser -S uwsgiusr
- USER uwsgiusr
- uwsgiusrで実行するので、ユーザを追加してからユーザ変更
- CMD ["uwsgi","--ini","/src/uwsgi.ini"]
- uwsgiの設定からflaskを呼び出す。
次にuwsgiの設定ファイルであるuwsgi.ini
[uwsgi]
wsgi-file = server.py
callable = app
master = true
processes = 1
socket = :3031
chmod-socket = 666
vacuum = true
die-on-term = true
py-autoreload = 1
uid = uwsgiusr
gid = uwsgiusr
細かいことはわからないが、他の設定ファイル等に関係ある部分だけ抜粋
※このページを参照
- wsgi-file
- このPythonファイルをuwsgiから呼ぶ
- callable
- uwsgiはflaskの
app = Flask('test')
を目印に呼ぶから
- uwsgiはflaskの
- socket
- ポート番号(HTTP socketではないらしい)
- uid/gid
- Dockerfileでこのユーザを追加する
次はnginx.conf
# 省略
location /api/v1/ {
proxy_request_buffering off;
include uwsgi_params;
uwsgi_pass app:3031;
}
}
}
- include uwsgi_params
- uwsgi_pass app:3031
- これでflaskに投げてくれる
flaskの部分は1行だけ
# 省略
if __name__ == '__main__':
app.run()
app.run()のところだけ変えている。
最後にdocker-compose.yml
# 省略
ports:
- 3031:3031
volumes:
- ./app/src:/src
portsだけ追加
HTTPSに対応する
HTTPSとなるとドメインが必要になってくるため、環境をGCPへ移す。
GCPでの環境の作り方はこちらで記事にしている。
※お名前.com + GCP + DockerでWebサーバの立ち上げ
- 目標
- QualysでA+を取って安全なWebサーバを目指す。実はjwilder/nginx-proxyを何も考えずに使うとA+になるので、これを動かせば何とかなる。
- 証明書
- これは無料のLet's Encrypt以外に選択肢はない
- jwilder/nginx-proxyにくっつけて使うcompanionがあるので何とかこれを動かす
- ドメイン
- お名前.comで1円
- 環境
- GCPのCotainer-Optimized-OS / f1-micro
- docker: 19.03.6
- docker-compose: 1.25.5
- GCPのCotainer-Optimized-OS / f1-micro
Let's encryptが一回目に行うchallengeをどう乗り切れば良いのかがわからなくてかなり悩んだ。
一応完成版はこのような構成。
変更したファイルはdocker-compose.ymlとnginx.conf
version: '3.7'
services:
nginx-proxy:
image: jwilder/nginx-proxy:alpine
container_name: nginx-proxy
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- cert:/etc/nginx/certs
- html:/usr/share/nginx/html
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
letsencrypt-nginx:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: lets
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- cert:/etc/nginx/certs:rw
- html:/usr/share/nginx/html
- vhost:/etc/nginx/vhost.d
depends_on:
- nginx-proxy
web:
build:
context: ./web
dockerfile: Dockerfile
container_name: web
environment:
- VIRTUAL_HOST=www.example.com
- LETSENCRYPT_HOST=www.example.com
- LETSENCRYPT_EMAIL=xxx@gmail.com
volumes:
- html:/usr/share/nginx/challenge
- ./web/html:/usr/share/nginx/html
depends_on:
- letsencrypt-nginx
app:
build:
context: ./app
dockerfile: Dockerfile
container_name: app
ports:
- 3031:3031
volumes:
- ./app/src:/src
volumes:
cert:
html:
vhost:
events {
worker_connections 1024;
}
http{
server{
listen 80;
server_name www.example.com;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api/v1/ {
proxy_request_buffering off;
include uwsgi_params;
uwsgi_pass app:3031;
}
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/challenge;
}
}
}
docker-compose.ymlにはコンテナが2つ追加してある
- nginx-proxy
-
jwilder/nginx-proxy
- 設定はいろいろ出来るがそのままなにも設定せずに使う
-
jwilder/nginx-proxy
- letencrypt-nginx
- docker-letsencrypt-nginx-proxy-companion
- これもそのまま使う
- environment
- VIRTUAL_HOST
- このドメイン名に来たアクセスは、このコンテナに転送されてくる
- LETSENCRYPT_HOST / LETSENCRYPT_EMAIL
- Let's Encryptがこの情報で証明書を作る
- VIRTUAL_HOST
- volumes
-
/var/run/docker.sock:/tmp/docker.sock:ro
- nginx-proxyがDockerと通信するためのもの。
-
cert
- 証明書を置くための場所。
-
html
- チャレンジファイルを置くための場所
-
vhost
- 書いておかないとエラーが出てLet's Encrypt Companionが立ち上がらない
-
Warning: '/etc/nginx/vhost.d' does not appear to be a mounted volume.
Error: can't access to '/etc/nginx/vhost.d' directory !
Check that '/etc/nginx/vhost.d' directory is declared as a writable volume.
- labels
- よくはわからないが、これでLet's Encrypt Companionがどのnginx-proxyを使えばよいか判定するらしい
- depends_on
- 無くても良いとは思うが、時々起動がうまくいかなかったのでnginx-proxy -> letsencrypt -> webの順で立ち上がるようにしている
nginx.confは一か所追加
- location ^~ /.well-known/acme-challenge
- ここにアクセスが来た時(チャレンジファイル)だけ/use/share/nginx/challenge(=Dockerのhtml volume)を見る
ここのチャレンジの部分が最初よくわからずどうしても証明書を取ることが出来なかった。
上記の設定とチャレンジによる証明書の取得の流れを絵にしてみた(間違ってたらどなたかご指摘ください)
<img width="780" alt="12.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629456/86006910-fc92-a2c5-2bc9-9fcbc638d502.png">
1. Let's Encrypt Companionが/usr/share/nginx/html(=Dockerのhtml volume)にチャレンジファイルを置く
2. Let's Encryptの外部サーバに対してACMEプロトコル通信で「ファイルが用意できました」と伝える
3. Let's Encryptの外部サーバからこのファイルを取りに来る(ログからすると4か所から取りに来ていた)
4. このGETはVIRTUAL_HOSTの設定によりWebサーバへ転送される
5. Webサーバはhtml volumeをマウントしているのでLet's Encrypt Companionが保存したファイルを読める
6. ファイルがLet's Encryptのサーバへ返る
7. 証明書が発行されて、Let's Encrypt Companionがcert volumeに保存する
上記の5.のステップがよくわからずこんなエラーが出ていた。
Let's Encrypt Companionのエラー
2020-05-09 09:01:21,734:ERROR:simp_le:1417: CA marked some of the authorizations as invalid, which likely means it could not access http://example.com/.well-known/acme-challenge/X. Did you set correct path in -d example.com:path or --default_root? Are all your domains accessible from the internet? Please check your domains' DNS entries, your host's network/firewall setup and your webserver config. If a domain's DNS entry has both A and AAAA fields set up, some CAs such as Let's Encrypt will perform the challenge validation over IPv6. If your DNS provider does not answer correctly to CAA records request, Let's Encrypt won't issue a certificate for your domain (see https://letsencrypt.org/docs/caa/). Failing authorizations: https://acme-v02.api.letsencrypt.org/acme/authz-v3/4467562815
Webサーバのログ
2020/05/09 09:01:20 [error] 6#6: *1 open() "/usr/share/nginx/html/.well-known/acme-challenge/sa_v8la8ZtiEBjC76rbJmu23r7AktikFmX1LXV1ow4w" failed (2: No such file or directory), client: 172.20.0.5, server: www.example.com, request: "GET /.well-known/acme-challenge/sa_v8la8ZtiEBjC76rbJmu23r7AktikFmX1LXV1ow4w HTTP/1.1", host: "www.example.com"
**http://www.example.com/.well-known/acme-challenge**へのアクセスがWebサーバの**/usr/share/nginx/html/.well-known/acme-challenge**に行っているが、Let's Encrypt Companionとvolumeを共有していなかったためチャレンジファイルを見ることが出来なかったのだ。
これがnginx.confを変更した理由である。
長かったがこれでようやくHTTPS化が終わりました。
この状態でQualysでチェックをかけるとA+を取れます。
# ドメインの追加
ここまで来ると新しいドメインでの追加は簡単です。
例えばwww.hogehoge.comを追加する場合はdocker-compose.ymlを以下のように記述すればよい。
```yml:docker-compose.yml
version: '3.7'
services:
nginx-proxy:
image: jwilder/nginx-proxy:alpine
container_name: nginx-proxy
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- cert:/etc/nginx/certs
- html:/usr/share/nginx/html
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy:
letsencrypt-nginx:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: lets
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- cert:/etc/nginx/certs:rw
- html:/usr/share/nginx/html
- vhost:/etc/nginx/vhost.d
depends_on:
- nginx-proxy
web:
build:
context: ./web
dockerfile: Dockerfile
container_name: web
environment:
- VIRTUAL_HOST=www.example.com
- LETSENCRYPT_HOST=www.example.com
- LETSENCRYPT_EMAIL=xxx@gmail.com
volumes:
- html:/usr/share/nginx/challenge
- ./web/html:/usr/share/nginx/html
depends_on:
- letsencrypt-nginx
hoge:
build:
context: ./hoge
dockerfile: Dockerfile
container_name: hoge
environment:
- VIRTUAL_HOST=www.hogehoge.com
- LETSENCRYPT_HOST=www.hogehoge.com
- LETSENCRYPT_EMAIL=xxx@gmail.com
volumes:
- html:/usr/share/nginx/challenge
- ./hoge/html:/usr/share/nginx/html
depends_on:
- letsencrypt-nginx
app:
build:
context: ./app
dockerfile: Dockerfile
container_name: app
ports:
- 3031:3031
volumes:
- ./app/src:/src
volumes:
cert:
html:
vhost: