Strapi v4をAmazon ECSで動かす
はじめに
オープンソースHeadless CMSのStrapiのv4をAmazon ECSで動かしてみます。v3まではDockerイメージが公式に提供されていたのですが、現状v4向けのイメージが提供されておらず手間取ったのでブログにまとめることにしました。コンテナで動かすと画像などのファイルを外部に保存する必要があります。今回は@strapi/provider-upload-aws-s3という保存先をS3バケットに変更する公式プラグインを利用しました。
AWSサービスの構築
Strapiを動かす為に必要なAWSサービスをAWS CDKを使って構築します。
プロジェクトディレクトリを作成して、CDKのコードを初期生成します。
cdk-strapi-on-ecs && cd $_
npx cdk@2 init app --language typescript ./
エントリーポイントとスタックのコードを書きます。
#!/usr/bin/env node
import 'source-map-support/register';
import {App} from 'aws-cdk-lib';
import {CdkStrapiOnEcsStack} from '../lib/cdk-strapi-on-ecs-stack';
const app = new App();
new CdkStrapiOnEcsStack(app, 'CdkStrapiOnEcsStack');
スタックもサクッとシングルスタックで作成してしまいます。
import {Stack, StackProps} from 'aws-cdk-lib';
import {
InstanceClass,
InstanceSize,
InstanceType,
Vpc,
} from 'aws-cdk-lib/aws-ec2';
import {
ContainerImage,
FargateTaskDefinition,
Secret,
} from 'aws-cdk-lib/aws-ecs';
import {ApplicationLoadBalancedFargateService} from 'aws-cdk-lib/aws-ecs-patterns';
import {
DatabaseInstance,
DatabaseInstanceEngine,
PostgresEngineVersion,
} from 'aws-cdk-lib/aws-rds';
import {Bucket, BucketAccessControl} from 'aws-cdk-lib/aws-s3';
import {Construct} from 'constructs';
import {resolve} from 'path';
export class CdkStrapiOnEcsStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const vpc = new Vpc(this, 'Vpc');
const db = new DatabaseInstance(this, 'Database', {
vpc,
engine: DatabaseInstanceEngine.postgres({
version: PostgresEngineVersion.VER_13_4,
}),
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MICRO),
databaseName: 'strapi',
multiAz: false,
});
if (db.secret === undefined) {
throw new Error('DatabaseInstance.secret is undefined');
}
const bucket = new Bucket(this, 'Bucket', {
accessControl: BucketAccessControl.PUBLIC_READ,
});
const taskDefinition = new FargateTaskDefinition(this, 'TaskDef');
bucket.grantReadWrite(taskDefinition.taskRole);
bucket.grantPutAcl(taskDefinition.taskRole);
const container = taskDefinition.addContainer('Strapi', {
image: ContainerImage.fromAsset(resolve(__dirname, '../strapi')),
environment: {
DATABASE_NAME: 'strapi',
AWS_REGION: this.region,
AWS_BUCKET_NAME: bucket.bucketName,
},
secrets: {
DATABASE_HOST: Secret.fromSecretsManager(db.secret, 'host'),
DATABASE_PORT: Secret.fromSecretsManager(db.secret, 'port'),
DATABASE_USERNAME: Secret.fromSecretsManager(db.secret, 'username'),
DATABASE_PASSWORD: Secret.fromSecretsManager(db.secret, 'password'),
},
});
container.addPortMappings({containerPort: 1337});
const {service} = new ApplicationLoadBalancedFargateService(
this,
'Service',
{vpc, taskDefinition}
);
db.connections.allowDefaultPortFrom(service);
}
}
Strapi Dockerイメージの作成
プロジェクトルートでStrapiのコードを初期生成します。
npx create-strapi-app@latest strapi --quickstart
## プロセスが立ち上がったらCtrl + Cで終了してください
必要なライブラリをインストールし、ビルドはnode_modules上で行うので不要なnode_modules
を削除します。
cd strapi
yarn add @strapi/provider-upload-aws-s3 pg
rm -rf node_modules
データベースにはPostgreSQLを使用する為、接続情報を環境変数から取得するように設定を変更します。
module.exports = ({env}) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', 'postgres'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi'),
ssl: env.bool('DATABASE_SSL', false),
},
},
debug: false,
});
静的コンテンツをS3バケットに保存するためにプラグインの設定を行います。
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
region: env('AWS_REGION'),
params: {
Bucket: env('AWS_BUCKET_NAME'),
},
},
},
},
});
静的コンテンツがS3バケットのドメイン名となりStrapiと別ドメインになるので、コンテンツセキュリティポリシーの為の設定を行います。middlewaresでは環境変数を取得出来ないため、S3バケットのドメイン名ワイルドカードで緩く指定しています。より厳密に制御したい場合はバケット名を指定して作成してください。
module.exports = [
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::favicon',
'strapi::public',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': ["'self'", 'data:', 'blob:', '*.s3.ap-northeast-1.amazonaws.com'],
'media-src': ["'self'", 'data:', 'blob:', '*.s3.ap-northeast-1.amazonaws.com'],
upgradeInsecureRequests: null,
},
},
},
},
];
Dockerfileを作成します。Strapiは扱うコンテンツの定義を追加したい場合は、ローカル環境で立ち上げWebブラウザから追加操作をした時に生成されるコードを環境に追加する必要があります。なので、ローカルでDocker上でStrapiを立ち上げた時にディレクトリをマウントする必要があります(strapi:/opt/app/strapi
)。マウントするとnode_modules
が参照出来なくなってしまいます。なので、Dockerイメージを作成するタイミングでnode_modules
をマウントされるディレクトリとは別のディレクトリ(/opt/node_modules
)に作成しておきます。
FROM node:16-alpine
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/
COPY package.json yarn.lock ./
ENV PATH /opt/node_modules/.bin:$PATH
RUN yarn config set network-timeout 600000 -g
RUN yarn install
WORKDIR /opt/app
COPY ./ .
RUN yarn build
EXPOSE 1337
CMD ["yarn", "start"]
ローカルで立ち上げるためのDocker Composeの設定
プロジェクトルートにDocker Composeファイルを作成します。データベースにPostgreSQLを使うように設定を変えているので、DockerでPostgreSQLを立ち上げます。
また、コンテンツを追加する際はstrapi develop
で立ち上げる必要があるので、コマンドをyarn develop
に上書きします。
version: '3'
services:
strapi:
container_name: strapi
command: yarn develop
build: strapi
environment:
DATABASE_NAME: strapi
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
NODE_ENV: development
volumes:
- ./strapi:/opt/app
ports:
- '1337:1337'
depends_on:
- postgres
postgres:
image: postgres:14.1
container_name: postgres
environment:
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
volumes:
- ./data:/var/lib/postgresql/data
ports:
- '5432:5432'
デプロイ
最後にデプロイして完了です。
npm run cdk deploy
あとがき
マウントするディレクトリからnode_modules
を逃がすという発想が浮かばず結構ハマりました。またmiddlewaresで環境変数が使えないポイントもワイルドカードが使えるかもと気づくまで結構時間がかかりました。
いわゆるJamstackは運用負荷が少ないところが大きな利点だと思っており、自分でHeadless CMSを建ててしまうとRDBMSのバージョンアップなどなど発生するので、極力SaaSに寄せたいなーと今更ながら思いました。
今回のコード全体はこちらに公開しています。https://github.com/intercept6/cdk-strapi-on-ecs
Discussion