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.
Today I Fixed
Short real fixes from production — posted daily
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 v2 — Deep Dive Comparison 2026
Both ArgoCD and Flux implement GitOps for Kubernetes, but they take very different approaches. Here's a detailed comparison to help you pick the right one.
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.
ArgoCD vs Jenkins X vs Tekton CD: Which GitOps Tool in 2026?
Honest comparison of ArgoCD, Jenkins X, and Tekton for Kubernetes CD in 2026. Architecture differences, GitOps maturity, community health, and when to use each.