Apacheをコンテナ化するコツ


Apache HTTP Server(以降Apache)をコンテナで稼働させたい場合の、コンテナ作成のコツを紹介します。

最近リバースプロキシコンテナをApacheで作成する機会があり、その時の経験から記事にしてみました。

なお、ApacheはDockerhubにて公式コンテナイメージが提供されており、こちらを使用すれば、以下のコツのうちの
1.ログを標準(エラー)出力に出す
2.Apache起動方法:daemon化しない
については対応済みで特段の検討は不要です(考え方は知っておいて損はありません)

1.ログを標準(エラー)出力に出す

Apacheは何も考えなければローカルディスクにアクセスログとエラーログを出すようになっています。このままコンテナ化すると、コンテナ内に入らないとログを見れないし、誰もログローテーションもしないままコンテナ内でログが肥大化し続ける事になってしまいます。

コンテナのログといえば、標準(エラー)出力に出して、コンテナ実行環境(ホスト)側に任せるというのが第一の選択肢です。
Apacheの公式コンテナも以下の通り設定ファイルで、エラーログは標準エラー出力に、アクセスログは標準出力に設定されています。

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 common

2.Apache起動方法:daemon化しない

Apacheの起動方法といえば多くの人が思い浮かべるのが

# apachectl start

が有名です。このコマンドはApacheを、daemon(デーモン)として起動します。何しろApacheの実行バイナリ名はhttpdと"d"が付くぐらいですので、ある意味当たり前の話です。
しかしコンテナで動かしたいプログラムがdaemon化しては困るので、daemon化しないでApacheを動かす必要があります。(コンテナの起動コマンドをsleep infinityにした上で裏でdaemon起動するようなことは止めましょう)

ではApacheを非daemonで起動する方法は?というと、以下のコマンドを実行しましょう。

# apachectl -DFOREGROUND

この-Dは様々なパラメータをApacheに与えるオプションです。FOREGROUNDは、daemon化しないで起動するためのパラメータです。1

試してみたい場合は、Apacheがインストールされた環境で、上記コマンドを実行してみて下さい。基本的にはあまり何も出ず、Apacheが実行状態になると思います。終了したい場合はCtrl+Cキーで終了できます。

apachectl startでdaemon化起動した場合は以下の動画の通り、起動コマンドは即座に終了し、バックグラウンドでApacheが動作しています。

apachectl -DFOREGROUNDで非daemon起動した場合は以下の動画の通り、起動コマンドは終了せずそのままApacheそのものになります。終了するにはCtrl+Cを押します。

よって、Apacheを動かすコンテナでは、Dockerfileに以下のように書くのが良いでしょう。

ENTRYPOINT ["/usr/local/bin/apachectl","-DFOREGROUND"]

または起動用シェルスクリプトを用意し、最後に

exec /usr/local/bin/apachectl -DFOREGROUND "$@"

のように書きます。

なお、

# httpd -DFOREGROUND

を紹介する情報も多いかと思います。httpdコマンドでも概ね結果は同じで大抵問題ありませんが、apachectlの場合はhttpdapachectlが置かれているディレクトリのenvvarsファイルを読み込む処理などをしてからhttpdコマンドを実行するという違いがあります。
Apache公式コンテナイメージでは当然、非daemonで起動されますが、apachectlではなくhttpdを実行しています。
https://github.com/docker-library/httpd/blob/cd9f3170df90ef341c9c27fb4d17ffccd60b4ac0/2.4/httpd-foreground

3.環境変数やコマンドライン引数を活用する

頻繁に変わる値は環境変数やコマンドライン引数で外部から注入することで、汎用的にコンテナイメージを作成することが出来ます。
Apacheの場合は特に、以下のような方法で活用する事が出来ます。

3.1.環境変数値を設定ファイルで使用する

Apacheは環境変数値を設定ファイルで取り込むことが出来るので、設定ファイルを文字列置換して生成するなどの処理は不要です。

具体的には、${環境変数名}の形式で設定ファイルに書くことで、起動時の環境変数値を使用することが出来ます。2

例えばポート番号を環境変数CONTAINER_PORTとした場合は、以下のように書きます。

設定例
Port ${CONTAINER_PORT}
<VirtualHost *:${CONTAINER_PORT}>
# いろいろな設定
</VirtualHost>

なおこのように環境変数を参照する記述をした時にその環境変数が存在しない場合、${環境変数名}がそのまま文字列として設定値になり、エラーログにWARNレベルでログが出力されます。環境変数が無かったときのデフォルト値を設定する機能はApacheにはありませんので、Dockerfileか起動シェルスクリプトでデフォルト値を設定しましょう。

3.2. コマンドライン引数でフラグを渡し、設定ファイルで処理を分岐する

Apacheの起動引数に、-DVARの形式でフラグを渡すことが出来ます。先ほど解説した-DFOREGROUNDもまさにこのフラグそのものです。
このフラグは、<IfDefine VAR></IfDefine>で囲うことで、VARが定義されている時のみ有効な設定を記述することが出来ます。

設定例
<IfDefine BASIC_AUTH_ENABLED>
    AuthType basic
    AuthName "private pages"
    AuthBasicProvider  file
    AuthUserFile "/etc/htpasswd"
    Require valid-user
</IfDefine>

3.3. 環境変数によって処理を分岐したい場合は?

3.2.をわざわざ「コマンドライン引数でフラグを渡し」として、環境変数としなかったのは、実は環境変数によって処理を分岐することは綺麗には行かない事情があるためです。

例えば、MAINTENANCE_HOST環境変数があったら、その値をホスト名としてリダイレクトをかける設定は以下のよう<If>3を使って書けば、動作します。

環境変数の有無で処理を分岐
<Location />
    <If "-n osenv('MAINTENANCE_HOST')">
        Redirect / https://${MAINTENANCE_HOST}/
    </If>
</Location>

しかしこの<If>を使う方法には欠点があります。リクエストの度にこの<If>の条件のチェックが行われてしまいますので、微々たるものではありますがCPUを使います。また、リクエストの度に評価されることが原因で、<If></If>内には「ディレクトリコンテキスト」つまり<Directory><Location>内に書けるものしか書けません。
${環境変数}の記述は起動時にのみ置き換え処理が走るのとは対照的です。

こんな設定をすると…
<If "-n osenv('ADDITIONAL_PORT')">
    Listen ${ADDITIONAL_PORT}
</If>
エラーになる
root@3ba5dffc5a20:/usr/local/apache2/conf# export ADDITIONAL_PORT=81
root@3ba5dffc5a20:/usr/local/apache2/conf# apachectl -DFOREGROUND
AH00526: Syntax error on line 554 of /usr/local/apache2/conf/httpd.conf:
Listen not allowed in <If> context

仕方ないので、起動シェルスクリプトで環境変数をもとにフラグを設定することで、「2. コマンドライン引数でフラグを渡し、設定ファイルで処理を分岐する」のパターンに持ち込むしかありません。Apacheに<IfOSEnv>のようなセクションがあればそれで済むのですが…。

4.起動スクリプトを用意することを検討する

先述の通り、Apacheの設定ファイルは環境変数値を取り込むことが出来るものの、環境変数による条件分岐はあまり得意では無いため、Apache起動前に前処理としてシェルスクリプトを挟むことで解決する方法があります。
また例えばApacheで提供するコンテンツをコンテナ内に仕込むのでは無く、Amazon S3に置いておき、起動時にダウンロードして利用するとか、設定ファイルを動的に生成するとか、様々な前処理を行うことが出来ます。

前処理完了後、最後にApacheを起動しましょう。

entrypoint.sh
## 様々な前処理を実施

## 最後にApacheを起動する
exec /usr/local/apache2/bin/apachectl -DFOREGROUND "$@"

5.コンテナを非rootで動作させる

Apacheはデフォルトではrootで起動した上で、子プロセスを起動した後にユーザとグループを変更するようになっています。

Apache公式コンテナイメージの設定ファイルの記述
# If you wish httpd to run as a different user or group, you must run
# httpd as root initially and it will switch.
#
# User/Group: The name (or #number) of the user/group to run httpd as.
# It is usually good practice to create a dedicated user and group for
# running httpd, as with most system services.
#
User www-data
Group www-data
親プロセスがrootで、子プロセスが一般ユーザで動作している様子
# ps u -C httpd
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root       456  0.0  0.0   5916  3848 ?        Ss   15:10   0:00 /usr/local/apache2/bin/httpd -k start
www-data   457  0.0  0.0 1997016 9568 ?        Sl   15:10   0:00 /usr/local/apache2/bin/httpd -k start
www-data   458  0.0  0.0 1997016 9568 ?        Sl   15:10   0:00 /usr/local/apache2/bin/httpd -k start
www-data   460  0.0  0.0 1997016 9568 ?        Sl   15:10   0:00 /usr/local/apache2/bin/httpd -k start

最初にrootで起動する理由は主に2つあり、

  1. 特権ポート(1024未満)でListenするため
  2. アクセスログ・エラーログをrootで追記モードでオープンするため(子プロセスがクラックされても、ログを消されないため)

ですが、前者はコンテナ内で8080番などでListenして、コンテナ実行環境の機能で80番などから転送することが一般的ですし、後者はログを標準(エラー)出力に出すなら関係ありません。よって、コンテナ上でApacheを動かす時は、rootである必要はありません。
コンテナ内はコンテナ外とはLinuxカーネルの機能(名前空間)で分離されており、root権限であってもコンテナ外に影響を及ぼすことは出来ませんが、脆弱性によってこの分離が破られた時に備えて、root権限を落としておくほうがより良いです。4

やり方は、Dockerfileの適切な場所でUSER命令でユーザを指定し、Apacheの設定ファイルからUserGroupの指定を消すだけです。

Dockerfile
# コンテナをroot以外で起動する
USER nobody

このようにすることで、万一コンテナ内からコンテナ外にアクセス出来る脆弱性があったときも、被害がかなり抑えられます。
なお、Apache公式コンテナはこのようにはなっておらず、rootで起動するようになっています。

6.コンテナ終了時の挙動を検討する

Apacheのマニュアル「停止と再起動」によると、Apacheの停止には2種類、「急な停止」と「緩やかな停止」があります。
コンテナ停止時の挙動をどちらにするか、検討する必要があります。

急な停止(stop) 緩やかな停止(graceful-stop)
シグナル SIGTERM SIGWINCH
挙動 処理中のリクエストを中止してApacheを終了する 処理中のリクエストが全て完了するのを、GracefulShutdownTimeoutだけ待ってからApacheを終了する

緩やかな停止では、例えばクライアントが低速回線で大きなファイルをダウンロードしている場合にApacheの停止にとても時間がかかってしまうため、待つ最大時間を設定する事が出来ます(デフォルトでは無期限)。
緩やかな停止を採用する場合は、DockerfileにてSTOPSIGNALを指定し、コンテナ停止時にSIGWINCHを送ってもらうようにします。

Dockerfile
STOPSIGNAL SIGWINCH

Apacheの公式コンテナイメージも、この設定がいれてあり、「緩やかな停止」を採用しています。GracefulShutdownTimeoutは設定されておらず無期限のようです。

また、コンテナ実行環境側にもコンテナ終了までのタイムアウトが設定されていて、これを超えても終了しないコンテナはSIGKILLで強制終了されることが一般的ですので、こちらのタイムアウトも意識する必要があります。Kubernatesの場合、Pod定義でterminationGracePeriodSecondsで設定することが出来るようです。

最後に

以上、Apacheをコンテナ化するコツでしたが、いくつかの内容はApacheに限らずコンテナ一般に応用できる考え方を紹介できたと思っておりますので、是非参考にして下さい。


  1. 実はこのFOREGROUNDはあまりマニュアルにきちんと書かれていません。マニュアルはこちらです。-Dオプションのところに -DNO_DETACH (prevent the parent from forking) and -DFOREGROUND (prevent the parent from calling setsid() et al). と書かれていますが、実はFOREGROUNDsetsid() et alet alにはfork(2)も含まれるので、FOREGROUNDNO_DETACHを包含しています。こんな書き方をしないで、一言「daemon化しない」と書けば分かりやすいのに…と思います。 

  2. http://httpd.apache.org/docs/2.4/en/configuring.html#syntax shell environment variables can be used in configuration file lines using the syntax ${VAR} 

  3. https://httpd.apache.org/docs/2.4/en/mod/core.html#if 

  4. Docker, Podman, Kubernatesなどのコンテナ実行環境では最近、ユーザ名前空間を使った"Rootlessコンテナ"機能が提供されています。この機能を使うと、コンテナ内のrootはホストのrootではなくなるので、コンテナを無理に非rootで動かす必要はないかもしれません。しかしRootlessコンテナ機能はまだ一般的とは言えませんので、それを前提とするのではなく、コンテナ側でroot権限を落としておくほうが良いです。