作成したWebアプリをDockerイメージ化してAppRunで動かしてみよう(第4回) ~セッション維持編~

コンピューティング # AppRun # Docker # PHP # Redis

ネットアシスト開発チームの yu-kinjo です。

本記事は、フルスクラッチで作成したWebアプリをDockerイメージ化して、さくらのクラウドのDockerコンテナ実行サービスである AppRun で実行してみようという趣旨の連載の第4回です。

第3回からは「AppRun専有型」を用いて複数のWebUIコンテナ間でセッションを維持する方法を模索していきます。今回は、前回作成したセッション維持用Redisコンテナと連携するようにWebアプリを作成し、Dockerイメージ化していきます。

作成したWebアプリをDockerイメージ化してAppRunで動かしてみよう(第3回) ~セッション維持編~

セッションをRedisに保存するPHP Webアプリの作成概要

今回もまたフルスクラッチでWebアプリを作成していきます。RDBとの連携は第1回~第2回で行いましたので、今回はDBの読み込みを省いた、複数WebUIコンテナ間でのセッション維持実現を主眼としたアプリを作成します。

  • Webアプリ本体はPHP 8.4で実装します。
  • TOPページに現在のセッション情報を表示します。
  • Webアプリケーションフレームワークを使わず、以下の2ファイルで完結する非常にシンプルな構成にしています。
  • index.php (TOPページ)
  • session.php (セッション管理ファイル。上記ファイルから require される。)

また、コンテナやミドルウェアも含めた構成は以下のようになります。

  • Webサーバーとして Nginx、PHPの実行方法として PHP-FPM の組み合わせで動作させます。どちらも起動状態を維持するために supervisor を利用します(supervisor の詳細は第1回を参照
  • Redisサーバーは第3回で構築したRedisコンテナを利用するようにします。
  • Redis接続情報はコンテナ内に含めず、コンテナ起動時にパラメータとして渡せるようにします。

AppRun専有型用のコンテナ作成時注意点

第1回~第2回では Apache + php-fpm を supervisor で起動するような構成になっていましたが、今回Webサーバーには Nginx を利用するようにしています。こちらはAppRun専有型の制限事項に合わせるためにNginxの方が都合がいいためそうなっています。 具体的には以下のような制限事項が有ります。

技術概要 | さくらのクラウド マニュアル

コンテナの実行ユーザと実行グループ
uid951:gid951 で実行します。

上記のように、AppRun専有型ではroot権限を持たない uid 951 のユーザーでプロセスが実行されるようになっています。この際、Apacheでは非root権限で動作させるために調整するべき箇所が多く対処しづらく、比較的対処しやすいNginxを利用するようにしています。

また、同様の理由で、特権ポートである HTTP: 80 をコンテナで直接公開するのではなく、非特権ポートである HTTP:8080 等で公開するようにしておく必要が有ります。

PHP Webアプリ部分の実装

index.php

セッション用のCSS読み込みと、セッション情報を表示するだけのシンプルな作りです。

<?php
require __DIR__ . '/session.php';
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>記事一覧</title>
<style>
  body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
  h1 { border-bottom: 2px solid #333; padding-bottom: .5rem; }
<?= session_panel_css() ?>
</style>
</head>
<body>
<h1>セッションの維持</h1>

<?= render_session_panel() ?>

</body>
</html>

session.php

セッションの管理を行うファイルです。アクセスするたびにアクセスカウンタをプラスし、セッションが正常に維持されていればカウンタが増えていくようになっています。

Redisの接続情報は環境変数から読み込むようにし、コンテナデプロイ時に注入できるようにしています。

<?php
/**
 * 外部Redisを使ったセッション共有の設定。
 *
 * Web側を複数コンテナで動かしても同じRedisにセッションを保存するため、
 * どのコンテナにリクエストが振り分けられてもセッションが維持される。
 *
 * 接続先は環境変数で指定する(docker-compose.yml参照)。
 *   REDIS_HOST : Redisのホスト名(既定: redis)
 *   REDIS_PORT : Redisのポート   (既定: 6379)
 *   REDIS_PASS : Redisのパスワード(任意)
 */

$redisHost = getenv('REDIS_HOST') ?: 'redis';
$redisPort = getenv('REDIS_PORT') ?: '6379';
$redisPass = getenv('REDIS_PASS') ?: '';

// php-redisのセッションハンドラ用のsave_path(tcp://host:port?auth=pass)
$savePath = sprintf('tcp://%s:%s', $redisHost, $redisPort);
if ($redisPass !== '') {
    $savePath .= '?auth=' . urlencode($redisPass);
}

ini_set('session.save_handler', 'redis');
ini_set('session.save_path', $savePath);

session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'httponly' => true,
]);

session_start();

// --- セッション維持を可視化するためのカウンタ等を更新 ---
if (!isset($_SESSION['first_access'])) {
    $_SESSION['first_access'] = date('Y-m-d H:i:s');
    $_SESSION['views']        = 0;
}
$_SESSION['views']++;
$_SESSION['last_access'] = date('Y-m-d H:i:s');

/**
 * セッションが維持されていることを示すパネルのHTMLを返す。
 * 複数コンテナ構成では「処理コンテナ」が切り替わっても閲覧回数が
 * 増え続けることで、Redis経由でセッションが共有されていると分かる。
 */
function render_session_panel(): string
{
    $rows = [
        'セッションID'   => session_id(),
        '初回アクセス'   => $_SESSION['first_access'] ?? '-',
        '直近アクセス'   => $_SESSION['last_access'] ?? '-',
        '閲覧回数'       => (string)($_SESSION['views'] ?? 0),
        '処理コンテナ'   => gethostname(),
    ];

    $html  = '<div class="session-panel">';
    $html .= '<h2>セッション状態(Redisで共有)</h2>';
    $html .= '<table>';
    foreach ($rows as $label => $value) {
        $html .= '<tr><th>' . htmlspecialchars($label) . '</th>'
               . '<td>' . htmlspecialchars($value) . '</td></tr>';
    }
    $html .= '</table>';
    $html .= '<p class="hint">ページを再読み込みすると「閲覧回数」が増加します。'
           . '「処理コンテナ」が切り替わっても回数が維持されれば、'
           . 'Redis経由でセッションが共有されています。</p>';
    $html .= '</div>';

    return $html;
}

/**
 * セッションパネル用のCSS(各ページのstyleに追記する)。
 */
function session_panel_css(): string
{
    return <<<CSS
  .session-panel { background:#f5f8ff; border:1px solid #c7d6f5; border-radius:8px;
                   padding:1rem 1.2rem; margin-bottom:1.5rem; }
  .session-panel h2 { margin:.2rem 0 .8rem; font-size:1rem; color:#234; }
  .session-panel table { border-collapse:collapse; width:100%; font-size:.9rem; }
  .session-panel th, .session-panel td { text-align:left; padding:.3rem .5rem;
                   border-bottom:1px solid #dde6f7; }
  .session-panel th { width:9rem; color:#456; font-weight:600; }
  .session-panel .hint { font-size:.8rem; color:#678; margin:.8rem 0 0; line-height:1.6; }
CSS;
}

Dockerfileの設定

コンテナイメージをビルドするための設定ファイルである「Dockerfile」を作成していきます。PHP公式のイメージである「php:8.4-fpm」を元に、必要な肉付けをしていきますが、第1回~第2回とはいくつかの点で差異が有ります。

  • 先述のように、第1回~第2回で利用したApacheではなく、Nginxを利用しています。
  • redis接続のために必要なモジュールを pecl install するように設定しています(代わりにDB接続に必要なモジュールは粉機利用しないため取り除きました)

Dockerfile

FROM php:8.4-fpm

# nginx + supervisor を導入
RUN apt-get update && apt-get install -y \
    nginx \
    supervisor \
    libpq-dev \
    libonig-dev \
    && rm -rf /var/lib/apt/lists/*

# PHPの拡張を有効化
RUN docker-php-ext-install mbstring opcache

# Redis拡張をPECLで導入(外部Redisを使ったセッション共有のため)
RUN pecl install redis \
    && docker-php-ext-enable redis

# nginxの設定、PHP-FPMの設定、PHPの設定、supervisordの設定をコピーする
# (nginxはメイン設定ごと差し替え。8080 listen・非root対応の内容を含む)
COPY conf/nginx/nginx.conf /etc/nginx/nginx.conf
COPY conf/php-fpm/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY conf/supervisord.conf /etc/supervisor/conf.d/app.conf
COPY conf/php/php.ini /usr/local/etc/php/conf.d/app.ini

# Webアプリケーション本体をコピーする
COPY src /var/www/html/
RUN chown -R www-data:www-data /var/www/html

EXPOSE 8080

CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/app.conf"]

conf/nginx/nginx.conf

イメージビルド時にコピーされるNginxの設定ファイルです。非特権ポートである HTTP:8080 を公開するようにしておきます。

# Webコンテナ内のnginx設定。
# AppRun専有型は uid951:gid951 の非rootで実行されるため、
# pid・一時ファイル・ログはすべて非rootでも書き込める場所へ向ける。
# (user ディレクティブは非root起動では無視されるため記載しない)

worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    access_log /dev/stdout;

    # 一時ファイル領域を /tmp 配下へ(非rootでも書き込み可能にする)
    client_body_temp_path /tmp/nginx-client-body;
    proxy_temp_path       /tmp/nginx-proxy;
    fastcgi_temp_path     /tmp/nginx-fastcgi;
    uwsgi_temp_path       /tmp/nginx-uwsgi;
    scgi_temp_path        /tmp/nginx-scgi;

    sendfile on;
    keepalive_timeout 65;

    server {
        # デプロイ先がhttp 8080をオープンする仕様のため8080でlisten
        listen 8080;
        server_name _;

        root /var/www/html;
        index index.php;

        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # PHPはphp-fpm(127.0.0.1:9000)へ受け渡す
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }
}

conf/php/php.ini

PHPの設定ファイルです。

[PHP]
date.timezone = Asia/Tokyo

conf/php-fpm/www.conf

php-fpmの設定です。

[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

conf/supervisord.conf

supervisorの設定ファイルです。ApacheではなくNginxを起動するように調整しています。

[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0

[program:php-fpm]
command=/usr/local/sbin/php-fpm -F
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Dockerイメージのビルド

ではDockerコンテナのイメージをビルドしていきます。

dockerコマンドでイメージのビルドを実行します。-tオプションで指定しているのはイメージ名です。第1回~第2回とは別のイメージ名にしています。

docker build --platform linux/amd64 -t sakura_app_run_session .

Dockerイメージをコンテナレジストリにプッシュ

Dockerイメージがビルド出来たら、さくらのクラウドのコンテナレジストリにプッシュしていきます。

コンテナレジストリの作成方法は 第2回の記事 をご参照ください。

# コンテナレジストリにログイン
docker login <コンテナレジストリのパス>

# イメージにタグ付け
docker tag sakura_app_run_session  <コンテナレジストリのパス>/sakura_app_run_session:lates

# イメージのプッシュ
docker push <コンテナレジストリのパス>/sakura_app_run_session:latest

これでAppRun専有型でWebUIを起動するためのイメージが用意出来ました。

参考: ローカルでの検証用 docker-compose

ローカルのDocker Desktop環境でもRedisコンテナと複数のWebUIコンテナ、ロードバランサ用コンテナを起動して動作検証ができます。詳細は割愛しますが、そのための docker-compose.yml ファイルをご紹介します。

docker-compose.yml

services:
  # 外部Redis(セッション共有用)
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--requirepass", "redispass"]
    # 検証用のため永続化は無効(最小構成)

  # Webコンテナ1台目
  app1:
    build: ..
    image: sakura_app_run_session
    environment:
      REDIS_HOST: redis               # セッション保存先Redis
      REDIS_PORT: 6379
      REDIS_PASS: redispass
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis

  # Webコンテナ2台目(同一イメージ。複数コンテナでのセッション維持検証用)
  app2:
    build: ..
    image: sakura_app_run_session
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASS: redispass
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis

  # ロードバランサ(app1/app2へラウンドロビン)。ブラウザからはこのポートへアクセスする。
  lb:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./conf/nginx/lb.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app1
      - app2

conf/nginx/lb.conf

ロードバランサとして起動するNginx用の設定ファイルです。

# 検証用ロードバランサ。
# 複数のWebコンテナ(app1, app2)へ順番(ラウンドロビン)にリクエストを振り分ける。
# AppRun本番では基盤側が負荷分散するため、これはローカル検証用の代替。
upstream web_backend {
    server app1:8080;
    server app2:8080;
}

server {
    listen 80;

    location / {
        proxy_pass http://web_backend;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

全体像

Webアプリケーションのツリー構造

docker-compose 起動

起動。イメージのビルドは済ませておく事。

docker compose up -d

起動後はWebブラウザから http://localhost:8080/ にアクセスして確認します。

停止

docker compose stop

次回へ続く

第3回ではセッション維持用のRedisコンテナの実行、第4回となる今回は、セッション維持用Redisで実際にセッションを維持するWebアプリを実装し、Dockerイメージ化しました。

次回は今回作成したWebUI部分をAppRunで起動します。また次回、お会いしましょう。

この記事を書いた人

yu-kinjo

ネットアシスト開発部の yu-kinjo です。 【取得資格】 ・さくらのクラウド検定 ・AWS Certified Solutions Architect - Associate ・AWS Certified AI Practitioner ・Oracle 認定Javaプログラマ SE6 ・JSTQB テスト技術者資格 ファンデーションレベル ・CIW (Certified Internet Webprofessional) ファンデーション ・XML技術者育成推進委員会 XMLマスター ベーシックV2 ・基本情報処理技術者 ・初級システムアドミニストレータ