Skip to main content

Command Palette

Search for a command to run...

Build a Real-World DevOps Pipeline with GitHub Actions, Terraform, EKS, and Argo CD (Step-by-Step)

Published
β€’15 min read
Build a Real-World DevOps Pipeline with GitHub Actions, Terraform, EKS, and Argo CD (Step-by-Step)
N

Hi! I'm a highly motivated Security and DevOps professional with 7+ years of combined experience. My expertise bridges penetration testing and DevOps engineering, allowing me to deliver a comprehensive security approach.

Introduction

DevOps isn’t just a buzzwordβ€”it’s the backbone of fast, secure, and scalable software delivery. If you’ve ever dreamed of building a real-world CI/CD pipeline that goes from source code to production using industry-standard tools, you’re in the right place.

In this guide, I’ll walk you through the exact blueprint used by top engineering teams to deploy production-ready microservices using:

  • GitHub Actions for CI

  • Docker for containerization

  • AWS EKS + Terraform for scalable Kubernetes infrastructure

  • Argo CD for GitOps-based continuous delivery

  • ALB + ACM for HTTPS and secure ingress routing

By the end, you won’t just understand the theoryβ€”you’ll have a full, running system, complete with pipelines, infrastructure as code, GitOps deployment, and HTTPS-secured access via a custom domain.

πŸ—‚οΈ GitHub Repository

You can explore the full source code and pipeline configurations in the public repository below:

πŸ“ GitHub Repo

πŸ”— https://github.com/neamulkabiremon/ultimate-devops-project-demo

Inside the repo:

ultimate-devops-project-demo/
β”œβ”€β”€ .github/
β”‚   └── workflows/                     # CI pipelines for each microservice (GitHub Actions)
β”‚       β”œβ”€β”€ ad.yaml
β”‚       β”œβ”€β”€ checkout.yaml
β”‚       β”œβ”€β”€ frontend.yaml
β”‚       β”œβ”€β”€ recommendation.yaml
β”‚       └── product-catalog-ci.yaml
β”‚
β”œβ”€β”€ infrastructure/
β”‚   └── eks-cluster/                   # Complete Terraform config for AWS EKS & Add-ons
β”‚       β”œβ”€β”€ *.tf                       # EKS, VPC, IAM, ALB, ArgoCD, Metrics Server, etc.
β”‚       └── values/                    # Helm values and overrides
β”‚
β”œβ”€β”€ kubernetes/
β”‚   β”œβ”€β”€ frontendproxy/                # Ingress, TLS, Certificate, and Service config
β”‚   └── (other services)/             # K8s manifests for deployments, services, etc.
β”‚
β”œβ”€β”€ argocd-apps/                      # GitOps apps for Argo CD
β”‚   └── frontend-app.yaml             # Syncs K8s manifests from GitHub to the cluster
β”‚
β”œβ”€β”€ src/                              # Source code for all microservices
β”‚   β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ ad/
β”‚   β”œβ”€β”€ checkout/
β”‚   β”œβ”€β”€ recommendation/
β”‚   └── product-catalog/
β”‚
└── docker-compose*.yml               # Local development & testing (optional)

Each .yaml file under .github/workflows defines a dedicated CI pipeline for an individual microservice, tailored to handle its testing, build, and deployment lifecycle. You’re welcome to fork the repository, customize the pipelines, and run them in your own DevOps lab to sharpen your automation skills and experiment freely.

Step-by-Step: Frontend Microservice CI Pipeline

Our CI pipeline for the frontend microservice is structured around four key stages, ensuring thorough validation and a smooth, automated delivery process.

βœ… Step 1: Set Up CI Pipeline (GitHub Actions)

To kick off our DevOps pipeline, we’ll configure a robust Continuous Integration (CI) workflow using GitHub Actions. This pipeline will:

  • βœ… Run unit tests to catch bugs early

  • βœ… Enforce clean code using ESLint

  • βœ… Build and push Docker images to Docker Hub

  • βœ… Automatically update Kubernetes manifests

We’ll use frontend.yaml under .github/workflows/ to define the CI process for the frontend microservice.

Frontend CI Pipeline Workflow

.github/workflows/frontend.yaml

πŸ”Ή 1.1 – Workflow Triggers

The CI pipeline is triggered automatically based on code changes, ensuring continuous feedback and automation:

name: frontend-services-ci

on:
  pull_request:
    branches: [main]
  push:
    paths:
      - '.github/workflows/frontend.yaml'
      - 'src/frontend/**'
  pull_request_target:
    branches: [main]
    paths:
      - '.github/workflows/frontend.yaml'
      - 'src/frontend/**'

Explanation of Triggers:

  • pull_request – Runs when a PR is opened or updated to the main branch.

  • push – Executes on direct commits to the workflow file or frontend source.

  • pull_request_target – Ensures safe execution of workflows triggered from forks (e.g., external contributors).

πŸ’‘ You can safely customize or remove these triggers based on your team’s workflow needs.

πŸ”Ή 1.2 – Unit Testing

We begin by validating the code using unit tests, catching regressions and bugs early in the development lifecycle.

unit-testing:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'

    - name: Install dependencies
      run: |
        cd src/frontend
        npm install

    - name: Run unit tests
      run: |
        cd src/frontend
        npm test -- --coverage --watchAll=false

πŸ”Ή 1.3 – Code Quality Checks (ESLint)

Next, we ensure the code meets industry standards using ESLint, which helps maintain readability and best practices.

code-quality:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'

    - name: Install dependencies
      run: |
        cd src/frontend
        npm install

    - name: Run ESLint (code quality check)
      run: |
        cd src/frontend
        npm run lint -- --max-warnings=0 --quiet

🚨 The --max-warnings=0 flag ensures that even minor code issues will break the build β€” keeping code quality strict and production-grade.

πŸ”Ή 1.4 – Docker Build & Push

After successful validation, we package the frontend app into a Docker image and publish it to Docker Hub.

build-and-push:
  runs-on: ubuntu-latest
  needs: [unit-testing, code-quality]
  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Install Docker
      uses: docker/setup-buildx-action@v1

    - name: Login to Docker
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v3
      with:
        context: .
        file: ./src/frontend/Dockerfile
        push: true
        tags: neamulkabiremon/frontend:${{ github.sha }}

🐳 Using github.sha as the image tag ensures each build is uniquely identifiable and traceable.

πŸ”Ή 1.5 – Update Kubernetes Deployment

Finally, we auto-update the Kubernetes deployment manifest with the new Docker image tag and commit the change back to the repo.

update-k8s-deployment:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}

    - name: Update Kubernetes Deployment
      run: |
        sed -i "s|image: .*|image: ${{ secrets.DOCKER_USERNAME }}/frontend:${{ github.sha }}|" kubernetes/frontend/deploy.yaml
        cat kubernetes/frontend/deploy.yaml

    - name: Commit and push updated Kubernetes manifest
      run: |
        git config --global user.email "neamulkabiremon@gmail.com"
        git config --global user.name "neamulkabiremon"
        git add kubernetes/frontend/deploy.yaml
        git commit -m "[CI]: Update frontend deployment image tag"
        git push

πŸ”„ This ensures your Kubernetes manifests stay in sync with the latest Docker images β€” a key principle of GitOps.

βœ… With this setup, you now have a fully automated CI pipeline that builds, tests, and prepares your microservice for deployment β€” all triggered by a simple Git push.

πŸ” Step 2: Securely Configure GitHub Secrets

To keep sensitive credentials like Docker Hub credentials and GitHub tokens safe and out of source control, we’ll use GitHub Actions Secrets. These secrets are securely encrypted and only accessible by your GitHub workflows at runtime.

Secrets to Configure:

πŸ”’ Best Practice: Never hardcode these values in your workflow files. Always use ${{ secrets.SECRET_NAME }} to reference them.

How to Add Secrets in GitHub

  1. Go to your GitHub repository.

  2. Navigate to: Settings β†’ Secrets and variables β†’ Actions

  3. Click on New repository secret

  4. Add each secret (DOCKER_USERNAME, DOCKER_PASSWORD, and optionally GITHUB_TOKEN)

  5. Save each one securely

☁️ Step 3: Provision EKS with Terraform

In this section, we'll provision our Kubernetes cluster on AWS using Terraform. I will be using AWS EKS for this project, but you can choose any other cloud provider for Kubernetes. Terraform will help us automate and manage the infrastructure required for our application seamlessly.

Directory Structure

Navigate to:

cd infrastructure/eks-cluster

πŸ”Ή 3.1 – Initialize Terraform

Initialize your Terraform project to download necessary provider plugins and modules:

terraform init

πŸ”Ή 3.2 – Terraform Plan

To ensure that Terraform will create the desired resources:

terraform plan

This command will outline the resources that Terraform intends to create.

πŸ”Ή 3.3 – Terraform Apply

To provision your EKS cluster, run the following command:

terraform apply -auto-approve

This command creates the defined AWS resources without an interactive approval type.

⏳ Wait for EKS & Node Group

The full provisioning (especially EKS cluster + node group) might take 10–15 minutes

πŸ”Ή 3.4 – Configure kubectl

After provisioning, verify your Kubernetes cluster is successfully created by configuring kubectl:

aws eks update-kubeconfig --region <your-region> --name <your-cluster-name>

Then test your connection:

kubectl get nodes

If successful, you will see the nodes of your newly created Kubernetes cluster listed.

🚒 Step 4: Deploy to Kubernetes

Now that our EKS cluster is up and running, let’s manually deploy the entire application to verify that everything is functioning as expected.

Run the following command to deploy all Kubernetes manifests:

kubectl apply -f kubernetes/

⏳ This will take around 2–5 minutes to roll out all services, deployments, and configurations.

🌐 Expose the Application via LoadBalancer

By default, all services are deployed as ClusterIP, which is not accessible from the internet. To make the app available publicly, we need to change the frontendproxy service type to LoadBalancer.

Run this command:

kubectl patch service opentelemetry-demo-frontendproxy \
  -p '{"spec": {"type": "LoadBalancer"}}'

Now, retrieve the external DNS or IP address assigned by AWS:

kubectl get service opentelemetry-demo-frontendproxy

Wait 2–10 minutes for the DNS to propagate. Once the EXTERNAL-IP field is populated, copy the address and open it in your browser:

http://a887d5c6ec63b4672b91878a986d4184-1991743320.us-east-2.elb.amazonaws.com:8080/

πŸŽ‰ You should now see the application frontend live! This is a full-scale production-like demo where you can browse products, simulate orders, and see distributed microservices in action.

🎯 Step 5: Install and Expose Argo CD

Now that the application is manually deployed, the next step is to automate deployments using Argo CD, following the GitOps methodology.

This means any changes pushed to your /kubernetes manifests (such as a new image tag from CI) will be automatically pulled and deployed to your EKS cluster by Argo CD.

πŸ”Ή 5.1 – Install Argo CD

First, create a namespace for Argo CD Then, install Argo CD using the official manifest:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

βœ… This will deploy all Argo CD components (API server, controller, repo server, and UI) inside the argocd namespace.

πŸ”Ή 5.2 – Get ArgoCD Password

By default, the admin password is stored in a Kubernetes secret. Run the following command:

kubectl get secret argocd-initial-admin-secret \
  -n argocd \
  -o jsonpath="{.data.password}" | base64 --decode && echo

Copy the password n81w4ABZjGAj7qv9; we will need it later to log in to the Argo CD console.

πŸ”Ή 5.3 – Expose ArgoCD UI

The Argo CD API server is deployed as a ClusterIP service by default. You can patch it to LoadBalancer like this:

kubectl patch svc argocd-server \
  -n argocd \
  -p '{"spec": {"type": "LoadBalancer"}}'

Now get the external IP:

kubectl get service argocd-server -n argocd

Once the EXTERNAL-IP is available (may take 2–5 mins), open it in your browser:

a48e45918ac1e4657b8cf7a77e353f9b-761261883.us-east-2.elb.amazonaws.com

πŸ§‘β€πŸ’» Log in to Argo CD Use the following default credentials:

  • Username: admin

  • Password: (use the one you just decoded)

πŸ” Step 6: Configure Argo CD App (GitOps)

Now that our application is running on the EKS cluster, let’s take it one step further and automate the deployment process using Argo CD. This way, every time we update a deployment manifest in our GitHub repo, Argo CD will automatically sync and apply it to the cluster β€” following the GitOps methodology.

We’ll configure Argo CD to watch the main branch of our GitHub repo and continuously apply any changes found inside the kubernetes/ directory.

1️⃣ Create the Argo CD Application YAML

Create a folder named argocd-apps and inside the folder create frontend-app.yamland paste the following configuration:

argocd-apps/frontend-app.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/neamulkabiremon/ultimate-devops-project-demo
    targetRevision: HEAD
    path: kubernetes/frontend
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Here’s what each field means:

  • repoURL: Your public GitHub repo URL.

  • path: The folder containing all your Kubernetes manifests.

  • syncPolicy: Enables auto-sync, pruning, and self-healing to ensure your cluster always matches Git state.

2️⃣ Apply the Argo CD App to the Cluster

Now apply this config to create the Argo CD app for the frondend services:

kubectl apply -f argocd-apps/frontend-app.yaml

This will register the ultimate-devops-app inside Argo CD.

3️⃣ Verify from ArgoCD Dashboard

  • Open your Argo CD UI:

    https://<your-argocd-external-ip>

  • Login using the admin username and decoded password.

  • You’ll now see a new application named frontend in the dashboard.

  • It will automatically sync the Kubernetes frontend manifests from the main branch of your repo and apply them to the cluster.

Step 7: Test the Complete Pipeline

With our Continuous Integration and Continuous Deployment pipelines fully configured using GitHub Actions and Argo CD, it’s time to validate everything works end-to-end. We’ll do this by making a small change in the frontend microservice β€” triggering the GitHub Actions workflow to:

  • Run tests and lint checks

  • Build and push a new Docker image

  • Update the deployment manifest in the kubernetes/frontend folder

  • Let Argo CD detect the change and sync the new deployment to the cluster

βœ… Step-by-Step: Make a Change in the Frontend App

1️⃣ Open the frontend app source code

Go to:

src/frontend/components/Banner/Banner.tsx (or any visible UI component)

Open the file Banner.tsx and locate this line:

<S.Title>The best telescopes to see the world closer</S.Title>

Replace it with something more meaningful for your DevOps project. Here’s a suggestion:

<S.Title>The best tools to see DevOps in action, closer than ever</S.Title>

βœ… After Editing:

  1. Save the file.

  2. Push the change to GitHub:

git add .
git commit -m "feat: updated frontend banner message to reflect DevOps context"
git push origin main

  1. Your GitHub Actions CI pipeline should now trigger, build the new Docker image, and ArgoCD will auto-deploy it.

βœ… Verifying the Complete CI/CD Pipeline

Now that we’ve made a small update to the frontend application (the banner message), let’s validate that our complete CI/CD pipeline works as expected.

As soon as we push the updated code to GitHub, the pipeline is triggered automatically. Here’s what happens under the hood:

  1. πŸ”¬ Unit Testing:

    The pipeline first runs comprehensive unit tests to catch bugs early and ensure the updated code doesn’t break any functionality.

  2. 🧹 Code Quality Checks:

    Next, it performs linting and code quality analysis to enforce clean coding standards using tools like ESLint.

  3. 🐳 Docker Image Build & Push:

    Once tests and quality checks pass, the pipeline builds a new Docker image using the updated code and pushes it to the configured Docker registry (e.g., Docker Hub).

  4. πŸ“ Kubernetes Manifest Update:

    The pipeline then automatically updates the Kubernetes deployment YAML inside the /kubernetes directory with the new image tag (using the current Git SHA). This ensures our manifests always reflect the latest version.

  5. πŸš€ ArgoCD GitOps Deployment:

    ArgoCD detected the change in the /kubernetes/frontend deployment file, pulls the updated manifest, and instantly applies it to the EKS cluster β€” completing the Continuous Delivery cycle.

πŸ” Testing the Updated Frontend

To verify the update (like banner text or frontend behavior), we can port-forward the service to our local machine using this command:

kubectl port-forward service/opentelemetry-demo-frontendproxy 8080:8080

Now open your browser and visit:

http://localhost:8080

You should see the updated version of your frontend, confirming that the CI/CD pipeline and ArgoCD synchronisation worked correctly.

πŸ”’ Step 8: Enable HTTPS with ACM + ALB Ingress

Now that our application is deployed and CI/CD is fully functional, it’s time to secure it for real-world usage.

In this step, we’ll expose the frontend proxy service using AWS ALB Ingress Controller and issue a valid SSL/TLS certificate via AWS Certificate Manager (ACM) β€” so users can access the app securely via https://.

βœ… Why Use AWS ALB Ingress + ACM?

  • ALB provides scalable, managed Layer 7 routing

  • ACM allows you to issue free, auto-renewing SSL certificates for public domains

  • Seamless integration with Kubernetes via AWS Load Balancer Controller

We have already installed the AWS LoadBalancer controller and necessary components during the cluster provisioning, and the IAM role is attached properly to the service account and has permissions. Now we have to

πŸ”Ή 8.1 – Request ACM Certificate (manual via AWS Console)

  • Go to AWS Certificate Manager

  • Request a public certificate for your domain (e.g., *.example.com)

  • Validate the domain (via DNS or email)

  • Copy the ACM ARN once validated

πŸ”Ή 8.2 – Create Ingress Resource

Create a file called kubernetes/frontendproxy/ingress.yaml with the following:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-proxy
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80, "HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:377027906194:certificate/647f2856-e897-4c78-a4c0-45834c3ad14b
    alb.ingress.kubernetes.io/ssl-redirect: '443'
spec:
  ingressClassName: alb
  rules:
    - host: devops.neamulkabiremon.com
      http:
        paths:
          - path: "/"
            pathType: Prefix
            backend:
              service:
                name: opentelemetry-demo-frontendproxy
                port:
                  number: 8080
  tls:
    - hosts:
        - devops.neamulkabiremon.com

βœ… Replace your-domain.com with your actual domain.

βœ… arn:aws:acm:REGION:ACCOUNT_ID:certificate/XXX with your ACM SSL cert ARN (must be in same region as your EKS cluster).

Example:

Apply the Ingress Resource

Once the ingress.yaml is ready with your custom domain and ACM certificate ARN, deploy it to your EKS cluster using:

kubectl apply -f kubernetes/frontendproxy/ingress.yaml

This will create an ALB (Application Load Balancer) and configure it with HTTPS termination using your ACM certificate.

πŸ”Ή 8.3 – Add Route53 CNAME Record

Now that the ALB is provisioned, get its DNS name using:

kubectl get ingress frontend-proxy

You will see an output like:

Copy the ADDRESS k8s-default-frontend-67db10afb2-681454757.us-east-1.elb.amazonaws.com

Then go to your DNS provider (e.g., Route 53 or your domain registrar) and add a CNAME record:

⚠️ DNS propagation may take a few minutes.

βœ… Final Step: Test Secure Access

Once DNS is propagated, open your browser and visit:

https://devops.neamulkabiremon.com

You should now see your frontend application served securely over HTTPS with a valid SSL certificate.

Look for the πŸ”’ lock in the address bar β€” this confirms the TLS setup is working perfectly.

βœ… What We’ve Achieved

By completing this step, you have:

  • Enabled secure HTTPS access to your application via a custom domain

  • Leveraged AWS ALB Ingress Controller for scalable ingress routing

  • Integrated ACM for free, managed, and auto-renewing SSL certificates

  • Set up production-grade traffic routing to your EKS workloads

Conclusion

Through this end-to-end DevOps pipeline implementation, we’ve successfully built a production-ready microservices architecture that integrates:

  • πŸ” Continuous Integration with GitHub Actions

  • ☸️ Kubernetes infrastructure provisioned with Terraform on AWS EKS

  • πŸš€ Continuous Deployment using Argo CD and GitOps

  • πŸ” HTTPS termination with AWS ACM + ALB Ingress Controller

  • πŸ› οΈ Modular and scalable architecture with real-world microservices

This project serves as a powerful foundation for deploying and managing cloud-native applications with reliability, speed, and security. Whether you’re preparing for production workloads, DevOps interviews, or client showcases, this setup reflects best practices in cloud-native DevOps engineering.

More from this blog

Untitled Publication

21 posts

Hi, I'm a highly motivated Security and DevOps professional with a combined 7+ years of experience in penetration testing and DevOps engineering. My dual expertise allows me to approach security from