Skip to main content

Cloud Run YAML Deploy

This guide describes an alternative way to deploy services: instead of using Skiff2's deploy shared GitHub Action, you swap it out for the default Google Cloud Run deploy action. This gives you a lot more flexibility and support for all of the Cloud Run features you might need, configured directly through the Cloud Run YAML service spec.

A full working example is available in the skiff-template repository: main...cloud-run-deploy.

How it works​

When deploying this way, the settings in skiff2.json are still used to configure the load balancer and URLs, but all service-related settings — CPU, RAM, min instances, and so on — come from the Google Cloud Run YAML spec.

Service naming matters

The load balancer relies on the service name to connect a URL to the correct Cloud Run service, so the name you deploy to Cloud Run must match the name the load balancer expects. That name is the environment prefix plus the service name from skiff2.json — in production, the prod- prefix followed by the service name suffix.

For example, a service named ui in skiff2.json is expected at the Cloud Run service named prod-ui. Because that ui service is marked as isRootService, the load balancer connects the root domain to prod-ui.

The following steps walk through the changes, mirroring the main...cloud-run-deploy diff in the skiff-template repository.

Step 1: Disable Skiff2 deploy for the service​

Keep the service in skiff2.json so the load balancer and URLs are still configured, but set deploy: false so Skiff2 no longer tries to deploy it. You will deploy it yourself in the workflow instead.

skiff2.json
{
"services": [
{
"name": "ui",
"cwd": "./ui",
"isRootService": true,
"deploy": false,
"sidecars": [
{
"name": "api",
...
}
]
}
]
}

Step 2: Swap the deploy step in the workflow​

Replace the Skiff2 Deploy Services step with a render step and a google-github-actions/deploy-cloudrun@v3 step. Leave Skiff2 Setup and Skiff2 Deploy Infra in place — the load balancer, NEGs, and SSL/DNS are still managed by Skiff2.

Before — Skiff2 deploys all services in one step:

- name: Skiff2 Deploy Services
id: deploy-services
uses: allenai/skiff2-actions/shared-actions/deploy-services@main
with:
project_id: ${{ vars.SKIFF2_PROJECT_ID }}
command: apply

After — render the spec, then deploy each service directly to Cloud Run:

# Render the Cloud Run Service spec from the template, substituting the
# service name and the freshly built image tags.
- name: Create Service declaration
run: envsubst < ./ui/ui-service.template.yaml > service.yaml
env:
SERVICE: ${{ env.DEPLOYMENT_NAME }}-ui
UI_IMAGE: ${{ fromJSON(steps.build.outputs.built-images).ui[0] }}
API_IMAGE: ${{ fromJSON(steps.build.outputs.built-images).api[0] }}

# Deploy the rendered spec directly to Cloud Run.
- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.DEPLOYMENT_NAME }}-ui
region: ${{ env.REGION }}
metadata: service.yaml

DEPLOYMENT_NAME is the environment prefix that produces the ${env}-${service} name the load balancer expects (prod on main, otherwise the branch name):

env:
# main -> "prod"; any other branch -> the branch name.
DEPLOYMENT_NAME: ${{ github.ref_name == 'main' && 'prod' || github.ref_name }}
REGION: us-west1
Deploy each service separately

Skiff's deploy-services action deploys all services at once. With google-github-actions/deploy-cloudrun@v3 you need a render + deploy step pair for each service you want to deploy.

Step 3: Add the Cloud Run service template​

Add a Cloud Run YAML service spec that the workflow renders. This is where all service-related settings — CPU, RAM, scaling, probes, sidecars — live. The ${SERVICE}, ${UI_IMAGE}, and ${API_IMAGE} placeholders are filled in by the envsubst step above.

ui/ui-service.template.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
# Must be "${env}-${service}" (e.g. prod-ui) so the load balancer routes to it.
name: ${SERVICE}
labels:
cloud.googleapis.com/location: us-west1
annotations:
# Only reachable through the Skiff2 load balancer, not the *.run.app URL.
run.googleapis.com/default-url-disabled: 'true'
run.googleapis.com/invoker-iam-disabled: 'true'
run.googleapis.com/ingress: internal-and-cloud-load-balancing
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: '1'
autoscaling.knative.dev/maxScale: '10'
# ui (nginx) proxies /api/* to api, so api must come up first.
run.googleapis.com/container-dependencies: '{"ui":["api"]}'
spec:
containerConcurrency: 80
timeoutSeconds: 300
containers:
# Public container: serves the app and proxies /api/* to the api sidecar.
# Only this container exposes the ingress port.
- name: ui
image: ${UI_IMAGE}
ports:
- name: http1
containerPort: 8080
# Internal sidecar. No ports block (only one container may expose the port).
- name: api
image: ${API_IMAGE}
resources:
limits:
memory: 1Gi
cpu: 1000m

See the Cloud Run YAML reference for the full set of available fields (probes, env vars, secrets, and more).

Authentication​

Normally Skiff2 puts Identity-Aware Proxy (IAP) in front of every service based on skiff2.json settings like allowUnauthenticated and allowedPrincipals. When you deploy with Cloud Run YAML, those settings no longer drive the service's authentication — you manage access in the service spec instead.

In the example template, the service is locked down to the Skiff2 load balancer via the metadata annotations:

annotations:
# Only reachable through the Skiff2 load balancer, not the *.run.app URL.
run.googleapis.com/default-url-disabled: 'true'
run.googleapis.com/invoker-iam-disabled: 'true'
run.googleapis.com/ingress: internal-and-cloud-load-balancing

Make sure your template's ingress and invoker settings reflect the access you want, and configure any IAP or other authentication you need directly on the service. See the Cloud Run YAML reference for the available annotations.

Secrets and environment variables​

This method lets you manage everything yourself. You can add environment variables directly to your templates, and you can use Cloud Run YAML to reference secrets from Secret Manager in any way you like.

Tip: start from an existing service's YAML​

It can be helpful to check the YAML of an existing service as a starting point. In the Google Cloud Console, go to the service and select the YAML tab to view its full spec.