Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング

Cloud Run と Firebase Hosting を組み合わせれば、動的コンテンツを生成して配信できるほか、REST API をマイクロサービスとしてビルドすることもできます。

Cloud Run を使用すると、コンテナ イメージにパッケージ化されたアプリケーションをデプロイできます。その後、Firebase Hosting を使用して HTTPS リクエストをリダイレクトすることで、リクエストに応じてコンテナ化アプリをトリガーできます。

  • Cloud Run は Go、Node.js、Python、Java などの複数の言語をサポートしているため、プログラミング言語とフレームワークを柔軟に選ぶことができます。
  • 受信リクエストに合わせて、コンテナ イメージが自動的に水平方向にスケールされます。需要が減少した場合はスケールダウンします。
  • 料金は、リクエスト処理中に使用した CPU、メモリ、ネットワークに対してのみ発生します。

Cloud Run と Firebase Hosting の統合のユースケースとサンプルについては、サーバーレスの概要をご覧ください。


このガイドでは、以下の方法について説明します。

  1. シンプルな Hello World アプリケーションを記述する
  2. アプリをコンテナ化して Container Registry にアップロードする
  3. コンテナ イメージを Cloud Run にデプロイする
  4. コンテナ化されたアプリに Hosting リクエストをリダイレクトする

動的コンテンツの配信パフォーマンスを向上させるために、必要に応じてキャッシュ設定を調整できます。

始める前に

Cloud Run を使用する前に、Cloud 請求先アカウントの設定、Cloud Run API の有効化、gcloud コマンドライン ツールのインストールなど、いくつかの準備を行う必要があります。

プロジェクトの請求先を設定する

Cloud Run では無料の使用量割り当てが提供されていますが、それでも Cloud Run を使用または試用するには Firebase プロジェクトに関連付けられた Cloud 請求先アカウントが必要です。

API を有効にして SDK をインストールする

  1. Google API Console で Cloud Run API を有効にします。

    1. Google API Console で Cloud Run API ページを開きます。

    2. プロンプトが表示されたら、Firebase プロジェクトを選択します。

    3. Cloud Run API ページで [有効にする] をクリックします。

  2. Cloud SDK をインストールして初期化します。

  3. 正しいプロジェクトに gcloud ツールが構成されていることを確認します。

    gcloud config list

ステップ 1: サンプル アプリケーションを記述する

Cloud Run は、以下のサンプルに示す言語以外にも多くの言語をサポートしています。

Go

  1. helloworld-go という名前の新しいディレクトリを作成し、そのディレクトリに移動します。

    mkdir helloworld-go
    cd helloworld-go
  2. helloworld.go という名前の新しいファイルを作成し、次のコードを追加します。

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    func handler(w http.ResponseWriter, r *http.Request) {
    	log.Print("helloworld: received a request")
    	target := os.Getenv("TARGET")
    	if target == "" {
    		target = "World"
    	}
    	fmt.Fprintf(w, "Hello %s!\n", target)
    }
    
    func main() {
    	log.Print("helloworld: starting server...")
    
    	http.HandleFunc("/", handler)
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    
    	log.Printf("helloworld: listening on port %s", port)
    	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
    }
    

    このコードは、PORT 環境変数で定義されたポートをリッスンする基本的なウェブサーバーを作成します。

これでアプリは完成しました。このアプリをコンテナ化し、Container Registry にアップロードします。

Node.js

  1. helloworld-nodejs という名前の新しいディレクトリを作成し、そのディレクトリに移動します。

    mkdir helloworld-nodejs
    cd helloworld-nodejs
  2. package.json ファイルを作成し、次の内容を追加します。

    {
      "name": "knative-serving-helloworld",
      "version": "1.0.0",
      "description": "Simple hello world sample in Node",
      "main": "index.js",
      "scripts": {
        "start": "node index.js"
      },
      "author": "",
      "license": "Apache-2.0",
      "dependencies": {
        "express": "^4.18.2"
      }
    }
    
  3. index.js という名前の新しいファイルを作成し、次のコードを追加します。

    const express = require('express');
    const app = express();
    
    app.get('/', (req, res) => {
      console.log('Hello world received a request.');
    
      const target = process.env.TARGET || 'World';
      res.send(`Hello ${target}!\n`);
    });
    
    const port = process.env.PORT || 8080;
    app.listen(port, () => {
      console.log('Hello world listening on port', port);
    });
    

    このコードは、PORT 環境変数で定義されたポートをリッスンする基本的なウェブサーバーを作成します。

これでアプリは完成しました。このアプリをコンテナ化し、Container Registry にアップロードします。

Python

  1. helloworld-python という名前の新しいディレクトリを作成し、そのディレクトリに移動します。

    mkdir helloworld-python
    cd helloworld-python
  2. app.py という名前の新しいファイルを作成し、次のコードを追加します。

    import os
    
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def hello_world():
        target = os.environ.get('TARGET', 'World')
        return 'Hello {}!\n'.format(target)
    
    if __name__ == "__main__":
        app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
    

    このコードは、PORT 環境変数で定義されたポートをリッスンする基本的なウェブサーバーを作成します。

これでアプリは完成しました。このアプリをコンテナ化し、Container Registry にアップロードします。

Java

  1. Java SE 8 以降の JDKCURL をインストールします。

    これは、次のステップで新しいウェブ プロジェクトを作成するためだけに行います。後述する Dockerfile により、すべての依存関係がコンテナに読み込まれます。

  2. コンソールから cURL を使用して新しい空のウェブ プロジェクトを作成し、コマンドを解凍します。

    curl https://start.spring.io/starter.zip \
        -d dependencies=web \
        -d name=helloworld \
        -d artifactId=helloworld \
        -o helloworld.zip
    unzip helloworld.zip

    これで SpringBoot プロジェクトが作成されます。

  3. src/main/java/com/example/helloworld/HelloworldApplication.java にある SpringBootApplication クラスで、/ マッピングを処理する @RestController と、TARGET 環境変数を提供する @Value フィールドを追加します。

    package com.example.helloworld;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @SpringBootApplication
    public class HelloworldApplication {
    
      @Value("${TARGET:World}")
      String target;
    
      @RestController
      class HelloworldController {
        @GetMapping("/")
        String hello() {
          return "Hello " + target + "!";
        }
      }
    
      public static void main(String[] args) {
        SpringApplication.run(HelloworldApplication.class, args);
      }
    }
    

    このコードは、PORT 環境変数で定義されたポートをリッスンする基本的なウェブサーバーを作成します。

これでアプリは完成しました。このアプリをコンテナ化し、Container Registry にアップロードします。

ステップ 2: アプリをコンテナ化して Container Registry にアップロードする

  1. ソースファイルと同じディレクトリに Dockerfile という名前の新しいファイルを作成することで、サンプルアプリをコンテナ化します。次の内容をファイルにコピーします。

    Go

    # Use the official Golang image to create a build artifact.
    # This is based on Debian and sets the GOPATH to /go.
    FROM golang:latest as builder
    
    ARG TARGETOS
    ARG TARGETARCH
    
    # Create and change to the app directory.
    WORKDIR /app
    
    # Retrieve application dependencies using go modules.
    # Allows container builds to reuse downloaded dependencies.
    COPY go.* ./
    RUN go mod download
    
    # Copy local code to the container image.
    COPY . ./
    
    # Build the binary.
    # -mod=readonly ensures immutable go.mod and go.sum in container builds.
    RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -mod=readonly -v -o server
    
    # Use the official Alpine image for a lean production container.
    # https://hub.docker.com/_/alpine
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM alpine:3
    RUN apk add --no-cache ca-certificates
    
    # Copy the binary to the production image from the builder stage.
    COPY --from=builder /app/server /server
    
    # Run the web service on container startup.
    CMD ["/server"]
    

    Node.js

    # Use the official lightweight Node.js 12 image.
    # https://hub.docker.com/_/node
    FROM node:12-slim
    
    # Create and change to the app directory.
    WORKDIR /usr/src/app
    
    # Copy application dependency manifests to the container image.
    # A wildcard is used to ensure both package.json AND package-lock.json are copied.
    # Copying this separately prevents re-running npm install on every code change.
    COPY package*.json ./
    
    # Install production dependencies.
    RUN npm install --only=production
    
    # Copy local code to the container image.
    COPY . ./
    
    # Run the web service on container startup.
    CMD [ "npm", "start" ]
    

    Python

    # Use the official lightweight Python image.
    # https://hub.docker.com/_/python
    FROM python:3.7-slim
    
    # Allow statements and log messages to immediately appear in the Knative logs
    ENV PYTHONUNBUFFERED True
    
    # Copy local code to the container image.
    ENV APP_HOME /app
    WORKDIR $APP_HOME
    COPY . ./
    
    # Install production dependencies.
    RUN pip install Flask gunicorn
    
    # Run the web service on container startup. Here we use the gunicorn
    # webserver, with one worker process and 8 threads.
    # For environments with multiple CPU cores, increase the number of workers
    # to be equal to the cores available.
    CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
    

    Java

    # Use the official maven/Java 8 image to create a build artifact: https://hub.docker.com/_/maven
    FROM maven:3.5-jdk-8-alpine as builder
    
    # Copy local code to the container image.
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    
    # Build a release artifact.
    RUN mvn package -DskipTests
    
    # Use the Official OpenJDK image for a lean production stage of our multi-stage build.
    # https://hub.docker.com/_/openjdk
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM openjdk:8-jre-alpine
    
    # Copy the jar to the production image from the builder stage.
    COPY --from=builder /app/target/helloworld-*.jar /helloworld.jar
    
    # Run the web service on container startup.
    CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/helloworld.jar"]
    

  2. Dockerfile を含むディレクトリから次のコマンドを実行し、Cloud Build を使用してコンテナ イメージをビルドします。

    gcloud builds submit --tag gcr.io/PROJECT_ID/helloworld

    ビルドが成功すると、イメージ名(gcr.io/PROJECT_ID/helloworld)を含む SUCCESS メッセージが表示されます。

これでコンテナ イメージが Container Registry に保存されました。このイメージは必要に応じて再利用できます。

Cloud Build の代わりに、Docker のローカルにインストールされたバージョンを使用してコンテナをローカルにビルドすることもできます。

ステップ 3: コンテナ イメージを Cloud Run にデプロイする

  1. 次のコマンドを使用してデプロイします。

    gcloud run deploy --image gcr.io/PROJECT_ID/helloworld

  2. プロンプトが表示されたら、次の操作を行います。

    • リージョンを選択します。例: us-central1
    • サービス名を確認します。例: helloworld
    • 未承認の呼び出しの許可を求めるメッセージに対して「Y」としてこれを許可します。

最高のパフォーマンスを得るには、次のリージョンを使用して、Cloud Run サービスを Hosting と同じ場所に配置します。

  • us-west1
  • us-central1
  • us-east1
  • europe-west1
  • asia-east1

Hosting から Cloud Run への書き換えは、次のリージョンでサポートされています

  • asia-east1
  • asia-east2
  • asia-northeast1
  • asia-northeast2
  • asia-northeast3
  • asia-south1
  • asia-south2
  • asia-southeast1
  • asia-southeast2
  • australia-southeast1
  • australia-southeast2
  • europe-central2
  • europe-north1
  • europe-southwest1
  • europe-west1
  • europe-west12
  • europe-west2
  • europe-west3
  • europe-west4
  • europe-west6
  • europe-west8
  • europe-west9
  • me-central1
  • me-west1
  • northamerica-northeast1
  • northamerica-northeast2
  • southamerica-east1
  • southamerica-west1
  • us-central1
  • us-east1
  • us-east4
  • us-east5
  • us-south1
  • us-west1
  • us-west2
  • us-west3
  • us-west4
  • us-west1
  • us-central1
  • us-east1
  • europe-west1
  • asia-east1
  1. デプロイが完了するまでしばらく待ちます。成功すると、コマンドラインにサービス URL が表示されます。次に例を示します。https://helloworld-RANDOM_HASH-us-central1.a.run.app

  2. ウェブブラウザでこのサービス URL を開き、デプロイしたコンテナにアクセスします。

次のステップでは、このコンテナ化されたアプリに Firebase Hosting URL からアクセスする方法を示します。これにより、Firebase でホストされているサイトの動的コンテンツを生成できます。

ステップ 4: コンテナ化されたアプリに Hosting リクエストをリダイレクトする

リライトルールを使用して、特定のパターンに一致するリクエストを単一の宛先に向けることができます。

次の例は、Hosting サイトのページ /helloworld からのすべてのリクエストを送信し、helloworld コンテナ インスタンスの起動と実行をトリガーする方法を示します。

  1. 以下のことを確認します。

    CLI のインストールと Hosting の初期化の詳細については、Hosting のスタートガイドをご覧ください。

  2. firebase.json ファイルを開きます。

  3. hosting セクションに次の rewrite 構成を追加します。

    "hosting": {
      // ...
    
      // Add the "rewrites" attribute within "hosting"
      "rewrites": [ {
        "source": "/helloworld",
        "run": {
          "serviceId": "helloworld",  // "service name" (from when you deployed the container image)
          "region": "us-central1",    // optional (if omitted, default is us-central1)
          "pinTag": true              // optional (see note below)
        }
      } ]
    }
    
  4. プロジェクト ディレクトリのルートから次のコマンドを実行して、ホスティング構成をサイトにデプロイします。

    firebase deploy --only hosting

次の URL でコンテナに接続できるようになりました。

  • Firebase のサブドメイン:
    PROJECT_ID.web.app/PROJECT_ID.firebaseapp.com/

  • 接続されたカスタム ドメイン:
    CUSTOM_DOMAIN/

リライトルールの詳細については、Hosting 構成ページをご覧ください。さまざまな Hosting 構成のレスポンスの優先順位についても記載されています。

ローカルでテストする

開発中に、コンテナ イメージをローカルで実行してテストできます。詳しい手順については、Cloud Run のドキュメントをご覧ください。

次のステップ