A cost-optimized production-style DevOps deployment for a full-stack InvoiceFlow application using AWS EC2, K3s, Terraform, Helm, ArgoCD, GitHub Actions, Docker Hub, Prometheus, and Grafana.
This project demonstrates how a containerized full-stack application can be deployed using GitOps principles on a lightweight Kubernetes cluster without the high monthly cost of a full EKS-based setup.
InvoiceFlow is deployed using a lightweight cloud-native architecture:
Developer pushes code to GitHub
↓
GitHub Actions builds Docker images
↓
Images are pushed to Docker Hub
↓
ArgoCD watches the GitHub repository
↓
ArgoCD deploys the Helm chart to K3s
↓
K3s runs frontend and backend containers
↓
Application is exposed through the EC2 public IPThis project was designed as a budget-friendly alternative to EKS while still demonstrating real DevOps practices such as infrastructure as code, containerization, Kubernetes deployment, GitOps, CI/CD, and monitoring.
Internet
|
v
AWS EC2 Instance
|
+-----------+-----------+
| |
K3s ArgoCD
|
+-----+-------------------------------+
| |
Frontend Pod Backend Pod
| |
Frontend Service Backend Service
| |
+------------- Kubernetes ------------+
|
PostgreSQL
Container DB / Optional RDS| Area | Technology |
|---|---|
| Cloud Provider | AWS |
| Infrastructure as Code | Terraform |
| Compute | EC2 |
| Kubernetes | K3s |
| GitOps | ArgoCD |
| Packaging | Helm |
| CI/CD | GitHub Actions |
| Container Registry | Docker Hub |
| Frontend | React / Vite |
| Backend | Node.js / Express |
| Database | PostgreSQL |
| Monitoring | Prometheus + Grafana |
| OS | Amazon Linux |
invoiceflow-devops-infrastructure/
├── app/
│ ├── frontend/
│ └── backend/
│
├── helm/
│ └── invoiceflow/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│
├── argocd/
│ └── application.yaml
│
├── terraform/
│ ├── provider.tf
│ ├── variables.tf
│ ├── main.tf
│ ├── security-group.tf
│ ├── user-data.sh.tpl
│ └── outputs.tf
│
├── .github/
│ └── workflows/
│ └── docker-build.yml
│
└── README.mdThis project uses a cost-optimized setup instead of a full EKS architecture.
A full EKS-based production setup usually requires:
- EKS control plane
- Managed node groups
- NAT Gateway
- Load Balancer
- RDS
- Public IPv4 charges
That setup is more expensive.
This project uses:
- Single EC2 instance
- K3s Kubernetes
- ArgoCD
- Helm
- Docker Hub
- Optional lightweight monitoring
This keeps cost low while still showing practical DevOps skills.
GitHub Actions automatically builds and pushes Docker images when changes are made inside the app/ folder.
Workflow trigger:
on:
push:
branches:
- main
paths:
- "app/**"
- ".github/workflows/docker-build.yml"The workflow builds:
invoicebackend:v2
invoicebackend:<github-sha>
invoicefrontend:v2
invoicefrontend:<github-sha>These images are pushed to Docker Hub and then used by the Helm chart.
ArgoCD watches the Helm chart path:
helm/invoiceflowWhen the Helm chart or values file changes, ArgoCD automatically syncs the application into the invoiceflow namespace.
GitHub Repository
↓
ArgoCD Application
↓
Helm Chart
↓
K3s Cluster
↓
InvoiceFlow Podscd terraformterraform initterraform fmtterraform validateterraform planterraform applyType:
yesTerraform creates the EC2 instance and runs the user-data script to install K3s and ArgoCD.
The EC2 user-data script performs the initial server bootstrap:
1. Install required packages
2. Install K3s
3. Configure kubeconfig for ec2-user
4. Create invoiceflow namespace
5. Create Kubernetes Secret
6. Install ArgoCD
7. Wait for ArgoCD CRD
8. Apply the ArgoCD Application manifestMonitoring is intentionally not installed in user-data because Prometheus and Grafana can be heavy.
sudo kubectl get nodessudo kubectl get pods -Asudo kubectl get pods -n invoiceflow
sudo kubectl get svc -n invoiceflow
sudo kubectl get ingress -n invoiceflowsudo kubectl get applications -n argocd
sudo kubectl describe application invoiceflow -n argocdsudo tail -n 100 /var/log/invoiceflow-userdata.logArgoCD is accessed securely using port-forwarding instead of exposing it publicly.
Run inside EC2:
sudo kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echoUsername:
adminInside EC2:
sudo kubectl -n argocd port-forward svc/argocd-server 8080:443 --address 127.0.0.1On local machine:
ssh -i "path/to/key.pem" -L 8080:127.0.0.1:8080 ec2-user@EC2_PUBLIC_IPOpen:
https://localhost:8080This project uses a lightweight Prometheus and Grafana setup instead of the full kube-prometheus-stack.
sudo KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
sudo KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm repo add grafana https://grafana.github.io/helm-charts
sudo KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm repo updatesudo KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install prometheus prometheus-community/prometheus \
--namespace monitoring \
--create-namespace \
--set alertmanager.enabled=false \
--set pushgateway.enabled=false \
--set server.persistentVolume.enabled=false \
--set server.retention=6h \
--set server.resources.requests.cpu=100m \
--set server.resources.requests.memory=256Mi \
--set server.resources.limits.cpu=300m \
--set server.resources.limits.memory=512Misudo KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install grafana grafana/grafana \
--namespace monitoring \
--set adminPassword=admin123 \
--set persistence.enabled=false \
--set service.type=ClusterIP \
--set resources.requests.cpu=50m \
--set resources.requests.memory=128Mi \
--set resources.limits.cpu=200m \
--set resources.limits.memory=256Misudo kubectl get pods -n monitoring
sudo kubectl get svc -n monitoringInside EC2:
sudo kubectl -n monitoring port-forward svc/grafana 3000:80 --address 127.0.0.1On local machine:
ssh -i "path/to/key.pem" -L 3000:127.0.0.1:3000 ec2-user@EC2_PUBLIC_IPOpen:
http://localhost:3000Login:
Username: admin
Password: admin123up
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
100 - ((node_filesystem_avail_bytes{mountpoint="/"} * 100) / node_filesystem_size_bytes{mountpoint="/"})
- K8 folder was created for local kubernetes testing before implementing helm.
- There is no HPA on kubernetes or Autoscaling on EC2 because this is a simple project built to run on a single EC2 instance with limited CPU and memory.























