Hello AWS (part 5:Amazon RDSを用いた永続化)
動機
いままで扱ってきたアプリケーションはステートレス(レスポンスはリクエストのみに依存)であったが、実際は何かしらの状態(例えばユーザ情報)をサーバで保持しなければならない場合がほとんどである。
データの永続化には色々な手段があるが、小規模から大規模までスケールでき、市民権を得ているツールの一つとしてデータベースが挙げられる。
目的
- Amazonが提供する永続化手段の一つであるAmazon RDSを用いてPostgreSQLデータベースを構築する
- Part 3で構築したNginxコンテナとPHPコンテナからなるシステムにPostgreSQLを加え、PHPコンテナから通信できるようにする
免責
内容の正確性に注意を払ってはいますが、不正確な理解による不正確な記述があり得ます。
定期的に見直し改善していく予定ですが、その点注意して読んで頂ければ幸いです。
構成
構成図は以下のようになる。
赤矢印がユーザによるリクエストの流れ、青矢印がそれ以外(AWSサービスからの通信など)を表す(青はレスポンスの流れではない)。
もう少しきちんと描いたもの
こちらはDrawIOを使用した。
Part 3で使用したサービスに加えて以下のものを使用する:
ざっくりとした構築手順としては、
- DB subnet groupを作成
- Security Group(SG)を作成
- データベースを作成
- PHPからの接続を設定、確立
となる。
DB Subnet Group
まずはデータベースを設置する対象のネットワークを準備する。
といっても新しくVPCやサブネットを作成するわけではなく、サブネットグループと呼ばれる既存のサブネットの集合を定義するだけである。
具体的にはAurora and RDS
ページのSubnet groups
、Create 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)
- Type:
- PHPからのリクエストを受け取る
-
outbound
- VPC内部の通信を許可する
- Type:
All trafic
- Protocol:
All
- Port range:
All
- Destination:
10.0.0.0/16
(VPCのCIDR)
- Type:
- VPC内部の通信を許可する
なおoutbound通信は特に想定していないが、PostgreSQLの都合上何かしらの需要がある可能性がある(DNSや時刻同期など?)ので、VPC内部に向けたものは許可する設定にしている。
AWS RDS
データベースを定義し、インスタンスを作成する。
Aurora and RDS
ページのCreate database
から作成できる。
備忘録として今回使用したオプションを以下に列挙するが、方針としては「できるだけ単純に、できるだけ最小で」である。
何も記述がない項目に関しては、初期値のまま使用している。
-
Database creation method
:Standard create
-
Engine options
-
Engine type
:PostgreSQL
(バージョンは最新のもの)
-
-
Templates
:Production
-
Availability and durability
:Single-AZ DB instance deployment (1 instance)
(簡単のため冗長化無し) -
Settings
-
DB instance identifier
:AWS上でユニークな適当な名前(DB内部の様々な名称とは関係ない) -
Credentials settings
-
Master username
:適当なもの(例えばpostgres
)。PHPコンテナのtask definitionで後ほど設定する環境変数DB_USERNAME
と一致させる。 -
Credentials management
:Self managed
。簡単のためクラシカルなパスワード認証(自動生成)を用いる。PHPコンテナのtask definitionで後ほど設定する環境変数DB_PASSWORD
と一致させる。
-
-
-
Instance configuration
-
DB instance class
:Burstable classes (includes t classes)
、db.t3.micro
(最小のもの)
-
-
Storage
-
Storage type
:General Purpose SSD (gp2)
(一番安いもの) -
Allocated storage
:20GiB
(最小)
-
-
Connectivity
-
VPC
:いつも使用しているものを設定する。 -
DB subnet group
:前ステップで作成したものを指定する。 -
VPC security group
:前ステップで作成したものを指定する。
-
-
Monitoring
:Database 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プロジェクトのルート(app
、routes
、public
などのディレクトリ)は/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