Argo CD で Crossplane を使った Cloud Run のデプロイ
クラウドエース北野です。
Crossplane を使って Cloud Run を Argo CD で管理する方法を紹介します。
概要
Kubernetes に Crossplane と Argo CD を次のように構築して、Cloud Run をデプロイします。
Crossplane で Cloud Run のデプロイに Provider provider-gcp-cloudrun
を使います。
Provider のインストールは次のマニフェストで実行します。
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 を使います。マニフェストは次のようになります。
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
に次のような設定をします。
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 とは
Crossplane は Upbound 社が開発した 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 は次のように構築します。
- Provider: provider-gcp-cloudrun
- Google Cloud への認証方法: Workload Identity
Crossplane を Workload Identity によって認証させる方法は、こちらの記事を参考にしてください。
構築
設計内容を構築していきます。構築では、Terraform と Kustomize を使ってシステムを構築します。コードの <>
で囲まれた部分は環境に合わせて読み替えてください。
事前準備
事前準備では、Argo CD の構築をします。GKE 上に Argo CD を構築する方法は、こちらの記事を参考にしてください。
Argo CD の設定
Argo CD が Crossplane の変更を正しく追跡するための設定を argocd-cm の ConfigMap に次のように定義します。
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 を定義します。
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 を作成します。
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>
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
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 |
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 を次のように定義します。
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 にプッシュします。
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