🐥

Hello AWS (part 5:Amazon RDSを用いた永続化)

に公開

動機

いままで扱ってきたアプリケーションはステートレス(レスポンスはリクエストのみに依存)であったが、実際は何かしらの状態(例えばユーザ情報)をサーバで保持しなければならない場合がほとんどである。
データの永続化には色々な手段があるが、小規模から大規模までスケールでき、市民権を得ているツールの一つとしてデータベースが挙げられる。

目的

  • Amazonが提供する永続化手段の一つであるAmazon RDSを用いてPostgreSQLデータベースを構築する
  • Part 3で構築したNginxコンテナとPHPコンテナからなるシステムにPostgreSQLを加え、PHPコンテナから通信できるようにする

免責

内容の正確性に注意を払ってはいますが、不正確な理解による不正確な記述があり得ます。
定期的に見直し改善していく予定ですが、その点注意して読んで頂ければ幸いです。

構成

構成図は以下のようになる。
赤矢印がユーザによるリクエストの流れ、青矢印がそれ以外(AWSサービスからの通信など)を表す(青はレスポンスの流れではない)。

もう少しきちんと描いたもの

こちらはDrawIOを使用した。

Part 3で使用したサービスに加えて以下のものを使用する:

ざっくりとした構築手順としては、

  1. DB subnet groupを作成
  2. Security Group(SG)を作成
  3. データベースを作成
  4. PHPからの接続を設定、確立

となる。

DB Subnet Group

まずはデータベースを設置する対象のネットワークを準備する。
といっても新しくVPCやサブネットを作成するわけではなく、サブネットグループと呼ばれる既存のサブネットの集合を定義するだけである。
具体的にはAurora and RDSページのSubnet groupsCreate DB subnet groupから作成する。
いつものVPCを選択し、ECSサービスが置かれているサブネットを選択する。
なおデータベースはPHPタスクからのアクセスのみを想定しているので、プライベートサブネットのみからサブネットグループを構成する(=パブリックサブネットは含めない)。

Security groupの作成

PHPタスクが所属するSG(ecs-sg-app)からの通信を受け付けるようにSGを作成する。
具体的には

  • inbound
    • PHPからのリクエストを受け取る
      • Type: Custom TCP
      • Protocol: TCP
      • Port range: 5432
      • Source: ecs-sg-app(PHPサービスのSG)
  • outbound
    • VPC内部の通信を許可する
      • Type: All trafic
      • Protocol: All
      • Port range: All
      • Destination: 10.0.0.0/16(VPCのCIDR)

なおoutbound通信は特に想定していないが、PostgreSQLの都合上何かしらの需要がある可能性がある(DNSや時刻同期など?)ので、VPC内部に向けたものは許可する設定にしている。

AWS RDS

データベースを定義し、インスタンスを作成する。
Aurora and RDSページのCreate databaseから作成できる。
備忘録として今回使用したオプションを以下に列挙するが、方針としては「できるだけ単純に、できるだけ最小で」である。
何も記述がない項目に関しては、初期値のまま使用している。

  • Database creation methodStandard create
  • Engine options
    • Engine typePostgreSQL(バージョンは最新のもの)
  • TemplatesProduction
  • Availability and durabilitySingle-AZ DB instance deployment (1 instance)(簡単のため冗長化無し)
  • Settings
    • DB instance identifier:AWS上でユニークな適当な名前(DB内部の様々な名称とは関係ない)
    • Credentials settings
      • Master username:適当なもの(例えばpostgres)。PHPコンテナのtask definitionで後ほど設定する環境変数DB_USERNAMEと一致させる。
      • Credentials managementSelf managed。簡単のためクラシカルなパスワード認証(自動生成)を用いる。PHPコンテナのtask definitionで後ほど設定する環境変数DB_PASSWORDと一致させる。
  • Instance configuration
    • DB instance classBurstable classes (includes t classes)db.t3.micro(最小のもの)
  • Storage
    • Storage typeGeneral Purpose SSD (gp2)(一番安いもの)
    • Allocated storage20GiB(最小)
  • Connectivity
    • VPC:いつも使用しているものを設定する。
    • DB subnet group:前ステップで作成したものを指定する。
    • VPC security group:前ステップで作成したものを指定する。
  • MonitoringDatabase Insights - Standardにして全てオフにする。
  • Additional configuration
    • Initial database name:空欄でもPostgreSQLデフォルトのデータベースpostgresが生成されるので空欄で構わない。PHPコンテナのtask definitionで後ほど設定する環境変数DB_DATABASEと一致させる。

いくつか補足する。

  • 作成後にcredentialsを確認するためのトーストが出てくるのでチェックして内容を控えておく(パスワードを控え忘れると、Modifyから再度パスワードを設定する必要がある)。
  • RDS instance overviewにて確認できるエンドポイント(<DataBaseName>.<hash>.ap-northeast-1.rds.amazonaws.com)はPHPコンテナのtask definitionで後ほど設定する環境変数DB_HOSTに設定する必要がある。
  • 今回は学習目的なので、コストや複雑さを抑えるためにMulti-AZ構成ではなくSingle-AZ構成を使用している。実用上は冗長化がほとんど必須だと思われる。

PHPとPostgreSQLの接続

Dockerイメージの更新

PHPコンテナから疎通するために、Dockerイメージを以下のように更新する。
PostgreSQLをPHPから操作するためのドライバを導入している。

FROM php:fpm

RUN apt-get update
RUN apt-get install -y git unzip libpq-dev
RUN docker-php-ext-install pdo pdo_pgsql

COPY index.php /var/www/html/index.php

CMD ["php-fpm", "-F"]

なおindex.phpも例えば以下のように更新し、DBと疎通を試みる。
環境変数を読み込んでDBとの接続を確立、DB側で適当なメッセージを作成してfetchし、エコーするだけの簡単なものである。

<?php

$host = getenv('DB_HOST');
$port = 5432;
$db   = getenv('DB_DATABASE');
$user = getenv('DB_USERNAME');
$pass = getenv('DB_PASSWORD');

try {
    $dsn = "pgsql:host=$host;port=$port;dbname=$db";
    $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    echo "Connected to PostgreSQL.";
    $res = $pdo->query("SELECT 'Hello from PostgreSQL' AS msg")->fetch();
    echo $res['msg'];
} catch (PDOException $e) {
    echo "Connection failed: " . $e->getMessage();
}

まとめてbuildし、ECRにpushする(part 1参照)。

ECS上のサービスとタスクの更新

次にPHPコンテナを定義しているtask definitionに環境変数を設定する。
Create revisionを開き、RDS作成時に得られた以下の環境変数を設定する:

  • DB_HOST
  • DB_DATABASE
  • DB_USERNAME
  • DB_PASSWORD

変更を反映するため、PHPのサービスで走っている元のタスクを一旦停止し、新しいtask definitionのrevisionを元に再度タスクを生成する。

うまく設定できていればしばらくすると疎通できるようになり、PHPとPostgreSQLからのメッセージが返却されるはずである。

初期化の実施

上の例はデータベースにただメッセージをエコーさせるだけだが、実際はテーブルを作成してCRUDなどデータの作成、更新、削除を行うのが一般的である。
ここでテーブル作成や初期データの注入は初回に一度だけ行われるべき操作なので、Dockerfileに含めることはできない(さもないとFargateタスクの起動時に毎回呼ばれてしまう)。
データベースに直接接続して行うこともできる(と思う)が、サービスに属さないタスクを作成し、必要なタイミングで手動で起動することで対応できる(後述:Laravelについて)。One-offタスクなどと呼ばれるようである。

データベースのログ確認

疎通がうまくいかない場合や想定したデータ操作ができない場合など、データベース側のログを確認したい場合がある。
error/postgresql.log.2025-01-01-00などのファイル名で定期的にログが吐かれているので、参照すると役にたつ場合もあると思われる。

補足:Laravelアプリへの発展

上の例では簡単のためPHPタスクでは単一のPHPスクリプトのみを用いていたが、

  • プロジェクトの規模が大きい
  • SQLの発行を一貫性を持って(安全に)管理したい
  • ルーティング・バリデーション・マイグレーションといった機能を一貫して扱いたい

などの理由から、フレームワークを使用するのが一般的である。
PHPでは特にLaravelと呼ばれるフレームワークが頻繁に使用される。
この記事の主眼はAWS(インフラ)ではあるが、参考のためLaravelの場合どのような変更を行う必要があるかについても簡単に触れる。
後述の通り基本的にAWS側のサービスの構成を変更する必要はなく、Dockerイメージの変更とECSタスクの設定を少し変更するだけで対応できる。
なおLaravelプロジェクトのルート(approutespublicなどのディレクトリ)は/var/www/html下に置かれているものとし、JSON形式をやり取りするREST APIサーバを前提とする。

Dockerイメージの変更

Nginx

公式ページにも記載があるように、index.phpではなくpublic/index.phpをルートとして用いる必要がある。
対応方法は色々あるが、わかりやすいやり方としてnginx.conf

- fastcgi_param SCRIPT_FILENAME /var/www/html/index.php;
+ fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;

のように変更する。

PHP(Laravel)

index.php以外のプロジェクト周りのファイルも含める必要がある。
また依存パッケージ(vendor以下にダウンロードされるもの)を取得するためにcomposerが必要になる。

- COPY index.php /var/www/html/index.php
+ # Move to project-root directory
+ WORKDIR /var/www/html
+ # Copy source files
+ COPY ./app ./app
+ COPY ./artisan ./artisan
+ COPY ./bootstrap ./bootstrap
+ COPY ./composer.json ./composer.json
+ COPY ./composer.lock ./composer.lock
+ COPY ./config ./config
+ COPY ./database ./database
+ COPY ./public ./public
+ COPY ./routes ./routes
+ COPY ./storage ./storage
+ # Install Composer and dependencies
+ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+ # Adjust permission
+ RUN chown -R www-data:www-data storage bootstrap/cache
+ RUN composer install --no-dev --optimize-autoloader

環境変数

Laravelは環境変数を.envファイルにまとめて記述するのが一般的であるが、これをDockerイメージ内に含めてしまうのは推奨されず、task definitionのenvironment variablesに記述するのが好ましい。
使用する機能によって必要な変数は異なるが、最低限Laravelを動かしてPostgreSQLと疎通させるのに必要なものを以下にまとめる(詳細は省略する)。

  • APP_DEBUG: false
  • APP_ENV: production
  • APP_KEY: base64:xxx(後述)
  • DB_CONNECTION: pgsql
  • DB_DATABASE: DB作成時のもの(デフォルトならpostgres
  • DB_HOST: DB作成時のもの(RDS instance overviewにて確認できるエンドポイント)
  • DB_PASSWORD: DB作成時のもの
  • DB_PORT: 5432
  • DB_USERNAME: DB作成時のもの(デフォルトならpostgres

データベースの初期化

本文で触れたようにデータベースは使用前に初期化を行う必要があるが、Laravelにも当然その機能は備わっており、今回もそれを利用する。

まずはPHPサービスからone-offタスクを起動する。
ECSのコンソールにおいて該当のclusterを選択し、TasksタブからRun new taskを選択する。
諸々設定する項目があるが、PHPサービスと同じ設定(VPC、サブネット、SG、データベース接続用の環境変数、etc.)を行えばよい。
唯一Container overridesの項目のみ実行したいコマンドを設定する必要がある。
今回のLaravel経由の初期化(マイグレーションとシーディング)であれば、

php artisan migrate:refresh --force --seed

を実行することになる。
ただし該当するinput要素のプレースホルダに書かれているようにコマンドはカンマ区切りで記述しなければならないため、

php,artisan,migrate:refresh,--force,--seed

という書式で与える必要がある。

Discussion