This episode discusses deploying to the cloud and Kubernetes automatically from the pipeline: Cloud Run and Vercel for serverless PaaS, then deploying manifests to a cluster with kubectl and updating Helm releases with helm upgrade.

In episode 13 we deployed to our own servers via SSH and Ansible — like managing your own home: free, but all maintenance is on us. Now imagine a service that takes care of the building, security, and maintenance for you. That's Cloud PaaS and Kubernetes: we focus on the application, the platform takes care of the infrastructure.
Episode 14 covers two major CD paths: PaaS/Serverless (Cloud Run, Vercel, AWS Lambda) which hides servers completely, and Kubernetes which gives full control with equal complexity. Both can be deployed automatically from a workflow.
Cloud Run runs containers without needing to manage nodes. Since it accepts container images (which we built in episode 12), the deploy flow is short: build the image, upload it to a registry, then tell Cloud Run to use the new version.
Authentication uses the OIDC from episode 10 — no static credentials. The GCP project, region, and service name can be set as vars:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123456789012/locations/global/workloadIdentityPools/ci-pool/providers/github-provider
service_account: ci-deploy@devvnull-prod.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud builds submit --tag gcr.io/devvnull-prod/app
- run: |
gcloud run deploy app \
--image gcr.io/devvnull-prod/app \
--region asia-southeast1 \
--allow-unauthenticatedAfter auth succeeds, all gcloud commands use the trusted service account identity. gcloud run deploy creates a new revision and does traffic shifting — rolling back on Cloud Run just means pointing at an old revision.
For frontends, Vercel is the darling: every push to the main branch can become a production release directly. Authentication via a token created in the Vercel dashboard, stored as a secret along with the project identity:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Deploy ke Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: npx vercel --prod --token $VERCEL_TOKEN --yesnpx vercel --prod builds and publishes a production release in a single command. The same pattern applies to similar platforms: AWS Lambda uses the aws CLI (with configure-aws-credentials from episode 10) or the SAM framework, and Netlify has the netlify deploy --prod command with an equivalent token.
On the Kubernetes side, you hold full control over deployments, services, and autoscaling — and you're responsible for all of it. The first step in the workflow: preparing the kubeconfig so kubectl knows which cluster to talk to. The kubeconfig is base64-encoded, stored as a secret, then written to the runner:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Siapkan kubeconfig
env:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }}
run: |
mkdir -p $HOME/.kube
echo "$KUBECONFIG_B64" | base64 -d > $HOME/.kube/config
chmod 600 $HOME/.kube/config
- name: Pasang kubectl
run: |
curl -fsSL -o kubectl \
https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
- name: Terapkan manifest
run: |
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/app -n appkubectl apply is declarative: it reconciles the cluster state with what's written in the manifests — not executing step by step. rollout status makes the workflow wait for the deployment to truly finish and pods to be healthy before declaring success, so a half-finished deploy can't slip through.
Tip
Limit the kubeconfig to a single namespace or a single service account with narrow RBAC. A production kubeconfig holds the ability to destroy a cluster — don't give it to a workflow that also runs on pull requests. Separate kubeconfigs per environment, and restrict that secret to the production environment only.
Raw manifests quickly get out of hand as environments and configurations accumulate. Helm packages everything into a chart — manifest templates with values injectable at deploy time. From the workflow, the chart is applied with helm upgrade --install, and the image version is injected via --set:
- name: Pasang Helm
run: |
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Upgrade release
run: |
helm upgrade --install app ./helm/app \
--namespace app \
--set image.tag=${{ github.sha }}helm upgrade --install means: install if it doesn't exist, update if it does. Injecting the commit hash as the image tag ensures the cluster uses exactly the same version that was just built and tested in the pipeline. The combination of kubectl apply and Helm underlies the GitOps pattern — the cluster follows the state stored in git.
| Aspect | Cloud Run | Vercel | Kubernetes |
|---|---|---|---|
| Manages servers | No (serverless) | No | Yes, fully |
| Best fit | General API/containers | Static frontend/Next.js | Complex apps, HA |
| Deploy tooling | gcloud + OIDC | vercel CLI | kubectl / helm |
| Configuration | Small service YAML | Project configuration | Manifests + charts |
| Learning curve | Low | Very low | High |
| Mistake | Symptom | Solution |
|---|---|---|
| Kubeconfig too broad | Workflow could delete other resources | Limit to a dedicated namespace/service account |
| Forgetting id-token: write | OIDC fails, cloud rejects | Add the id-token permission on the job |
| Deploy without waiting for rollout | Pipeline succeeds though pods crash | Use kubectl rollout status |
| Vercel token leaking in logs | Frontend credentials exposed | Pass via env, not as a command argument |
| Helm without an existing --namespace | Release fails to create | Create the namespace first with kubectl apply |
Deployment now spreads across the whole cloud and cluster ecosystem:
gcloud run deploy with OIDC authentication.kubectl apply -f, complete with rollout status verification.helm upgrade --install and dynamic image tags.In the next episode 15, we move to the most fundamental layer of the entire pipeline: Setup & Management of Self-Hosted Runners — when you need your own runner, how to register it on Linux, securing it, and autoscaling it on Kubernetes with Actions Runner Controller. Because all those great pipelines above run on runners!