As microservice architectures continue to dominate modern software development, the complexity of managing individual service lifecycles grows exponentially. While many teams rush toward Kubernetes for orchestration, a significant portion of the industry still relies on simpler deployment targets such as virtual machines, PaaS offerings (like AWS Elastic Beanstalk or Heroku), or bare-metal servers. For these environments, the CI/CD pipeline must not only automate builds but also handle deployment logic, environment configuration, and rollback strategies with precision.
GitHub Actions offers a powerful, code-centric approach to continuous integration and continuous delivery (CI/CD). In this post, we will explore how to construct a robust pipeline for non-Kubernetes microservices, focusing on modularity, security, and reliability.
The Philosophy of Modular Pipelines
One of the most common mistakes developers make is hardcoding every step of the deployment process into a single monolithic YAML file. For a microservice architecture, this approach becomes unmanageable quickly. Instead, we should treat our GitHub Actions workflows as composable units.
By leveraging reusable workflows and environment-specific configuration, we can maintain a single source of truth for our build logic while allowing deployment targets to vary. This is particularly crucial for non-Kubernetes environments where infrastructure-as-code might be managed differently across services (e.g., some using Terraform, others using simple shell scripts).
Core Components of the Pipeline
A robust pipeline for non-Kubernetes services typically consists of three distinct phases: Build, Test, and Deploy. Let's look at how to structure a GitHub Actions workflow that encapsulates these phases efficiently.
1. The Build and Test Phase
The foundation of any CI/CD pipeline is speed and reliability. We want to fail fast if tests break. Using GitHub Actions, we can parallelize test execution to reduce feedback loops.
Here is an example of a workflow that handles dependency installation, linting, and unit testing:
name: Build and Test
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run unit tests
run: npm test -- --coverage
2. Containerization for Consistency
Even if you are not using Kubernetes, containerization remains the gold standard for ensuring consistency between development, staging, and production environments. Using Docker allows your microservice to run predictably regardless of the underlying host OS.
We can integrate Docker build and push directly into the pipeline. This ensures that the artifact deployed is the exact same one that passed all tests.
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
3. Environment-Specific Deployment
For non-Kubernetes services, deployment often involves SSHing into a server or calling an API of a PaaS provider. Security is paramount here. We should never hardcode credentials. Instead, use GitHub Secrets combined with environment protection rules.
Let's assume we are deploying to a Linux server via SSH. We can use the `appleboy/ssh-action` to execute deployment scripts.
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Staging Server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/my-service
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker-compose up -d --no-deps my-service
# Perform health check
curl -f http://localhost:8080/health || exit 1
Best Practices for Resilience
- Idempotency: Ensure your deployment scripts can be run multiple times without side effects. Docker makes this easier, but application-level idempotency is still required for database migrations.
- Rollback Strategy: In non-Kubernetes environments, rolling back can be manual. Automate this by tagging Docker images with semantic versions and keeping previous versions available. If a health check fails, the pipeline should automatically trigger a rollback workflow.
- Secret Management: Rotate your SSH keys and API tokens regularly. Use GitHub Environments to restrict who can approve deployments to production.
Conclusion
Building CI/CD pipelines for non-Kubernetes microservices requires a shift in mindset. Instead of relying on the orchestration engine to manage rollouts and rollbacks, the responsibility shifts entirely to the pipeline itself. By leveraging GitHub Actions' modular nature, containerization, and secure secret management, you can create a deployment system that is as robust and scalable as its Kubernetes counterparts. Embrace automation, prioritize security, and always validate your deployments with automated health checks.