Let's Encrypt と Nginx を使った Web アプリの HTTPS 化


https://qiita.com/propella/items/7209dfda27f665766867 の続きです。

AWS 上に docker-compose で自家用 redmine を作成したら、今度は HTTPS 化したくなります。よくある話ですがやってみると案外迷ったのでメモに残しておきます。なお、Let's Encrypt 界隈は変化が激しくこの内容も既に古い可能性がありますが、今日現在では使えています。

ここでは次のような構成にしています。

* docker-compose で Web アプリが 3000 ポートで動いている (redmine + mysql)
* HTTPS にするために Nginx のプロキシ機能を使う。443 ポートを開ける。
* certbot (Let's Encrypt のツール) はホストで動かす。80 ポートは証明書の取得と更新だけのために使う。

これから HTTPS 化するホストを例えば hoge.example.com とします。certbot は Let's Encrypt サーバから証明書を取得する時、自分が hoge.example.com の管理者である事を証明するために証拠のファイルを http://hoge.example.com/証拠のファイル として公開します。そこで次のような準備が必要になります。

* HTTPS 化したいドメインの DNS 情報を編集して対象のホストを指すようにしておく。
* ホストの 80 ポートに外部からアクセスできるようにしておく。

## Certbot のインストールと証明書の取得

Amazon Linux 2 では次のコマンドで certbot をインストールします。

    sudo amazon-linux-extras install -y epel
    sudo yum install -y certbot

証明書の取得は以下のコマンドです。

    sudo certbot certonly --standalone -n --agree-tos --email hoge@example.org -d hoge.example.com

オプションの意味です。

* `certonly`:
    * 証明書を取得だけします。
    * オプション無しだとインストール(Web 設定ファイルの書き換え) までやってくれるのですが、Docker を使う都合上インストール機能は使わない事にします。
* `--standalone`:
    * certbot は自分で Web サーバとして動作し証拠ファイルを提供します。他の選択肢を使うと、別の Web サーバの機能を使って証拠ファイルを提供できます。
    * Web サーバを起動する前に証明書を取得しておきたいので `--standalone` を使いました。
* `-n`:
    * 非対話モード。自動化したいのでこうしました。
* `--agree-tos`:
    * https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf に同意する。
* `--email`:
    * メールアドレス
* `-d`:
    * 登録するドメイン

certbot を実行すると、証明書は以下のパスに保存されます。履歴を残せるようにパスはシンボリックリンクになっています。

* /etc/letsencrypt/live/hoge.example.org/fullchain.pem: 証明書
* /etc/letsencrypt/live/hoge.example.org/privkey.pem: 秘密鍵

証明書を取得したあとでこのコマンドをもう一度実行すると怒られるのかなと思いましたが、

    Certificate not yet due for renewal; no action taken.

と表示されました。証明書実行後の再実行は更新扱いになるようです。[Rate Limits](https://letsencrypt.org/docs/rate-limits/) によると、一週間に 5 回までは同じドメインの証明書を取得できます。自動化スクリプトを書いていて EC2 を作ったり消したりする状況だと注意した方が良いです。

## Docker Nginx の設定

証明書と秘密鍵があれば HTTPS 化できます。以下に Nginx 公式 Docker 向けの設定ファイル /etc/nginx/conf.d/default.conf の例です。
ここではアプリ (redmine) が http://app:3000/ で動いているとします。

```
server {
       listen 443 ssl;
       server_name hoge.example.com;
       ssl_certificate     /etc/nginx/certs/server.crt;
       ssl_certificate_key /etc/nginx/certs/server.key;
       location / {
                proxy_pass http://app:3000/;
       }
}
```

docker-compose 側の設定です。設定ファイルと証明書をホストから bind mount しています。

```yaml
  proxy:
    image: nginx
    volumes:
      - type: bind
        source: ./default.conf
        target: /etc/nginx/conf.d/default.conf
      - type: bind
        source: /etc/letsencrypt/live/hoge.example.org/fullchain.pem
        target: /etc/nginx/certs/server.crt
      - type: bind
        source: /etc/letsencrypt/live/hoge.example.org/privkey.pem
        target: /etc/nginx/certs/server.key
    ports:
      - 443:443
```

## 証明書の更新

あとは証明書を自動更新するだけです。AWS のドキュメントによる自動更新の crontab 例です。一日二回 certbot renew をやれば必要に応じて更新してくれるそうです。

    39      1,13    *       *       *       root    certbot renew

私は深夜 3:17 に毎晩実行するようこんな感じに Ansible で設定しました。nginx が新しい証明書を読み込むよう nginx の再起動も行っています。

```
  - name: Renew cert
    cron:
      name: Renew cert
      hour: "18"
      minute: "17"
      job: "certbot renew && /usr/local/bin/docker-compose -f /home/ec2-user/docker-compose.yml restart proxy"
```

最新の snap とか言うのを使ったインストーラーでは最初から自動更新も設定済らしいです。Amazon Linux 2 では面倒そうだったので私は止めときました。

## 参考

* AWS Certbot をインストールして実行する:
    * https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#install
* Certbot User Guide:
    * https://certbot.eff.org/docs/using.html
* Let's Encrypt Rate Limits:
    * https://letsencrypt.org/docs/rate-limits/
    
* Nginx HTTPS 設定:
    * https://nginx.org/en/docs/http/configuring_https_servers.html
* Nginx official docker:
    * https://hub.docker.com/_/nginx

## おまけ EC2 で固定 IP アドレス (Elastic IP)

普通 AWS EC2 では、インスタンスを立ち上がるたびに IP アドレスが変わります。これを防ぐには Elastic IP というのを使うのですが、CloudFormation だとめちゃくちゃ簡単に設定出来ます。例えば EC2 のリソース名が WebServer だと以下を追加するだけです。

```yaml
  WebServerIP:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref WebServer
```

ただ、HTTPS 化するにあたりドメイン名を割り当てるので、それほど要らないかなと思いました。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip.html

また、Elastic IP を設定すると EC2 をわざと消して作り直したくなりますが、作り直すには CloudFormation テンプレートのリソース名を変えるのが一番良いようです。AWS Console で無理やり消すと CloudFormation で管理不能になります。