Te folder /iac contains the AWS infrastructure for the TurboVets DevOps assessment, implemented using CDK for Terraform (CDKTF) in TypeScript.
The goal is to deploy the Dockerized Express + TypeScript application behind a public load balancer on ECS Fargate, within an isolated multi-AZ VPC, using infrastructure that is:
- Modular and environment-aware
- Least-privilege by default (IAM roles & scoped access)
- Portable across regions/accounts
- Easy to deploy, update, and destroy
All resources are deployed in one AWS region but across two Availability Zones for high availability.
- One VPC per environment (
dev,staging,prod) - CIDR:
10.0.0.0/16 - Two public subnets (ALB + ECS Fargate tasks)
- Internet gateway for ALB/Fargate egress
- Public subnets map public IPs (
assignPublicIp = truefor Fargate tasks in this assessment)
Future improvement: move ECS tasks to private subnets with NAT (see Section 8).
- Stores app Docker images
- Named:
${SERVICE_NAME}-${ENVIRONMENT}(for this assessment:turbovets-app-dev) - Images pushed from developer machine or CI pipeline
- ECS cluster per environment
- Fargate service with:
- CPU: 256
- Memory: 512
- Desired count: 1
- Task definition references ECR image
- Internet-facing
- Listens on port
80 - Forwards traffic to ECS tasks on port
3000 - Health check on
/health
- ALB SG
- Allows inbound HTTP from the internet (port 80)
- ECS SG
- Only allows inbound traffic from ALB SG on port
3000
- Only allows inbound traffic from ALB SG on port
- Task Execution Role
- Pull from ECR
- Push logs to CloudWatch
- Task Role
- Empty (least privilege, extend when needed)
- CloudWatch Log Group:
/ecs/turbovets-app-<env>(e.g./ecs/turbovets-app-dev) - ECS task logs automatically streamed
The deployment is parameterized using:
cdktf.json- Environment variables
- CDKTF context
No AWS account, region, or sensitive values are hardcoded.
| Variable | Description |
|---|---|
ENVIRONMENT |
Logical environment name (dev, staging, prod). Used for naming and isolation. |
AWS_REGION |
AWS region where all resources are deployed. |
SERVICE_NAME |
Base name used across ECS Service, ECS Cluster, ECR repo, ALB, etc. |
AWS_PROFILE |
Name of the local AWS CLI profile used for manual cdktf deploy. |
VPC_CIDR |
VPC CIDR block for the environment (ex: 10.0.0.0/16). |
ALB_ALLOWED_CIDR |
CIDR block allowed to access the ALB (0.0.0.0/0 = public internet). |
DESIRED_COUNT |
Number of ECS tasks to run (default: 1). |
FARGATE_CPU |
CPU units for each ECS task (e.g., 256 = 0.25 vCPU). |
FARGATE_MEMORY |
Memory allocated to each ECS task in MiB (default: 512). |
AWS_ACCOUNT_ID |
AWS Account ID where the stack is deployed. |
ECR_REPOSITORY |
ECR repository name (ex: turbovets-app). Must match Terraform ECR configuration. |
CONTAINER_PORT |
Port exposed by the application inside the container (ex: 3000). |
ECR_URI |
Computed URI for your ECR repo (<account>.dkr.ecr.<region>.amazonaws.com/<repo>). |
CONTAINER_IMAGE |
Full ECR image URI + tag used for ECS deployment. |
ENABLE_HTTPS |
Enables HTTPS + ACM certificate + Route53 integration when set to true. |
DOMAIN_NAME |
Fully Qualified Domain Name served by the ALB (ex: app.example.com). |
HOSTED_ZONE_ID |
Route53 Hosted Zone ID used to create the ALB alias record. |
From the project root:
cd iac
cp .env.example .envEdit .env to override defaults, and source your variables in the command line:
set -a # export all variables defined from now on
source .env # load variables from .env
set +a # stop auto-exporting
# Verify the variables
printenv | grep -E '^(ENVIRONMENT|AWS_REGION|SERVICE_NAME|AWS_PROFILE|VPC_CIDR|ALB_ALLOWED_CIDR|DESIRED_COUNT|FARGATE_CPU|FARGATE_MEMORY|AWS_ACCOUNT_ID|ECR_REPOSITORY|ECR_URI|CONTAINER_PORT|TF_STATE_BUCKET|TF_LOCK_TABLE|ENABLE_HTTPS|DOMAIN_NAME|HOSTED_ZONE_ID)='From the project root:
cd remote_tf_s3_dynamo_db_state_backend
cp .env.example .envEdit .env to override defaults, and source your variables in the command line:
set -a # export all variables defined from now on
source .env # load variables from .env
set +a # stop auto-exporting
# Verify the variables
printenv | grep -E '^(TF_STATE_BUCKET|TF_LOCK_TABLE)='Run once per environment locally before the first
cdktf deploy(from your laptop is fine).
From the project root:
aws configure sso
# SSO session name:
# SSO start URL: https://yourcompany.awsapps.com/start # Choose the right user associated with the environment
# SSO region: us-east-1
# or "aws configure": quicker for this project, but it's less secure
cd remote_tf_s3_dynamo_db_state_backend
# Variables required in .env:
# ENVIRONMENT, AWS_REGION, SERVICE_NAME, AWS_PROFILE, CONTAINER_PORT,
# AWS_ACCOUNT_ID, ECR_REPOSITORY, ECR_URI, CONTAINER_IMAGE
set -a
source .env
set +a
chmod +x create_backend.sh
./create_backend.shFrom the iac directory:
cd iac
# Variables required in .env:
# ENVIRONMENT, AWS_REGION, SERVICE_NAME, AWS_PROFILE, CONTAINER_PORT,
# AWS_ACCOUNT_ID, ECR_REPOSITORY, ECR_URI, CONTAINER_IMAGE
set -a
source .env
set +a
npx cdktf deploy --auto-approvecd app
docker build -f Dockerfile -t turbovets-app-local .From the iac folder:
cd ../iacSet the ECR_URI variable (either via .env or directly):
# Generic pattern – works for any AWS account
# Make sure AWS_ACCOUNT_ID, AWS_REGION, and ECR_REPOSITORY are set in .env
set -a # export all variables defined from now on
source .env # load variables from .env
set +a # stop auto-exporting
# Or compute it manually:
ECR_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}"Authenticate Docker to ECR using your IAM profile (no keys in the codebase):
aws ecr get-login-password \
--profile "$AWS_PROFILE" \
--region "$AWS_REGION" \
| docker login --username AWS --password-stdin "$ECR_URI"docker tag turbovets-app-local:latest "$ECR_URI:manual-test"
docker push "$ECR_URI:manual-test"From the iac directory:
cd iac
# Variables required in .env:
# ENVIRONMENT, AWS_REGION, SERVICE_NAME, AWS_PROFILE, CONTAINER_PORT,
# AWS_ACCOUNT_ID, ECR_REPOSITORY, ECR_URI, CONTAINER_IMAGE
set -a
source .env
set +a
npx cdktf deploy --auto-approveSave the Application Load Balancer DNS name for later:
ALB_DNS=... # The ALB URL printed in the cdktf deploy outputs (alb_dns_name)Deployment creates:
- VPC, subnets, route tables, internet gateway
- ALB + target group + listener
- ECS cluster, task definition, service
- CloudWatch log group
Outputs include:
alb_dns_name
ecr_repository_url
ecs_service_name
config_environment
config_region
curl -i http://$ALB_DNS/healthExpected:
HTTP/1.1 200 OK
{"status":"ok"}curl -i http://$ALB_DNS/Expected:
Hello from Express + TypeScript!
aws logs tail /ecs/$ECR_REPOSITORY \
--follow \
--profile $AWS_PROFILE \
--region $AWS_REGIONaws logs tail /ecs/${ECR_REPOSITORY}${ENVIRONMENT} \
--since 1d \
--profile $AWS_PROFILE \
--region $AWS_REGIONaws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:... \
--profile $AWS_PROFILE \
--region $AWS_REGIONShould show:
State: healthy
aws ecs describe-task-definition \
--task-definition $ECR_REPOSITORY \
--profile $AWS_PROFILE \
--region $AWS_REGION \
| jq '.taskDefinition.containerDefinitions[0].image'If port 3000 is free:
docker run --rm -p 3000:3000 turbovets-app-local
curl http://localhost:3000/healthExpected:
{"status":"ok"}
To destroy all deployed AWS infrastructure:
cd iac
npx cdktf destroyThis safely deletes:
- VPC + subnets + routing
- ALB + listener + target group
- ECS cluster + service + task definition
- CloudWatch log group
ECR repository is preserved so image history is not accidentally destroyed.
In AWS console:
- Go to Route53 → Hosted zones → Create hosted zone.
- Domain name:
marvinmeite.cloud - Type: Public hosted zone
- Click Create hosted zone.
You’ll get:
- A set of NS records (4 name servers).
- An SOA record.
Copy the 4 NS values (they look like ns-XXXX.awsdns-YY.net, etc.).
The zone will have an ID like Z1234567890ABCDEFG – that’s your HOSTED_ZONE_ID.
On the site where you bought marvinmeite.cloud (Namecheap, OVH, GoDaddy, whatever):
- Go to Domain management / DNS settings.
- Find Nameservers.
- Switch from “Default” to “Custom nameservers” (wording depends on the registrar).
- Paste the 4 NS records from Route53.
- Save changes.
DNS propagation can take anywhere from a few minutes to a couple of hours (sometimes up to 24h, but usually faster).
You can check when Route53 is in control:
dig NS marvinmeite.cloud +shortWhen it returns the Route53 NS servers, you’re good.
Exemple:
aws-docker-terrafrom-github-actions.marvinmeite.cloud
Use that as your DOMAIN_NAME in .env:
DOMAIN_NAME=aws-docker-terrafrom-github-actions.marvinmeite.cloud
HOSTED_ZONE_ID=Z1234567890ABCDEFG #Z1234567890ABCDEFG is just an exemple
ENABLE_HTTPS=trueThen re-deploy:
cd iac
set -a
source .env
set +a
npx cdktf deploy --auto-approveWhen Terraform finishes and ACM is Issued:
curl -i https://app.marvinmeite.cloud/health- Move tasks to fully private subnets and remove public IP assignment
- Add auto-scaling policies on CPU/memory
- Migrate CI pipeline from IAM user access keys to GitHub OIDC federation
- Add environment-scoped GitHub deployments (dev/staging/prod)
- Add Secrets Manager integration for runtime secrets
::contentReference[oaicite:0]{index=0}
You will be working from a pre-built Express.js + TypeScript starter application. Your task is to:
- Containerize the application and create a local dev environment
- Define a production-ready cloud deployment using CDK for Terraform
- Automate deployment via GitHub Actions
You may deploy to your own AWS account for testing, but your solution must be fully portable and documented so we can deploy it into our AWS environment.
Fork or clone the starter repository:
🔗 https://github.com/TurboVets/tv-devops-assessment
This contains the basic Express app you’ll be building on.
You must submit one GitHub repository with the following folder structure in the root:
Contains the application code and:
Dockerfiledocker-compose.yml- GitHub Actions workflows
README.mdwith local setup and CI/CD instructions
Contains the infrastructure code and:
- CDK for Terraform (in TypeScript)
- Configuration templates
README.mdwith deployment instructions for our AWS account
- Create a production-optimized
Dockerfile(multi-stage build, minimal layers, small image) - Create a
docker-compose.ymlto orchestrate the app - Add a
.dockerignoreto reduce build context size - App must respond to
http://localhost:3000/health
Use CDK for Terraform (TypeScript) to define:
- ECR repository
- ECS service (Fargate or EC2)
- VPC, subnets, security groups
- IAM roles (least privilege)
- (Optional) Load Balancer or API Gateway
- You may deploy to your own AWS account for validation
- DO NOT hardcode account IDs, regions, or credentials
- All infrastructure must be configurable using:
cdktf.json.envor config files- Environment variables
- Include clear instructions on how to:
- Override variables to use our AWS account
- Deploy and destroy the stack
- The final deployment must produce a publicly accessible
/healthendpoint
Set up GitHub Actions to:
- Trigger on push to
main - Build and tag a Docker image
- Push to ECR
- Deploy via
cdktf deploy
- Use GitHub Secrets to store:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY- Any other required env vars
- Parameterize everything (ECR URI, region, etc.)
- Include instructions for configuring secrets
- Add a CI badge to the
README.md
Record a screen-share video where you walk through:
- Your Docker and Compose setup
- CDKTF constructs and structure
- GitHub Actions workflows
- How we can configure and deploy it in our AWS account
- Any challenges or decisions worth noting
You do not need to appear on camera.
| Area | Expectation |
|---|---|
| Docker Setup | Clean, production-ready image; Compose works locally |
| IaC Quality | CDKTF code is modular, portable, and secure |
| CI/CD Flow | GitHub Actions runs cleanly; secrets handled properly |
| Portability | Can be deployed in our AWS account without code changes |
| Documentation | Detailed, step-by-step usage and setup instructions |
| Security | No hardcoded secrets or account info; uses IAM and GitHub Secrets |
| Communication | Clear, concise walkthrough video explaining design and deployment |
- Add Route53 and HTTPS
- CloudWatch logs and alerts
- Support multiple environments (dev/staging/prod)
- Use remote Terraform backend (S3 + DynamoDB)