SSL化に失敗した話

※今日はサーバ管理 (DevOps) の話です.

結論から言うと, (この記事を読んでいればお分かりだと思うが), 何とかSSL化には成功した!

なぜ失敗と書くのかと言うと, すぐ終わるだろうと軽く考えて検証環境も作らず改修していたら, 案の定 CVこのブログも作業期間中接続不可にさせてしまい, 時間も想定よりかかってしまったから. これには本当に参った.

順を追って説明すると, 先日から二日間 (合計で丸一日半) 掛けて, 公開しているWebサーバー (このブログも対象) のSSL化を図った. 18日の時点で完遂出来ず, 一時敗退 (ロールバック) となった.

仕事なら検証環境を準備し, 動作確認が済んで提供するのが通例だが, このブログは趣味なのでそこまでしなかった. 結果的に, 目算で検証環境を作るより約15倍~20倍の労力を要した (後日, 検証環境は準備).

背景

container orchestration という言葉を聞いたのはいつだったか. そんなに昔では無い. 自分がこの概念に触れたのは現役でエンジニアとして活躍している人よりもずっと遅かったが, そんな私でも dockerk8s を初めて触ってしばらく経っているので, 業界的には標準の概念としても実用としても十分時間を掛けて発展し浸透してきてると思う (そして今も発展している).

システム・アーキテクチャ概念を実装する手段であり, DSLの一種であり, SaaSモデルに適合する “サービスの配布” を実現させるフレームワークと見れるdockerは, container orchestrationを実装するために今や必須の技術となった.

かつてLinux+Apache+MySQL+PHP という異なるシステムレイヤに属する (OS, Middleware, Preprocessor) サービス群をまとめてLAMPと呼び, そのクロスプラットフォーム版OSSパッケージとしてXAMPPが出た. 私もWindows上でXAMPPを使ったことがあるが, 完成度の高いパッケージで, docker環境の構築に少しハードルを感じる場合や, プログラミング学習目的であれば今でも有用だと思う.

一方XAMPPが業界で話題になっていた頃, ミドルウェアのコンパイルをちまちまやっていたような私の興味は,

MySQLをPostgreSQLに代えるには?
VPN経由の接続環境もパッケージに内包するには?

といった類の事に向くのであって, vmwareをラップトップに入れて (上記のLAMPの意味での) サービスのポータビリティを享受していた私にとって, サービスのパッケージングへの興味は薄れつつあった.


少し戻って, 最初にこのブログを開始したのは格安のVPSサーバを借りた時だったか. その頃からつい最近まで, サーバー環境を変える度に一日がかりでスクラッチからサーバ構築をしていたのだ. 最近になってそういう風に考えると, 将来に亘る蓄積は実際寿命に関わるということが冗談と思えなくなってきた.

事業を進めるにあたって (特に何のとは言わないが), 少なくとも事業に関わる

  • メディア,
  • 開発環境,
  • バックアップ,
  • 統合的なセキュリティ関連対策 (パスワードやネットワーク制御・管理)

等のパッケージ化を早急に迫られた. それが昨年2020年9月のことである.

それから紆余曲折あって外見では分からない構成をじわじわ改善したが, それでも今日に至るまでSSL化は達成されなかった. これはdockerを使いこなすのにそれだけ時間がかかったとも言えるし, 当時ネットで挙がっていた letsencrypt を活用した証明書発行から自動更新までの一連の仕込みをいざ開始したら, 完了までどれだけ掛かるのか, 余りに未知数だったためでもある.

そしてその時が来た.

前提

詳細はこちらの記事の通りなので, わざわざ書き直しはしません.
目的は以下のdocker imageから適宜containerを作成し, 適切に配置することである.

container作成には何を使っても良いが, 例えばdocker-composeを利用し依存関係等含めた操作・管理を簡略化する. 適切な配置とは以下のような構成のことである:

docker containerの構成

検証

然るべき (少なくともdockerdが立ち上がる) 環境で,上の記事通りやればこの構成を取るまでは難しくないだろう. 但し動くかは別問題で, 案の定構築一日目は結局動かせなかった (static file: index.html等はSSL経由で表示されたが, プログラムファイル: index.php等の実行までは至らなかった).

以下は二日目からの検証の記録である.

まずSSL設定プログラム実行部どちらに問題があるのかを切り分けるために, 最小構成の設定ファイルを作成する.
プログラム実行部というのは, 公式のWordPressコンテナがbackendとしてNginxを採用しているので, phpを動作させる際にFastCGIを使用する. これはApache等のmodule版PHPと異なり, サーバー起動時に正常に実行されるかは保障されないので, 簡単なプログラムが実行されるかの確認が必要である.

# 「非SSL最小構成」の docker-compose.yml

version: '3.8'

services:
  nginx:
    depends_on:
      - fpm-test
    image: nginx:1.21.3-alpine
    volumes:
      - ./var/wp:/var/www/wp
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
    restart: always
    networks:
      fpm-test-network:
    ports:
      - "8090:80"
    expose:
      - 8090
  fpm-test:
    image: wordpress:5.8-php7.4-fpm-alpine
    container_name: fpm-test
    restart: unless-stopped
    volumes:
      # このwphtmlにしていたために, 丸3時間ずっと "primary script unknown" エラーで悩んでいた.
      - ./var/wp:/var/www/wp
    networks:
      - fpm-test-network

networks:
  fpm-test-network:
    driver: bridge
# index.php

<?php
echo "hey php!\n";
# test command

[stma ~]$ curl http://example.com:8090/index.php
hey php!
[stma ~]$
# vhost-example.com.conf

server {
    server_name  example.com;

    # HTTPだけでテストする場合はrootを設定する
    # root         /var/www/wp;

    listen 80;
    listen [::]:80;

    # 全てのリクエストをSSLサイトにリダイレクト
    location / {
        return 301 https://$host$request_uri;
    }

    # 例外的に証明書更新時のlet's encryptからのリクエストは80番で受ける(443に飛ばしても実は問題ない)
    location /.well-known/acme-challenge/ {
        root /var/www/wp;
    }
}

server {
    server_name  example.com;
    root         /var/www/wp;

    index index.php index.html index.htm;
    server_tokens off;

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ...ssl settings
    ...add_header settings

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        # 9000はphp-fpmの標準ポートになっているようだ.
        fastcgi_pass wp:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        # ここはスラッシュが要るという記事もあったが, nginx-1.21の時点ではあっても無くても動作する.
        # fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }


    ...other settings
}

HTTP経由であればphpが実行できることが分かった. phpinfo等も実行されるし, WordPressを実行するのに十分な拡張機能が備えられているようだ (php-fpmはそういうものだから).

結局この非SSL最小構成では, document_root にwordpress-5.8.1.tar.gz を解凍したものを配置してセットアップ画面まで行けてしまった.


ここまでで非SSL最小構成であれば問題無いと書いたが, 実はSSL設定を加えても問題が無いことが最後になって分かった. 最後の最後までSSL設定の影響を否定できなかった.

今回構築に時間がかかったのは, docker-composeの挙動を理解していないからだった. 取り分け以下の二つはあちこちに飛び火して, 丸一日を食いつぶすのに十分な検証を要した.

  1. 起動オプション由来の問題,
  2. Named Volumeの問題

起動オプション由来の問題

以下の3つ:

--force-recreate
--always-recreate-deps
--no-deps

について, docker-compose up のオプションの中でも優先的に習熟するべきだった. オプションの説明だけを見て挙動を検証せずに使って想定外のことが数多く起こった.

例えば今回の構成において, docker-compose up -d nginx だけで起動すると ( --force-recreate なしで起動すると), DBコンテナは立ち上がっているのに Database Connection Error が出ることがある. この原因は究明中だが, nginxコンテナのネットワークからはdbコンテナが見えていないようなので, 可能性としてはDBのような起動に時間のかかるコンテナに依存するコンテナを立ち上げる際, 指定されている依存関係に対応する起動順序が入れ替わってしまっているのではないかと思われる. 少なくともそのような挙動を取る.

逆にnginxの設定だけ変更して再起動する場合は, docker restart webserverとするか, 以下のNamed Volumeの問題で述べるようなマウントの問題が発生するなら docker down webserver等とし, 既に起動中のDBやfpmサーバはそのままにするために docker-compose up --force-recreate --no-deps -d nginxとすると大抵問題が起こらない.

それ以外にも, (複合的な原因で) 最適な起動オプションを指定しなかったことにより以下のような問題を経験した:

  • mysql – MySQL 5.7で書かれたDB Dataを, MySQL 8.0コンテナが書き換えてしまった,
  • wordpress – document_rootに無用な初期ファイルを配置してしまう,

Named Volumeの問題

実は今の時点でも挙動を全て把握するには検証が十分ではない.
例えばdocument_rootをNamed Volume指定した場合に起こる, 403 Forbidden: directory index of "/var/www/wp" is forbidden エラーの原因は未だに特定できていない (Volume指定を止めれば無くなる).

この他にも類似した共有ボリュームの初期化 / 配置 / マウントが (ドキュメントを見る限り) されるはずだが実際にはされない挙動を経験した. これらの経験だけで「Named Volumeは悪手だ!」と言ってしまいたくなる気持ちもあるが, きっと条件が違うのだろう. これについてはstackoverflow等を参考に引き続き検証する必要がある.

総括

前提に書いた構成の技術スタックをいくら検索しても, 1から10までのHOWTOはどこにも書いてないし, 検証した結果失敗することも多く, その原因も理不尽なことが多い. きちんと動かそうとすると最後は古い情報や断片的なものを自身で検証して組み合わせるしかないが, その断片的な情報すら十分でないのが実情だと思う (それでも昔に比べれば充実して来た!).

エンジニアの方々には, 是非文章を書く練習と思って検証の記録を書いてもらえると嬉しい (媒体はブログでもstackoverflowでも, 何でも良いけども, 皆助かるだろう. 普段先人の知恵を拝借してる分!と思って).