All Articles

Build a Complete CI/CD Pipeline with GitHub Actions + ArgoCD + EKS (2026)

A full project walkthrough — from a simple app to a production-grade GitOps pipeline with automated builds, image scanning, and deployments to AWS EKS using ArgoCD.

DevOpsBoysMar 30, 20266 min read
Share:Tweet

This is a complete project walkthrough. By the end, you'll have a working GitOps pipeline where every code push automatically builds, tests, scans, and deploys to Kubernetes on AWS EKS.

No hand-waving. Actual files you can use.


What You're Building

Developer pushes code
        │
        ▼
GitHub Actions (CI)
├── Run tests
├── Build Docker image
├── Scan image with Trivy
├── Push to ECR
└── Update GitOps repo with new image tag
        │
        ▼
ArgoCD (CD) detects GitOps repo change
        │
        ▼
Deploy to AWS EKS

Two separate repositories:

  • app-repo — your application code + Dockerfile + GitHub Actions workflow
  • gitops-repo — Kubernetes manifests / Helm chart values (no application code)

This separation is important. Your deployment config lives in a separate repo, giving you a clean audit trail of every deployment.


Prerequisites

  • AWS account with EKS cluster running (AWS Free Tier covers a lot)
  • kubectl configured to reach your cluster
  • helm installed
  • GitHub account
  • AWS CLI configured

If you don't have an EKS cluster yet, create one:

bash
# Using eksctl (easiest)
eksctl create cluster \
  --name devops-demo \
  --region us-east-1 \
  --nodegroup-name workers \
  --node-type t3.medium \
  --nodes 2 \
  --nodes-min 1 \
  --nodes-max 3

Step 1: The Application

Use any simple app. We'll use a Node.js app for this walkthrough.

app/server.js:

javascript
const express = require('express');
const app = express();
 
app.get('/', (req, res) => {
  res.json({
    message: 'Hello from DevOpsBoys',
    version: process.env.APP_VERSION || '1.0.0'
  });
});
 
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});
 
app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Dockerfile:

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
 
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
 
# Run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
 
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
 
CMD ["node", "server.js"]

Step 2: Create ECR Repository

bash
aws ecr create-repository \
  --repository-name devops-demo-app \
  --region us-east-1 \
  --image-scanning-configuration scanOnPush=true

Note your ECR URI: <account-id>.dkr.ecr.us-east-1.amazonaws.com/devops-demo-app


Step 3: GitHub Actions Workflow (CI)

Create .github/workflows/ci-cd.yml in your app repo:

yaml
name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: devops-demo-app
  IMAGE_TAG: ${{ github.sha }}
 
jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run tests
        run: npm test
 
  build-and-push:
    name: Build, Scan & Push
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
 
    permissions:
      id-token: write
      contents: read
 
    outputs:
      image: ${{ steps.build-image.outputs.image }}
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
 
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
 
      - name: Build Docker image
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
                       -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
 
      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.build-image.outputs.image }}
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          severity: 'CRITICAL'
 
      - name: Push image to ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
 
  update-gitops:
    name: Update GitOps Repo
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'
 
    steps:
      - name: Checkout GitOps repo
        uses: actions/checkout@v4
        with:
          repository: your-org/gitops-repo
          token: ${{ secrets.GITOPS_PAT }}
          path: gitops
 
      - name: Update image tag
        run: |
          cd gitops
          # Update the image tag in values file
          sed -i "s|tag: .*|tag: ${{ env.IMAGE_TAG }}|" \
            apps/devops-demo/values.yaml
 
          git config user.email "ci@devopsboys.com"
          git config user.name "GitHub Actions"
          git add apps/devops-demo/values.yaml
          git commit -m "chore: update devops-demo image to ${{ env.IMAGE_TAG }}"
          git push

Step 4: Set Up GitHub Secrets

In your app repo → Settings → Secrets and variables → Actions:

AWS_ROLE_ARN         = arn:aws:iam::<account-id>:role/github-actions-role
GITOPS_PAT           = <GitHub Personal Access Token with repo scope>

Using OIDC (the role-to-assume approach) is more secure than long-lived AWS access keys. Create the IAM role:

bash
# Trust policy for GitHub Actions OIDC
cat > trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<account-id>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/app-repo:*"
        }
      }
    }
  ]
}
EOF
 
aws iam create-role \
  --role-name github-actions-role \
  --assume-role-policy-document file://trust-policy.json
 
aws iam attach-role-policy \
  --role-name github-actions-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser

Step 5: GitOps Repo Structure

Create your gitops-repo with this structure:

gitops-repo/
├── apps/
│   └── devops-demo/
│       ├── Chart.yaml
│       ├── values.yaml
│       └── templates/
│           ├── deployment.yaml
│           ├── service.yaml
│           └── hpa.yaml
└── argocd/
    └── app.yaml

apps/devops-demo/values.yaml:

yaml
replicaCount: 2
 
image:
  repository: <account-id>.dkr.ecr.us-east-1.amazonaws.com/devops-demo-app
  tag: latest  # GitHub Actions will update this
  pullPolicy: Always
 
service:
  type: ClusterIP
  port: 80
  targetPort: 3000
 
resources:
  requests:
    memory: "64Mi"
    cpu: "50m"
  limits:
    memory: "128Mi"
    cpu: "100m"
 
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
 
livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 15
 
readinessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 5

Step 6: Install ArgoCD on EKS

bash
kubectl create namespace argocd
 
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
 
# Wait for ArgoCD to be ready
kubectl wait --for=condition=available deployment -l "app.kubernetes.io/name=argocd-server" \
  -n argocd --timeout=120s
 
# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d
 
# Access the ArgoCD UI (port forward for now)
kubectl port-forward svc/argocd-server -n argocd 8080:443

Open https://localhost:8080 — login with admin and the password from above.


Step 7: Create the ArgoCD Application

argocd/app.yaml:

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: devops-demo
  namespace: argocd
  annotations:
    # Slack notification on sync (optional)
    notifications.argoproj.io/subscribe.on-sync-succeeded.slack: devops-alerts
spec:
  project: default
 
  source:
    repoURL: https://github.com/your-org/gitops-repo
    targetRevision: HEAD
    path: apps/devops-demo
    helm:
      valueFiles:
        - values.yaml
 
  destination:
    server: https://kubernetes.default.svc
    namespace: production
 
  syncPolicy:
    automated:
      prune: true      # Remove resources not in Git
      selfHeal: true   # Re-sync if cluster drifts from Git
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Apply it:

bash
kubectl apply -f argocd/app.yaml

ArgoCD will now watch your GitOps repo. Every time GitHub Actions pushes a new image tag, ArgoCD detects the change and deploys automatically.


Step 8: See It Work End-to-End

  1. Push a code change to your app repo
  2. GitHub Actions runs: tests → build → scan → push to ECR → update GitOps repo
  3. ArgoCD detects the GitOps repo change
  4. ArgoCD syncs the new image tag to EKS
  5. New pods roll out with zero downtime
bash
# Watch the rollout
kubectl rollout status deployment/devops-demo -n production
 
# Check the pods
kubectl get pods -n production
 
# Check ArgoCD sync status
argocd app get devops-demo

What You've Built

ComponentPurpose
GitHub ActionsCI: test, build, scan, push
Amazon ECRContainer image registry
TrivyContainer image vulnerability scanning
GitOps repoSingle source of truth for deployments
ArgoCDCD: sync GitOps repo to Kubernetes
AWS EKSProduction Kubernetes cluster

Going Further

Add to this pipeline:

  • Slack notifications on deploy success/failure (ArgoCD notifications controller)
  • Automated rollback on failed deployment (ArgoCD --auto-rollback)
  • Environment promotion (dev → staging → prod via GitOps)
  • Sealed Secrets for managing K8s secrets in Git
  • Progressive delivery with Argo Rollouts (canary, blue-green)

This is a real production-grade pipeline. The same architecture is used at companies running thousands of microservices.

KodeKloud's ArgoCD course goes deep on GitOps patterns — highly recommended if you want to master this stack.

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments