Building a Cloud-Native DevOps Pipeline from First Principles

Table of contents
- Introduction
- Project Architecture Overview
- First Principles: Understanding the Core Concepts
- Implementing the Infrastructure with Terraform
- Application Architecture and Containerization
- Kubernetes Deployment with Helm
- CI/CD Pipeline with GitHub Actions
- The Complete System: Integration and Flow
- Security and Best Practices
- Challenges and Learning Outcomes
- Conclusion
- Future Enhancements

Introduction
In today's rapidly evolving technological landscape, understanding how to build and deploy applications using cloud-native methodologies is essential for any software engineer. This blog post details the implementation of a complete DevOps pipeline for the vProfile application, a multi-tier Java web application, using Infrastructure as Code (IaC), containerization, Kubernetes orchestration, and CI/CD practices.
This project applies modern DevOps practices from first principles, creating a robust, scalable, and automated deployment pipeline on AWS cloud infrastructure. We'll explore each component of the system, understand the underlying principles, and see how they work together to create a seamless deployment experience.
Project Architecture Overview
The vProfile project utilizes a microservices architecture deployed on AWS using Kubernetes. Before diving into the implementation details, let's understand the high-level architecture:
flowchart TD
subgraph "CI/CD Pipeline"
GH[GitHub Repositories]
GA[GitHub Actions]
TS[Test & SonarQube]
DB[Docker Build]
ECR[Amazon ECR]
end
subgraph "AWS Infrastructure"
TF[Terraform]
VPC[AWS VPC]
EKS[Amazon EKS]
S3[S3 State Backend]
end
subgraph "Kubernetes Deployment"
HC[Helm Charts]
ING[NGINX Ingress]
APP[Vprofile App]
DB2[MySQL]
MC[Memcached]
RMQ[RabbitMQ]
end
GH --> GA
GA --> TS
TS --> DB
DB --> ECR
GH --> TF
TF --> VPC
TF --> EKS
TF --> S3
TF --> ING
ECR --> HC
HC --> APP
HC --> DB2
HC --> MC
HC --> RMQ
ING --> APP
The project is divided into two main repositories:
- iac-vprofile: Responsible for infrastructure provisioning using Terraform
- vprofile-action: Contains the application code and deployment configurations
This separation of concerns ensures that infrastructure and application code can evolve independently while maintaining a cohesive deployment strategy.
First Principles: Understanding the Core Concepts
Infrastructure as Code (IaC)
At its core, Infrastructure as Code is about managing infrastructure through machine-readable definition files rather than manual processes. This approach offers several key benefits:
- Reproducibility: Infrastructure can be consistently reproduced across different environments
- Version Control: Infrastructure changes can be tracked, reviewed, and rolled back
- Automation: Reduces manual errors and increases deployment speed
- Documentation: The code itself documents the infrastructure
In our project, we use Terraform to define our AWS infrastructure, including VPC, subnets, and the EKS cluster.
Containerization
Containers encapsulate an application and its dependencies into a self-contained unit that can run anywhere. The key principles include:
- Isolation: Applications run in isolated environments
- Portability: Containers run consistently across different environments
- Efficiency: Lighter weight than virtual machines
- Scalability: Containers can be easily scaled horizontally
We use Docker to containerize our vProfile application, creating a multi-stage build process that optimizes the final image size.
Orchestration
Container orchestration automates the deployment, scaling, and management of containerized applications. Core principles include:
- Service Discovery: Containers can find and communicate with each other
- Load Balancing: Traffic is distributed across containers
- Self-healing: Failed containers are automatically replaced
- Scaling: Applications can scale up or down based on demand
Amazon EKS (Elastic Kubernetes Service) serves as our orchestration platform, providing a managed Kubernetes environment.
Continuous Integration and Continuous Deployment (CI/CD)
CI/CD bridges the gap between development and operations by automating the building, testing, and deployment processes:
- Continuous Integration: Code changes are regularly built and tested
- Continuous Delivery: Code is always in a deployable state
- Continuous Deployment: Code changes are automatically deployed to production
- Feedback Loops: Developers get quick feedback on changes
GitHub Actions powers our CI/CD pipeline, automating everything from code testing to Kubernetes deployment.
Implementing the Infrastructure with Terraform
VPC Configuration
The foundation of our AWS infrastructure is a well-architected Virtual Private Cloud (VPC):
flowchart TD
subgraph "AWS Region us-east-2"
VPC["VPC: 172.20.0.0/16"]
subgraph "Availability Zones"
AZ1["AZ 1"]
AZ2["AZ 2"]
AZ3["AZ 3"]
end
subgraph "Private Subnets"
PS1["172.20.1.0/24"]
PS2["172.20.2.0/24"]
PS3["172.20.3.0/24"]
end
subgraph "Public Subnets"
PUS1["172.20.4.0/24"]
PUS2["172.20.5.0/24"]
PUS3["172.20.6.0/24"]
end
NG["NAT Gateway"]
IGW["Internet Gateway"]
end
VPC --> AZ1
VPC --> AZ2
VPC --> AZ3
AZ1 --> PS1
AZ2 --> PS2
AZ3 --> PS3
AZ1 --> PUS1
AZ2 --> PUS2
AZ3 --> PUS3
PUS1 --> IGW
PUS2 --> IGW
PUS3 --> IGW
PS1 --> NG
PS2 --> NG
PS3 --> NG
NG --> IGW
Our Terraform configuration creates a VPC with a CIDR block of 172.20.0.0/16, spanning three availability zones for high availability. It includes:
- Three private subnets for EKS worker nodes
- Three public subnets for the load balancer
- NAT gateway for outbound internet access from private subnets
- Appropriate tags for Kubernetes integration
Here's a key excerpt from our vpc.tf
:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2"
name = "vprofile-eks"
cidr = "172.20.0.0/16"
azs = slice(data.aws_availability_zones.available.names, 0, 3)
private_subnets = ["172.20.1.0/24", "172.20.2.0/24", "172.20.3.0/24"]
public_subnets = ["172.20.4.0/24", "172.20.5.0/24", "172.20.6.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
public_subnet_tags = {
"kubernetes.io/cluster/${local.cluster_name}" = "shared"
"kubernetes.io/role/elb" = 1
}
private_subnet_tags = {
"kubernetes.io/cluster/${local.cluster_name}" = "shared"
"kubernetes.io/role/internal-elb" = 1
}
}
EKS Cluster Configuration
Amazon EKS provides a managed Kubernetes control plane, while our worker nodes run in the private subnets of our VPC:
flowchart TD
subgraph "Amazon EKS"
CP["Control Plane"]
subgraph "Node Group 1"
NG1N1["t3.small"]
NG1N2["t3.small"]
end
subgraph "Node Group 2"
NG2N1["t3.small"]
end
end
subgraph "VPC"
PS["Private Subnets"]
end
subgraph "Autoscaling"
ASG["ASG Config:
Group 1: 1-3 nodes
Group 2: 1-2 nodes"]
end
CP --> NG1N1
CP --> NG1N2
CP --> NG2N1
NG1N1 --> PS
NG1N2 --> PS
NG2N1 --> PS
ASG --> NG1N1
ASG --> NG1N2
ASG --> NG2N1
Our EKS configuration creates a cluster with version 1.27 and two managed node groups running on t3.small instances:
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "19.19.1"
cluster_name = local.cluster_name
cluster_version = "1.27"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
cluster_endpoint_public_access = true
eks_managed_node_group_defaults = {
ami_type = "AL2_x86_64"
}
eks_managed_node_groups = {
one = {
name = "node-group-1"
instance_types = ["t3.small"]
min_size = 1
max_size = 3
desired_size = 2
}
two = {
name = "node-group-2"
instance_types = ["t3.small"]
min_size = 1
max_size = 2
desired_size = 1
}
}
}
Terraform Workflow Automation
We use GitHub Actions to automate the Terraform workflow, ensuring consistent infrastructure deployments:
sequenceDiagram
actor Developer
participant GitHub as GitHub Repository
participant Actions as GitHub Actions
participant S3 as S3 Backend
participant AWS as AWS Services
Developer->>GitHub: Push code to main branch
GitHub->>Actions: Trigger workflow
Actions->>Actions: terraform init
Actions->>S3: Retrieve state
S3->>Actions: Return state
Actions->>Actions: terraform fmt check
Actions->>Actions: terraform validate
Actions->>Actions: terraform plan
alt if main branch
Actions->>Actions: terraform apply
Actions->>AWS: Create/update resources
AWS-->>Actions: Resources created/updated
Actions->>AWS: Configure kubectl
Actions->>AWS: Install NGINX Ingress
end
The workflow includes:
- Terraform initialization with S3 backend
- Format checking and validation
- Planning the infrastructure changes
- Applying changes only on the main branch
- Configuring kubectl and installing the NGINX ingress controller
Application Architecture and Containerization
vProfile Application Components
The vProfile application consists of multiple microservices:
flowchart TD
subgraph "vProfile Application"
WA["Web Application (Tomcat)"]
DB["MySQL Database"]
MC["Memcached"]
RMQ["RabbitMQ"]
end
User["User"] --> WA
WA --> DB
WA --> MC
WA --> RMQ
Each component is containerized and deployed as a separate service in Kubernetes.
Multi-stage Docker Build
We use a multi-stage Docker build to optimize our application container:
flowchart LR
subgraph "Build Stage"
JDK["OpenJDK 11"]
MVN["Maven Build"]
WAR["vprofile-v2.war"]
end
subgraph "Final Stage"
TC["Tomcat 9"]
DEPLOY["Deploy WAR"]
end
JDK --> MVN
MVN --> WAR
WAR --> DEPLOY
TC --> DEPLOY
The Dockerfile efficiently builds the application and creates a lean production image:
FROM openjdk:11 AS BUILD_IMAGE
RUN apt update && apt install maven -y
COPY ./ vprofile-project
RUN cd vprofile-project && mvn install
FROM tomcat:9-jre11
LABEL "Project"="Vprofile"
LABEL "Author"="Imran"
RUN rm -rf /usr/local/tomcat/webapps/*
COPY --from=BUILD_IMAGE vprofile-project/target/vprofile-v2.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080
CMD ["catalina.sh", "run"]
Kubernetes Deployment with Helm
Helm Charts Structure
Helm is used to template and parameterize our Kubernetes manifests:
flowchart TD
subgraph "Helm Chart Structure"
CH["vprofilecharts/"]
VAL["values.yaml"]
TPL["templates/"]
subgraph "Templates"
APP["vproappdep.yml"]
SVC["Service definitions"]
ING["vproingress.yaml"]
DB["Database templates"]
MC["Memcached templates"]
RMQ["RabbitMQ templates"]
end
end
CH --> VAL
CH --> TPL
TPL --> APP
TPL --> SVC
TPL --> ING
TPL --> DB
TPL --> MC
TPL --> RMQ
Application Deployment
The application deployment includes initialization containers to ensure dependencies are available:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vproapp
labels:
app: vproapp
spec:
replicas: 1
selector:
matchLabels:
app: vproapp
template:
metadata:
labels:
app: vproapp
spec:
containers:
- name: vproapp
image: {{ .Values.appimage}}:{{ .Values.apptag}}
ports:
- name: vproapp-port
containerPort: 8080
initContainers:
- name: init-mydb
image: busybox
command: ['sh', '-c', 'until nslookup vprodb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done;']
- name: init-memcache
image: busybox
command: ['sh', '-c', 'until nslookup vprocache01.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done;']
Ingress Configuration
The NGINX ingress controller routes external traffic to our application:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vpro-ingress
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- host: majorproject.nikhilmishra.live
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 8080
CI/CD Pipeline with GitHub Actions
Application CI/CD Workflow
Our GitHub Actions workflow for the application pipeline includes testing, building, and deploying:
sequenceDiagram
actor Developer
participant GitHub as GitHub
participant GHCI as GitHub Actions
participant Sonar as SonarQube
participant Docker as Docker Build
participant ECR as Amazon ECR
participant EKS as Amazon EKS
Developer->>GitHub: Push code
GitHub->>GHCI: Trigger workflow
GHCI->>GHCI: Maven test
GHCI->>GHCI: Checkstyle
GHCI->>Sonar: SonarQube scan
GHCI->>Docker: Build image
Docker->>ECR: Push image
GHCI->>EKS: Configure kubectl
GHCI->>EKS: Create Docker registry secret
GHCI->>EKS: Deploy with Helm
The workflow includes:
Testing Phase:
- Maven tests
- Code style checks
- SonarQube analysis for code quality
Build and Publish Phase:
- Docker image building
- Push to Amazon ECR
Deployment Phase:
- Configure kubectl
- Create registry credentials
- Deploy using Helm
Here's a key excerpt from the workflow file:
name: vprofile actions
on: workflow_dispatch
env:
AWS_REGION: us-east-2
ECR_REPOSITORY: vprofileapp
EKS_CLUSTER: vprofile-eks
jobs:
Testing:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Maven test
run: mvn test
- name: Checkstyle
run: mvn checkstyle:checkstyle
# More testing steps...
BUILD_AND_PUBLISH:
needs: Testing
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Build & Upload image to ECR
uses: appleboy/docker-ecr-action@master
with:
access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
registry: ${{ secrets.REGISTRY }}
repo: ${{ env.ECR_REPOSITORY }}
region: ${{ env.AWS_REGION }}
tags: latest,${{ github.run_number }}
daemon_off: false
dockerfile: ./Dockerfile
context: ./
DeployToEKS:
needs: BUILD_AND_PUBLISH
runs-on: ubuntu-latest
steps:
# Deployment steps...
- name: Deploy Helm
uses: bitovi/github-actions-deploy-eks-helm@v1.2.8
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
cluster-name: ${{ env.EKS_CLUSTER }}
chart-path: helm/vprofilecharts
namespace: default
values: appimage=${{ secrets.REGISTRY }}/${{ env.ECR_REPOSITORY }},apptag=${{ github.run_number }}
name: vprofile-stack
The Complete System: Integration and Flow
Now that we've examined each component individually, let's see how they work together in a complete CI/CD pipeline:
flowchart TD
subgraph "Developer Workflow"
IC["Infrastructure Code Changes"]
AC["Application Code Changes"]
subgraph "GitHub Repositories"
IRep["iac-vprofile"]
ARep["vprofile-action"]
end
end
subgraph "Infrastructure Pipeline"
IGH["GitHub Actions"]
TInit["Terraform Init"]
TPlan["Terraform Plan"]
TApply["Terraform Apply"]
KConfig["kubectl Config"]
NGinst["NGINX Ingress Install"]
end
subgraph "Application Pipeline"
AGH["GitHub Actions"]
Test["Maven Tests"]
CS["Checkstyle"]
SQ["SonarQube Analysis"]
DocBuild["Docker Build"]
DocPush["Push to ECR"]
HDepl["Helm Deployment"]
end
subgraph "AWS Infrastructure"
VPC["AWS VPC"]
EKS["Amazon EKS"]
ECR["Amazon ECR"]
S3["S3 State Bucket"]
end
subgraph "Kubernetes Resources"
ING["Ingress"]
APP["vProfile App"]
DB["MySQL"]
MC["Memcached"]
RMQ["RabbitMQ"]
end
subgraph "End Users"
User["Users"]
end
IC --> IRep
AC --> ARep
IRep --> IGH
IGH --> TInit
TInit --> TPlan
TPlan --> TApply
TApply --> KConfig
KConfig --> NGinst
ARep --> AGH
AGH --> Test
Test --> CS
CS --> SQ
SQ --> DocBuild
DocBuild --> DocPush
DocPush --> HDepl
TApply --> VPC
TApply --> EKS
TApply --> S3
DocPush --> ECR
HDepl --> ING
HDepl --> APP
HDepl --> DB
HDepl --> MC
HDepl --> RMQ
NGinst --> ING
ING --> APP
User --> ING
The workflow proceeds as follows:
- Infrastructure changes trigger the Terraform workflow to create or update AWS resources
- Application changes trigger the application workflow for testing, building, and deployment
- The application is deployed to the EKS cluster created by the infrastructure pipeline
- Users access the application through the NGINX ingress controller
Security and Best Practices
Throughout this project, we've implemented several security best practices:
- Least Privilege: Using IAM roles with minimal permissions
- Infrastructure Segregation: Separating public and private subnets
- Secrets Management: Storing sensitive information in GitHub Secrets
- Image Security: Using multi-stage builds to minimize attack surface
- Code Quality: Implementing automated testing and code analysis
Challenges and Learning Outcomes
Building this project presented several interesting challenges:
- Terraform State Management: Learning to manage state files securely using S3 backends
- Kubernetes Networking: Understanding the intricacies of Kubernetes ingress and service discovery
- CI/CD Integration: Connecting multiple pipelines with appropriate dependencies
- Container Optimization: Creating efficient Docker images using multi-stage builds
Conclusion
The vProfile project demonstrates a comprehensive implementation of modern DevOps principles and practices. By leveraging Infrastructure as Code, containerization, Kubernetes orchestration, and CI/CD pipelines, we've created a robust, scalable, and easily maintainable deployment pipeline.
This approach offers several key benefits:
- Speed: Automated deployments reduce time-to-market
- Consistency: Infrastructure and application deployments are reproducible
- Scalability: Kubernetes allows for easy scaling of application components
- Maintainability: Code-based infrastructure and pipelines simplify maintenance
- Resilience: Multi-AZ deployment ensures high availability
The knowledge and skills gained from building this project provide a solid foundation for implementing similar architectures in other enterprise contexts. Understanding these DevOps principles from first principles enables you to adapt these patterns to various cloud platforms and application architectures.
Future Enhancements
While the current implementation is robust, several enhancements could further improve the system:
- Multiple Environments: Extend the infrastructure to support development, staging, and production
- Advanced Monitoring: Implement comprehensive monitoring with Prometheus and Grafana
- Service Mesh: Add Istio or Linkerd for advanced traffic management and security
- GitOps: Implement ArgoCD or Flux for GitOps-based continuous deployment
- Automated Testing: Add more comprehensive integration and end-to-end tests
By continuing to evolve this architecture, we can create an even more powerful and flexible DevOps platform.
This project was developed as a major project for college by Nikhil Mishra. The source code is available in the iac-vprofile and vprofile-action repositories.
Subscribe to my newsletter
Read articles from Nikhil Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nikhil Mishra
Nikhil Mishra
I am a student studying in Mumbai University, learning DevOps, looking for opportunities to learn more things by gaining experience at prestigious institutions