🌩️

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

2020/10/05に公開

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

過去に社内環境の 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

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

こちらの対応が終わったら、コンテナを停止しても大丈夫です。
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 の新しいプロジェクトの作成ページにアクセスします。

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

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

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

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

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 が表示されますので、そこからアクセスして確認してみましょう。
以下の画面が表示されます。

strapi の起動確認画面

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

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

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

strapi のダッシュボード画面

これで完了です。

その後の対応

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

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