Cloud Run(フルマネージド サーバレス)に strapi(ヘッドレス CMS)をデプロイする

公開:2020/10/05
更新:2020/10/06
21 min読了の目安(約19000字TECH技術記事

私は基本的にはフロントエンドの領域を飯の種としていますが、それでも最近はバックエンドとかインフラとかについても理解がないと、中途半端な技術力しか持たない私では生き抜いていけないと感じるわけです。まあ、それはある意味は言い訳でただただやりたくなったというのが正直なところです。

過去に社内環境の VPS などをこねくり回していた時期はあるのですが、時代とはかくも無残なものでそんなものは過去の栄光にすらならないわけでして(まあ、さして昔でもないのでその頃から AWS とかありはしましたが)。その上、WordPress などの CMS もなぜか奴から避けて通るように仕事では構築することもありませんでした。

さてさて、話を戻しまして表題のとおりに Cloud Runstrapi をデプロイしてみようと思いました。なぜにこの組み合わせかといいますと、これは別に求められた結果でもなんでなく、なんとなく私自身が好みっぽいものを選んだ結果です。ちなみに、もしフロントエンドの環境を加えるのであれば躊躇なく 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 を使いました。

作業の前提

作業手順

項目としては以下の手順になります。

  1. Docker を使って strapi のインストール
  2. GCP 用の Node.js モジュールの追加
  3. 無視するファイルの設定
  4. GCP プロジェクトの作成とサービスの有効化
  5. シェル変数の用意
  6. 環境変数の設定
  7. コンテナの設定
  8. Cloud SQL インスタンスの作成と設定
  9. Cloud Storage にバケットの作成と設定
  10. ビルドとデプロイ

それでは、それぞれの詳細を説明していきます。

1. Docker を使って strapi のインストール

これは公式ドキュメントに記載がありますので、基本的にはそのままです。

1-1. プロジェクト名の設定

はじめにシェル変数にプロジェクト名を定義しておきます。
任意に決めて大丈夫です。今回は strapi-project としておきます。

$ PROJECT_NAME=strapi-project

1-2. プロジェクトフォルダの作成

プロジェクト名でプロジェクトフォルダの作成をします。

$ mkdir $PROJECT_NAME
  cd $PROJECT_NAME

1-3. docker-compose.yaml の作成

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/strapipostgres のコンテナイメージを取得します。

$ docker-compose pull

1-5. コンテナの構築と起動

コンテナイメージから strapit と PostgreSQL のコンテナの構築と起動をします。

$ docker-compose up

個人的には不要なので外していますが、デーモン化したければ公式ドキュメントどおりに -d オプションをつけてください。
コンテナのログを表示できるので、デーモン化しない方がオススメではあります。ターミナルを複数起動する必要はありますが。

初回はコンテナの構築にけっこう時間がかかると思います。
strapi_1 | - Installing dependencies: の状態で固まっているようにも思えるかもですが、気長にお待ちください。

1-6. strapi の起動を確認

ローカル開発環境用の対応はこれで完了です。
http://localhost:1337/admin にアクセスして strapi が正常に起動しているかを確認します。以下の画面が表示されていれば問題ないです。

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

コンテナの起動中にしか実行できませんので、もし停止している場合は $ docker-compose up を実行してください。

こちらの対応が終わったら、コンテナを停止しても大丈夫です。
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 プロジェクトの作成と設定

コンテナの起動時に別のターミナルを開いて作業している場合は、始めに設定した $PROJECT_NAME の定義が消えているかと思います。その場合は再度シェル変数の定義をします。

$ PROJECT_NAME=strapi-project

4-1-1. GCP プロジェクトの作成

以下の GCP の新しいプロジェクトの作成ページにアクセスします。

https://console.cloud.google.com/projectcreate

プロジェクト名に $PROJECT_NAME の値(今回は strapi-project)を入力して、プロジェクトを作成します。

GCP のプロジェクト作成画面

gcloud CLI からも作成できるのですが、プロジェクト ID を自動生成したかったので管理パネルから作成しました。
もし CLI から自動生成できる方法があれば教えてもらえると助かります。

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 になっていたら問題ないです。

ターミナルでの gcloud CLI のデフォルトの表示

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 プロジェクトのシークレットに登録して、そのデータから環境変数ファイルを生成することで環境変数を使用できるようします。

これは以前に対応した Django on Cloud Run のチュートリアルを参考にしています。
python なので記述は全然違いますが、やってることは大体一緒です。
(全体的に参考にしていますが、主にこの部分が特徴的に感じました)

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. バケットに閲覧/書き込み権限を追加

gcloud CLI で対応できず困っていましたが、記事「Google Cloud Storage (GCS)にIAMカスタムロールを設定する」を読んで解決することができました。

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

ローカル開発環境には設定の必要がないので空ですが、production 環境用の設定を上書きするベースとして必要となっています。

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

--allow-unauthenticated一般公開アクセスを許可するオプションですので、この時点で公開しないのであれば削除しておく必要があります。

デプロイに成功していれば URL が表示されますので、そこからアクセスして確認してみましょう。
以下の画面が表示されます。

strapi の起動確認画面

スペックを最低にしているため、PostgreSQL の最大同時接続数を超えてしまい strapi の起動に失敗してしまうかもしれません。その場合しばらく待ってから再度アクセスしてみてください。

また、価格は上がりますが Cloud SQL インスタンスのマシンスペックをアップグレードするのもよいかと思います。

次に admin/ ディレクトリにアクセスしてみると、ローカル開発環境時に確認できた管理者ユーザの作成画面がでてきます。

strapi の管理者ユーザの作成画面

問題がないか確認する為にそのまま管理者ユーザを作成してみます。
作成後に以下の画面が表示されていれば問題ないです。

strapi のダッシュボード画面

これで完了です。

このあとは strapi で API の実装おこなっていくのですが、GCP 上で起動しているとき(
production モード)ではコンテントタイプの変更ができないので、実装はローカル開発環境でおこなっていくことになります。(できたとしてもローカルやリポジトリと同期できないですし)

その後の対応

一応はデプロイまで持っていきましたが、実際に実装していく上ではまだまだ足りていないことがあるでしょう。ですので、軽くいくつかの対応について述べておきます。

1. Cloud Run の場合

1-1. バケットの CORS 構成

CORS(Cross-Origin Resource Sharing)により、API を通して strapi にファイルをアップロードすることができない状態です。必要があればバケットの CORS 構成を設定することで対応できます。

1-2. カスタムドメインのマッピング

カスタムドメインを使用したいことはよくあるかと思います。Cloud Run の比較的かんたんですので、こちらの公式ドキュメントを見ながら対応するとよいです。

バケットの CORS 構成を設定している場合は、合わせて変更が必要になるかと思います。

1-3. オートデプロイの実装

GitHub などに push した際にオートデプロイされる設定があるとよいかもしれません。GitHub などの Webhook を利用して、Cloud Build によるコンテナイメージのビルドとデプロイをするようにできます。

1-4. コールドスタートの対策

アクセスする際にレスポンスが遅いと感じるときはないでしょうか?Cloud Run の特徴として自動スケーリングによりインスタンスがゼロになるところがあります。だからこそコストパフォーマンスにすぐれいているところもあるのですが、これによる問題というか仕様としてコールドスタートが発生します。必要であればそれについて対策する必要があるかもしれません。

対策が必要な場合にでも、対応はそれぞれの状況によるかと思います。ですので、今回は言及しませんがコールドスタートについての記事はいくつか見受けられますので調べてみてください。そして、もしベストプラクティスがあれば教えてもらえると嬉しいです。

2. strapi の場合

場合もなにも構築しただけ何も実装していないですね。
手順中にも軽くは述べてはいますが公式ドキュメントのクイックスタートガイドがありますので、初めての方はそちらをひととおり対応してみるのがよいです。それで大まかな実装は理解できると思います。まあ、私もそれ以上のことはやっていないんですけれど。

所感

まだまだ GCP に慣れていないことと、そもそも世の中にこの構成のチュートリアルや記事が見当たらないので、けっこう時間がかかってしまいました。というか、あればこんな記事かかないんですけれどね。おかげさまで、いくつかパータンが違う構成で Cloud Run にデプロイすることができたので、大まかな対応方法は理解できてきました。

まあ、これが実際に仕事に結びつくはわからないですが、少なくとも Cloud Run まわりのついての知識は平均値を超えれた気がします。現時点では。それにこういった知識は純粋にフロントエンドエンジニアとしても、サーバサイドエンジニアとやりとりする上でスムーズにコミュニケーションをとりやすくなったという点で役に立つかと思います。たぶんですけれど。

そんなこんなで理解できてくると楽しくなってくるものです。このままインフラエンジニアへの転向はありえるのか、フルスタックエンジニアへの道を目指すのか、はたまたこの業界をやめて野菜を育てはじめるのかは私自身にも分かりませんが、とりあえず楽しめるだけ楽しんでいければと思いっています。

Zenn に記事を書くのは初めてですが、私は記事中(というか主に最初と最後)に軽くポエミーさを織り交ぜる手法なのでちょっとどうしようかと思いつつ、基本は技術記事しかかかないので Tech カテゴリで投稿しました。また記事を書くとしてもそんな感じになるかと。それでは、また。