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.
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 workflowgitops-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)
kubectlconfigured to reach your clusterhelminstalled- GitHub account
- AWS CLI configured
If you don't have an EKS cluster yet, create one:
# 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 3Step 1: The Application
Use any simple app. We'll use a Node.js app for this walkthrough.
app/server.js:
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:
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
aws ecr create-repository \
--repository-name devops-demo-app \
--region us-east-1 \
--image-scanning-configuration scanOnPush=trueNote 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:
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 pushStep 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:
# 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/AmazonEC2ContainerRegistryPowerUserStep 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:
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: 5Step 6: Install ArgoCD on EKS
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:443Open https://localhost:8080 — login with admin and the password from above.
Step 7: Create the ArgoCD Application
argocd/app.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: 3mApply it:
kubectl apply -f argocd/app.yamlArgoCD 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
- Push a code change to your app repo
- GitHub Actions runs: tests → build → scan → push to ECR → update GitOps repo
- ArgoCD detects the GitOps repo change
- ArgoCD syncs the new image tag to EKS
- New pods roll out with zero downtime
# 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-demoWhat You've Built
| Component | Purpose |
|---|---|
| GitHub Actions | CI: test, build, scan, push |
| Amazon ECR | Container image registry |
| Trivy | Container image vulnerability scanning |
| GitOps repo | Single source of truth for deployments |
| ArgoCD | CD: sync GitOps repo to Kubernetes |
| AWS EKS | Production 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.
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
ArgoCD vs Flux vs Jenkins — GitOps Comparison 2026
A deep-dive comparison of the three most popular GitOps and CI/CD tools — ArgoCD, Flux CD, and Jenkins. Learn which one fits your team, use case, and Kubernetes setup.
Build a Complete AWS Infrastructure with Terraform from Scratch (2026)
Full project walkthrough: provision a production-grade AWS VPC, EKS cluster, RDS, S3, and IAM with Terraform. Real code, real architecture, ready to use.
What is GitOps? Explained Simply for Beginners (2026)
GitOps explained in plain English — what it is, how it's different from traditional CI/CD, and how tools like ArgoCD and Flux work. No jargon.