見出し画像

HonoをGitHub Actionsを使用してCloud Runにデプロイする



どうも。
今日はGitHub Actionsを使ってCloud Runにデプロイしてみる方法についてお話ししたいと思います

本日はHonoをWebアプリケーションとして、GitHub Actionsを駆使してCloud Run上で稼働させることです。

HonoはEdgesで動くフレームワークです。
Edgesでの動作が前提と思われるかもしれませんが、実はNode.jsでも動かすことができます。

Edgesでの動作にはいくつかの制限があります。
たとえば、スクリプトのサイズに制限があったり、実行時間にも制限があります。

WebアプリケーションのバックエンドとしてAPIサーバーを構築したい場合、EdgesではなくCloud Runを使用することも十分に考えられます。
では本日はHonoをCloud Runで動かしてみましょう。

1.Honoを導入する

HonoのドキュメントにあるNode.jsの情報を参考に進めていきましょう。

$ npm create hono@latest hono-app
$ cd hono-app

Cloud Runにデプロイする前に、エラーを回避するために微修正を行います。
まず、package.jsonファイルにTypeScriptを追加しましょう。

$ npm i -D typescript

次に、package.jsonのscriptsに"start"と"build"を追加しましょう。

{
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "@hono/node-server": "^1.3.5",
    "hono": "^3.12.0",
    "typescript": "^5.3.3"
  },
  "devDependencies": {
    "tsx": "^3.12.2"
  }
}

次に、tsconfig.jsonを修正しましょう。

// tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}

これでアプリケーションをビルドし、TypeScriptをコンパイルできるようになりました。

2.portを変更する

Honoプロジェクトを生成すると、デフォルトでsrc/index.tsが作成されています。

// src/index.ts

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono! Cloud Run!')
})

const port = 3000
console.log(`Server is running on port ${port}`)

serve({
  fetch: app.fetch,
  port
})

デフォルトでは、ポート番号は3000ですが、Cloud Runでは8080ポートで動作する必要があるため、ポート番号を8080に変更しておきましょう。

// src/index.ts

const port = 3000 // 8080に変更する

3.Dockerを導入する

Cloud Runでは、Dockerイメージをビルドしてリビジョンを作成し、それをCloud Runにデプロイすることができます。
それでは、Dockerfileを作成していきましょう。

$ touch Dockerfile

Dockerfileの中身を書いていきましょう。

以下のドキュメントを参照して、独自のディレクトリ構造に最適なパスにカスタムします。

シンプルな内容であることが理解できます。

FROM node:20-alpine AS base

FROM base AS builder

RUN apk add --no-cache libc6-compat
WORKDIR /app

# Build
COPY package*json tsconfig.json ./
COPY ./src ./src
RUN npm ci && \
    npm run build && \
    npm prune --production

FROM base AS runner
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 hono

COPY --from=builder --chown=hono:nodejs /app/node_modules /app/node_modules
COPY --from=builder --chown=hono:nodejs /app/dist /app/dist
COPY --from=builder --chown=hono:nodejs /app/package.json /app/package.json

USER hono
EXPOSE 8080

CMD ["node", "/app/dist/index.js"]

4.DockerイメージをPushする

gcloud CLIを導入するために、以下のドキュメントを参照してインストール手順に従います。

CLIのインストールが完了したら、Google Cloudにアクセスして、新しいプロジェクトを作成します。
今回は、プロジェクト名を "development" とします。

プロジェクトが作成されたら、Dockerfileからイメージを作成してContainer RegistryにPushするために、以下のCLIコマンドを実行します。

$ gcloud builds submit --tag=asia.gcr.io/{projectId}/hono-app --project={projectId}

プロジェクトIDは、先程作成したプロジェクトから参照できます。

Dockerfileから作成したコンテナイメージがContainer Registryにプッシュされているはずです。

5.Cloud Runを設定する

サービスを作成するオプションを選択します。

コンテナイメージを選択すると、Container Registryに先程のプッシュされたコンテナイメージが存在します。最新の"latest"を選択します。

サービス名は任意に設定、リージョンは東京を選択します。
本番環境では認証設定が必要だと思いますが、今回は割愛します。
サービスを作成すると、リビジョンのバージョンが反映されます。

6.Cloud Runへのデプロイを確認する

反映が成功すると、Cloud RunのURLが発行されます。

URLにアクセスして、デプロイが正常に行われていることを確認しました。

7.変更を反映する

コードを変更して、変更内容を再度反映させるには以下のステップを実行します。

// src/index.ts

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono! Hello Cloud Run!') // ここを適当に変更する
})

const port = 3000
console.log(`Server is running on port ${port}`)

serve({
  fetch: app.fetch,
  port
})

コードを変更した場合、次のステップで再度イメージをビルドしてContainer Registryにプッシュします。

$ gcloud builds submit --tag=asia.gcr.io/{projectId}/hono-app --project={projectId}

Cloud Runのコンソールで、「新しいリビジョンの編集とデプロイ」を選択します。

先ほどと同様に、最新のリビジョンを反映させます。

URLをチェックしてみます。
変更が正常に反映されていることが確認できました。

8.継続的にデプロイする

今回はCLIを使用してデプロイしました。
通常の開発環境では、GitHub ActionsやCloud Buildを使用して、Gitブランチの変更に応じてテストやデプロイを自動化することが一般的です。
そちらのフローも実施してみます。

9. サービスアカウントを作成する

まず、サービスアカウントを作成していきましょう。
GCPのIdentity and Access Management (I AM) にあるサービスアカウントにアクセスします。

サービスアカウントを作成します。

ロールには、以下の条件を選択します。

9-1. Container Registryのpushの際に付与するロール

● セキュリティ管理者(roles/iam.securityAdmin
● Project IAM 管理者(roles/resourcemanager.projectIamAdmin

9-2. Cloud RunのDeployの際に付与するロール

● サービスアカウントユーザー(roles/iam.serviceAccountUser
● Cloud Run 管理者(roles/run.admin)

9-3. Cloud Storageのロール

● ストレージのバケット オーナー(roles/storage.legacyBucketOwner)

ロールがサービスアカウントに付与されたら、サービスアカウントの作成は完了です。
次に、サービスアカウントのキーを生成してダウンロードします。
このキーは通常、 "サービスアカウント名.json" 形式のファイルとしてダウンロードされます。

$ cd DLしたキーの場所まで移動
$ cat DLしたキー.json | base64 | pbcopy

これでダウンロードしたキーのbase64変換コードをコピーすることができます。このあとのGithub Secretsの設定に使用します。

10. Github Secretsを設定する

GitHubの設定(Settings)からシークレット(Secrets)にアクセスします。

● GCP_PROJECT / プロジェクトのIDを設定する
● GCP_SA_KEY / 先程のサービスアカウントのキーをBase64変換したコードを設定する

これでSecretsの設定が完了しました。

11. Github Actionsをつくる

それでは、GitHub Actionsを設定していきましょう。

$ mkdir .github // .githubディレクトリを作成
$ cd .github
$ mkdir workflows // workflowsディレクトリを作成
$ cd workflows
$ touch {develop.yml,production.yml} // actionsのymlファイルを作成

GitHub Actionsのymlファイルの中身を書いていきます。

今回はサービスアカウントキーを使用しますが、推奨されている方法としては、Workload Identity連携を使って認証し、サービスアカウント情報をgcloudで取得するというGitHub Actionsのワークフローを構築することが推奨されています。

develop.ymlはdevelopブランチに、production.ymlはmainブランチに差分を取り込んだタイミングでGitHub Actionsが動くように事前にブランチを設定しておきます。

// develop.yml

name: develop

on:
  push:
    branches:
      - develop

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT }}
  GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
  SERVICE: hono-app
  REGION: asia-northeast1
  IMAGE: asia.gcr.io/${{ secrets.GCP_PROJECT }}/hono-app
  TAG: develop

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Cloud SDK
        uses: google-github-actions/auth@v1
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Docker Authorize
        run: gcloud auth configure-docker

      - name: Docker Build and Push
        run: |
          docker build \
            -f Dockerfile \
            --build-arg BUILD_ID=$BUILD_ID \
            -t ${{ env.IMAGE }}:${{ github.sha }} \
            -t ${{ env.IMAGE }}:latest .
      - name: Publish
        run: |
          docker push ${{ env.IMAGE }}:${{ github.sha }}
          docker push ${{ env.IMAGE }}

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy ${{ env.SERVICE }} \
            --image ${{ env.IMAGE }}:${{ github.sha }} \
            --project ${{ env.PROJECT_ID }} \
            --region ${{ env.REGION }} \
            --platform managed \
            --no-traffic \
            --tag ${{ env.TAG }}
// production.yml

name: production

on:
  push:
    branches:
      - main

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT }}
  GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
  SERVICE: hono-app
  REGION: asia-northeast1
  IMAGE: asia.gcr.io/${{ secrets.GCP_PROJECT }}/hono-app
  TAG: production

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Cloud SDK
        uses: google-github-actions/auth@v1
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Docker Authorize
        run: gcloud auth configure-docker

      - name: Docker Build and Push
        run: |
          docker build \
            -f Dockerfile \
            --build-arg BUILD_ID=$BUILD_ID \
            -t ${{ env.IMAGE }}:${{ github.sha }} \
            -t ${{ env.IMAGE }}:latest .
      - name: Publish
        run: |
          docker push ${{ env.IMAGE }}:${{ github.sha }}
          docker push ${{ env.IMAGE }}

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy ${{ env.SERVICE }} \
            --image ${{ env.IMAGE }}:${{ github.sha }} \
            --project ${{ env.PROJECT_ID }} \
            --region ${{ env.REGION }} \
            --platform managed \
            --no-traffic \
            --tag ${{ env.TAG }}

12. コードを更新して動作を確認する

// src/index.ts

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono! Cloud Run! Run!Run!Run!dev! dev! dev!') // ここを変更する
})

const port = 8080
console.log(`Server is running on port ${port}`)

serve({
  fetch: app.fetch,
  port
})

12-1. Actionsを確認する

コードを変更してmainブランチにプッシュしましょう。
GitHub Actionsが正常に動作することを確認できました。

12-2. Cloud Runを確認する

Cloud Runのリビジョンを確認し、デプロイが正常に実行されていることを確認できました。

13. タグ付きリビジョン

GitHub Actionsのymlファイルを確認します。

- name: develop
  run: |
    gcloud run deploy ${{ env.SERVICE }} \
      --image ${{ env.IMAGE }}:${{ github.sha }} \
      --project ${{ env.PROJECT_ID }} \
      --region ${{ env.REGION }} \
      --platform managed \
      --no-traffic \
      --tag ${{ env.TAG }} // dev(このTAGに注目)

Cloud Runでは、デプロイ時にタグを付与することができます。
このタグを使用することで、既存のトラフィック100%のリビジョンに影響を与えることなく、新しいタグ付きリビジョンをデプロイできます。
また、タグ付きリビジョンは独自のURLを持ちます。

この機能を利用することで、異なるバージョンのアプリケーションをトラフィックの切り替えなしでテストしたり、デプロイしたりすることが可能です。

タグが接頭辞としてURLに付与され、新しいURLが発行されたことを確認できました。

develop---のURLにアクセスしてみましょう。

develop---を削除したURLにアクセスしてみましょう。

このように、既存のトラフィック100%のバージョンに影響を与えずに、Cloud Runにデプロイし、変更内容を確認できました。
タグ付きリビジョンへのトラフィック移行により、本番testから本番productionへのスムーズな切り替えフローを実現できました。

Github ActionsとCloud Runの組み合わせですが、とても簡単に構築できて便利ですね。それでは。

この記事が気に入ったらサポートをしてみませんか?