Loading repository dataβ¦
Loading repository dataβ¦
enesdemir0 / repository
VisionMetric: A production-grade AI Image Captioning platform built with a Microservices Architecture. Features a distributed pipeline with Node.js, Python (FastAPI), TensorFlow, and Next.js. Orchestrated via Kubernetes (EKS/Kind) and provisioned with Terraform (IaC). Fully automated CI/CD via GitHub Actions.
A production-grade microservices platform that accepts image uploads and returns AI-generated captions via a custom TensorFlow model. Deployable locally with Docker Compose/Kind or fully on AWS EKS with a single git push.
flowchart TD
subgraph Internet
Browser["π Browser"]
end
subgraph "AWS / Local Ingress"
ALB["βοΈ ALB / NGINX Ingress"]
end
subgraph "Application Layer (EKS / Kind)"
Frontend["π₯οΈ Next.js Frontend :3001"]
Auth["π Auth Service\nNode.js :3000"]
Gateway["πͺ Gateway\nFastAPI :8000"]
Worker["π€ AI Worker\nTensorFlow"]
MetaAPI["π Metadata API\nFastAPI :8001"]
Redis[("π₯ Redis Queue")]
end
subgraph "Storage Layer"
S3["βοΈ S3 Bucket\n(EKS: STORAGE_TYPE=s3)"]
Volume["π Shared PVC\n(Local: STORAGE_TYPE=local)"]
RDS[("π RDS PostgreSQL\n(EKS: auth_db + metadata_db)")]
LocalDB[("π Local Postgres\n(Docker / Kind)")]
end
Browser -- "HTTPS" --> ALB
ALB --> Frontend
ALB -- "/api/auth/*" --> Auth
ALB -- "/api/gateway/*" --> Gateway
ALB -- "/api/metadata/*" --> MetaAPI
Auth --- RDS
Auth --- LocalDB
Gateway -- "Verify JWT" --> Auth
Gateway -- "STORAGE_TYPE=s3" --> S3
Gateway -- "STORAGE_TYPE=local" --> Volume
Gateway -- "RPUSH task_json" --> Redis
Redis -- "BLPOP" --> Worker
Worker -- "STORAGE_TYPE=s3 β download" --> S3
Worker -- "STORAGE_TYPE=local β read" --> Volume
Worker -- "POST /save" --> MetaAPI
MetaAPI --- RDS
MetaAPI --- LocalDB
| Concern | Local (Docker Compose / Kind) | Cloud (AWS EKS) |
|---|---|---|
| Image storage | STORAGE_TYPE=local β shared PVC at /app/uploads | STORAGE_TYPE=s3 β S3 bucket via IRSA |
| Databases | Two Postgres containers (auth-db, metadata-db) | Single db.t3.micro RDS with logical auth_db + metadata_db databases |
| Credentials | .env file / K8s Secrets with dev values | IAM Roles for Service Accounts (IRSA) β zero hardcoded keys |
| Ingress | NGINX Ingress Controller (k8s/ingress.yaml) | AWS ALB via Load Balancer Controller (k8s/ingress-alb.yaml) |
| Redis | Docker Compose service / K8s Deployment | K8s Deployment (in-cluster, no ElastiCache to save cost) |
| CI trigger | k8s-validation.yml on feature branches | deploy-aws.yml on push to main |
The Python code switches between storage backends using a single environment variable:
if settings.STORAGE_TYPE == "s3":
# uploads to S3 via aioboto3 (gateway) / downloads via boto3 (ai-worker)
else:
# reads/writes /app/uploads on shared PVC
| Service | Language | Port | Role |
|---|---|---|---|
| frontend | Next.js 14 / TypeScript | 3001 | UI β login, upload, caption gallery |
| auth-service | Node.js / Express | 3000 | JWT auth, HttpOnly cookie sessions |
| gateway-service | Python / FastAPI | 8000 | Upload proxy, file storage, Redis producer |
| ai-worker | Python / TensorFlow | β | Redis consumer, image captioning inference |
| metadata-api | Python / FastAPI | 8001 | Caption persistence (PostgreSQL) |
| Redis | Redis 7 | 6379 | Async task queue |
| Postgres (local) | PostgreSQL 15 | 5432 / 5433 | Auth DB + Metadata DB |
| RDS (AWS) | PostgreSQL 15 | 5432 | Single instance, two logical databases |
git, make (optional)# 1. Clone
git clone https://github.com/your-org/aws-eks-microservices-terraform-cicd.git
cd aws-eks-microservices-terraform-cicd
# 2. Configure
cp .env.example .env
# Edit .env β set JWT_SECRET to any random string
# 3. Start all 8 containers
docker compose up --build
# 4. Open app
open http://localhost:3001
All services are ready in ~60 seconds. The AI Worker loads the TensorFlow model on first start.
# Auth (Jest + Supertest β spins up Postgres)
cd services/auth-service && npm test
# Gateway (Pytest β fully mocked, no external deps)
cd services/gateaway && pip install -r requirements.txt && pytest tests/ -v
# Metadata API (Pytest + SQLite in-memory)
cd services/metadata-api && pip install -r requirements.txt && pytest tests/ -v
# Full end-to-end (requires all services running)
docker compose up -d
pip install httpx pytest
pytest tests/integration/ -v -s --timeout=120
The k8s-validation.yml workflow validates the full K8s stack on every PR:
# Manual validation (requires kind + kubectl)
kind create cluster --config .github/kind-config.yaml --name local
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/kind/deploy.yaml
docker build -t auth-service:latest ./services/auth-service
docker build -t gateway-service:latest ./services/gateaway
docker build -t ai-worker:latest ./services/ai-worker
docker build -t metadata-api:latest ./services/metadata-api
docker build -t frontend:latest ./services/frontend
kind load docker-image auth-service:latest gateway-service:latest \
ai-worker:latest metadata-api:latest frontend:latest --name local
kubectl apply -f k8s/auth/secret.yaml
kubectl apply -f k8s/metadata-api/secret.yaml
kubectl apply -f k8s/shared/
kubectl apply -f k8s/auth/
kubectl apply -f k8s/gateway/
kubectl apply -f k8s/ai-worker/
kubectl apply -f k8s/metadata-api/
kubectl apply -f k8s/frontend/
kubectl apply -f k8s/ingress.yaml
aws-cli v2 configured (aws configure)kubectl, helmcd terraform
# Copy and fill in variables
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars: set db_password to a strong value
terraform init
terraform plan -out=tfplan
terraform apply tfplan
Terraform provisions:
visionmetric-eks) with 2Γ t3.medium managed nodesdb.t3.micro PostgreSQL (Free Tier) β auth_db + metadata_dbCapture outputs for CI/CD secrets:
terraform output -json
In your GitHub repository β Settings β Secrets β Actions, add:
| Secret | Value (from terraform output) |
|---|---|
AWS_ACCESS_KEY_ID | CI/CD IAM user access key |
AWS_SECRET_ACCESS_KEY | CI/CD IAM user secret key |
RDS_ENDPOINT | terraform output -raw rds_endpoint |
DB_USERNAME | Value of var.db_username in tfvars |
DB_PASSWORD | Value of var.db_password in tfvars |
JWT_SECRET | A strong random string (β₯32 chars) |
S3_BUCKET | terraform output -raw s3_bucket_name |
IRSA_ROLE_ARN | terraform output -raw irsa_role_arn |
ALB_CONTROLLER_ROLE_ARN | From terraform output β eks module alb role |
git push origin main
The deploy-aws.yml workflow:
kubectl for the EKS clustervisionmetric namespacevisionmetric-worker service account with the IRSA role ARNSTORAGE_TYPE=s3, RDS endpoints, S3 bucket)metadata_db on RDS (idempotent K8s Job)Access your application at the printed URL:
β
Application URL: http://visionmetric-eks-XXXXX.us-east-1.elb.amazonaws.com
# 1. Delete K8s resources (removes ALB and EBS volumes first)
aws eks update-kubeconfig --region us-east-1 --name visionmetric-eks
kubectl delete namespace visionmetric
kubectl delete -f k8s/ingress-alb.yaml --ignore-not-found
# Wait ~2 min for ALB/ENI cleanup
sleep 120
# 2. Destroy all Terraform resources
cd terraform && terraform destroy
Important: Always delete K8s resources before
terraform destroy. ALBs and EBS volumes provisioned by Kubernetes controllers are not tracked in Terraform state and will block VPC deletion.
terraform/
βββ provider.tf # AWS provider, required versions
βββ variables.tf # All input variables with defaults
βββ main.tf # Root module wiring
βββ outputs.tf # Key outputs (cluster name, ECR URLs, etc.)
βββ terraform.tfvars.example
βββ modules/
βββ vpc/ # VPC, 2 public + 2 private subnets, 1 NAT GW
βββ eks/ # EKS cluster, managed node group, OIDC provider
β βββ alb-controller-policy.json
βββ rds/ # db.t3.micro PostgreSQL, private subnet
βββ s3/ # Uploads bucket, SSE, lifecycle to IA/Glacier
βββ ecr/ # 5 repos, lifecycle policy (keep 5 images)
βββ irsa/ # IAM role for visionmetric-worker K8s SA
| Resource | Type | Estimated Cost |
|---|---|---|
| EKS Control Plane | β | $73/month |
| EC2 Worker Nodes | 2Γ t3.medium | ~$60/month (On-Demand) |
| RDS PostgreSQL | db.t3.micro | Free Tier (750h/month for 12mo) |
| S3 Storage | 5 GB | Free Tier (5 GB/month for 12mo) |
| NAT Gateway | 1Γ | ~$32/month + $0.045/GB |
| ALB | 1Γ | ~$16/month + LCU charges |
| ECR Storage | 5 repos, 5 images | ~$0.10/month |
| Total | ~$180/month |
After Free Tier: ~$220/month total.
| Decision | Monthly Saving | Trade-off |
|---|---|---|
| 1 NAT Gateway instead of 3 | ~$64/month | Single AZ egress β AZ failure loses outbound |
| Redis in K8s (not ElastiCache) | ~$25/month | No replication, tasks lost on pod restart |
| Single RDS for auth + metadata | ~$15/month | Noisy-neighbour risk between services |
| 2 nodes max, no cluster autoscaler | Capped EC2 bill | Manual scaling required for traffic spikes |
| S3 lifecycle to GLACIER after 90d | Storage savings | 90-day access delay for old images |
Every resource is configured for clean destruction:
aws_s3_bucket.force_destroy = true β empties bucket before deletionaws_ecr_repository.force_delete = true β deletes all imagesaws_db_instance.skip_final_snapshot = true β no orphaned snapshotaws_db_instance.deletion_protection = false β allows destroy| Workflow | Trigger | Purpose |
|---|---|---|
auth-ci.yml | services/auth-service/** | Jest + Postgres integration tests |
gateway-ci.yml | services/gateaway/** | Pytest (fully mocked) |
metadata-ci.yml | services/metadata-api/** | Pytest + SQLite in-memory |
frontend-ci.yml | services/frontend/** | TypeScript check + Next.js build |
k8s-validation.yml | PRs to main, feature/k8s-* | Full Kind cluster smoke test |
deploy-aws.yml | Push to main | Build β ECR β EKS deploy |
TensorFlow inference takes 2β10 seconds. A synchronous HTTP call would block gateway threads and time out under load. Redis RPUSH/BLPOP gives O(1) enqueue/dequeue, and the queue survives worker restarts without losing in-flight tasks.
The STORAGE_TYPE env