Cloud Runで新規サービスを構築・運用するためにSREとして取り組んだこと

OGP

はじめに

こんにちは。メディアプラットフォーム本部 WEAR部 WEAR-SREの笹沢(@sasamuku)です。

ZOZOが新しく展開する「FAANS」というショップスタッフ向けアプリをクローズドβ版としてテスト運用しています。本アプリは、WEARと連携したコーディネート投稿や、その成果を可視化する機能などをショップスタッフの皆さんに提供するtoBのソリューションです。現在、正式リリースに向け開発を進めています。

そして、FAANSのAPIはCloud Runと呼ばれるサーバレスなコンテナ実行基盤で稼働しています。本記事では、FAANSの実行基盤としてCloud Runを選定した理由や、構築・運用するためにSREとして取り組んだことをご紹介します。

Cloud Runを選んだ理由

まず、クラウドサービスはGCPを選択しています。FAANSでは開発速度の向上と運用負荷の軽減のため、認証やメッセージング、Webホスティングの機能にFirebaseを採用することにしました。そのため、クラウドサービスとしてもGCPを選択することが開発やコスト管理の面で最も妥当な判断でした。

FAANSの実行基盤に求められる要件には、大きく以下のものがあります。そのため、これらを満たすサービスをGCPの中から選定しました。

  1. 管理が容易なサーバレスプラットフォームであること
  2. Goのバージョン1.16をサポートしていること

まず、「1.」を満たすサービスとして、Google App Engine, Cloud Functions, Cloud Run, GKEが挙げられました。さらに、2021年6月の選定時点で「2.」を満たせるものに絞ると、Cloud RunとGKEの2つが選択肢に残りました。

Cloud Runは一部の制約を満たせば、任意のプログラミング言語をサポートできます。また、オートスケールや従量課金、ミドルウェア管理が不要な点などのマネージドサービスとしての一般的な利点も備えています。

一方のGKEには、スケールやCI/CDの細やかな設定ができるという魅力がありました。しかし、リリースまでの期間やSREチームの規模を踏まえ、マニュフェストファイルなしで即座に利用開始できるフルマネージド版のCloud Runを選択することにしました。なお、以降で「Cloud Run」と呼称するものはフルマネージド版を指します。

実際にCloud Runを利用してみると、そのシンプルさに驚きました。サービスを作成しコンテナをデプロイするだけで、URLの発行と証明書取得が自動で行われ、ものの数分でHTTPS通信を開始できます。

しかし、Cloud Run単体ではWAFを導入できない、Datadog APMを設定できないなどの制約事項もありますので事前調査が大切です。

サービスを運用していくための取り組み

次に、Cloud Runでサービス運用していく上で行っている、SREとしての取り組みをいくつかご紹介します。Cloud Run特有の課題とその対応についても触れているので、Cloud Runでこれからサービスを公開したい方の参考になれば幸いです。

アーキテクチャ概観

アーキテクチャの概観は、一部検証フェーズの構成も含まれますが下図の通りです。

アーキテクチャ概観

アプリケーションは全てCloud Runで稼働しています。処理時間の長い一部のデータベース更新は、レスポンス時間短縮のためにCloud Tasksへオフロードしています。また、社内の別システムからのイベントを取得するために、Cloud Pub/Subを用意して疎結合になるよう連携しています。

IaCへの取り組み

FAANSではクラウドサービスにGCP、監視にDatadog、オンコール通知にPagerDutyを利用しており、それらのほぼ全てをTerraformで管理しています。これにより、共通の手続きで異なるサービスの構成を管理できるようにしています。その他にも、変更管理やコードレビューなどのIaCで一般的なメリットも享受しています。

Terraformの公式ドキュメントは簡潔で、すぐ実践できます。しかし、トラブルシューティング関連の記載が少なく、問題発生時にはこのドキュメントだけで対応することが難しいという一面もあります。その点に関しては、既に利用している第三者の情報を参考にして解決できることもあるので、本記事もそのような有益な情報になるよう、密かに期待しています。

Cloud Runにおいても、構築段階でうまくいかない場面が多々ありました。ここでは、抜粋したtfファイルを元に、特に注意しておくべき点をお伝えします。

resource "google_cloud_run_service" "default" {
  provider = google-beta # secret key を扱うため (2021/9/16時点)
 
  name     = "cloudrun-srv-${var.env}"
  location = "asia-northeast1"
 
  template {
    spec {
      containers {
        image = var.docker_image
        env {
          name  = "STAGE"
          value = var.env
        }
        env {
          name = "KEY"
          value_from {
            secret_key_ref {
              name = google_secret_manager_secret.key.secret_id
              key  = "latest"
            }
          }
        }
      }
      service_account_name = google_service_account.default.email
    }
    metadata {
      annotations = {
        "run.googleapis.com/vpc-access-connector" = "${google_vpc_access_connector.serverless.name}"
        "run.googleapis.com/vpc-access-egress"    = "all-traffic"
        "autoscaling.knative.dev/maxScale"        = "100"
      }
    }
  }
  metadata {
    annotations = {
      generated-by                      = "magic-modules"
      "run.googleapis.com/launch-stage" = "BETA"
      "run.googleapis.com/ingress"      = "all"
    }
  }
 
  autogenerate_revision_name = true
 
  traffic {
    percent         = 100
    latest_revision = true
  }
  lifecycle {
    ignore_changes = [
      template[0].metadata[0].annotations["run.googleapis.com/client-version"],
      template[0].metadata[0].annotations["client.knative.dev/user-image"],
      template[0].metadata[0].annotations["run.googleapis.com/client-name"],
      template[0].metadata[0].annotations["run.googleapis.com/sandbox"],
      metadata[0].annotations["client.knative.dev/user-image"],
      metadata[0].annotations["run.googleapis.com/client-name"],
      metadata[0].annotations["run.googleapis.com/client-version"]
    ]
  }
}

まず、autogenerate_revision_nameフィールドをtrueに設定することは、ほぼ必須です。これは、リビジョン名をTerraformで管理せず、GCPで発行させるための設定です。falseあるいは設定されていない状態だと、同一のリビジョン名が発行されてコンフリクトが発生し、リビジョンが作成できません。詳細はこちらで詳しく説明されています。

また、lifecycleブロックを利用して特定のannotationに対する更新を無視するよう設定しています。Terraform管理外からリビジョンを作成した場合、例えばgcloudコマンドで作成した場合に、一部のannotationが自動的に変更または作成されます。すると、実際の状態とtfstateとの差分が生じるため、terraform planの出力が煩雑になるという問題が生じます。なるべく意味のある変更差分のみを表示させたいと考えたため、今回はこのような対策を施しました。しかし、重要なannotationに対して適用しないよう注意が必要です。例えば、Cloud Runサービスと後述するサーバレスVPCアクセスコネクタとの紐付けはannotationを使って指定しています。

その他の内容は、公式ドキュメントを参照ください。

監視への取り組み

私達のチームでは主な監視ツールとしてDatadogを利用していますが、監視においてもCloud Run特有の課題がありましたので、対応策を含めご紹介します。

その課題とは、「Cloud RunはDatadog APMをサポートしていないこと」でした。なお、最新のサポート状況はこちらをご確認ください。

Datadog APMはライブラリを組み込んでAgentを構成することで、アプリケーションからインフラまでの一貫した監視を可能にするツールです。アプリケーションエラーやレイテンシはもちろん、リクエスト処理状況をクエリ単位で可視化できるなど豊富な機能を提供していたため、チームでは積極的に活用し障害対応や改善業務に役立てていました。

Datadog APMに非対応な点は残念でしたが、リクエスト数やエラー数などの基本的なメトリクスはDatadog Integrationsで取得できていました。さらに、アプリケーションエラーはSentryで取得できていたため、直近で大きな問題にはならないと判断し、他の方法でDatadog APMが提供する指標を補完できないか検討しました。

その結果、エンドポイント毎のレイテンシはCloud RunのリクエストログをDatadogに転送し、それをメトリクス化することで可視化できると分かりました。今後のサービス拡大に向けSLI/SLOを策定したい背景もあり、エンドポイント毎のレイテンシはぜひ取得したいというモチベーションがありました。なお、サービス全体のレイテンシだと特定のリクエストにおけるレスポンス遅延を検知できない可能性があるので注意が必要です。

以下では、Cloud Runのエンドポイント毎のレイテンシをDatadogでメトリクス化する手順をご説明します。

Cloud Runはリクエストログを自動的にCloud Loggingに転送しています。リクエストログには、エンドポイントのパスやレイテンシが格納されているため、Datadogでメトリクス化することによりダッシュボードでの閲覧が可能となります。

まず、下図のようにCloud Pub/Subを使ってDatadogにリクエストログをPushします。

ログ転送の流れ

Datadogでログからメトリクスを作成する流れは次の通りです。

  1. リクエストログ内latencyフィールドをnumberとしてパース

    • 詳細は拙稿をご参照ください
  2. パースされたlatency定量的ファセットに登録

  3. 新しいログベースメトリクスを作成

最終的に完成したダッシュボードが下図です。作成したメトリクスを利用し、図内の赤枠で示している通り、エンドポイント毎のレイテンシを表示させています。

ダッシュボード

CI/CDへの取り組み

CI/CDにはGitHub Actionsを使用しています。コード管理、レビュー、マージ、CI/CDといったコードのライフサイクル全てをGitHubで完結できる点が非常に便利です。また、クラウドベンダー公式のActionも公開されており、今後ますます充実していくことが期待されます。

FAANSのアプリケーションは、下図のような流れでデプロイをしています。

アプリケーションデプロイ

コンテナデプロイの前に、FirestoreのIndex作成と初期データ投入をします。Cloud Runのデプロイについてはこちらよりワークフローの詳細をご確認ください。

そして、GCPやDatadogのプロビジョニングにもGitHub Actionsを利用しています。Pull Requestを作成するとterraform planの出力結果がConversationタブに表示されます。これにより、コードレビュー時にplan結果を確認でき、より安全にapplyを実行できます。

Terraform

GitHub Actionsにおけるterraform planの表示方法はこちらを参照ください。

外部向きIPアドレスの固定

最後に、Cloud Runにおける外部向きIPアドレスの固定方法をご紹介します。

FAANSはAWSに構築された社内システムと通信する必要がありましたが、クラウドが異なるためピアリング接続によるプライベート通信はできませんでした。専用線での通信はコストや障害点の多さから構成が難しかったため、IPアドレス制限を設けてインターネット経由で接続する構成を選択しました。

Cloud Runの外部向きIPアドレスは動的であるため、下図のようなサーバレスVPCアクセスコネクタを用いた構成を取り、予約済みの静的アドレスを使えるようにしました。

VPCアクセスコネクタ

VPC内のリソースは以下のtfファイルで定義しています。

# vpc
resource "google_compute_network" "vpc-network" {
  name                    = "vpc-${var.env}"
  mtu                     = 1460
  auto_create_subnetworks = false
}
 
# subnet for serverless vpc access connector
resource "google_compute_subnetwork" "serverless" {
  name          = "subnetwork-serverless-${var.env}"
  ip_cidr_range = "10.124.0.0/28" # VPCコネクタの制限により/28を指定
  region        = "asia-northeast1"
  network       = google_compute_network.vpc-network.id
}
 
# vpc connector
resource "google_vpc_access_connector" "serverless" {
  provider = google-beta # subnet と紐付けるため (2021/9/16時点)
  name     = "vpc-connector-${var.env}"
  subnet {
    name = google_compute_subnetwork.serverless.name
  }
  region = google_compute_subnetwork.serverless.region
}
 
# cloud router
resource "google_compute_router" "serverless" {
  name    = "router-serverless-${var.env}"
  network = google_compute_network.vpc-network.name
  region  = google_compute_subnetwork.serverless.region
}
 
# ip address
resource "google_compute_address" "serverless" {
  count        = 1
  name         = "ip-serverless-${var.env}"
  address_type = "EXTERNAL"
  region       = google_compute_subnetwork.serverless.region
}
 
# nat
resource "google_compute_router_nat" "serverless" {
  name                               = "nat-serverless-${var.env}"
  router                             = google_compute_router.serverless.name
  region                             = google_compute_subnetwork.serverless.region
  nat_ip_allocate_option             = "MANUAL_ONLY"
  nat_ips                            = google_compute_address.serverless.*.self_link
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"
  subnetwork {
    name                    = google_compute_subnetwork.serverless.id
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
}

Cloud Runに対しては、外部向きトラフィックの全てをサーバレスVPCアクセスコネクタ経由でルーティングするように設定します。

gcloudコマンドを利用する場合と、Terraformを利用する場合のそれぞれの設定方法はこちらです。

gcloud run deploy SERVICE_NAME \
   --image=IMAGE_URL \
   --vpc-connector=CONNECTOR_NAME \
   --vpc-egress=all-traffic
resource "google_cloud_run_service" "run" {
  template {
    metadata {
      annotations = {
        "run.googleapis.com/vpc-access-connector" = "${google_vpc_access_connector.serverless.name}"
        "run.googleapis.com/vpc-access-egress"    = "all-traffic"
      }
      ~中略~
    }
    ~中略~
  }
  ~中略~
}

なお、コマンドラインから構成する手順はこちらをご参照ください。

まとめ

Cloud Runでサービスを構築・運用する際のSREとしての取り組みをご紹介しました。FAANSはまだテスト運用を開始してから日も浅く、取り組めていない課題も存在します。今後は、パフォーマンスチューニング、WAF導入、SLO/SLI策定などを視野に入れつつ、ユーザが快適に利用できるサービス作りに貢献していきたいです。

さいごに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

カテゴリー