Cloud Runから内部ネットワーク経由で別のCloud Runサービスを呼び出す

記事タイトルとURLをコピーする

G-gen の佐々木です。当記事では同一プロジェクトにある Cloud Run 間の通信をプライベートな通信経路で行う方法を解説します。

Cloud Run から Cloud Run へのアクセス方法

パブリックアクセス

マイクロサービスなどのユースケースにおいて、Cloud Run から別の Cloud Run にアクセスするケースがあります。この場合、アクセスされる側の Cloud Run にて、設定項目である「上り(内向き)の制御」を「すべて」に設定することで、他の Cloud Run からアクセスすることができます。

上り(内向き)の制御を「すべて」に設定する

しかし、このような設定の場合、たとえアクセスされる側の Cloud Run がインターネットに公開したくないサービスであっても、インターネットから到達できてしまう状態となっています。IAM による認証を必須にすることでアクセスをブロックすることもできますが、なるべくはアクセス元を制限し、ネットワークレイヤで制限をかけたほうが、よりセキュアです。

パブリックアクセスが可能な Cloud Run

プライベートアクセス

Cloud Run では、「上り(内向き)の制御」を「内部」に設定することで、同一プロジェクトの VPC を経由した通信のみが Cloud Run にアクセスできるように設定することができます。

上り(内向き)の制御を「内部」に設定する

「内部」に設定された Cloud Run に対して別の Cloud Run からアクセスする場合、通信は VPC を経由しなければならないため、アクセス元の Cloud Run は Direct VPC Egressもしくはサーバーレス VPC アクセス(両者の比較についてはこちらの記事を参照)を使用して VPC にアクセスできるようにします。

このとき、Cloud Run から接続される VPC 内のサブネットで限定公開の Google アクセスを有効にしておく必要があります。

VPC を経由した場合のみ呼び出し可能な Cloud Run

Cloud Run の呼び出しに IAM 認証を必須にすることで、VPC を経由し、かつ IAM で許可された場合のみ Cloud Run にアクセスできるようになります。

IAM で許可されているアクセス元が VPC を経由した場合のみ呼び出し可能な Cloud Run

なお、上記の方法は、Cloud Run が両方とも同一のプロジェクトに存在する場合のみ利用可能です。別々のプロジェクトにある Cloud Run 同士の通信を「内部」で行いたい場合、VPC Service Controls や Private Service Connect を使用する必要があります(参考)。

構成図

当記事では Direct VPC Egress を使用することで、フロントエンドとして作成した Cloud Run サービスからバックエンドの Cloud Run サービスに対して、VPC を経由したプライベートな通信経路で接続を行います。

バックエンドの Cloud Run では、上り(内向き)の通信を「内部」のみ許可するように設定することで、インターネットからのアクセスを防ぎます。また、認証を必須にすることで、VPC からの通信であっても IAM で許可されたアクセス元のみがサービスを利用できるようにします。

VPC を経由してバックエンドのサービスに内部アクセスする Cloud Run

事前準備

シェル変数の設定

当記事では gcloud コマンドを使用して各種リソースを作成していきます。

コマンド内で何度か使用する値は、以下のようにシェル変数として設定しておきます。

PROJECT 変数の値にはリソースを作成するプロジェクトを、LOCATION 変数の値にはリージョンを設定してください。残りの変数は各種リソースの名前を指定する際に使用します。

PROJECT=my-project
LOCATION=asia-northeast1
NETWORK=my-vpc  # VPCの名前
SUBNET=my-subnet  # サブネットの名前
REPO=my-repo  # Artifact Registory リポジトリの名前
RUN_SA=run-frontend  # Cloud Run(フロントエンド)に紐付けるサービスアカウントの名前

Artifact Registry リポジトリの作成

Cloud Run 用のコンテナイメージを格納するための Artifact Registory リポジトリを作成します。

# Artifact Registry リポジトリを作成する
$ gcloud artifacts repositories create ${REPO} \
    --repository-format=docker \
    --project=${PROJECT} \
    --location=${LOCATION}

バックエンドサービスの作成

まずはアクセス対象となるバックエンドの Cloud Run サービスを作成していきます。

バックエンドのサービスは、フロントエンドの Cloud Run のみが呼び出すことができる認証付きの API 機能を想定します。サービスに対してインターネットからアクセスできないようにし、また IAM による認証を必須とします。

使用するコード(Go)

当記事では Go を使用してサービスを実装していきます。

フロントエンドからのリクエストに対して、ステータスコード 200 と「Hello from backend!」というメッセージを JSON で返却するシンプルな内容となっています。

// backend/main.go
package main
  
import (
    "encoding/json"
    "log"
    "net/http"
)
  
type Response struct {
    Status  int
    Message string
}
  
func main() {
    log.Print("Service is running on port 8080")
  
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        body := Response{
            http.StatusOK,
            "Hello from backend!",
        }
        res, err := json.Marshal(body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
  
        w.Header().Set("Content-Type", "application/json")
        w.Write(res)
    })
  
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
  

コンテナイメージのビルド

Cloud Build を使用してコンテナイメージをビルドし、Artifact Registory リポジトリにプッシュします。

当記事では Buildpacks を使用することで、Dockerfile を用意せずにコンテナイメージを作成していきます。

# Cloud Build でコンテナイメージをビルドする
$ gcloud builds submit --pack image=${LOCATION}-docker.pkg.dev/${PROJECT}/${REPO}/run-backend

Cloud Run デプロイ用 YAML ファイルの作成

当記事では YAML ファイルを使用して Cloud Run サービスを作成していきます。

# backend.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: run-backend  # サービスの名前
  annotations:
    run.googleapis.com/ingress: internal  # 上り(内向き)トラフィックを内部に制限
spec:
  template:
    spec:
      containers:
      - image: asia-northeast1-docker.pkg.dev/${プロジェクトID}/${リポジトリ名}/run-backend  # コンテナイメージの URL
  

上記の YAML ファイルで重要な箇所、および変更が必要な箇所を以下のようになります。

項目 説明
metadata.annotations.
run.googleapis.com/ingress
internal Cloud Run がインターネットからのトラフィックを受信できるようにする。
spec.template.spec.containers[0].image バックエンド用のコンテナイメージ 以下のコマンドで確認できる。
$ echo asia-northeast1-docker.pkg.dev/${PROJECT}/${REPO}/run-backend

Cloud Run サービスの作成

gcloud コマンドで YAML ファイルを使用して Cloud Run を作成します。

YAML ファイルを使用する場合、サービスを新規に作成する場合であっても gcloud run service replace コマンドを使用します。

# YAML ファイルを使用して Cloud Run サービスをデプロイする
$ gcloud run services replace backend.yaml \
    --project=${PROJECT} \
    --region=${LOCATION}

上り(内向き)が「内部」かつ IAM 認証が必須のバックエンドサービス

VPC とサブネットの作成

後ほど作成するフロントエンドの Cloud Run から接続するための VPC とサブネットを作成します。バックエンドのサービスへの通信は、限定公開の Google アクセスを有効にしたサブネットを経由することで、インターネットではなく内部からのアクセスとなります。

当記事では Direct VPC Egress を使用して VPC に接続します。Direct VPC Egress はフロントエンドの Cloud Run 作成時に同時に作成します。

# VPC の作成
$ gcloud compute networks create ${NETWORK} \
    --project=${PROJECT} \
    --subnet-mode=custom

サブネット作成時に--enable-private-ip-google-access フラグを指定することで、限定公開の Google アクセスを有効にします。また、Cloud Run の制限事項として、通信の宛先となるサブネットの IP アドレス範囲が 192.168.1.0/24 の場合、通信することができない点には注意が必要です(参考)。

# サブネットの作成
$ gcloud compute networks subnets create ${SUBNET} \
    --project=${PROJECT} \
    --network=${NETWORK} \
    --range=192.168.101.0/24 \
    --region=${LOCATION} \
    --enable-private-ip-google-access

限定公開の Google アクセスを有効化したサブネット

フロントエンドサービスの作成

バックエンドの Cloud Run サービスにアクセスするフロントエンドサービスを作成していきます。

フロントエンドのサービスは、インターネットから不特定のユーザーにアクセスされる Web サービスを想定します。そのためサービスに対しては認証なしでアクセスできるようにします。

サービスアカウントの作成

バックエンドの Cloud Run にアクセスするための権限を付与するためのサービスアカウントを作成します。

# Cloud Run(フロントエンド)用のサービスアカウントを作成する
$ gcloud iam service-accounts create ${RUN_SA} --project=${PROJECT}

作成したサービスアカウントに、バックエンドの Cloud Run サービスを呼び出すための Cloud Run 起動元(roles/run.invoker) 権限を付与します。

# Cloud Run(バックエンド)を呼び出す権限を付与する
$ gcloud run services add-iam-policy-binding run-backend \
    --role="roles/run.invoker" \
    --member="serviceAccount:${RUN_SA}@${PROJECT}.iam.gserviceaccount.com" \
    --project=${PROJECT} \
    --region=${LOCATION}

使用するコード(Go)

フロントエンドのサービスは、/api にアクセスすることでバックエンドのサービスにリクエストを送信し、その後バックエンドから返ってきたメッセージを表示するように実装します。

また、/ にアクセスした場合は、フロントエンドのサービスからそのまま「Hello from frontend!」というメッセージを返します。

// frontend/main.go
package main
  
import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
  
    "google.golang.org/api/idtoken"
)
  
type Response struct {
    Status  int    `json:"status"`
    Message string `json:"message"`
}
  
func main() {
    log.Print("Service is running on port 8080")
  
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from frontend!")
    })
  
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
  
        targetUrl := os.Getenv("BACKEND_URL")
        audience := targetUrl + "/"
  
        resp, err := makeGetRequest(w, targetUrl, audience)
        if err != nil {
            fmt.Fprintf(w, "Error: %v", err)
            return
        }
        defer resp.Body.Close()
  
        if resp.StatusCode != http.StatusOK {
            fmt.Fprintf(w, "Error: %s", resp.Status)
            return
        }
    })
  
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
  
// バックエンドの Cloud Run サービスに対してリクエストを送信する
func makeGetRequest(w io.Writer, targetURL string, audience string) (*http.Response, error) {
    ctx := context.Background()
  
    client, err := idtoken.NewClient(ctx, audience)
    if err != nil {
        return nil, fmt.Errorf("idtoken.NewClient: %w", err)
    }
  
    resp, err := client.Get(targetURL)
    if err != nil {
        return nil, fmt.Errorf("client.Get: %w", err)
    }
    defer resp.Body.Close()
    if _, err := io.Copy(w, resp.Body); err != nil {
        return nil, fmt.Errorf("io.Copy: %w", err)
    }
  
    return resp, nil
}
  

バックエンドの Cloud Run サービスは IAM 認証を必須としているため、Google APIs のクライアントライブラリから、認証用のパッケージである idtoken を使用しています。

Cloud Run で実行する場合、Cloud Run に紐付けられたサービスアカウントの認証情報が自動で使用されます。

コンテナイメージのビルド

バックエンドサービスと同様の手順でコンテナイメージをビルドし、Artifact Registory リポジトリにプッシュします。

# Cloud Build でコンテナイメージをビルドする
$ gcloud builds submit --pack image=${LOCATION}-docker.pkg.dev/${PROJECT}/${REPO}/run-frontend

Cloud Run デプロイ用 YAML ファイルの作成

フロントエンド用の Cloud Run を作成するための YAML ファイルは以下のように記述します。

# frontend.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: run-frontend  # サービスの名前
  annotations:
    run.googleapis.com/ingress: all  # インターネットからの上り(内向き)トラフィックを許可
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/vpc-access-egress: all-traffic  # 全ての下り(外向き)トラフィックを Direct VPC Egress 経由で送信
        run.googleapis.com/network-interfaces: '[{"network":"${作成したVPCの名前}","subnetwork":"${作成したサブネットの名前}"}]'
    spec:
      serviceAccountName: ${作成したサービスアカウントのメールアドレス}
      containers:
      - image: asia-northeast1-docker.pkg.dev/${プロジェクトID}/${リポジトリ名}/run-frontend  # コンテナイメージの URL
        env:
          - name: BACKEND_URL
            value: ${Cloud Run サービス(バックエンド)の URL}
  

上記の YAML ファイルで重要な箇所、および変更が必要な箇所を以下のようになります。

項目 説明
metadata.annotations.
run.googleapis.com/ingress
all Cloud Run がインターネットからのトラフィックを受信できるようにする。
spec.template.metadata.annotations.
run.googleapis.com/vpc-access-egress
all-traffic Cloud Run から送信される全てのトラフィックを VPC 経由で送信する。
spec.template.metadata.annotations.
run.googleapis.com/network-interfaces
Direct VPC Egress で接続する VPC とサブネットの名前 以下のコマンドの出力をそのまま記載する。
$ echo \'[{\"network\":\"${NETWORK}\"\,\"subnetwork\":\"${SUBNET}\"}]\'
spec.template.spec.serviceAccountName 作成したサービスアカウントの名前 以下のコマンドで確認できる。
$ echo ${RUN_SA}@${PROJECT}.iam.gserviceaccount.com
spec.template.spec.containers[0].image フロントエンド用のコンテナイメージ 以下のコマンドで確認できる。
$ echo ${LOCATION}-docker.pkg.dev/${PROJECT}/${REPO}/run-frontend
spec.template.spec.containers[0].env[0].value バックエンドの Cloud Run サービスの URL 以下のコマンドで確認できる。
$ gcloud run services describe run-backend --project=${PROJECT} --region=${LOCATION} --format='value(status.url)'

Cloud Run サービスの作成

gcloud コマンドで YAML ファイルから Cloud Run サービスを作成します。

# YAML ファイルを使用して Cloud Run サービスをデプロイする
$ gcloud run services replace frontend.yaml \
    --project=${PROJECT} \
    --region=${LOCATION}

YAML ファイルからのデプロイでは metadata.annotations.run.googleapis.com/ingressall を設定しても「未認証の呼び出しを許可」の設定はされないので、別途 gcloud コマンドを使用して allUsers に対して Cloud Run 起動元 のロールを付与します。

# 未認証で Cloud Run サービスを呼び出せるようにする
$ gcloud run services add-iam-policy-binding run-frontend \
    --role="roles/run.invoker" \
    --member="allUsers" \
    --project=${PROJECT} \
    --region=${LOCATION}

これでインターネット上から認証なしでフロントエンドのサービスにアクセスすることができます。

疎通の確認

以下のコマンドでフロントエンドのサービスの URL を確認し、ブラウザや curl コマンドなどでアクセスします。

# Cloud Run サービス(フロントエンド)の URL を確認する
$ gcloud run services describe run-frontend \
    --project=${PROJECT} \
    --region=${LOCATION} \
    --format='value(status.url)'

サービスの / にアクセスした場合、フロントエンドのサービスから返ってきた「Hello from frontend!」というメッセージが表示されます。

# フロントエンドのサービスからメッセージが返る
$ curl https://run-frontend-ai4xxxxxxx-an.a.run.app/
Hello from frontend!

サービスの /api パスにアクセスすると、IAM で許可された内部からのアクセスのみ可能なバックエンドサービスのレスポンスが表示されます。

# フロントエンドのサービスからバックエンドにアクセスする
$ curl https://run-frontend-ai4xxxxxxx-an.a.run.app/api
{"Status":200,"Message":"Hello from backend!"}

佐々木 駿太 (記事一覧)

G-gen最北端、北海道在住のクラウドソリューション部エンジニア

2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2024に選出。好きなGoogle CloudプロダクトはCloud Run。

趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。