Deploying independent services safely is less about preventing all failures and more about ensuring that when they happen, the blast radius is contained and recovery is swift.
Imagine you’ve got a fleet of microservices, each doing its own thing. You’ve just pushed a change to service-a, and you want to get it to production without taking down service-b, service-c, or the entire damn company. This is the core problem CI/CD for microservices aims to solve: enabling frequent, independent deployments while maintaining stability.
Here’s a simplified, but functional, deployment pipeline. We’ll use a containerized application (Docker) orchestrated by Kubernetes, with Git as our source control and GitHub Actions for CI/CD.
# GitHub Actions workflow for deploying a microservice
name: Deploy Microservice
on:
push:
branches:
- main # Trigger on pushes to the main branch
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: your-dockerhub-username/my-microservice:${{ github.sha }}
deploy-to-staging:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.25.0' # Specify your kubectl version
- name: Configure Kubectl
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG_STAGING }}" > $HOME/.kube/config
- name: Deploy to Staging
run: |
kubectl apply -f k8s/staging/deployment.yaml
kubectl apply -f k8s/staging/service.yaml
# Manual approval step for production
promote-to-production:
needs: deploy-to-staging
runs-on: ubuntu-latest
environment: production # This links to an environment in GitHub settings
steps:
- name: Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: user1,user2 # GitHub usernames who can approve
minimum-approvals: 1
exclude-workflow-initiator-as-approver: false
- name: Set up Kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.25.0'
- name: Configure Kubectl
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG_PRODUCTION }}" > $HOME/.kube/config
- name: Deploy to Production
run: |
kubectl apply -f k8s/production/deployment.yaml
kubectl apply -f k8s/production/service.yaml
This workflow does a few key things:
- Build and Push: It checks out your code, builds a Docker image tagged with the unique Git commit SHA, and pushes it to a container registry (Docker Hub in this example). This ensures immutability – each deployment artifact is traceable to a specific commit.
- Deploy to Staging: It then deploys this image to a staging Kubernetes environment. This is where you’d run your automated tests (unit, integration, end-to-end).
- Manual Approval: Crucially, it includes a manual approval step before production. This gatekeeper is vital for microservices; you don’t want to blindly push changes that could break downstream dependencies.
- Deploy to Production: After approval, the same immutable Docker image is deployed to your production Kubernetes cluster.
The mental model here is about controlled progression. Each stage acts as a filter. You start with code, turn it into an immutable artifact (the Docker image), test it in a safe environment, get human confirmation, and then deploy it to production.
The problem this solves is deployment risk. In a monolith, a bad deploy can halt everything. With microservices, a bad deploy to service-a should only affect service-a and its direct consumers. The CI/CD pipeline ensures this by isolating deployments to individual services and using strategies like rolling updates or blue/green deployments within Kubernetes.
What are the levers you control?
- Branching Strategy:
mainfor production-ready code, feature branches for development. - Tagging: Using the Git SHA for immutable image tags is paramount. Never deploy
latest. - Environments: Distinct staging and production Kubernetes clusters (or namespaces) with appropriate configurations.
- Approval Gates: Who can approve, how many approvals are needed, and when are they triggered.
- Deployment Strategy: Kubernetes
Deploymentobjects, configured for rolling updates (default), or you could implement blue/green or canary using additional tooling. - Testing: Integrating automated tests at various stages (e.g., after staging deploy).
The most surprising thing about microservices CI/CD is how much testing shifts left. You can’t rely on a single, massive integration test suite at the end. Instead, each service needs robust unit tests, and you need contract testing between services to ensure they can still communicate after independent deployments.
The next concept you’ll grapple with is managing the complexity of inter-service communication and dependency management during deployments.