Skip to content

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

Tooling Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                        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

Terraform Cloud Configuration

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

  • Mono-repo build tooling: Nx vs Turborepo vs custom scripts?
  • Python dependency management across services: uv workspaces?
  • Secrets management: AWS Secrets Manager + External Secrets Operator?
  • ArgoCD SSO integration?
  • Database migration strategy in CD pipeline?
  • Rollback automation criteria?

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