GitOps Canary Deployments with ArgoCD
Build a GitOps pipeline with canary deployments and automated rollback on Kubernetes.
Introduction
30 Second Summary
Every time you deploy new code, there is a risk of breaking things for real users. The safest teams never flip a switch and hope for the best. They gradually roll out changes to a small percentage of traffic, watch the metrics, and automatically pull back if anything goes wrong.
In this project, you will build a complete GitOps progressive delivery pipeline that takes your code from commit to canary deployment with automated metric-based rollback. Everything runs locally on your machine at zero cost.
What You'll Build
A local GitOps pipeline on Minikube that builds, scans, deploys, and progressively rolls out a containerized application using real production-grade tools.
By the end of this project, you'll have:
- A GitHub Actions CI pipeline that builds your Docker image, scans it for vulnerabilities with Trivy, and pushes it to GitHub Container Registry.
- An ArgoCD setup that watches your Git repo and automatically syncs changes to your Kubernetes cluster.
- A working canary deployment powered by Argo Rollouts that gradually shifts traffic and rolls back automatically if Prometheus metrics degrade.
- Secret Mission: Add a blue-green deployment strategy and configure webhook notifications on rollout events.
Are there any prerequisites?
You should have basic Docker knowledge (building images, running containers) and a basic understanding of Kubernetes (pods, deployments, services). You also need a GitHub account that can create public repositories.
Start Minikube and Deploy the Monitoring Stack
Your GitOps pipeline needs a local Kubernetes cluster to deploy to. It also needs a metrics system that can tell Argo Rollouts whether a new deployment is healthy or broken.
In this step, you will start a Minikube cluster with enough resources to run the full stack, then install Prometheus and Grafana for metrics collection.
In this step, get ready to:
- Start Minikube with enough CPU and memory for the full pipeline.
- Install the kube-prometheus-stack for metrics collection.
- Verify Prometheus is running and collecting cluster metrics.
Start Minikube with sufficient resources
Running ArgoCD, Argo Rollouts, and Prometheus together requires more resources than Minikube's default 2 CPUs and 2 GB of memory. You need to allocate at least 4 CPUs and 8 GB.
- Make sure Docker Desktop is running on your machine.
- Start your Minikube cluster with extra resources by running this command:
minikube start --cpus=4 --memory=8192
What does this command do?
- minikube start creates and boots a local Kubernetes cluster inside a Docker container.
- --cpus=4 allocates 4 CPU cores to the cluster (double the default).
- --memory=8192 allocates 8 GB of RAM in megabytes. This gives ArgoCD, Argo Rollouts, and Prometheus enough room to run simultaneously.
- Wait for the command to finish. This can take 1-3 minutes depending on your machine.
- Verify your cluster is running by running:
kubectl get nodes
You should see a single node named minikube with a STATUS of Ready.
Minikube not starting?
- Make sure Docker Desktop is running. Minikube uses Docker as its default driver and cannot start without it.
- If you have a previous Minikube cluster, delete it first with minikube delete then retry the start command.
- If your machine has less than 8 GB of free RAM, close other applications to free up memory.
Why won't my Minikube cluster start with Docker Desktop?
Install the kube-prometheus-stack with Helm
The kube-prometheus-stack is a Helm chart that installs Prometheus, Grafana, and related monitoring tools in one command. Argo Rollouts will query this Prometheus instance to decide whether to promote or roll back canary deployments.
- Add the prometheus-community Helm repository by running this command:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
- Update your local Helm chart index by running:
helm repo update
- Install the kube-prometheus-stack into a monitoring namespace by running:
helm install prometheus oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack --namespace monitoring --create-namespace
What does this command do?
- helm install prometheus installs a Helm chart with the release name prometheus.
- The OCI path oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack points to the chart stored in GitHub Container Registry.
- --namespace monitoring puts all monitoring resources in their own namespace so they stay organized.
- --create-namespace creates the monitoring namespace if it does not already exist.
This single chart installs Prometheus, Grafana, kube-state-metrics, and the Prometheus Node Exporter by default.
The install process takes 1-2 minutes as Kubernetes pulls the container images and starts the pods.
Seeing a timeout or pull error?
- Make sure your internet connection is active. Helm needs to download the chart and Kubernetes needs to pull container images.
- If you see an OCI-related error, confirm you are running Helm 3 or newer by running helm version.
Why is my Helm install failing for kube-prometheus-stack?
Verify Prometheus is running
Before moving on, you need to confirm that Prometheus and the other monitoring pods are healthy. If they are not running, Argo Rollouts will not be able to query metrics later.
- Check the pods in the monitoring namespace by running:
kubectl get pods -n monitoring
You should see several pods with names starting with prometheus-, alertmanager-, and prometheus-grafana-. All pods should show a STATUS of Running (or Pending if they are still starting up).
Pods still pending?
It can take a few minutes for all pods to reach Running status. Run the command again after 30 seconds. If pods stay in Pending for more than 3 minutes, check your Minikube resource allocation with minikube config view.
- Confirm the Prometheus service exists by running:
kubectl get svc -n monitoring | grep prometheus-kube-prometheus-prometheus
You should see a service named prometheus-kube-prometheus-prometheus with port 9090. This is the endpoint that Argo Rollouts will query for metrics in later steps.
Why does the service name matter?
In Step 6, your AnalysisTemplate will query Prometheus at http://prometheus-kube-prometheus-prometheus.monitoring:9090. This is the in-cluster DNS address that Kubernetes generates from the service name and namespace.
Don't see the Prometheus service?
- Run kubectl get all -n monitoring to see everything deployed in the namespace.
- If the namespace is empty, the Helm install may have failed silently. Run helm list -n monitoring to check whether the release exists.
- If no release is listed, re-run the helm install command from the previous substep.
Help me troubleshoot my Prometheus installation in the monitoring namespace.
Your monitoring stack is live. Next up, you will install ArgoCD and configure it to watch your GitHub repository for changes.
Install ArgoCD and Configure GitOps
Your monitoring stack is collecting metrics from the cluster. Now you need the piece that turns your Git repository into the single source of truth for what runs on Kubernetes.
ArgoCD is a GitOps controller that watches a Git repo and automatically syncs its contents to your cluster. In this step, you will install ArgoCD, set up the CLI, and log in so you can manage deployments from both the terminal and the browser.
In this step, get ready to:
- Install ArgoCD on your Minikube cluster and access the UI.
- Install the ArgoCD CLI on Windows using PowerShell.
- Log in to ArgoCD and verify access.
Install ArgoCD on the Minikube cluster
ArgoCD runs as a set of pods inside a dedicated namespace. You will create the namespace, apply the official install manifest, and then wait for the server to become available.
- Create the argocd namespace by running this command:
kubectl create namespace argocd
- Install ArgoCD v3.4.1 by applying the official manifest:
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.1/manifests/install.yaml
What does this command do?
This pulls the full ArgoCD installation manifest (API server, repo server, application controller, Redis, and Dex) and applies it to your cluster in the argocd namespace.
The --server-side and --force-conflicts flags ensure the apply succeeds even if there are field ownership conflicts from previous installs.
- Wait for the ArgoCD server deployment to become available by running:
kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=120s
You should see a message confirming the deployment condition is met.
Deployment not becoming available?
If the wait times out, check the pod status with kubectl get pods -n argocd. Pods stuck in Pending usually mean Minikube does not have enough resources. Make sure you started Minikube with at least 4 CPUs and 8 GB of memory.
help me troubleshoot why my ArgoCD pods are not starting on Minikube
Access the ArgoCD UI via port-forward
The ArgoCD server is not exposed outside the cluster by default. Port-forwarding maps the server's HTTPS port to your local machine so you can reach the UI in a browser.
- Start a port-forward to the ArgoCD server by running:
kubectl port-forward svc/argocd-server -n argocd 8080:443
What does this command do?
This forwards traffic from localhost:8080 on your machine to port 443 on the argocd-server service inside the cluster. The ArgoCD UI becomes accessible at https://localhost:8080.
- Open a browser and go to https://localhost:8080.
- You will see a certificate warning because ArgoCD uses a self-signed TLS certificate. Accept the warning to continue.
You should see the ArgoCD login page with fields for username and password.
Keep this terminal open
The port-forward runs in the foreground. Open a new terminal window or tab for the remaining commands in this step.
Install the ArgoCD CLI on Windows
The ArgoCD CLI lets you manage applications, sync deployments, and check status from the terminal. On Windows, you install it by downloading the executable with PowerShell.
- Open a new PowerShell window and download the ArgoCD CLI by running these commands:
# Fetch the latest release tag from GitHub
$version = (Invoke-RestMethod https://api.github.com/repos/argoproj/argo-cd/releases/latest).tag_name
# Build the download URL for the Windows binary
$url = "https://github.com/argoproj/argo-cd/releases/download/" + $version + "/argocd-windows-amd64.exe"
# Download the executable to your user folder
$output = "C:\Users\$env:USERNAME\argocd.exe"
Invoke-WebRequest -Uri $url -OutFile $output
What does this command do?
The first line queries the GitHub API to find the latest ArgoCD release tag. The second builds the download URL for the Windows binary. The last two lines download the executable to your user folder.
- Add the CLI to your PATH so you can run it from any terminal:
[Environment]::SetEnvironmentVariable("Path", "$env:Path;C:\Users\$env:USERNAME", "User")
- Close and reopen your PowerShell window for the PATH change to take effect.
- Verify the CLI is available by running:
argocd version --client
You should see version information printed that includes v3.4.1.
argocd is not recognized?
Make sure you reopened PowerShell after setting the PATH. If it still does not work, verify the file exists at the path you specified with ls C:\Users\$env:USERNAME\argocd.exe.
help me fix the ArgoCD CLI not being found on Windows after adding it to PATH
Log in to ArgoCD
ArgoCD generates a random admin password on first install. You need to retrieve it and use it to authenticate both in the CLI and the browser UI.
- Retrieve the initial admin password by running:
argocd admin initial-password -n argocd
What does this command do?
This reads the auto-generated password from the argocd-initial-admin-secret in the argocd namespace and prints it to your terminal. Copy this password for the next step.
- Log in to ArgoCD from the CLI by running:
argocd login localhost:8080 --insecure
- When prompted, enter admin as the username.
- Paste the password you copied from the previous command.
Why the --insecure flag?
The --insecure flag tells the CLI to skip TLS certificate verification. This is safe here because you are connecting to your own local Minikube cluster through a port-forward, not over an untrusted network.
You should see a message confirming you are logged in successfully.
- Switch to your browser at https://localhost:8080.
- Enter admin as the username and the same password from your terminal.
- Click SIGN IN.
You should see the ArgoCD dashboard with an empty applications list. This is where deployed applications will appear once you create them in a later step.
Login failing with invalid credentials?
Make sure you copied the full password output without any trailing whitespace or newline characters. Run the argocd admin initial-password -n argocd command again and carefully select only the password string.
help me troubleshoot why my ArgoCD admin login is not working
ArgoCD is installed and you are logged in as admin. In the next step, you will install Argo Rollouts to enable progressive delivery strategies like canary deployments.
Install Argo Rollouts and the kubectl Plugin
ArgoCD handles getting your manifests from Git to the cluster. But standard Kubernetes Deployments only support basic rolling updates.
For canary deployments with traffic splitting and automated rollback, you need a progressive delivery controller. Argo Rollouts fills this gap by replacing the standard Deployment resource with a Rollout resource that supports canary and blue-green strategies driven by real metrics.
In this step, get ready to:
- Install the Argo Rollouts controller in the cluster.
- Download and install the kubectl argo rollouts plugin for Windows.
- Verify the plugin works by checking the version.
Install the Argo Rollouts controller
The Argo Rollouts controller watches for Rollout resources in your cluster and orchestrates the progressive delivery steps you define. It runs in its own namespace.
- Create the argo-rollouts namespace and install the controller by running these commands:
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
What do these commands do?
The first command creates a dedicated namespace called argo-rollouts to isolate the controller from your application workloads.
The second command downloads the official install manifest from the Argo Rollouts v1.9.0 release and applies it. This installs the controller, CRDs (like Rollout, AnalysisTemplate, and AnalysisRun), and the necessary RBAC permissions.
- Verify the controller pod is running by running:
kubectl get pods -n argo-rollouts
You should see a pod named argo-rollouts- followed by a random suffix, with a status of Running.
Pod not showing Running?
If the pod status shows ContainerCreating, wait 30 seconds and run the command again. Minikube may need time to pull the container image.
If the pod is in CrashLoopBackOff, check that your Minikube cluster has enough memory allocated (at least 8 GB).
help me debug why my Argo Rollouts controller pod is not running
Download and install the kubectl plugin for Windows
The kubectl argo rollouts plugin gives you commands to manage and visualize rollouts directly from the terminal. It is how you will watch canary progress and promote deployments.
- Download the Windows plugin binary by running this command in PowerShell:
Invoke-WebRequest -Uri https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-windows-amd64 -OutFile kubectl-argo-rollouts.exe
What does this command do?
This downloads the pre-built kubectl-argo-rollouts-windows-amd64 binary from the official Argo Rollouts GitHub releases page. The -OutFile flag saves it as kubectl-argo-rollouts.exe so Windows recognizes it as an executable.
- Move the plugin to a directory on your PATH so kubectl can discover it. Run this command in PowerShell:
Move-Item -Force kubectl-argo-rollouts.exe C:\Windows\System32\kubectl-argo-rollouts.exe
Why System32?
The C:\Windows\System32 directory is already on your PATH by default. Moving the plugin here means kubectl can find it without any additional PATH configuration.
If you prefer a different location, place the .exe file in any folder that is on your PATH environment variable.
Access denied when moving the file?
You need to run PowerShell as Administrator to write to C:\Windows\System32. Right-click the PowerShell icon and select Run as administrator, then retry the command.
help me fix a permissions error when installing the kubectl argo rollouts plugin on Windows
Verify the plugin version
- Confirm the plugin is installed correctly by running:
kubectl argo rollouts version
You should see output showing the plugin version v1.9.0 and build information.
Command not recognized?
If you see an error about the command not being found, verify that kubectl-argo-rollouts.exe exists in a directory on your PATH. You can check by running where kubectl-argo-rollouts in a new terminal window.
If you just added the file to PATH, close and reopen your terminal for the change to take effect.
help me troubleshoot the kubectl argo rollouts plugin not being found on Windows
Your cluster now has the progressive delivery controller ready to manage canary and blue-green deployments. Next up, you will build the Go application that Argo Rollouts will deploy.
Create the Application and Dockerfile
For Argo Rollouts to make intelligent promotion decisions during a canary deployment, it needs real metrics from your application. Prometheus collects those metrics, and the AnalysisTemplate you'll create later queries them to decide whether to promote or roll back.
In this step, you'll build a Go web application with built-in Prometheus instrumentation, package it in a secure multi-stage Docker image, and push everything to your GitHub repository.
In this step, get ready to:
- Create a Go web app with Prometheus metrics endpoints.
- Write a multi-stage Dockerfile with a non-root user.
- Push the code to your GitHub repository.
Create the Go web application
Your application needs three HTTP endpoints: a root path that serves responses (generating request metrics), a /health path for Kubernetes readiness probes, and a /metrics path that exposes Prometheus-formatted data.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir gitops-canary-demo
cd gitops-canary-demo
- Initialize a git repository and connect it to your GitHub remote by running:
git init
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/gitops-canary-demo.git
Seeing a 'remote origin already exists' error?
This means the remote was already added. You can verify it points to the right URL by running git remote -v. If the URL is wrong, remove it with git remote remove origin and re-add it.
help me fix my git remote configuration
- Create a new file called main.go inside your gitops-canary-demo folder.
- Add the following code to define the package, imports, and Prometheus metric registrations:
package main
import (
"fmt"
"net/http"
"os"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"status", "path"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
What does this code do?
- httpRequestsTotal is a Prometheus counter that tracks the total number of HTTP requests, labeled by status code and path. Argo Rollouts will query this metric to calculate your app's success rate.
- httpRequestDuration is a histogram that records how long each request takes to process. This gives you latency percentiles for monitoring.
- The init() function registers both metrics with Prometheus so they appear on the /metrics endpoint.
You won't see a change yet. The next chunk adds the main function that uses these metrics.
- Add the main function below the init function in main.go by pasting this code:
func main() {
version := os.Getenv("APP_VERSION")
if version == "" {
version = "1.0.0"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timer := prometheus.NewTimer(httpRequestDuration.WithLabelValues("/"))
defer timer.ObserveDuration()
httpRequestsTotal.WithLabelValues("200", "/").Inc()
fmt.Fprintf(w, "Hello from version %s\n", version)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
httpRequestsTotal.WithLabelValues("200", "/health").Inc()
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
})
http.Handle("/metrics", promhttp.Handler())
port := ":8080"
fmt.Printf("Starting server version %s on port %s\n", version, port)
if err := http.ListenAndServe(port, nil); err != nil {
fmt.Printf("Server failed: %v\n", err)
os.Exit(1)
}
}
What does this code do?
- APP_VERSION is read from an environment variable. This lets the Kubernetes manifest set the version string without rebuilding the image.
- The root handler / records both request count and duration before responding. These are the metrics Argo Rollouts will query during the canary analysis.
- The /health endpoint returns a simple ok response for Kubernetes readiness probes to confirm the pod is ready to receive traffic.
- The /metrics endpoint uses promhttp.Handler() to expose all registered metrics in Prometheus format. Prometheus scrapes this endpoint automatically.
- Save main.go.
Pasted the code but seeing red squiggles in your editor?
Your editor may flag import errors because the Go dependencies haven't been downloaded yet. This is expected. The dependencies will resolve after you create go.mod and run the download command in the next few steps.
help me fix Go import errors in my editor
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.go before moving on.
ⓧ I'd like to double check the full code
package main
import (
"fmt"
"net/http"
"os"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"status", "path"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
func main() {
version := os.Getenv("APP_VERSION")
if version == "" {
version = "1.0.0"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timer := prometheus.NewTimer(httpRequestDuration.WithLabelValues("/"))
defer timer.ObserveDuration()
httpRequestsTotal.WithLabelValues("200", "/").Inc()
fmt.Fprintf(w, "Hello from version %s\n", version)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
httpRequestsTotal.WithLabelValues("200", "/health").Inc()
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
})
http.Handle("/metrics", promhttp.Handler())
port := ":8080"
fmt.Printf("Starting server version %s on port %s\n", version, port)
if err := http.ListenAndServe(port, nil); err != nil {
fmt.Printf("Server failed: %v\n", err)
os.Exit(1)
}
}
- Create a new file called go.mod in your gitops-canary-demo folder.
- Paste in the following module definition to declare the project's dependencies:
module gitops-canary-demo
go 1.22
require github.com/prometheus/client_golang v1.20.5
require (
github.com/beorn7/perp v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
google.golang.org/protobuf v1.36.1 // indirect
)
What is go.mod?
The go.mod file declares the module name, the Go version, and all dependencies. The direct dependency is prometheus/client_golang which provides the metrics library. The indirect dependencies are packages that client_golang itself depends on.
The Dockerfile uses this file to download dependencies before copying your source code. This layer-caching strategy means Docker only re-downloads packages when go.mod changes.
- Save go.mod.
- Generate the go.sum checksum file by running this command:
docker run --rm -v "${PWD}:/app" -w /app golang:1.22-alpine go mod download
Why use Docker to generate go.sum?
You don't need Go installed locally for this project. The Docker container uses the same golang:1.22-alpine image that your Dockerfile uses to build the app. Running go mod download inside the container downloads all dependencies and writes their checksums to go.sum on your local filesystem via the volume mount.
- Confirm the file was generated by running:
ls
You should see go.mod, go.sum, and main.go listed in the output.
Don't see go.sum?
Make sure Docker Desktop is running. The command needs Docker to pull the golang:1.22-alpine image and execute go mod download inside a container.
If you see a permissions error, check that the volume mount path is correct. On Windows PowerShell, ${PWD} should resolve to your current directory.
help me troubleshoot the Docker volume mount for generating go.sum
Write the Dockerfile
A multi-stage Dockerfile keeps your final image small and secure. The first stage compiles the Go binary, and the second stage copies only the binary into a minimal Alpine image running as a non-root user.
- Create a new file called Dockerfile in your gitops-canary-demo folder.
- Add the following multi-stage build configuration:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM alpine:3.20
RUN adduser -D -u 1000 appuser
COPY --from=builder /app/server /server
USER appuser
EXPOSE 8080
ENTRYPOINT ["/server"]
What does this Dockerfile do?
- The builder stage uses golang:1.22-alpine to compile a static binary. Copying go.mod and go.sum first lets Docker cache the dependency download layer.
- CGO_ENABLED=0 produces a fully static binary with no C library dependencies. This means the final image doesn't need any build tools.
- The final stage uses alpine:3.20 (a tiny base image) and creates a non-root user appuser with UID 1000. Running as non-root is a security best practice that Trivy and other scanners check for.
- Only the compiled binary is copied from the builder stage. No source code, no build tools, no package manager in the final image.
- Save Dockerfile.
- Confirm all four files exist by running:
ls
You should see Dockerfile, go.mod, go.sum, and main.go in the output.
Missing a file?
Make sure you created each file inside the gitops-canary-demo folder, not in a parent directory. Run pwd to confirm you are in the correct directory.
If go.sum is missing, re-run the docker run command from the previous step. Make sure Docker Desktop is running.
help me figure out why my project files are missing
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before pushing to GitHub.
ⓧ I'd like to double check the full code
Here is the full Dockerfile for comparison:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM alpine:3.20
RUN adduser -D -u 1000 appuser
COPY --from=builder /app/server /server
USER appuser
EXPOSE 8080
ENTRYPOINT ["/server"]
Push the code to GitHub
- Commit all files and push them to your GitHub repository by running these commands:
git add .
git commit -m "Add Go application and Dockerfile"
git branch -M main
git push -u origin main
What do these commands do?
- git add . stages all four files for commit.
- git branch -M main ensures your local branch is named main to match GitHub's default branch. This is important because the ArgoCD Application you create later targets the main branch.
- git push -u origin main uploads your code to GitHub and sets origin/main as the upstream tracking branch.
- Open your browser and navigate to your repository at https://github.com/ followed by your username and /gitops-canary-demo.
- Verify you can see Dockerfile, go.mod, go.sum, and main.go listed on the repository page.
Push rejected or authentication failed?
If you see an authentication error, GitHub requires a Personal Access Token (PAT) for HTTPS pushes. Generate one at github.com/settings/tokens with the repo scope, then use it as your password when prompted.
If you see a 'rejected' error mentioning that the remote contains work you don't have, your GitHub repo may have been initialized with a README. Run git pull origin main --allow-unrelated-histories first, then push again.
help me push my code to GitHub
Your application code is ready on GitHub. Next up, you'll create a CI pipeline with GitHub Actions that builds this code into a Docker image, scans it for security vulnerabilities, and publishes it to GitHub Container Registry.
Build the CI Pipeline with GitHub Actions
You have a containerized Go app and a Dockerfile sitting in your GitHub repo. Right now, building and pushing images is a manual process.
In this step, you will create a GitHub Actions workflow that automates three things every time you push code: building the Docker image, scanning it for security vulnerabilities with Trivy, and publishing it to GitHub Container Registry. This gives ArgoCD a fresh, verified image to deploy on every commit.
In this step, get ready to:
- Create a GitHub Actions workflow that builds, scans, and pushes your container image.
- Configure the workflow to trigger automatically on pushes to main.
- Verify the pipeline runs and the image appears in GitHub Packages.
Create the CI workflow file
GitHub Actions workflows live in a special directory at the root of your repository. Any .yaml file inside .github/workflows/ becomes a workflow that GitHub can run.
- In your terminal, make sure you are inside your gitops-canary-demo folder, then create the workflow directory by running this command:
mkdir -p .github/workflows
- Create a new file at .github/workflows/ci.yaml and paste the following workflow content:
name: CI Pipeline
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-scan-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
run: docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.36.0
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
- name: Push Docker image
run: docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
What does this workflow do?
This workflow defines a single job called build-scan-push that runs on every push to the main branch.
- The permissions block grants the workflow read access to your code and write access to GitHub Packages. This is what allows it to push images to ghcr.io without any extra tokens.
- The Build Docker image step tags the image with the commit SHA, giving every build a unique, traceable identifier.
- The Run Trivy vulnerability scanner step scans the built image for CRITICAL and HIGH severity vulnerabilities. If any are found, exit-code: '1' causes the workflow to fail, blocking the push.
- The Push Docker image step only runs if Trivy passes. This ensures only security-verified images reach your registry.
Why scan before pushing?
Trivy acts as a security gate in your pipeline. If it finds a CRITICAL or HIGH vulnerability in your image's OS packages or Go libraries, the workflow fails before the image is published. This means ArgoCD can never accidentally deploy a known-vulnerable image to your cluster.
- Save .github/workflows/ci.yaml.
You will not see a change yet. The workflow only triggers when you push to GitHub.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file at .github/workflows/ci.yaml.
ⓧ I'd like to double check the full code
name: CI Pipeline
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-scan-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
run: docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.36.0
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
- name: Push Docker image
run: docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
Push to GitHub and verify the pipeline
Pushing the workflow file to the main branch is what triggers it. GitHub detects the new workflow and starts the build-scan-push job automatically.
- Commit and push the new workflow file by running these commands:
git add .github/workflows/ci.yaml
git commit -m "Add CI pipeline with Trivy scanning"
git push origin main
- Open your browser and go to your gitops-canary-demo repository on GitHub.
- Click the Actions tab at the top of the repository page.
- You should see a workflow run named CI Pipeline in progress or completed. Click on it to view the details.
- Click the build-scan-push job to expand its steps and confirm each one passed with a green checkmark.
What should I see?
Each step in the job should show a green checkmark. The Trivy step will output a table of scanned packages. If no CRITICAL or HIGH vulnerabilities are found, the pipeline proceeds to push the image.
The entire run typically takes 1-3 minutes depending on image build time.
- Verify your published image by looking at the right sidebar of your repository's main page on GitHub.
- Click Packages in the right sidebar. You should see your gitops-canary-demo container image listed there with a tag matching your latest commit SHA.
Pipeline failed or no image in Packages?
- If the Trivy step failed, check the output table for the vulnerability it flagged. You may need to update the base image in your Dockerfile. For this project, alpine:3.20 should pass cleanly.
- If the push step shows a permission error, confirm your repository is set to Public under Settings on GitHub. The permissions: packages: write block in your workflow handles the token scope automatically.
- If you do not see the Actions tab at all, make sure your push actually reached GitHub by running git log --oneline -1 and comparing the SHA with what GitHub shows.
Why is my CI pipeline failing on GitHub Actions?
Your CI pipeline is live. Every push to main now builds, scans, and publishes a verified container image. Next up, you will create the Kubernetes manifests that tell ArgoCD and Argo Rollouts how to deploy this image using a canary strategy.
Deploy with ArgoCD and Argo Rollouts Canary Strategy
Your CI pipeline builds a Docker image, scans it for vulnerabilities, and pushes it to GitHub Container Registry. Now you need to tell Kubernetes how to deploy that image with progressive delivery.
Standard Kubernetes Deployments use a rolling update strategy. If a bug ships, 100% of traffic is affected before anyone notices. In this step, you'll create manifests that use Argo Rollouts' canary strategy to gradually shift traffic to new versions while Prometheus metrics decide whether to promote or roll back automatically.
In this step, get ready to:
- Create Kubernetes manifests defining the canary Rollout, Services, and AnalysisTemplate.
- Create an ArgoCD Application manifest that syncs your repo to the cluster.
- Apply the ArgoCD Application and verify the initial deployment is healthy.
Create the Kubernetes manifests
A canary deployment sends a small percentage of traffic to the new version first. If metrics stay healthy, the percentage increases until the new version handles everything.
- Create a manifests directory inside your gitops-canary-demo project folder by running this command:
mkdir manifests
- Confirm the directory was created by running:
ls
You should see manifests listed alongside your existing files.
- Create a new file called rollout.yaml inside the manifests folder with the following content:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: gitops-canary-demo
namespace: default
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: gitops-canary-demo
template:
metadata:
labels:
app: gitops-canary-demo
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: gitops-canary-demo
image: ghcr.io/[[GITHUB_USERNAME="your-github-username"]]/gitops-canary-demo:latest
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "1.0.0"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
strategy:
canary:
canaryService: gitops-canary-demo-canary
stableService: gitops-canary-demo-stable
analysis:
templates:
- templateName: success-rate
startingStep: 1
steps:
- setWeight: 20
- pause: {duration: 30s}
- setWeight: 50
- pause: {duration: 30s}
- setWeight: 80
- pause: {duration: 30s}
What does this manifest do?
This is a Rollout resource (from Argo Rollouts), not a standard Kubernetes Deployment. It defines how your app deploys progressively.
- The strategy.canary.steps section sends 20% of traffic to the new version, pauses 30 seconds for analysis, then increases to 50%, then 80%.
- The analysis.templates section references a success-rate AnalysisTemplate that checks Prometheus metrics at each step.
- The canaryService and stableService fields tell Argo Rollouts which Services handle new-version traffic vs current-version traffic.
- The Prometheus annotations ensure your pods' metrics are scraped automatically.
- Save manifests/rollout.yaml.
YAML indentation errors?
YAML is whitespace-sensitive. Make sure you use spaces (not tabs) and that nested keys are indented by exactly 2 spaces relative to their parent.
help me fix YAML indentation in my rollout.yaml
- Create a new file called services.yaml inside the manifests folder with the following content:
apiVersion: v1
kind: Service
metadata:
name: gitops-canary-demo-stable
namespace: default
spec:
selector:
app: gitops-canary-demo
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: gitops-canary-demo-canary
namespace: default
spec:
selector:
app: gitops-canary-demo
ports:
- port: 80
targetPort: 8080
Why two Services?
Argo Rollouts needs two separate Services to split traffic during a canary rollout. The stable Service always points to the current proven version. The canary Service points to the new version being tested.
During a rollout, Argo Rollouts adjusts how much traffic flows to each Service based on the setWeight steps in your Rollout.
- Save manifests/services.yaml.
- Create a new file called analysis-template.yaml inside the manifests folder with the following content:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: default
spec:
metrics:
- name: success-rate
interval: 10s
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus-kube-prometheus-prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{status="200",app="gitops-canary-demo"}[1m]))
/
sum(rate(http_requests_total{app="gitops-canary-demo"}[1m]))
What does the AnalysisTemplate check?
This template tells Argo Rollouts how to decide if the canary is healthy. Every 10 seconds, it queries Prometheus for the ratio of successful HTTP requests (status 200) to total requests.
If the success rate drops below 95% (result[0] >= 0.95) three times in a row (failureLimit: 3), Argo Rollouts automatically aborts the rollout and shifts all traffic back to the stable version.
- Save manifests/analysis-template.yaml.
- Confirm all three files exist by running:
ls manifests/
You should see analysis-template.yaml, rollout.yaml, and services.yaml listed.
Missing a file?
Double-check you created each file inside the manifests folder, not in the project root. The filenames must be lowercase with hyphens.
help me figure out why my manifest files are in the wrong location
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is manifests/rollout.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: gitops-canary-demo
namespace: default
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: gitops-canary-demo
template:
metadata:
labels:
app: gitops-canary-demo
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: gitops-canary-demo
image: ghcr.io/[[GITHUB_USERNAME="your-github-username"]]/gitops-canary-demo:latest
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "1.0.0"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
strategy:
canary:
canaryService: gitops-canary-demo-canary
stableService: gitops-canary-demo-stable
analysis:
templates:
- templateName: success-rate
startingStep: 1
steps:
- setWeight: 20
- pause: {duration: 30s}
- setWeight: 50
- pause: {duration: 30s}
- setWeight: 80
- pause: {duration: 30s}
Here is manifests/services.yaml:
apiVersion: v1
kind: Service
metadata:
name: gitops-canary-demo-stable
namespace: default
spec:
selector:
app: gitops-canary-demo
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: gitops-canary-demo-canary
namespace: default
spec:
selector:
app: gitops-canary-demo
ports:
- port: 80
targetPort: 8080
Here is manifests/analysis-template.yaml:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: default
spec:
metrics:
- name: success-rate
interval: 10s
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus-kube-prometheus-prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{status="200",app="gitops-canary-demo"}[1m]))
/
sum(rate(http_requests_total{app="gitops-canary-demo"}[1m]))
Create the ArgoCD Application manifest
ArgoCD needs an Application resource that tells it where your manifests live in Git and where to deploy them in the cluster.
- Create a new file called argocd-application.yaml inside the manifests folder with the following content:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: gitops-canary-demo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/gitops-canary-demo.git
targetRevision: main
path: manifests
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
What does this Application do?
This is the heart of GitOps. The Application resource tells ArgoCD to watch the manifests directory on the main branch of your GitHub repo.
- automated.prune deletes resources from the cluster if their manifests are removed from Git.
- automated.selfHeal reverts any manual changes made directly to the cluster, keeping Git as the single source of truth.
- Whenever you push a change to manifests/ on main, ArgoCD detects the drift and automatically syncs the cluster to match.
- Save manifests/argocd-application.yaml.
Not sure about your repo URL?
The repoURL must match your GitHub repository URL exactly, including your username in lowercase. You can find it on your GitHub repo page by clicking the green Code button and copying the HTTPS URL.
help me find my GitHub repository URL for ArgoCD
Push manifests and apply the ArgoCD Application
ArgoCD watches your Git repository, so the manifests need to be pushed to GitHub before ArgoCD can sync them.
- Stage, commit, and push your new manifests to GitHub by running these commands:
git add manifests/
git commit -m "Add Kubernetes manifests for canary deployment"
git push origin main
What does this push include?
This commit adds your four manifest files: the Rollout, two Services, the AnalysisTemplate, and the ArgoCD Application definition. Once pushed, ArgoCD can read them from GitHub.
- Apply the ArgoCD Application to your cluster by running:
kubectl apply -f manifests/argocd-application.yaml
Why apply this directly instead of letting ArgoCD discover it?
The ArgoCD Application resource is a bootstrapping object. It tells ArgoCD what to watch. You apply it once with kubectl to register the application. After that, ArgoCD handles all syncing automatically.
- Check that ArgoCD has picked up and synced your application by running:
argocd app get gitops-canary-demo
You should see the application status showing Synced and Healthy. This means ArgoCD read your manifests from GitHub and deployed them to the cluster.
- Verify the Rollout is running by checking it with the Argo Rollouts plugin:
kubectl argo rollouts get rollout gitops-canary-demo --watch
You should see a tree view showing 3 pods running under a single ReplicaSet. The status should show Healthy with all replicas available. Press Ctrl+C to stop watching.
- Confirm the Services were created by running:
kubectl get services
You should see both gitops-canary-demo-stable and gitops-canary-demo-canary listed.
Application not syncing?
If the application shows OutOfSync or Unknown, check these common causes:
- Make sure your GitHub repository is public. ArgoCD needs read access to the repo.
- Verify the repoURL in argocd-application.yaml matches your actual GitHub repo URL (including correct username casing).
- Confirm you pushed the manifests to the main branch by running git log --oneline -1 and checking it matches what GitHub shows.
help me debug why ArgoCD is not syncing my application
Your GitOps pipeline is now live. ArgoCD is watching your repo, and your canary strategy is ready to progressively roll out changes. Next up, you'll trigger a canary rollout and watch Argo Rollouts execute the traffic-shifting steps in real time.
Trigger a Canary Rollout and Observe Progressive Delivery
Your pipeline is fully wired. Code gets built, scanned, and pushed to a container registry. ArgoCD syncs manifests from Git to your cluster. Argo Rollouts manages how traffic shifts to new versions.
Now it is time to see the entire flow in action. You will push a version change that triggers the CI pipeline, and then watch Argo Rollouts execute a canary deployment with real Prometheus metrics deciding whether to promote or roll back.
In this step, get ready to:
- Update the application to version 2.0.0 and push to trigger the CI pipeline.
- Watch ArgoCD detect the change and Argo Rollouts execute canary traffic steps.
- Observe the AnalysisRun checking Prometheus metrics and automatically promoting the rollout.
Update the application version and push to GitHub
To trigger a canary rollout, you need to change the pod template spec in your Rollout resource. Changing the APP_VERSION environment variable is enough to trigger Argo Rollouts to create a new ReplicaSet and start the canary process.
- In main.go, find the fallback version line inside the main() function:
version = "1.0.0"
- Change "1.0.0" to "2.0.0", so the line now looks like this:
version = "2.0.0"
- In manifests/rollout.yaml, find the APP_VERSION environment variable value:
- name: APP_VERSION
value: "1.0.0"
- Change the value from "1.0.0" to "2.0.0":
- name: APP_VERSION
value: "2.0.0"
- Commit and push both changes to GitHub by running these commands:
git add main.go manifests/rollout.yaml
git commit -m "Bump version to 2.0.0"
git push origin main
What does this trigger?
Pushing to main triggers your GitHub Actions CI pipeline. It builds a new Docker image tagged with the commit SHA, scans it with Trivy, and pushes it to ghcr.io.
Separately, ArgoCD is watching the manifests/ directory in your repo. When it detects the updated APP_VERSION value in rollout.yaml, it syncs the change to the cluster. This updated pod template spec tells Argo Rollouts to start a canary rollout.
- Open your GitHub repository in the browser and click the Actions tab.
- Verify the CI workflow is running (or has completed with a green checkmark).
CI pipeline failed?
- Check that go.mod and go.sum are committed and up to date. Missing dependency files cause the Docker build to fail.
- If Trivy reports a HIGH or CRITICAL vulnerability, it will exit with code 1 and block the push. Check the scan output for details on which package is affected.
- Make sure your repository is public. Private repositories require additional authentication configuration for ghcr.io.
Help me debug my CI pipeline failure
Watch ArgoCD detect the change and execute canary steps
ArgoCD polls your Git repository on a regular interval. Once it detects the manifest change, it syncs the new rollout.yaml to the cluster. Argo Rollouts then sees the pod template change and starts the canary process.
- Start watching the rollout in real time by running this command:
kubectl argo rollouts get rollout gitops-canary-demo --watch
What does the watch output show?
The watch command displays a live tree view of your Rollout resource. You will see ReplicaSets (old and new), Pods, and AnalysisRuns as child resources. As the canary progresses, the output updates to show weight percentages and step progress.
Your canary strategy has these steps: 20% traffic to canary, pause 30 seconds, 50% traffic, pause 30 seconds, 80% traffic, pause 30 seconds, then full promotion to 100%.
The AnalysisRun needs real HTTP traffic to evaluate the success rate metric. Without requests flowing, the Prometheus query returns no data and the analysis cannot pass.
- Open a second terminal window and generate traffic to the canary service by running this command:
kubectl port-forward svc/gitops-canary-demo-canary 8081:80
- Open a third terminal window and send repeated requests by running this command:
for i in $(seq 1 100); do curl -s http://localhost:8081; sleep 1; done
Why generate traffic?
Your AnalysisTemplate queries Prometheus for the ratio of successful (status 200) requests to total requests. If there are zero requests, the query returns no data and the analysis cannot determine a success rate. The curl loop sends steady traffic so Prometheus has metrics to evaluate.
- Switch back to your first terminal where the watch is running.
- You should see the rollout progressing through the canary steps: 20% weight, then after 30 seconds, 50%, then 80%, then finally full promotion.
Rollout not progressing?
- If the rollout stays at the initial state, ArgoCD may not have synced yet. Check with argocd app get gitops-canary-demo to see if the sync status is OutOfSync or Synced.
- If the rollout started but is stuck at a weight step, the AnalysisRun may be failing. Check the next substep to inspect the AnalysisRun status.
- Make sure your traffic generation loop is running. Without requests, the analysis cannot evaluate the success rate.
Help me debug my canary rollout
Observe the AnalysisRun and confirm full promotion
As the canary progresses, Argo Rollouts creates an AnalysisRun resource that queries Prometheus at 10-second intervals. If the success rate stays at or above 0.95, the analysis passes and the rollout continues to the next step.
- Check the AnalysisRun status by running this command:
kubectl get analysisrun -n default
- You should see an AnalysisRun with a status of Successful (or Running if the rollout is still in progress).
How does the analysis decide?
Your success-rate AnalysisTemplate runs a Prometheus query every 10 seconds. It checks that at least 95% of requests return a 200 status code. If 3 consecutive measurements fail (failureLimit: 3), the rollout is automatically aborted and traffic shifts back to the stable version.
Since your application returns 200 for every valid request, the analysis passes and the rollout is promoted through all weight steps to 100%.
- Once the rollout completes, verify all pods are running the new version by running:
kubectl get pods -l app=gitops-canary-demo -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].env[0].value}{"\n"}{end}'
- You should see all 3 pods showing 2.0.0 as the APP_VERSION value.
- Stop the traffic generation loop in your third terminal with Ctrl+C.
- Stop the port-forward in your second terminal with Ctrl+C.
AnalysisRun shows Failed?
- If the analysis failed, it means the success rate dropped below 0.95. This could happen if your traffic loop was not running, or if the canary pods failed readiness probes.
- Check the AnalysisRun details with kubectl get analysisrun -n default -o yaml and look at the metricResults section for the actual measured values.
- If the rollout was aborted, you can retry by pushing another commit (even a no-op change to the version env var) to trigger a fresh rollout.
Help me debug my failed AnalysisRun
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files and the rollout is fully promoted to version 2.0.0.
ⓧ I'd like to double check the full code
Here is the updated main.go with the version change:
package main
import (
"fmt"
"net/http"
"os"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"status", "path"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
func main() {
version := os.Getenv("APP_VERSION")
if version == "" {
version = "2.0.0"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timer := prometheus.NewTimer(httpRequestDuration.WithLabelValues("/"))
defer timer.ObserveDuration()
httpRequestsTotal.WithLabelValues("200", "/").Inc()
fmt.Fprintf(w, "Hello from version %s\n", version)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
httpRequestsTotal.WithLabelValues("200", "/health").Inc()
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
})
http.Handle("/metrics", promhttp.Handler())
port := ":8080"
fmt.Printf("Starting server version %s on port %s\n", version, port)
if err := http.ListenAndServe(port, nil); err != nil {
fmt.Printf("Server failed: %v\n", err)
os.Exit(1)
}
}
Here is the updated manifests/rollout.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: gitops-canary-demo
namespace: default
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: gitops-canary-demo
template:
metadata:
labels:
app: gitops-canary-demo
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: gitops-canary-demo
image: ghcr.io/YOUR_GITHUB_USERNAME/gitops-canary-demo:latest
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "2.0.0"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
strategy:
canary:
canaryService: gitops-canary-demo-canary
stableService: gitops-canary-demo-stable
analysis:
templates:
- templateName: success-rate
startingStep: 1
steps:
- setWeight: 20
- pause: {duration: 30s}
- setWeight: 50
- pause: {duration: 30s}
- setWeight: 80
- pause: {duration: 30s}
You have just witnessed the full GitOps progressive delivery loop. A code change flowed from commit to container to canary deployment, with Prometheus metrics automatically deciding the promotion. Next up, stretch your skills with the Secret Mission.
Secret mission
Blue-Green Strategy and Notifications
You have built a canary deployment pipeline that gradually shifts traffic. But canary is not the only progressive delivery strategy senior DevOps engineers need to know. Blue-green deployments run two full environments simultaneously, switching traffic instantly after validation. Add a blue-green strategy and webhook notifications to your pipeline for complete progressive delivery coverage.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local Minikube cluster and uses GitHub's free tier for public repositories. There are no ongoing cloud costs.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Minikube cluster (includes ArgoCD, Argo Rollouts, Prometheus, and all Kubernetes resources).
- Local project files (the gitops-canary-demo folder on your Desktop).
- GitHub repository (gitops-canary-demo).
- GitHub Container Registry images (ghcr.io packages).
- ArgoCD CLI binary (downloaded .exe file).
- Argo Rollouts kubectl plugin binary (downloaded .exe file).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your pipeline.
- Your Minikube cluster will continue running and using system resources (CPU and RAM) until you stop it.
- The ArgoCD CLI and Argo Rollouts plugin remain available on your PATH.
- Your GitHub repository and container images remain accessible at no cost.
Pause - I'll come back to this later
Stop the Minikube cluster to free up CPU and memory. Your cluster configuration and data are preserved so you can restart later.
- Stop the Minikube cluster by running:
minikube stop
- When you are ready to come back, start it again by running:
minikube start
- Your GitHub repository, container images, CLI binaries, and local files all remain in place.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete the Minikube cluster (this removes the VM, all Kubernetes resources, ArgoCD, Argo Rollouts, and Prometheus) by running:
minikube delete
- Delete your local project files by running:
cd ~/Desktop
rmdir /s /q gitops-canary-demo
macOS and Linux users
Use this command instead:
rm -rf ~/Desktop/gitops-canary-demo
- Delete the ArgoCD CLI binary. Remove the argocd.exe file from the folder you placed it in (the folder you added to your PATH).
- Delete the Argo Rollouts kubectl plugin binary. Remove the kubectl-argo-rollouts.exe file from the folder you placed it in.
- Delete the GitHub Container Registry images. Navigate to your repository on GitHub, click the Packages section in the right sidebar, select the container image, then click Package settings, scroll to the Danger Zone, and click Delete this package.
- Delete the GitHub repository. Go to your repository page, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Confirm by typing the repository name.
Nice Work!
Nice Work!
You just built a complete GitOps progressive delivery pipeline with automated canary deployments driven by real metrics. This is the exact deployment pattern that senior DevOps job postings demand in 2026.
You've learned how to:
- Build a CI pipeline with GitHub Actions that builds, security-scans with Trivy, and publishes container images to GitHub Container Registry.
- Deploy ArgoCD for GitOps-based continuous delivery that automatically syncs from Git to Kubernetes.
- Configure Argo Rollouts with a canary strategy driven by Prometheus metrics for automated promotion and rollback.
- Added blue-green deployments and webhook notifications for complete progressive delivery coverage.
Ready to quiz yourself?