Counter Service is a small HTTP API that maintains a single integer counter and exposes:
GET /count→ current counter valuePOST /count→ increments the counter by 1GET /metrics→ Prometheus metrics
The service is designed to run on AWS EKS and be deployed via Helm.
Persistence is pluggable:
- Redis (recommended; default for the Helm chart) — supports multiple replicas and rolling updates safely.
- File (PVC-backed) — useful for local/dev, but not suitable for multi-replica HA with a single RWO volume.
- Developer pushes code to GitHub.
- CI (GitHub Actions):
- runs tests (
pytest) - builds a Docker image
- pushes to Amazon ECR with tags:
{commit_sha}andlatest
- runs tests (
- CD (GitHub Actions):
- triggers on successful CI run on
main - selects the environment (
prodby default) - deploys to EKS using
helm upgrade --install
- triggers on successful CI run on
- Runtime (EKS):
Deploymentruns 1+ podsServiceexposes port 80 in-clusterIngress(AWS Load Balancer Controller) exposes the app externally- optional:
HPA,PDB,ServiceMonitor
- If
storageBackend=redis:- app connects to ElastiCache/Redis via
COUNTER_REDIS_URLand stores the counter under a key (defaultcounter)
- app connects to ElastiCache/Redis via
- If
storageBackend=file:- app reads/writes
{dataDir}/{filename}and requires a PVC (oremptyDirfor ephemeral mode)
- app reads/writes
.
├── app/ # FastAPI application code
│ ├── main.py # API + /metrics
│ ├── settings.py # env-driven configuration
│ └── storage/
│ ├── file_store.py # file persistence
│ └── redis_store.py # redis persistence
├── tests/ # pytest tests
├── helm/counter-service/ # Helm chart
│ ├── Chart.yaml
│ ├── values.yaml # defaults
│ ├── values-dev.yaml # environment overrides
│ ├── values-staging.yaml
│ └── values-prod.yaml
└── terraform/
├── bootstarp/ # IAM role for Terraform execution (bootstrap)
├── ci-pipeline/ # ECR + GitHub OIDC role for Actions
├── eks_cluster/ # VPC + EKS + addons + controllers + monitoring
│ └── envs/{dev,staging,prod}
└── redis/ # ElastiCache/Redis (recommended persistence)
Local tooling:
awsCLIterraform(1.x)kubectlhelm- Docker (for local builds)
AWS-side prerequisites:
- An AWS account with permissions to create IAM, VPC, EKS, ECR, ElastiCache.
- An S3 bucket (per environment) for Terraform remote state (see
terraform/eks_cluster/envs/*/backend.tf).
GitHub prerequisites:
- GitHub Environments:
dev,staging,prod(recommended) - Secrets configured per environment (see below)
python -m pip install -r requirements.txt
export COUNTER_STORAGE_BACKEND=file
export COUNTER_DATA_DIR="$(pwd)/.tmpdata"
export COUNTER_COUNTER_FILENAME="counter.txt"
mkdir -p "$COUNTER_DATA_DIR"
uvicorn app.main:app --host 0.0.0.0 --port 8080Test it:
curl -sS http://127.0.0.1:8080/count && echo
curl -sS -X POST http://127.0.0.1:8080/count && echo
curl -sS http://127.0.0.1:8080/metrics >/dev/null && echo "OK metrics"The recommended provisioning order is:
- Bootstrap (Terraform execution role)
- CI pipeline infra (ECR + GitHub OIDC role)
- Redis (ElastiCache)
- EKS cluster (VPC + EKS + addons/controllers + monitoring)
Purpose:
- Creates the Terraform execution role used by later stacks.
Files to update:
terraform/bootstarp/terraform.tfvarsaws_regionterraform_role_nameterraform_state_bucket_name(naming convention / shared state bucket)
Commands:
cd terraform/bootstarp
terraform init
terraform apply
terraform outputImportant output:
terraform_role_arn— referenced by other Terraform stacks.
Purpose:
- Creates the ECR repository (
counter-service) - Creates GitHub OIDC provider + IAM role for GitHub Actions (
AWS_ROLE_ARN)
Files to update:
terraform/ci-pipeline/terraform.tfvarsaws_regionecr_repository_namegithub_org/github_repo(OIDC trust policy subject)
Commands:
cd terraform/ci-pipeline
terraform init
terraform apply
terraform outputImportant outputs:
ecr_repository_urlgithub_actions_role_arn← put this into GitHub Secrets asAWS_ROLE_ARN
Purpose:
- Creates an ElastiCache Redis Replication Group (recommended backend for HA)
Files to update:
terraform/redis/terraform.tfvarsaws_regionenvironmentvpc_id(VPC where Redis is created)eks_node_security_group_id(node SG allowed to connect to Redis)allowed_cidr_blocks(optional internal access controls)subnet_ids/azsmust match your VPC design
Commands:
cd terraform/redis
terraform init
terraform apply
terraform outputImportant output:
- Redis endpoint (hostname + port). Use it in Helm values as:
app.redisUrl: "redis://<endpoint>:6379/0"(non-TLS)- or
rediss://...if you enforce TLS on your Redis.
Purpose:
- Creates VPC and EKS cluster
- Creates a managed node group
- Installs key components:
- AWS Load Balancer Controller (for Ingress)
- kube-prometheus-stack (Prometheus/Grafana) where enabled
- CloudWatch observability components where configured
Files to update:
terraform/eks_cluster/envs/<env>/terraform.tfvarsaws_regionenvironmentcluster_nameterraform_role_arn(from bootstrap)admin_role_arn(your admin role)github_actions_role_arn(from ci-pipeline output)- VPC CIDR, subnets/AZs, node group sizing, instance type, etc.
terraform/eks_cluster/envs/<env>/backend.tf- Ensure the S3 bucket exists and is in the correct region.
Commands (example for dev):
cd terraform/eks_cluster/envs/dev
terraform init
terraform apply
terraform outputAfter creation:
aws eks update-kubeconfig --name <cluster_name> --region <aws_region>
kubectl get nodes
kubectl get nsThe Helm chart lives in helm/counter-service.
- Image:
image.repository(ECR URL)image.tag(commit SHA)
- Storage mode:
app.storageBackend:redisorfile- Redis:
app.redisUrl(required whenredis)app.redisKey(defaults tocounter)
- File:
app.file.dataDir,app.file.filenamepersistence.enabled,persistence.size,persistence.storageClassName
values-dev.yamlvalues-staging.yamlvalues-prod.yaml
Each file typically sets:
replicaCountingress.hosts[0].hostapp.storageBackendapp.redisUrl(for Redis mode)- optional
autoscaling/pdb
If you render without an env overrides file, values.yaml defaults to storageBackend=redis but does not include app.redisUrl, so templating will fail by design.
Correct examples:
helm template counter-service helm/counter-service -f helm/counter-service/values-dev.yamlor for file backend:
helm template counter-service helm/counter-service \
--set app.storageBackend=file \
--set persistence.enabled=falseWhat it does:
- Installs Python dependencies
- Runs
pytestwith a file backend (isolated temp directory) - On
push(not PR):- assumes AWS role via OIDC (
secrets.AWS_ROLE_ARN) - logs in to ECR
- builds and pushes Docker image:
ECR:{sha}ECR:latest
- assumes AWS role via OIDC (
Required GitHub secret:
AWS_ROLE_ARN(created byterraform/ci-pipeline)
Triggers:
workflow_runafter CI succeeded onmain(deploys toprod)workflow_dispatchfor manual deploy todev/staging/prodwith optionalimage_tag
What it does:
- Chooses environment (
prodby default) - Picks Helm values file (
values-<env>.yaml) - Determines image tag:
workflow_run→ CIhead_sha- manual → input
image_tagorGITHUB_SHA
- Deploys via Helm (
upgrade --install)
These are the typical checks evaluators look for: pods healthy, endpoints work, persistence survives restart, and ingress works.
kubectl -n prod get deploy,po,svc,ingress
kubectl -n prod describe deploy counter-service | sed -n '/Image:/,/Conditions:/p'kubectl -n prod run curl-check --rm -i --restart=Never --image=curlimages/curl -- \
sh -lc 'set -e;
echo "GET /metrics"; curl -m 5 -sS http://counter-service.prod.svc.cluster.local/metrics >/dev/null; echo "OK metrics";
echo "GET /count"; curl -m 5 -sS http://counter-service.prod.svc.cluster.local/count; echo;
echo "POST /count"; curl -m 5 -sS -X POST http://counter-service.prod.svc.cluster.local/count; echo;
echo "GET /count"; curl -m 5 -sS http://counter-service.prod.svc.cluster.local/count; echo;
echo "OK counter"'# bump counter a few times
kubectl -n prod run curl-bump --rm -i --restart=Never --image=curlimages/curl -- \
sh -lc 'set -e; for i in 1 2 3; do curl -sS -X POST http://counter-service.prod.svc.cluster.local/count; echo; done; curl -sS http://counter-service.prod.svc.cluster.local/count; echo'
# restart deployment
kubectl -n prod rollout restart deploy/counter-service
kubectl -n prod rollout status deploy/counter-service --timeout=5m
# confirm value persisted
kubectl -n prod run curl-after --rm -i --restart=Never --image=curlimages/curl -- \
sh -lc 'set -e; echo "AFTER RESTART:"; curl -sS http://counter-service.prod.svc.cluster.local/count; echo'kubectl -n prod get ingress counter-service -o wide
# then curl the external hostname / DNS you configured:
curl -sS http://<your-domain>/count && echo- Regions: ensure Terraform (
aws_region) and GitHub Actions (AWS_REGION) match your target region. - Domains / ingress hostnames: update
helm/counter-service/values-*.yaml. - Redis endpoint: update
app.redisUrlin the environment values file (or inject via secrets). - Terraform tfvars: fill in your AWS account IDs, role ARNs, and state bucket names.
Internal / educational project.