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.
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.
{
"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
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.
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.