Skip to content

Repository files navigation

Multi-Node GitOps Kubernetes Cluster

Production-ready Kubernetes on CSC cPouta with CI/CD GitOps pipeline using ArgoCD and GitHub Actions.

πŸ“‹ Table of Contents


🎯 Overview

Technology Stack: Terraform + k3s + ArgoCD + GitHub Actions + React + GHCR
Key Features: Zero-downtime deployments, auto-scaling (3-10 pods), automated security scanning, ~25MB images, 10-12 min deployment time

Component Technology
Cloud CSC cPouta (OpenStack)
IaC Terraform
Kubernetes k3s
GitOps ArgoCD
CI/CD GitHub Actions
Registry GHCR
App React + Vite + Tailwind
Runtime Nginx Alpine
Security Trivy

πŸ—οΈ Architecture

System Overview

Internet β†’ Floating IP (86.50.229.25:30007) β†’ Security Group β†’ K8s Cluster
  β”œβ”€ k8s-master (192.168.1.10) - Control Plane + ArgoCD
  └─ k8s-worker (192.168.1.11) - App Pods (reactapp-ui Γ—3, HPA 3-10, CPU 70%)

CI/CD Flow: Push β†’ GitHub Actions (build+scan) β†’ GHCR (image) β†’ Update manifest (SHA tag) β†’ ArgoCD sync (3min poll) β†’ K8s rolling update β†’ Live (zero downtime)

Network: Private 192.168.1.0/24, public IP on master, NodePort 30007
Security: Public SG (SSH/HTTP/HTTPS/6443/30007), Internal SG (full subnet access)

Detailed Architecture Diagram

graph TD
    classDef git fill:#F05032,stroke:#fff,stroke-width:2px,color:#fff;
    classDef github fill:#181717,stroke:#fff,stroke-width:2px,color:#fff;
    classDef generic fill:#444,stroke:#fff,stroke-width:1px,color:#fff;
    classDef docker fill:#2496ED,stroke:#fff,stroke-width:2px,color:#fff;
    classDef k8s fill:#326CE5,stroke:#fff,stroke-width:2px,color:#fff;
    classDef argo fill:#EF7B4D,stroke:#fff,stroke-width:2px,color:#fff;
    classDef user fill:#222,stroke:#fff,stroke-width:1px,color:#fff;

    subgraph DevSpace ["1. Developer Workspace"]
        Coder((Developer)):::user
    end

    subgraph GitHub ["2. Source Control & CI"]
        RepoFront[reactapp/ Source<br/>React + Vite + Tailwind]:::github
        ActionCI[GitHub Actions CI<br/>Build & Push Workflow]:::github
        RepoManifests[k8s/ Manifests<br/>Deployment + Service + HPA]:::github
        GHCR(GitHub Container Registry<br/>GHCR.io):::docker
    end

    subgraph K8sCluster ["3. CSC Pouta Kubernetes Cluster"]
        direction TB

        subgraph K8sControl [Control Plane]
            ArgoCD[Argo CD<br/>GitOps Controller]:::argo
        end

        subgraph K8sWorkers [Worker Nodes]
            subgraph ReactAppNS [reactapp Namespace]
                direction LR
                AppService[React App Service<br/>NodePort 30007]:::k8s
                IngressRes[Ingress Resource<br/>reactapp-ui]:::k8s
                HPA[HPA<br/>Autoscaler 3-10]:::k8s
                Pod1[Pod 1]:::k8s
                Pod2[Pod 2]:::k8s
                Pod3[Pod 3]:::k8s
            end
        end

        K8sNet(Kubernetes Networking / Kube-Proxy):::k8s
    end

    subgraph External ["4. External Access"]
        Visitor((End User)):::user
        CPoutaSG(cPouta Security Group<br/>k8s-public-secgroup):::generic
    end

    %% CI Flow
    Coder -- "1. git push" --> RepoFront
    RepoFront -- "Triggers" --> ActionCI
    ActionCI -- "2. Build + Scan + Push Image" --> GHCR
    ActionCI -- "3. Commit New SHA Tag" --> RepoManifests

    %% CD Flow (GitOps)
    ArgoCD -- "4. Auto-Sync Poll (3min)" --> RepoManifests
    ArgoCD -- "5. Apply Manifests" --> ReactAppNS
    Pod1 & Pod2 & Pod3 -- "6. Pull Image (Always)" --> GHCR

    %% Scale Flow
    HPA -. "Monitors & Scales" .-> Pod1 & Pod2 & Pod3

    %% User Flow
    Visitor -- "http://86.50.229.25:30007" --> CPoutaSG
    CPoutaSG -- "Allow Port 30007" --> K8sNet
    K8sNet --> AppService
    AppService -- "Load Balances" --> Pod1 & Pod2 & Pod3
Loading

Workflow Explanation:

  1. Developer pushes code to reactapp/ directory
  2. GitHub Actions builds Docker image, scans with Trivy, pushes to GHCR
  3. Workflow updates k8s/reactapp/deployment.yaml with new image SHA
  4. ArgoCD detects manifest change via polling (every 3 minutes)
  5. ArgoCD syncs changes to cluster using kubectl apply
  6. Kubernetes performs rolling update with zero downtime
  7. End User accesses application via floating IP and NodePort

πŸ“¦ Prerequisites

Required:

  • Terraform (v1.0+): winget install HashiCorp.Terraform
  • Git: winget install Git.Git
  • CSC cPouta account with API access (2 VMs, 1 floating IP, 1 network quota)
  • OpenStack credentials: auth_url, user_name, password, tenant_name, region

Optional:


πŸš€ Infrastructure Setup

Quick Start (Automated - Recommended)

# 1. Clone repo
git clone https://github.com/Junaid-sadiq/multinode-gitops-cluster.git
cd multinode-gitops-cluster

# 2. Configure credentials - Create terraform/terraform.tfvars
auth_url    = "https://pouta.csc.fi:5001/v3"
user_name   = "your-username"
password    = "your-password"
tenant_name = "your-project-name"
region      = "regionOne"
# ssh_key_name = "pouta-dokploy-key"  # Optional

# 3. Deploy everything (~10-15 min)
.\deploy-k8s-cluster.ps1

# Options:
# .\deploy-k8s-cluster.ps1 -DestroyFirst    # Destroy + redeploy
# .\deploy-k8s-cluster.ps1 -SkipTerraform   # Skip Terraform step

Script does: Terraform apply β†’ Create VMs β†’ Install k3s (master+worker) β†’ Install ArgoCD β†’ Save credentials

Output:

βœ“ Cluster ready! Access at http://86.50.229.25:30007
βœ“ ArgoCD: https://86.50.229.25:30008 (admin / see .argocd-credentials)

Manual Deployment (Step-by-Step)

Click to expand manual steps

Terraform:

cd terraform
terraform init
terraform apply
$MASTER_IP = terraform output -raw master_floating_ip

k3s Master:

ssh -i terraform/cpouta_key.pem ubuntu@$MASTER_IP
curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644
sudo cat /var/lib/rancher/k3s/server/node-token  # Save token

k3s Worker:

ssh -J ubuntu@$MASTER_IP -i terraform/cpouta_key.pem ubuntu@192.168.1.11
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.10:6443 K3S_TOKEN=<TOKEN> sh -

Verify:

kubectl get nodes  # Both Ready

ArgoCD:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"NodePort","ports":[{"port":443,"targetPort":8080,"nodePort":30008}]}}'
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d > .argocd-credentials

βš›οΈ React Application

Location: reactapp/ - React 18 + Vite + Tailwind v4 + TypeScript
Features: Animated shader background, newsletter signup, confetti celebration, responsive design
Build: Multi-stage Docker (Node build β†’ Nginx runtime, ~25MB final image)
Health: /health endpoint, gzip compression, security headers, SPA routing

# Local dev
cd reactapp
npm install && npm run dev  # β†’ http://localhost:5173

# Docker test
docker build -t reactapp:local . && docker run -p 8080:80 reactapp:local

πŸ”„ CI/CD Pipeline

GitHub Actions Workflows

1. PR Checks (pr-checks.yml) - Runs on PRs to main:

Trigger: pull_request (reactapp/**)
Jobs: Lint β†’ Type check β†’ Build verification

2. Build & Deploy (ci-build-simple.yml) - Runs on push to main:

Trigger: push to main (reactapp/**)
Steps:
  1. Checkout code
  2. Build multi-stage Docker image (linux/amd64)
  3. Scan with Trivy (continue-on-error: true)
  4. Login to GHCR (ghcr.io)
  5. Push image: ghcr.io/junaid-sadiq/multinode-gitops-cluster/reactapp:sha-{COMMIT_SHA}
  6. Update k8s/reactapp/deployment.yaml with new SHA
  7. Commit + push manifest changes
  8. Trigger ArgoCD sync (auto-detects in 3 min)
Timeout: 25 minutes

Critical Configuration

⚠️ GHCR requires lowercase: Use junaid-sadiq not Junaid-sadiq
⚠️ Platform: linux/amd64 only (multi-platform causes 30min+ hangs)
⚠️ Image pull: imagePullPolicy: Always in deployment.yaml forces fresh pulls
⚠️ Security: Trivy scans but doesn't block deployment (continue-on-error: true)

Secrets Required

Configure in GitHub repo β†’ Settings β†’ Secrets:

GHCR_TOKEN - GitHub Personal Access Token with packages:write

Deployment Timeline

  • Code push β†’ GitHub Actions triggered (instant)
  • Build + scan + push (4-6 min)
  • Manifest update (30s)
  • ArgoCD sync poll (up to 3 min)
  • K8s rolling update (2-3 min)
  • Total: 10-12 minutes βœ…

πŸ”„ GitOps with ArgoCD

Setup ArgoCD Application

ArgoCD automatically deploys from Git. Application is pre-configured in k8s/reactapp/argocd-application.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: reactapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Junaid-sadiq/multinode-gitops-cluster.git
    targetRevision: HEAD
    path: k8s/reactapp
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true       # Delete resources not in Git
      selfHeal: true    # Auto-sync if manual changes made
    syncOptions:
      - CreateNamespace=true

Deploy Application

# SSH to master node
ssh -i terraform/cpouta_key.pem ubuntu@86.50.229.25

# Apply ArgoCD application
kubectl apply -f https://raw.githubusercontent.com/Junaid-sadiq/multinode-gitops-cluster/main/k8s/reactapp/argocd-application.yaml

# Watch sync status
kubectl get applications -n argocd
# NAME       SYNC STATUS   HEALTH STATUS
# reactapp   Synced        Healthy

# Check deployed resources
kubectl get pods,svc,hpa

Verify Deployment

# Check pods (should see 3 replicas)
kubectl get pods -l app=reactapp-ui

# Check service
kubectl get svc react-app-service
# TYPE: NodePort, PORT: 80:30007/TCP

# Check HPA
kubectl get hpa reactapp-ui-hpa
# MIN: 3, MAX: 10, TARGET: 70% CPU

Application URL: http://86.50.229.25:30007

How GitOps Works

  1. Developer commits code β†’ Triggers GitHub Actions
  2. GitHub Actions builds image β†’ Pushes to GHCR β†’ Updates deployment.yaml with new SHA
  3. ArgoCD polls repo every 3 minutes β†’ Detects manifest change
  4. ArgoCD applies changes β†’ kubectl apply -k k8s/reactapp/
  5. Kubernetes performs rolling update β†’ Zero downtime
  6. ArgoCD UI shows sync status β†’ Green = Healthy & Synced

ArgoCD UI Access

  • URL: https://86.50.229.25:30008
  • Username: admin
  • Password: Check .argocd-credentials file
  • Features: Visual topology, sync history, logs, manual sync, rollback

🌐 Accessing Services

Application Access

Public URL: http://86.50.229.25:30007

Features:

  • Newsletter signup with email validation
  • Confetti celebration on submit
  • Success message: "Congrats! You're on the waiting list"
  • Animated shader background
  • Fully responsive design

ArgoCD Dashboard

URL: https://86.50.229.25:30008
Login: admin / (password in .argocd-credentials)

Dashboard shows:

  • Application health (Healthy/Progressing/Degraded)
  • Sync status (Synced/OutOfSync)
  • Resource tree (deployments, pods, services, HPA)
  • Deployment history and logs

SSH Access

# Master node (public access)
ssh -i terraform/cpouta_key.pem ubuntu@86.50.229.25

# Worker node (via master as jump host)
ssh -J ubuntu@86.50.229.25 -i terraform/cpouta_key.pem ubuntu@192.168.1.11

kubectl Commands

# Get cluster info
kubectl cluster-info

# Get all resources
kubectl get all

# Get pods with details
kubectl get pods -o wide

# Check pod logs
kubectl logs -l app=reactapp-ui --tail=50

# Check HPA metrics
kubectl get hpa
kubectl top pods  # Requires metrics-server

# Check ArgoCD application
kubectl get applications -n argocd

# Describe pod (troubleshooting)
kubectl describe pod <pod-name>

# Port forward for local testing
kubectl port-forward svc/react-app-service 8080:80
# Access: http://localhost:8080

Container Registry

GHCR URL: https://github.com/Junaid-sadiq?tab=packages
Image: ghcr.io/junaid-sadiq/multinode-gitops-cluster/reactapp:sha-{COMMIT_SHA}

View all published images and their scan results.


πŸ” Monitoring & Troubleshooting

Check Application Health

# Pod status
kubectl get pods -l app=reactapp-ui
# All should be Running

# Service endpoints
kubectl get endpoints react-app-service
# Should show 3 pod IPs

# HPA status
kubectl get hpa reactapp-ui-hpa
# REPLICAS should be 3/3 (or scaled up if under load)

# Check pod logs
kubectl logs -l app=reactapp-ui --tail=100 -f

# Health check
curl http://86.50.229.25:30007/health
# Should return 200 OK

Common Issues

Issue: Pods stuck in ImagePullBackOff

# Check image name (must be lowercase)
kubectl describe pod <pod-name> | grep Image

# Verify GHCR image exists
# Go to: https://github.com/Junaid-sadiq?tab=packages

# Fix: Ensure workflow uses lowercase username
# GitHub Actions: GITHUB_REPOSITORY_OWNER | tr '[:upper:]' '[:lower:]'

Issue: ArgoCD shows OutOfSync

# Check application status
kubectl get application reactapp -n argocd -o yaml

# Manual sync
kubectl patch application reactapp -n argocd -p '{"operation":{"sync":{"prune":true}}}' --type merge

# Or via UI: Click "Sync" β†’ "Synchronize"

Issue: Deployment not updating

# Check if imagePullPolicy is set to Always
kubectl get deployment reactapp-ui -o yaml | grep imagePullPolicy
# Should show: imagePullPolicy: Always

# Force pod recreation
kubectl rollout restart deployment reactapp-ui

Issue: Cannot access application

# Check service
kubectl get svc react-app-service
# Type: NodePort, NodePort: 30007

# Check security group allows port 30007
# Verify in cPouta dashboard: k8s-public-secgroup

# Test from master node
curl http://localhost:30007

Monitoring Commands

# Watch pod status
kubectl get pods -l app=reactapp-ui -w

# Watch HPA scaling
kubectl get hpa -w

# Resource usage (requires metrics-server)
kubectl top nodes
kubectl top pods

# Events (recent cluster activity)
kubectl get events --sort-by='.lastTimestamp' | tail -20

# ArgoCD sync history
kubectl get application reactapp -n argocd -o jsonpath='{.status.history}'

Load Testing (Trigger HPA)

# Generate load to test auto-scaling
# Install hey: https://github.com/rakyll/hey
hey -z 60s -c 50 http://86.50.229.25:30007

# Watch HPA scale up
kubectl get hpa -w
# REPLICAS will increase from 3 β†’ 10 based on CPU

Logs & Debugging

# Application logs
kubectl logs -l app=reactapp-ui --tail=200 -f

# ArgoCD logs
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller

# Deployment events
kubectl describe deployment reactapp-ui

# Pod describe (shows pull errors, crashes, etc.)
kubectl describe pod <pod-name>

πŸ“ Project Structure

multinode-gitops-cluster/
β”œβ”€β”€ .github/workflows/           # CI/CD pipelines
β”‚   β”œβ”€β”€ ci-build-simple.yml     # Main build & deploy workflow
β”‚   └── pr-checks.yml           # PR validation checks
β”œβ”€β”€ terraform/                   # Infrastructure as Code
β”‚   β”œβ”€β”€ main.tf                 # OpenStack resources
β”‚   β”œβ”€β”€ variables.tf            # Input variables
β”‚   β”œβ”€β”€ outputs.tf              # Output values
β”‚   β”œβ”€β”€ terraform.tfvars        # Credentials (gitignored)
β”‚   └── .terraform/             # Terraform state & providers
β”œβ”€β”€ k8s/reactapp/               # Kubernetes manifests
β”‚   β”œβ”€β”€ deployment.yaml         # App deployment (3 replicas, HPA)
β”‚   β”œβ”€β”€ service.yaml            # NodePort service (30007)
β”‚   β”œβ”€β”€ hpa.yaml                # Horizontal Pod Autoscaler
β”‚   β”œβ”€β”€ ingress.yaml            # Ingress resource (optional)
β”‚   β”œβ”€β”€ argocd-application.yaml # ArgoCD app definition
β”‚   └── kustomization.yaml      # Kustomize config
β”œβ”€β”€ reactapp/                    # React application source
β”‚   β”œβ”€β”€ src/                    # React components
β”‚   β”‚   β”œβ”€β”€ App.tsx            # Main app component
β”‚   β”‚   β”œβ”€β”€ components/        # UI components
β”‚   β”‚   └── assets/            # Images, SVGs
β”‚   β”œβ”€β”€ public/                 # Static assets
β”‚   β”‚   └── favicon.svg        # Rocket favicon
β”‚   β”œβ”€β”€ Dockerfile              # Multi-stage Docker build
β”‚   β”œβ”€β”€ nginx.conf              # Nginx configuration
β”‚   β”œβ”€β”€ package.json            # Dependencies
β”‚   └── vite.config.js          # Vite build config
β”œβ”€β”€ deploy-k8s-cluster.ps1      # Automated deployment script
β”œβ”€β”€ .argocd-credentials         # ArgoCD password (gitignored)
β”œβ”€β”€ .gitignore                  # Git ignore rules
β”œβ”€β”€ README.md                   # This file
└── SECURITY.md                 # Security policies

Key Files

  • deploy-k8s-cluster.ps1 - One-command deployment automation
  • ci-build-simple.yml - GitHub Actions workflow (build β†’ push β†’ update manifest)
  • deployment.yaml - K8s deployment with imagePullPolicy: Always
  • argocd-application.yaml - GitOps application config
  • Dockerfile - Multi-stage build (Node build β†’ Nginx runtime)
  • terraform.tfvars - OpenStack credentials (create this manually)

πŸ”’ Security

Infrastructure Security

βœ… Network Segmentation: Private network (192.168.1.0/24), public access only on master
βœ… Security Groups: Minimal port exposure (SSH, HTTP, HTTPS, K8s API, NodePort)
βœ… SSH Key Auth: Password auth disabled, key-based only
βœ… Firewall Rules: OpenStack security groups restrict traffic
βœ… Private Worker: Worker node has no public IP

Application Security

βœ… Container Scanning: Trivy scans all images for vulnerabilities
βœ… Minimal Base Image: Alpine Linux (~25MB, reduced attack surface)
βœ… Non-Root User: Nginx runs as non-root
βœ… Security Headers: CSP, X-Frame-Options, X-Content-Type-Options
βœ… HTTPS Ready: ArgoCD exposed with TLS (self-signed)
βœ… Image Integrity: SHA-tagged images ensure immutable deployments

CI/CD Security

βœ… Secrets Management: GitHub Secrets for GHCR token
βœ… Least Privilege: GHCR token has only packages:write scope
βœ… Audit Trail: All deployments tracked in Git history
βœ… Automated Scanning: Every image scanned before deployment
βœ… GitOps Principles: No direct kubectl access needed

Best Practices

  • πŸ” Rotate ArgoCD admin password after first login
  • πŸ” Use GitHub PAT with minimal scopes
  • πŸ” Never commit terraform.tfvars or .argocd-credentials
  • πŸ” Review Trivy scan results in GitHub Actions logs
  • πŸ” Enable ArgoCD RBAC for team access
  • πŸ” Use network policies for pod-to-pod communication
  • πŸ” Regular security updates: kubectl set image deployment/reactapp-ui ...

Security Contacts

Report vulnerabilities: See SECURITY.md


πŸš€ Quick Reference

Common Commands

# Deploy application via ArgoCD
kubectl apply -f k8s/reactapp/argocd-application.yaml

# Check deployment status
kubectl get pods,svc,hpa

# View application logs
kubectl logs -l app=reactapp-ui -f

# Manual sync (if ArgoCD shows OutOfSync)
kubectl patch application reactapp -n argocd -p '{"operation":{"sync":{"prune":true}}}' --type merge

# Scale manually (overrides HPA temporarily)
kubectl scale deployment reactapp-ui --replicas=5

# Rollback to previous version
kubectl rollout undo deployment reactapp-ui

# Check rollout history
kubectl rollout history deployment reactapp-ui

# Restart pods (useful for pulling new image)
kubectl rollout restart deployment reactapp-ui

URLs

Credentials

  • ArgoCD: admin / (see .argocd-credentials)
  • SSH: terraform/cpouta_key.pem or ~/.ssh/pouta-dokploy-key
  • GHCR: GitHub PAT in GHCR_TOKEN secret

πŸ“– Additional Resources


πŸ“ License

This project is provided as-is for educational purposes.


🀝 Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Submit a pull request

Built with ❀️ using Terraform, k3s, ArgoCD, and GitHub Actions

About

Production-ready Multi-Node Kubernetes cluster on OpenStack (cPouta) featuring a complete GitOps CI/CD pipeline built with Terraform, k3s, GitHub Actions, GHCR, and Argo CD.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages