GCP: Cloud Run にデプロイする(TypeScript & Express.js の例)

GCP: Cloud Run にデプロイする(TypeScript & Express.js の例)

October 31, 2022

この記事では以下の手順を記載します。

  1. アプリを作成する
  2. Dockerfile を作成する
  3. Artifact Registry にリポジトリを作成する
  4. コンテナイメージをビルドする
  5. コンテナイメージを Artifact Registry に push する
  6. コンテナイメージを Artifact Registry から Cloud Run にデプロイする

(なおデプロイするアプリの例として TypeScript & Express.js を使用していますがここはほとんど関係ありません。)

まず前提として Cloud Run にデプロイする方法は複数あります。

大きく分けると、ソースコードから直接デプロイする方法と、事前にコンテナイメージを作成してからデプロイする方法です。

前置き:ソースコードから直接デプロイする方法 #

もっとも最近(といっても 2020 年末)に追加された方法です。

具体的には gcloud run deploy コマンドに –source オプションを指定することで実現できます。

この方法を利用すると、冒頭に記載した手順はすべて必要なくなり、ローカルで gcloud run deploy ... --source ... を実行するだけで完了します。

内部的には Buildpacks を用いてソースファイルからコンテナイメージを作成し、、、といったように、冒頭に記載した手順を裏側で自動実行してくれているようです。

Buildpacks の参考

次の記事が詳しいです。

実際にソースコードからデプロイを実行した後で GCP の Cloud Storage および Artifact Registry を確認してみると、自動で資産が追加されていることが確認できます。

ということで非常に簡略化された手順でデプロイが可能となったものの、自分で Dockerfile を定義してデプロイしたいケースも引き続き存在します。

そのような場合は、これから記載する手順に従ってデプロイします。

本文:コンテナイメージをビルドして Cloud Run にデプロイする #

1. アプリを作成する #

デプロイ対象の例として次の Express.js アプリ(node.js)を作成しました。

app.ts

import express, { Express, Request, Response } from 'express';

const app: Express = express();

const PORT = process.env['PORT'];
const NODE_ENV = process.env['NODE_ENV'];

app.listen(PORT, () => {
  console.log(`App listening on port ${PORT} with ${NODE_ENV} environment.`);
});

app.get('/', (_: Request, response: Response) => {
  response.send('Hello World!');
});

2. Dockerfile を作成する #

Dockerfile を作成しましょう。今回は次の内容にしました。

Dockerfile

# マルチステージビルド

###
# 依存関係のインストール
###
FROM node:18-bullseye-slim as deps-stage
WORKDIR /app
COPY ./package*.json ./
RUN npm ci --omit=dev --no-progress

###
# ビルド
###
FROM node:18-bullseye-slim as build-stage
WORKDIR /work
COPY ./package*.json ./
RUN npm ci --no-progress
COPY . .
RUN npm run build

###
# ランタイム
###
FROM node:18-bullseye-slim
WORKDIR /usr/src/app
COPY ./package*.json ./
COPY --from=deps-stage /app/node_modules ./node_modules
COPY --from=build-stage /work/dist ./dist
EXPOSE 8080
USER node
CMD [ "node", "./dist/app.js" ];

3. Artifact Registry にリポジトリを作成する #

先に進む前に GCP の Artifact Registry にリポジトリを作成しましょう。ここで作成したリポジトリに、これからビルドするコンテナイメージを登録する流れとなります。

GCP にログインし、Artifact Registry サービスのページを開いてください。

「CREATE REPOSITORY」を選択し、リポジトリを作成します。

Artifact Registry

補足

なお GCP 内の類似サービスとして Container Registry があり、こちらを利用して Cloud Run にデプロイすることも可能です(コンテナイメージを Artifact Registry に保存するか Container Registry に保存するかだけの違いであり、デプロイ作業の内容はほとんど変わりません)。

雑に言ってしまえば Container Registry は旧サービス、Artifact Registry は新サービスという位置付けになります。

そのため今後新規利用する場合は Artifact Registry を選択すれば大丈夫です。

4. コンテナイメージをビルドする #

IMAGE_NAME には任意の名前を設定してください。

IMAGE_NAME=example-app

docker build --no-cache ./ -t $IMAGE_NAME

作成したコンテナイメージをタグ(別名)付けします。後続作業で Artifact Registry に push するためには決まったルールに沿って命名されている必要があるためです。

HOST_NAME に設定する値は先の手順で Artifact Registry のどのリージョンにリポジトリを作成したかで変わります。下記は asia-northeast1 に作成した場合です。

PROJECT_ID には自身の GCP プロジェクト ID を設定します。

REPOSITORY_NAME には Artifact Registry に作成したリポジトリの名前を指定します。

IMAGE_NAME=example-app
HOST_NAME=asia-northeast1-docker.pkg.dev
PROJECT_ID=my-project
REPOSITORY_NAME=example-app-source

docker tag $IMAGE_NAME $HOST_NAME/$PROJECT_ID/$REPOSITORY_NAME/$IMAGE_NAME

あるいは上記のように作成・タグ付けと2ステップ踏まずとも、最初から push するための名称でコンテナイメージをビルドしても構いません。

HOST_NAME=asia-northeast1-docker.pkg.dev
PROJECT_ID=my-project
REPOSITORY_NAME=example-app-source
IMAGE_NAME=example-app

docker build --no-cache ./ -t $HOST_NAME/$PROJECT_ID/$REPOSITORY_NAME/$IMAGE_NAME

5. コンテナイメージを Artifact Registry に push する #

HOST_NAME=asia-northeast1-docker.pkg.dev
PROJECT_ID=my-project
REPOSITORY_NAME=example-app-source
IMAGE_NAME=example-app

docker push $HOST_NAME/$PROJECT_ID/$REPOSITORY_NAME/$IMAGE_NAME

初めて Artifact Registry に push する場合は次のエラーが出るはずです。push するには Artifact Registry を認証する必要があります。

denied: Permission "artifactregistry.repositories.uploadArtifacts" denied on resource

次のコマンドを実行します。

HOST_NAME=asia-northeast1-docker.pkg.dev

gcloud auth configure-docker $HOST_NAME

~/.docker/config.json に次の記述が追加されます。

{
  "credHelpers": {
    "asia-northeast1-docker.pkg.dev": "gcloud"
  }
}

その後再び push コマンドを実行すると今度は成功するはずです。

ここで Artifact Registry のリポジトリの中を開くと、push したイメージが格納されていることが確認できます。

6. コンテナイメージを Artifact Registry から Cloud Run にデプロイする #

いよいよ Cloud Run にデプロイします。

ここではコンソールで作業しますが、gcloud CLI を利用して作業することも可能です。

GCP の Cloud Run のページから「CREATE SERVICE」を実行します。

「Deploy one revision from an existing container image」で Artifact Registry に格納されているコンテナイメージを選択します。

その他の値は任意です。下記は設定後の例です。

Cloud Run

最後に「CREATE」を実行するとデプロイが始まります。しばらく待つと URL が発行されます。

URL の例は次のようなものです。

https://example-app-znjzrhj67a-od.a.run.app/

この URL にアクセスすると、アプリからレスポンスが返ってくるはずです。

今回、アプリのコードを次の通りにしていたため、画面には「Hello World!」が表示されました。

app.get('/', (_: Request, response: Response) => {
  response.send('Hello World!');
});

備考:Cloud Run でハマったこと #

以下はまだ Cloud Run がわかっていなかった頃の備忘録です。

docker-compose.yml は使用できない #

ローカルで docker-compose を使って開発して、そのまま Cloud Run にデプロイしたらエラーになる。

https://teratail.com/questions/317073

Volume のマウントみたいなこともできない。

https://stackoverflow.com/questions/63782456/docker-compose-yml-in-google-cloud-run

docker-compose が存在しないと起動できないような作り方をしていたので、Dockerfile だけで起動するように見直したのち docker-compose は廃止した。その後デプロイしたらうまくいった。

Buildpacks で TypeScript を扱う際の考慮事項 #

ソースコードからデプロイを実行した場合、Buildpacks が TypeScript をビルドしてくれるわけではない。

https://stackoverflow.com/questions/71618132/how-to-deploy-typescript-project-on-cloud-run

Cloud Run works with Typescript. However, Buildpacks doesn’t know how to manage it.

そのため Cloud Run 上で以下のようなエラーになるはず。(ビルドされないことからファイルが存在しないため)

Error: Cannot find module '/workspace/dist/app.js'

ソースコードからデプロイを実行する場合は、事前に TypeScript から JavaScript へビルドしておく必要がある。

それが嫌な場合は、ソースコードからデプロイではなく、自分で Dockerfile を記述し、その中でビルドのフローを組み入れる必要がある(今回説明した例がこれ)。