CI/CD and DevOps Architecture
Purpose
This document defines the CI/CD pipelines, infrastructure-as-code strategy, and deployment patterns for the Farmer1st platform mono-repo.
Current State
┌─────────────────────────────────────────────────────────────────────────────┐
│ CI/CD & DEVOPS STACK │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ SOURCE & CI IaC DEPLOYMENT │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│
│ │ │ │ │ │ ││
│ │ GitHub │ │ Terraform Cloud │ │ ArgoCD ││
│ │ (Mono-repo) │ │ │ │ ││
│ │ │ │ • AWS infra │ │ • GitOps ││
│ │ • Single repo │ │ • Cloudflare │ │ • K8s deploys ││
│ │ • Actions (CI) │ │ • State mgmt │ │ • Sync & rollback│
│ │ • GHCR images │ │ │ │ ││
│ │ │ │ │ │ ││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Decisions and Rationale
Mono-Repo CI Strategy
| Decision |
Rationale |
| Path-based triggers |
Only build/test what changed; efficient CI |
| Affected detection |
Understand dependency graph; rebuild dependents when shared code changes |
| Single pipeline file |
Easier to maintain; consistent patterns |
| Parallel jobs |
Fast feedback; build affected services in parallel |
Source Control & CI
| Decision |
Choice |
Rationale |
| Source Control |
GitHub (mono-repo) |
Single source of truth for everything |
| CI Pipeline |
GitHub Actions |
Native integration, path filtering, matrix builds |
| Container Registry |
GHCR |
Unified with source, simple auth |
Infrastructure as Code
| Decision |
Choice |
Rationale |
| IaC Tool |
Terraform |
Industry standard, excellent AWS/Cloudflare providers |
| State Management |
Terraform Cloud |
Managed state, team collaboration |
| Location |
In mono-repo (/infra/terraform/) |
AI sees infra + code together |
Deployment
| Decision |
Choice |
Rationale |
| Kubernetes Deployment |
ArgoCD |
GitOps pattern, declarative, automatic sync |
| GitOps Location |
In mono-repo (/infra/k8s/) |
Atomic code + manifest changes |
| Deployment Model |
GitOps |
Git as source of truth, auditable |
CI/CD Pipeline Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ MONO-REPO CI/CD PIPELINE FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Developer │ │
│ │ pushes code │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ GITHUB ACTIONS │ │
│ │ │ │
│ │ 1. DETECT CHANGES │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ What paths changed? │ │ │
│ │ │ │ │ │
│ │ │ apps/farmer-pwa/* → build farmer-pwa │ │ │
│ │ │ services/surveys/* → build all surveys services │ │ │
│ │ │ packages/shared-types/* → build ALL dependents │ │ │
│ │ │ infra/terraform/* → run terraform plan │ │ │
│ │ │ infra/k8s/overlays/prod/* → require approval │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 2. BUILD & TEST (parallel) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ farmer-pwa │ │ surveys-bff│ │ survey-svc │ ... │ │
│ │ │ lint, test │ │ lint, test │ │ lint, test │ │ │
│ │ │ build │ │ build │ │ build │ │ │
│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ 3. PUSH IMAGES TO GHCR │ │
│ │ ghcr.io/farmer1st/farmer-pwa:sha-abc123 │ │
│ │ ghcr.io/farmer1st/surveys-bff:sha-abc123 │ │
│ │ ghcr.io/farmer1st/survey-service:sha-abc123 │ │
│ │ │ │
│ │ 4. UPDATE K8S MANIFESTS │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ infra/k8s/overlays/dev/surveys/kustomization.yaml │ │ │
│ │ │ images: │ │ │
│ │ │ - name: surveys-bff │ │ │
│ │ │ newTag: sha-abc123 ← Updated automatically │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ ARGOCD │ │
│ │ │ │
│ │ Watches: infra/k8s/overlays/{env}/* │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Dev Cluster │ │Staging Clstr│ │ Prod Cluster│ │ │
│ │ │ Auto-sync │ │ Auto-sync │ │ Manual sync │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
GitHub Actions Workflows
Main CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ==========================================
# DETECT WHAT CHANGED
# ==========================================
changes:
runs-on: ubuntu-latest
outputs:
farmer-pwa: ${{ steps.filter.outputs.farmer-pwa }}
stakeholder-portal: ${{ steps.filter.outputs.stakeholder-portal }}
user-management: ${{ steps.filter.outputs.user-management }}
access-management: ${{ steps.filter.outputs.access-management }}
surveys: ${{ steps.filter.outputs.surveys }}
payments: ${{ steps.filter.outputs.payments }}
shared: ${{ steps.filter.outputs.shared }}
terraform: ${{ steps.filter.outputs.terraform }}
k8s-prod: ${{ steps.filter.outputs.k8s-prod }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
farmer-pwa:
- 'apps/farmer-pwa/**'
stakeholder-portal:
- 'apps/stakeholder-portal/**'
user-management:
- 'services/user-management/**'
access-management:
- 'services/access-management/**'
surveys:
- 'services/surveys/**'
payments:
- 'services/payments/**'
shared:
- 'packages/**'
terraform:
- 'infra/terraform/**'
k8s-prod:
- 'infra/k8s/overlays/prod/**'
# ==========================================
# BUILD FRONTEND APPS
# ==========================================
build-farmer-pwa:
needs: changes
if: needs.changes.outputs.farmer-pwa == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: apps/farmer-pwa/package-lock.json
- run: npm ci
working-directory: apps/farmer-pwa
- run: npm run lint
working-directory: apps/farmer-pwa
- run: npm run test
working-directory: apps/farmer-pwa
- run: npm run build
working-directory: apps/farmer-pwa
- name: Build and push Docker image
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: apps/farmer-pwa
push: true
tags: ghcr.io/farmer1st/farmer-pwa:${{ github.sha }}
build-stakeholder-portal:
needs: changes
if: needs.changes.outputs.stakeholder-portal == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
steps:
# Similar to farmer-pwa
- uses: actions/checkout@v4
# ... build steps ...
# ==========================================
# BUILD BACKEND SERVICES
# ==========================================
build-user-management:
needs: changes
if: needs.changes.outputs.user-management == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
service: [bff, auth-service, profile-service, preferences-service]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install uv
uv pip install -e ".[dev]"
working-directory: services/user-management/${{ matrix.service }}
- name: Lint
run: ruff check .
working-directory: services/user-management/${{ matrix.service }}
- name: Test
run: pytest
working-directory: services/user-management/${{ matrix.service }}
- name: Build and push Docker image
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: services/user-management/${{ matrix.service }}
push: true
tags: ghcr.io/farmer1st/${{ matrix.service }}:${{ github.sha }}
build-surveys:
needs: changes
if: needs.changes.outputs.surveys == 'true' || needs.changes.outputs.shared == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
service: [bff, survey-service, response-service, analytics-service]
steps:
# Similar pattern to user-management
- uses: actions/checkout@v4
# ... build steps ...
# (Similar jobs for access-management, payments)
# ==========================================
# UPDATE GITOPS MANIFESTS
# ==========================================
update-dev-manifests:
needs: [build-farmer-pwa, build-stakeholder-portal, build-user-management, build-surveys]
if: always() && github.ref == 'refs/heads/main' && !contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update image tags in dev overlay
run: |
# Update kustomization.yaml files with new image tags
# This script updates infra/k8s/overlays/dev/*/kustomization.yaml
./tools/scripts/update-image-tags.sh dev ${{ github.sha }}
- name: Commit and push
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add infra/k8s/overlays/dev/
git commit -m "deploy(dev): update images to ${{ github.sha }}" || exit 0
git push
# ==========================================
# TERRAFORM PLAN (on PR)
# ==========================================
terraform-plan:
needs: changes
if: needs.changes.outputs.terraform == 'true' && github.event_name == 'pull_request'
runs-on: ubuntu-latest
strategy:
matrix:
environment: [aws-dev, aws-staging, aws-prod, cloudflare]
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Plan
working-directory: infra/terraform/environments/${{ matrix.environment }}
env:
TF_CLOUD_TOKEN: ${{ secrets.TF_CLOUD_TOKEN }}
run: |
terraform init
terraform plan -no-color
Staging/Prod Deployment Workflow
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
workflow_dispatch:
inputs:
sha:
description: 'Git SHA to deploy (default: latest main)'
required: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update staging manifests
run: |
SHA=${{ github.event.inputs.sha || github.sha }}
./tools/scripts/update-image-tags.sh staging $SHA
- name: Commit and push
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add infra/k8s/overlays/staging/
git commit -m "deploy(staging): update images to $SHA"
git push
# .github/workflows/deploy-prod.yml
name: Deploy to Production
on:
workflow_dispatch:
inputs:
sha:
description: 'Git SHA to deploy'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update prod manifests
run: |
./tools/scripts/update-image-tags.sh prod ${{ github.event.inputs.sha }}
- name: Commit and push
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add infra/k8s/overlays/prod/
git commit -m "deploy(prod): update images to ${{ github.event.inputs.sha }}"
git push
Container Registry Strategy
Decision: Use GHCR Only
| Factor |
GHCR |
| Simplicity |
✅ Single registry for everything |
| Auth |
✅ Same as GitHub access |
| Cost |
✅ Included with GitHub |
| Location |
Pulls from GitHub, not AWS-local |
Future consideration: If pull latency becomes an issue, implement ECR replication.
Image Naming Convention
ghcr.io/farmer1st/<service-name>:<tag>
Examples:
ghcr.io/farmer1st/farmer-pwa:sha-abc123
ghcr.io/farmer1st/farmer-pwa:v1.2.3
ghcr.io/farmer1st/user-management-bff:sha-abc123
ghcr.io/farmer1st/auth-service:sha-abc123
Workspace Structure
┌─────────────────────────────────────────────────────────────────────────────┐
│ TERRAFORM CLOUD WORKSPACES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ farmer1st-cloudflare → infra/terraform/environments/cloudflare │
│ farmer1st-aws-dev → infra/terraform/environments/aws-dev │
│ farmer1st-aws-staging → infra/terraform/environments/aws-staging │
│ farmer1st-aws-prod → infra/terraform/environments/aws-prod │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Each workspace points to a subfolder in the mono-repo.
ArgoCD Configuration
Per-Environment ArgoCD
Each EKS cluster runs its own ArgoCD instance, pointing to the mono-repo:
# ArgoCD ApplicationSet for all services in an environment
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: farmer1st-apps
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/farmer1st/farmer1st.git
revision: main
directories:
- path: infra/k8s/overlays/prod/* # or dev/staging
template:
metadata:
name: '{{path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/farmer1st/farmer1st.git
targetRevision: main
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
| Environment |
Sync Policy |
| Dev |
Auto-sync enabled |
| Staging |
Auto-sync enabled |
| Prod |
Manual sync (requires approval via GitHub environment) |
Deployment Strategies
| Environment |
Strategy |
Rollback |
| Dev |
Rolling update, immediate |
ArgoCD auto-rollback on failure |
| Staging |
Rolling update |
ArgoCD manual or auto |
| Prod |
Canary via feature flags + rolling |
ArgoCD + manual verification |
Security Considerations
Secrets Management
| Secret Type |
Storage |
Injection |
| GitHub Tokens |
GitHub Secrets |
Actions env vars |
| AWS Credentials |
Terraform Cloud |
Workspace variables |
| Cloudflare API |
Terraform Cloud |
Workspace variables |
| K8s Secrets |
AWS Secrets Manager |
External Secrets Operator |
| GHCR Pull Secret |
K8s Secret |
Image pull secret per namespace |
Path Protection (CODEOWNERS)
# .github/CODEOWNERS
# Production paths require platform team approval
/infra/k8s/overlays/prod/ @farmer1st/platform-team
/infra/k8s/overlays/staging/ @farmer1st/platform-team
/infra/terraform/ @farmer1st/platform-team
/platform/ @farmer1st/platform-team
# Shared packages affect everything - require review
/packages/ @farmer1st/platform-team
Open Questions
Dependencies
- GitHub mono-repo with Actions enabled
- GHCR enabled
- Terraform Cloud organization
- ArgoCD installed per EKS cluster
- AWS and Cloudflare API credentials in Terraform Cloud
Last Updated: 2025-12-30