Cloud Run(フルマネージド サーバレス)に strapi(ヘッドレス CMS)をデプロイする
私は基本的にはフロントエンドの領域を飯の種としていますが、それでも最近はバックエンドとかインフラとかについても理解がないと、中途半端な技術力しか持たない私では生き抜いていけないと感じるわけです。まあ、それはある意味は言い訳でただただやりたくなったというのが正直なところです。
過去に社内環境の VPS などをこねくり回していた時期はあるのですが、時代とはかくも無残なものでそんなものは過去の栄光にすらならないわけでして(まあ、さして昔でもないのでその頃から AWS とかありはしましたが)。その上、WordPress などの CMS もなぜか奴から避けて通るように仕事では構築することもありませんでした。
さてさて、話を戻しまして表題のとおりに Cloud Run に strapi をデプロイしてみようと思いました。なぜにこの組み合わせかといいますと、これは別に求められた結果でもなんでなく、なんとなく私自身が好みっぽいものを選んだ結果です。ちなみに、もしフロントエンドの環境を加えるのであれば躊躇なく Next.js を選ぶことでしょう。
まず、Cloud Run についてです。これは GCP のサーバレス コンピューティングのひとつです。GCP のサーバレス コンピューティングは複数あるのですが、コンテナ化したアプリケーションを使うというのがよいです。strapi も公式でコンテナイメージを用意しており、それ構築するためのドキュメントも用意していますので、相性はよいかと思います。あと、費用が比較的に安価であるというのもメリットです。一応、App Engine へのデプロイであれば strapi の公式ドキュメントにありますので、そちらを確認するとよいでしょう。
次に、strapi についてです。これは Node.js 製の ヘッドレス CMS です。特徴としては、オープンソースであること、REST API か GraphQL を選べること、管理画面から API を設計できること。などがあるかなと思います。とりあえずローカル開発環境を構築するまでなら音速の域です。あと、個人的には Node.js なので親しみやすいってのがありますかね。ただ、ほとんどコードは触らないですけれど。
順番が前後するようですが、なぜに Cloud Run に ヘッドレス CMS をデプロイしたのかという話を軽く。少し前に Cloud Run を試すために Next.js をデプロイしたり、Django REST framework の環境を構築したので Cloud Run にデプロイしたり(このチュートリアルを参考)してました。フロントエンドに Python の API 、そして今回は Node.js の ヘッドレス CMS と。まあ、つまりはなんでも Cloud Run にデプロイしたいお年頃という訳です。
といったところで、本題を進めていきます。
構成
まずは、どのような構成になるのかを事前に説明します。
使用するもの
役割 | 使用するもの | 補足 |
---|---|---|
サーバーレス コンピューティング | Cloud Run(フルマネージド) | 今回は strapi のコンテナを起動 |
データベース | Cloud SQL | 今回は PostgreSQL を使用 |
ストレージ | Cloud Storage | 今回は CMS のアップロードメディアを格納 |
CI / CD | Cloud Build | 今回はコンテナイメージのビルドと登録に使用 |
コンテナレジストリ | Container Registry | |
シークレット管理 | Secret Manager | 今回は環境変数を管理 |
ヘッドレス CMS | strapi |
構成図
初めて書いてみたのですが、まあまあ上手く書けたような気もします。
ちなみに Lucidchart を使いました。
作業の前提
-
Google Cloud Platform の登録 (登録する為には以下が必要です)
- Google アカウント
- クレジットカードまたはデビットカード
- Google Cloud SDK のインストールと初期化
- macOS での作業(Windows の場合などは環境に合わせて打ち替えてください)
作業手順
項目としては以下の手順になります。
- Docker を使って strapi のインストール
- GCP 用の Node.js モジュールの追加
- 無視するファイルの設定
- GCP プロジェクトの作成とサービスの有効化
- シェル変数の用意
- 環境変数の設定
- コンテナの設定
- Cloud SQL インスタンスの作成と設定
- Cloud Storage にバケットの作成と設定
- ビルドとデプロイ
それでは、それぞれの詳細を説明していきます。
1. Docker を使って strapi のインストール
これは公式ドキュメントに記載がありますので、基本的にはそのままです。
1-1. プロジェクト名の設定
はじめにシェル変数にプロジェクト名を定義しておきます。
任意に決めて大丈夫です。今回は strapi-project
としておきます。
$ PROJECT_NAME=strapi-project
1-2. プロジェクトフォルダの作成
プロジェクト名でプロジェクトフォルダの作成をします。
$ mkdir $PROJECT_NAME
cd $PROJECT_NAME
docker-compose.yaml
の作成
1-3. strapi と PostgreSQL のコンテナを構築するための設定ファイルを作成します。
$ touch docker-compose.yaml
docker-compose.yaml
に以下を記述。
version: '3'
services:
strapi:
image: strapi/strapi
environment:
DATABASE_CLIENT: postgres
DATABASE_NAME: strapi
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
links:
- postgres:postgres
volumes:
- ./app:/srv/app
ports:
- '1337:1337'
postgres:
image: postgres
environment:
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
volumes:
- ./data:/data/postgres
ports:
- '5432:5432'
1-4. コンテナイメージの取得
strapi/strapi と postgres のコンテナイメージを取得します。
$ docker-compose pull
1-5. コンテナの構築と起動
コンテナイメージから strapit と PostgreSQL のコンテナの構築と起動をします。
$ docker-compose up
1-6. strapi の起動を確認
ローカル開発環境用の対応はこれで完了です。
http://localhost:1337/admin にアクセスして strapi が正常に起動しているかを確認します。以下の画面が表示されていれば問題ないです。
2. GCP 用の Node.js モジュールの追加
シークレットの取得と Cloud Storage にファイルをアップロードするための、2 つの Node.js のモジュールを追加します。
$ docker-compose exec strapi yarn add \
@google-cloud/secret-manager \
strapi-provider-upload-google-cloud-storage
こちらの対応が終わったら、コンテナを停止しても大丈夫です。
ctrl + c
(デーモン化されている場合は $ docker-compose stop
)で停止します。
3. 無視するファイルの設定
3-1. Git に含めないファイルの設定
.gitignore が app/.gitignore
に配置されるのですが、ルートにあったほうがよいと思いますので移動とそれに伴う修正をします。
$ mv app/.gitignore .gitignore
sed -i '' -e 's/public\/uploads\/\*/app\/public\/uploads\/\*/' .gitignore
sed -i '' -e 's/!public\/uploads\/.gitkeep/app\/!public\/uploads\/.gitkeep/' .gitignore
3-2. デプロイ対象から外すファイルの設定
GCP 上へのデプロイに不要なファイルを対象から外すために .gcloudignore
を作成します。
$ echo "docker-compose.yaml" > .gcloudignore
echo ".gcloudignore" >> .gcloudignore
echo ".git" >> .gcloudignore
echo ".gitignore" >> .gcloudignore
echo "data/" >> .gcloudignore
echo "#\!include:.gitignore" >> .gcloudignore
4. GCP プロジェクトの作成とサービスと有効化
今回は設定したプロジェクトフォルダと同じプロジェクト名を使用します。
4-1. GCP プロジェクトの作成と設定
4-1-1. GCP プロジェクトの作成
以下の GCP の新しいプロジェクトの作成ページにアクセスします。
プロジェクト名に $PROJECT_NAME
の値(今回は strapi-project
)を入力して、プロジェクトを作成します。
CLI でのプロジェクトの作成について
プロジェクト ID を自動生成しないのであれば、以下のコマンドでプロジェクトを作成することもできます。
$ gcloud projects create {PROJECT_ID} \
--name=$PROJECT_NAME \
--set-as-default
--set-as-default
オプションにより作成したプロジェクトをデフォルトに設定しています。
(ですので、次の項目「4-1-3. gcloud CLI のデフォルトに設定」の対応は不要です)
プロジェクト ID の命名については、こちらの公式ドキュメントの「始める前に > プロジェクト ID」を確認してください。
4-1-2. gcloud CLI のデフォルトに設定
$ gcloud config set project $(gcloud projects list --format 'value(projectId)' --filter name=$PROJECT_NAME)
デフォルトが切り替わっているかの確認方法について
以下のコマンドで確認ができます。
$ gcloud config list
表示される project
が以下のように、作成したプロジェクト ID になっていたら問題ないです。
4-2. GCP の各サービスの有効化
$ gcloud services enable \
run.googleapis.com \
sql-component.googleapis.com \
sqladmin.googleapis.com \
cloudbuild.googleapis.com \
secretmanager.googleapis.com
5. シェル変数の用意
使いまわす値があるのと、つどつど説明するのも面倒ですので、まとめてシェル変数として定義しておきます。
$ PROJECT_ID=$(gcloud projects list --format 'value(projectId)' --filter name=$PROJECT_NAME)
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format 'value(projectNumber)')
REGION=us-central1
IMAGE_NAME=strapi-project-image
CLOUD_RUN_NAME=strapi-project
CLOUD_SQL_NAME=strapi-project
CLOUD_SQL_MACHINE_TYPE=db-f1-micro
DATABASE_NAME=strapi
DATABASE_USER=strapi
DATABASE_PASSWORD=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 30 | head -n 1)
SECRET_NAME=application_settings
SECRET_VERSION=latest
BUCKET_NAME=strapi-project-bucket
ざっくりとですが説明しておきます。
シェル変数名 | 説明 | 補足 |
---|---|---|
$PROJECT_ID | GCP プロジェクト ID | 自動取得 |
$PROJECT_NUMBER | プロジェクト番号 | 自動取得 |
$REGION | リージョン | リージョンから選択(今回は全てのサービスで統一) |
$IMAGE_NAME | コンテナイメージ名 | 任意 |
$CLOUD_RUN_NAME | Cloud Run のインスタンス名 | 任意 |
$CLOUD_SQL_NAME | Cloud SQL のインスタンス名 | 任意 |
$CLOUD_SQL_MACHINE_TYPE | Cloud SQL インスタンスのマシンタイプ | マシンタイプから選択 |
$DATABASE_NAME | Cloud SQL のデータベース名 | 任意 |
$DATABASE_USER | Cloud SQL のユーザ名 | 任意 |
$DATABASE_PASSWORD | Cloud SQL のユーザのパスワード | 任意(サンプルは自動生成) |
$SECRET_NAME | プロダクション環境変数のシークレット名 | 任意 |
$SECRET_VERSION | シークレットのバージョン | 存在するバージョンを選択(ただし基本は最新の latest でよいと思う) |
$BUCKET_NAME | Cloud Storage のバケット名 | 任意 |
6. 環境変数の設定
環境変数を GCP プロジェクトのシークレットに登録して、そのデータから環境変数ファイルを生成することで環境変数を使用できるようします。
6-1. .env の作成
$ echo "DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:$CLOUD_SQL_NAME" > .env
echo "DATABASE_NAME=$DATABASE_NAME" >> .env
echo "DATABASE_USERNAME=$DATABASE_USER" >> .env
echo "DATABASE_PASSWORD=$DATABASE_PASSWORD" >> .env
echo "GCS_BUCKET_NAME=$BUCKET_NAME" >> .env
6-2. シークレットの作成
$ gcloud secrets create $SECRET_NAME --replication-policy automatic
6-3. .env の内容をシークレットに登録
$ gcloud secrets versions add $SECRET_NAME --data-file .env
6-4. Cloud Run にシークレットへのアクセスを許可
$ gcloud secrets add-iam-policy-binding $SECRET_NAME \
--member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role roles/secretmanager.secretAccessor
6-5. .env ファイルを削除
ローカル開発環境では不要なファイルなので .env
を削除します。
ついでにサンプルで配置されていた app/.env.example
も今回は使用しないので削除します。
$ rm .env
rm app/.env.example
6-6. 環境変数を生成するスプリクトの作成
production 時にシークレットから環境変数を取得して .env
として生成するスクリプトを作成します。
$ touch app/create-env.js
app/create-env.js
に以下を記述します。
const { writeFile } = require('fs')
const { promisify } = require('util')
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager')
const writeFileAsync = promisify(writeFile)
const { SECRET_NAME } = process.env
if (!SECRET_NAME) {
console.log('Must set "SECRET_NAME" environment variable')
process.exit(1)
}
async function getSecret() {
const client = new SecretManagerServiceClient()
try {
const [version] = await client.accessSecretVersion({
name: SECRET_NAME,
})
return version.payload.data.toString('utf8')
}
catch (e) {
console.error(`error: could not retrieve secret: ${e}`)
return
}
}
(async () => {
const secret = await getSecret()
if(!secret) {
console.error('error: failed to create "env" file')
return
}
await writeFileAsync('.env', secret)
console.log('success: created ".env" file')
})()
7. コンテナの設定
7-1. Dockerfile の作成
コンテナの構築と起動の設定をします。
$ touch Dockerfile
Dockerfile
に以下を記述します。
FROM node:12-slim
WORKDIR /usr/src/app
ENV NODE_ENV=production
COPY ./app/package.json ./
COPY ./app/yarn.lock ./
RUN yarn install \
--prefer-offline \
--frozen-lockfile\
--non-interractive \
--production=true
COPY ./app ./
RUN yarn build
COPY startup.sh /startup.sh
RUN chmod 744 /startup.sh
CMD ["/startup.sh"]
7-2. ラッパー・スクリプトの作成
環境変数ファイルの生成と strapi の起動をする為の 2 つの実行をするので、ラッパー・スクリプトの作成をします。
$ echo "#\!/bin/bash" > startup.sh
echo "" >> startup.sh
echo "node create-env.js" >> startup.sh
echo "yarn start" >> startup.sh
8. Cloud SQL インスタンスの作成と設定
ローカル開発環境では PostgreSQL のコンテナを使用していましたが、GCP 上では Cloud SQL を使用するようにします。
8-1. Cloud SQL インスタンスの作成
$ gcloud sql instances create $CLOUD_SQL_NAME \
--project $PROJECT_ID \
--database-version POSTGRES_11 \
--tier $CLOUD_SQL_MACHINE_TYPE \
--region $REGION
8-2. Cloud SQL インスタンスにデータベースを作成
$ gcloud sql databases create $DATABASE_NAME \
--instance $CLOUD_SQL_NAME
8-3. Cloud SQL インスタンスにユーザを追加
$ gcloud sql users create $DATABASE_USER \
--instance $CLOUD_SQL_NAME \
--password $DATABASE_PASSWORD
9-4. サービスアカウントに Cloud SQL クライアントのロールを追加
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
--member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role roles/cloudsql.client
9. Cloud Storage にバケットの作成と設定
strapi にアップロードするファイルの格納場所を設定をします。
ローカル開発環境では app/public/uploads
に格納されますが、GCP 上では Cloud Storage に格納するようにします。
9-1. Cloud Storage にバケットを作成
$ gsutil mb -l $REGION gs://$BUCKET_NAME
9-2. バケットに閲覧/書き込み権限を追加
9-2-1. 閲覧/書き込み権限の設定ファイルを取得
$ gsutil iam get gs://$BUCKET_NAME > iam.json
9-2-2. iam.json を更新
iam.json
に以下の内容をマージします。
{
"bindings": [
{
"members": [
"serviceAccount:{PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
],
"role": "roles/storage.legacyBucketWriter"
}
]
}
プロジェクト番号をシェル変数に登録したものに書き換えます。
$ sed -i '' -e s/{PROJECT_NUMBER}/$PROJECT_NUMBER/ iam.json
9-2-3. バケットに閲覧/書き込み権限を設定
$ gsutil iam set iam.json gs://$BUCKET_NAME
9-2-4. iam.json を削除
ローカル開発環境では不要なファイルなので iam.json
を削除します。
$ rm iam.json
9-3. アップロードファイルの格納先を Cloud Storage に設定
9-3-1. strapi のプラグインファイルを作成
$ echo "module.exports = () => {}" > app/config/plugins.js
9-3-2. production 環境用の strapi のプラグインファイルを作成
$ mkdir app/config/env
mkdir app/config/env/production
touch app/config/env/production/plugins.js
app/config/env/production/plugins.js
に以下を記述します。
module.exports = ({ env }) => ({
upload: {
provider: 'google-cloud-storage',
providerOptions: {
bucketName: env('GCS_BUCKET_NAME'),
baseUrl: 'https://storage.cloud.google.com/{bucket-name}',
uniform: true,
},
},
})
10. ビルドとデプロイ
10-1. コンテナイメージをビルドして登録
Cloud Build でコンテナイメージをビルドして、Container Registry に登録します。
$ gcloud builds submit \
--tag gcr.io/$PROJECT_ID/$IMAGE_NAME \
--substitutions=_DB_USER=$DATABASE_USER,_DB_PASS=$DATABASE_PASSWORD
10-2. Cloud Run にデプロイ
Container Registry に登録されているコンテナイメージを Cloud Run にデプロイして、コンテナの作成と起動をします。
$ gcloud run deploy $CLOUD_RUN_NAME \
--platform managed \
--region $REGION \
--image gcr.io/$PROJECT_ID/$IMAGE_NAME \
--update-env-vars SECRET_NAME=projects/$PROJECT_NUMBER/secrets/$SECRET_NAME/versions/$SECRET_VERSION \
--add-cloudsql-instances $PROJECT_ID:$REGION:$CLOUD_SQL_NAME \
--allow-unauthenticated
デプロイに成功していれば URL が表示されますので、そこからアクセスして確認してみましょう。
以下の画面が表示されます。
次に admin/
ディレクトリにアクセスしてみると、ローカル開発環境時に確認できた管理者ユーザの作成画面がでてきます。
問題がないか確認する為にそのまま管理者ユーザを作成してみます。
作成後に以下の画面が表示されていれば問題ないです。
これで完了です。
その後の対応
一応はデプロイまで持っていきましたが、実際に実装していく上ではまだまだ足りていないことがあるでしょう。ですので、軽くいくつかの対応について述べておきます。
1. Cloud Run の場合
1-1. バケットの CORS 構成
CORS(Cross-Origin Resource Sharing)により、API を通して strapi にファイルをアップロードすることができない状態です。必要があればバケットの CORS 構成を設定することで対応できます。
1-2. カスタムドメインのマッピング
カスタムドメインを使用したいことはよくあるかと思います。Cloud Run では比較的かんたんですので、こちらの公式ドキュメントを見ながら対応するとよいです。
1-3. オートデプロイの実装
GitHub などに push した際にオートデプロイされる設定があるとよいかもしれません。GitHub などの Webhook を利用して、Cloud Build によるコンテナイメージのビルドとデプロイをするようにできます。
1-4. コールドスタートの対策
アクセスする際にレスポンスが遅いと感じるときはないでしょうか?Cloud Run の特徴として自動スケーリングによりインスタンスがゼロになるところがあります。だからこそコストパフォーマンスにすぐれいているところもあるのですが、これによる問題というか仕様としてコールドスタートが発生します。必要であればそれについて対策する必要があるかもしれません。
2. strapi の場合
場合もなにも構築しただけ何も実装していないですね。
手順中にも軽くは述べてはいますが公式ドキュメントのクイックスタートガイドがありますので、初めての方はそちらをひととおり対応してみるのがよいです。それで大まかな実装は理解できると思います。まあ、私もそれ以上のことはやっていないんですけれど。
所感
まだまだ GCP に慣れていないことと、そもそも世の中にこの構成のチュートリアルや記事が見当たらないので、けっこう時間がかかってしまいました。というか、あればこんな記事かかないんですけれどね。おかげさまで、いくつかパータンが違う構成で Cloud Run にデプロイすることができたので、大まかな対応方法は理解できてきました。
まあ、これが実際に仕事に結びつくはわからないですが、少なくとも Cloud Run まわりのついての知識は平均値を超えれた気がします。現時点では。それにこういった知識は純粋にフロントエンドエンジニアとしても、サーバサイドエンジニアとやりとりする上でスムーズにコミュニケーションをとりやすくなったという点で役に立つかと思います。たぶんですけれど。
そんなこんなで理解できてくると楽しくなってくるものです。このままインフラエンジニアへの転向はありえるのか、フルスタックエンジニアへの道を目指すのか、はたまたこの業界をやめて野菜を育てはじめるのかは私自身にも分かりませんが、とりあえず楽しめるだけ楽しんでいければと思いっています。
Zenn に記事を書くのは初めてですが、私は記事中(というか主に最初と最後)に軽くポエミーさを織り交ぜる手法なのでちょっとどうしようかと思いつつ、基本は技術記事しかかかないので Tech カテゴリで投稿しました。また記事を書くとしてもそんな感じになるかと。それでは、また。
Discussion