🐙

Argo CD で Crossplane を使った Cloud Run のデプロイ

2025/03/24に公開

クラウドエース北野です。
Crossplane を使って Cloud Run を Argo CD で管理する方法を紹介します。

概要

Kubernetes に Crossplane と Argo CD を次のように構築して、Cloud Run をデプロイします。

Crossplane で Cloud Run のデプロイに Provider provider-gcp-cloudrun を使います。
Provider のインストールは次のマニフェストで実行します。

provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-family-gcp
spec:
  package: xpkg.crossplane.io/crossplane-contrib/provider-family-gcp:v1.12.1
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-gcp-cloudrun
spec:
  package: xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.12.1
  controllerConfigRef:
    name: <CONTROLLER NAME>

Cloud Run の ManagedResource は、V2Service を使います。マニフェストは次のようになります。

cloudrun.yaml
apiVersion: cloudrun.gcp.upbound.io/v1beta2
kind: V2Service
metadata:
  name: helloworld

spec:
  forProvider:
    location: asia-northeast1
    template:
      containers:
        - image: us-docker.pkg.dev/cloudrun/container/hello:latest

  providerConfigRef:
    name: <PROVIDER CONFIG NAME>

Argo CD で Crossplane のオブジェクトの変更を追跡するため、Configmap argocd-cm に次のような設定をします。

cm-argocd.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  application.resourceTrackingMethod: annotation
  resource.customizations: |
        "*.upbound.io/*":
          health.lua: |
            health_status = {
            status = "Progressing",
            message = "Provisioning ..."
            }

            local function contains (table, val)
              for i, v in ipairs(table) do
                if v == val then
                  return true
                end
              end
              return false
            end

            local has_no_status = {
              "ProviderConfig",
              "ProviderConfigUsage"
            }

            if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
              health_status.status = "Healthy"
              health_status.message = "Resource is up-to-date."
              return health_status
            end

            if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
              if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
                health_status.status = "Healthy"
                health_status.message = "Resource is in use."
                return health_status
              end
              return health_status
            end

            for i, condition in ipairs(obj.status.conditions) do
              if condition.type == "LastAsyncOperation" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Synced" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Ready" then
                if condition.status == "True" then
                  health_status.status = "Healthy"
                  health_status.message = "Resource is up-to-date."
                  return health_status
                end
              end
            end

            return health_status

        "*.crossplane.io/*":
          health.lua: |
            health_status = {
              status = "Progressing",
              message = "Provisioning ..."
            }

            local function contains (table, val)
              for i, v in ipairs(table) do
                if v == val then
                  return true
                end
              end
              return false
            end

            local has_no_status = {
              "Composition",
              "CompositionRevision",
              "DeploymentRuntimeConfig",
              "ControllerConfig",
              "ProviderConfig",
              "ProviderConfigUsage"
            }
            if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
              health_status.status = "Healthy"
              health_status.message = "Resource is up-to-date."
              return health_status
            end

            if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
              if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
                health_status.status = "Healthy"
                health_status.message = "Resource is in use."
                return health_status
              end
              return health_status
            end

            for i, condition in ipairs(obj.status.conditions) do
              if condition.type == "LastAsyncOperation" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Synced" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if contains({"Ready", "Healthy", "Offered", "Established"}, condition.type) then
                if condition.status == "True" then
                  health_status.status = "Healthy"
                  health_status.message = "Resource is up-to-date."
                  return health_status
                end
              end
            end

            return health_status

はじめに

Argo CD は Kubernetes のリソースを継続的にデプロイするツールです。Kubernetes を使ってシステムを構築する場合、アプリケーションのデプロイに広く使われています。

アプリケーションを実行する環境がすべて Kubernetes であれば、Argo CD ですべてのアプリケーションのデプロイを管理できます。しかし、費用の問題などで Cloud Run などの各パブリッククラウドのコンテナ実行環境と Kubernetes が混在した構成でシステムを構築すると、Argo CD だけですべてのアプリケーションを管理できません。そのため、Argo CD と Cloud Deploy など複数の CD ツールを使って運用することになり、認知負荷が増えてオペレーションミスが起こる可能性が高くなります。

そこで、Crossplane を Argo CD で使うと、Argo CD で Kubernetes のリソースと Cloud Run などのアプリケーションの両方をデプロイできます。この記事では、Cloud Run だけのデプロイを紹介します。

Crossplane とは

CrossplaneUpbound 社が開発した OSS の Kubernetes 拡張機能で、Kubernetes 以外のプラットフォームのリソースを Kubernetes で管理することを目的としたツールです。

Crossplane は Google Cloud や AWS など操作する外部のプラットフォームごとに Provider をインストールし、Provider が Kubernetes の API を外部プラットフォームの API に変換しリソースを作成します。また、作成したリソースを ManagedResource というオブジェクトで Kubernetes 内で管理します。
この ManagedResource に作成、更新、削除などの操作を Kubernetes から実行すると、Provider が外部プラットフォームのリソースを操作します。

そのため、Argo CD が ManagedResource を管理することで、Kubernetes 以外のリソースの管理が可能になります。
本記事では、Argo CD で Crossplane をインストールし、Cloud Run の ManagedResource の管理方法を紹介します。

設計

本記事では Google Cloud 上で次のようなシステムを構築して、Argo CD から Cloud Run をデプロイします。

Kubernetes は Google Kubernetes Engine (以降 GKE と呼びます)で構築します。GKE 上に Argo CD と Crossplane を構築して、GitHub のイベントに応じて Argo CD が Crossplane の ManagedResource を操作して、Cloud Run をデプロイします。

Crossplane は次のように構築します。

Crossplane を Workload Identity によって認証させる方法は、こちらの記事を参考にしてください。

構築

設計内容を構築していきます。構築では、Terraform と Kustomize を使ってシステムを構築します。コードの <> で囲まれた部分は環境に合わせて読み替えてください。

事前準備

事前準備では、Argo CD の構築をします。GKE 上に Argo CD を構築する方法は、こちらの記事を参考にしてください。

Argo CD の設定

Argo CD が Crossplane の変更を正しく追跡するための設定を argocd-cm の ConfigMap に次のように定義します。

cm-argocd.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  application.resourceTrackingMethod: annotation
  resource.customizations: |
        "*.upbound.io/*":
          health.lua: |
            health_status = {
            status = "Progressing",
            message = "Provisioning ..."
            }

            local function contains (table, val)
              for i, v in ipairs(table) do
                if v == val then
                  return true
                end
              end
              return false
            end

            local has_no_status = {
              "ProviderConfig",
              "ProviderConfigUsage"
            }

            if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
              health_status.status = "Healthy"
              health_status.message = "Resource is up-to-date."
              return health_status
            end

            if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
              if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
                health_status.status = "Healthy"
                health_status.message = "Resource is in use."
                return health_status
              end
              return health_status
            end

            for i, condition in ipairs(obj.status.conditions) do
              if condition.type == "LastAsyncOperation" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Synced" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Ready" then
                if condition.status == "True" then
                  health_status.status = "Healthy"
                  health_status.message = "Resource is up-to-date."
                  return health_status
                end
              end
            end

            return health_status

        "*.crossplane.io/*":
          health.lua: |
            health_status = {
              status = "Progressing",
              message = "Provisioning ..."
            }

            local function contains (table, val)
              for i, v in ipairs(table) do
                if v == val then
                  return true
                end
              end
              return false
            end

            local has_no_status = {
              "Composition",
              "CompositionRevision",
              "DeploymentRuntimeConfig",
              "ControllerConfig",
              "ProviderConfig",
              "ProviderConfigUsage"
            }
            if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
              health_status.status = "Healthy"
              health_status.message = "Resource is up-to-date."
              return health_status
            end

            if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
              if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
                health_status.status = "Healthy"
                health_status.message = "Resource is in use."
                return health_status
              end
              return health_status
            end

            for i, condition in ipairs(obj.status.conditions) do
              if condition.type == "LastAsyncOperation" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if condition.type == "Synced" then
                if condition.status == "False" then
                  health_status.status = "Degraded"
                  health_status.message = condition.message
                  return health_status
                end
              end

              if contains({"Ready", "Healthy", "Offered", "Established"}, condition.type) then
                if condition.status == "True" then
                  health_status.status = "Healthy"
                  health_status.message = "Resource is up-to-date."
                  return health_status
                end
              end
            end

            return health_status

Crossplane のインストール

Argo CD から Crossplane をインストールします。helm からインストールするように次のような Application を定義します。

application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: crossplane
  namespace: argocd

spec:
  project: default
  destination:
    namespace: crossplane-system
    server: https://kubernetes.default.svc
  source:
    chart: crossplane
    repoURL: https://charts.crossplane.io/stable
    targetRevision: 1.19.0
    helm:
      releaseName: stable
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated: null

マニフェストを実行すると、次のように Application が作成されます。SYNC ボタンを押して、デプロイします。

次に Cloud Run を作成する Provider と Provider を Google Cloud に認証させるための ProviderConfig と ControllerConfig を作成します。

controller_config.yaml
apiVersion: pkg.crossplane.io/v1alpha1
kind: ControllerConfig
metadata:
  name: sa-k8s-crossplane
  annotations
    iam.gke.io/gcp-service-account: <GOOGLE CLOUD SERVICEACCOUNT>
spec:
  serviceAccountName: <KUBERNETES SERVICEACCOUNT>
provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-family-gcp
spec:
  package: xpkg.crossplane.io/crossplane-contrib/provider-family-gcp:v1.12.1
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-gcp-cloudrun
spec:
  package: xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.12.1
  controllerConfigRef:
    name: sa-k8s-crossplane
pc.yaml
apiVersion: gcp.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: pc-workload-identity
spec:
  credentials:
    source: InjectedIdentity
  projectID: <PROJECT ID>

これらのマニフェストを実行します。

kubectl apply -f controller_config.yaml
kubectl apply -f provider.yaml
kubectl apply -f pc.yaml

暫くすると、次のように Provider がインストールされます。

kubectl get provider

NAME                    INSTALLED   HEALTHY   PACKAGE                                                               AGE
provider-family-gcp     True        True      xpkg.crossplane.io/crossplane-contrib/provider-family-gcp:v1.12.1     2m10s
provider-gcp-cloudrun   True        True      xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudrun:v1.12.1   2m10s

Cloud Run を作成する ManagedResource を provider-gcp-cloudrun の V2Service を使い定義します。今回、以下の構成で Cloud Run を作成します。

設定項目
name helloworld
docker image us-docker.pkg.dev/cloudrun/container/hello:latest
location asia-northeast1
cloudrun.yaml
apiVersion: cloudrun.gcp.upbound.io/v1beta2
kind: V2Service
metadata:
  name: helloworld

spec:
  forProvider:
    location: asia-northeast1
    template:
      containers:
        - image: us-docker.pkg.dev/cloudrun/container/hello:latest

  providerConfigRef:
    name: pc-workload-identity

Cloud Run の ManagedResource をデプロイする Application を次のように定義します。

application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cloudrun
  namespace: argocd

spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    path: <PATH>
    repoURL: <GITHUB REPOSITORY URL>
    targetRevision: HEAD
  syncPolicy:
    automated: null

このマニフェストを実行すると、cloudrun の Application が Argo CD 上にできます。

Cloud Run が存在しないことを Argo CD の作成の前に確認します。

gcloud run services describe --region asia-northeast1 --project <PROJECT ID> helloworld

ERROR: (gcloud.run.services.describe) Cannot find service [helloworld]

SYNC ボタンを押して、helloworld の Cloud Run を作成します。

次のように Cloud Run が作成されているのが分かります。

gcloud run services describe --region asia-northeast1 --project <PROJECT ID> helloworld
✔ Service helloworld in region asia-northeast1

URL:     https://helloworld-<PROJECT NUMBER>.asia-northeast1.run.app
Ingress: all
Traffic:
  100% LATEST (currently helloworld-00001-wcp)

Scaling: Auto (Min: 0)

Last updated on 2025-03-22T09:07:05.583722Z by <GOOGLE CLOUD SERVICEACCOUNT>:
  Revision helloworld-00001-wcp
  Container None
    Image:           us-docker.pkg.dev/cloudrun/container/hello:latest
    Port:            8080
    Memory:          512Mi
    CPU:             1000m
    Startup Probe:
      TCP every 240s
      Port:          8080
      Initial delay: 0s
      Timeout:       240s
      Failure threshold: 1
      Type:          Default
  Service account:   <PROJECT DEFAULT SERVICEACCOUNT>
  Concurrency:       80
  Min instances:     0
  Max instances:     100
  Timeout:           300s
  Session Affinity:  false

次に Argo CD から Cloud Run の イメージバージョンsample-public-image-71cb7d367a8875eef4e0d1599b2046a8edfbb018f20d2e0c40fe0124fd5e3106 に変更してみます。ManagedResource を次のように変更し、GitHub にプッシュします。

cloudrun.yaml
apiVersion: cloudrun.gcp.upbound.io/v1beta2
kind: V2Service
metadata:
  name: helloworld

spec:
  forProvider:
    location: asia-northeast1
    template:
      containers:
        - image: us-docker.pkg.dev/cloudrun/container/hello:sample-public-image-71cb7d367a8875eef4e0d1599b2046a8edfbb018f20d2e0c40fe0124fd5e3106

  providerConfigRef:
    name: pc-workload-identity

暫くすると、次のように差分が表われるので、Diff から差分を確認します。


Sync ボタンを押すと、Cloud Run が更新されます。Cloud Run のイメージバージョンを確認すると、更新したイメージバージョンとなっています。

❯ gcloud run services describe --region asia-northeast1 --project <PROJECT ID> helloworld
✔ Service helloworld in region asia-northeast1

URL:     https://helloworld-<PROJECT NUMBER>.asia-northeast1.run.app
Ingress: all
Traffic:
  100% LATEST (currently helloworld-00002-76f)

Scaling: Auto (Min: 0)

Last updated on 2025-03-22T09:15:21.075105Z by <GOOGLE CLOUD SERVICEACCOUNT>:
  Revision helloworld-00002-76f
  Container None
    Image:           us-docker.pkg.dev/cloudrun/container/hello:sample-public-image-71cb7d367a8875eef4e0d1599b2046a8edfbb018f20d2e0c40fe0124fd5e3106
    Port:            8080
    Memory:          512Mi
    CPU:             1000m
    Startup Probe:
      TCP every 240s
      Port:          8080
      Initial delay: 0s
      Timeout:       240s
      Failure threshold: 1
      Type:          Default
  Service account:   <PROJECT DEFAULT SERVICEACCOUNT>
  Concurrency:       80
  Min instances:     0
  Max instances:     100
  Timeout:           300s
  CPU Allocation:    CPU is only allocated during request processing
  Session Affinity:  false

さいごに

Crossplane を使って、Cloud Run を Argo CD でデプロイする方法を紹介しました。Kubernetes と Cloud Run が混在するシステムでこの方法を使うと、デプロイ方法を統一することができ運用の認知負荷を下げることができるのでオペレーションミスが減るかと思います。
また、すべての Google Cloud のリソースを Argo CD で管理すると、Argo CD の自動 SYNC 機能を使って構成管理をすると、GitOps の実現も可能となります。

Discussion