A Java Spring Boot microservices project with Service A calling Service B, deployed on AWS ECS Fargate with multi-environment support using Terraform and GitHub Actions.
- Project Architecture
- Network Structure
- Project Structure
- Prerequisites
- Multi-Environment Setup
- Local Development
- Deployment
- Monitoring and Logs
- Cleanup/Destroy
- Terraform Learning Resources
┌─────────────┐ HTTP Call ┌─────────────┐
│ Service A │ ───────────────► │ Service B │
│ (Port 8080) │ │ (Port 8081) │
└─────────────┘ └─────────────┘
│ │
└───────────────┬───────────────┘
│
┌─────────────┐
│ ALB │
│ (Port 80) │
└─────────────┘
│
┌─────────────┐
│ Internet │
│ Gateway │
└─────────────┘
Service Flow:
- Service A exposes
/call-service-bendpoint - Service A calls Service B's
/helloendpoint internally - Both services are deployed on ECS Fargate
- Application Load Balancer distributes traffic
- Services communicate via internal network
VPC CIDR: 10.0.0.0/16
├── Public Subnets (for ALB)
│ ├── 10.0.1.0/24 (AZ-a)
│ └── 10.0.2.0/24 (AZ-b)
│
├── Private Subnets (for ECS Tasks)
│ ├── 10.0.11.0/24 (AZ-a)
│ └── 10.0.12.0/24 (AZ-b)
│
├── Internet Gateway
├── NAT Gateways (for outbound traffic)
└── Security Groups
├── ALB Security Group (80, 443)
├── Service A Security Group (8080)
└── Service B Security Group (8081)
- Staging: 10.0.0.0/16
- Production: 10.1.0.0/16
ecs-microservices-test/
├── README.md # This file
├── MULTI_ENVIRONMENT.md # Multi-environment guide
├── TERRAFORM_LEARNING.md # Terraform learning resources
├── docker-compose.yml # Local development
├── build.sh # Build script
├── deploy-env.sh # Environment deployment script
│
├── service-a/ # Service A (Java Spring Boot)
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/main/java/com/example/servicea/
│ ├── ServiceAApplication.java
│ └── controller/MainController.java
│
├── service-b/ # Service B (Java Spring Boot)
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/main/java/com/example/serviceb/
│ ├── ServiceBApplication.java
│ └── controller/HelloController.java
│
├── terraform/ # Infrastructure as Code
│ ├── variables.tf # Variable definitions
│ ├── vpc.tf # VPC, subnets, gateways
│ ├── ecs.tf # ECS cluster, services
│ ├── load_balancer.tf # ALB configuration
│ ├── task_definitions.tf # ECS task definitions
│ ├── outputs.tf # Output values
│ └── environments/ # Environment-specific configs
│ ├── staging.tfvars
│ └── production.tfvars
│
└── .github/workflows/ # CI/CD Pipeline
├── staging-deploy.yml
└── production-deploy.yml
# Install required tools
sudo apt-get update
sudo apt-get install -y docker.io maven openjdk-21-jdk
# Install Terraform
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install# Configure AWS CLI
aws configure
# Enter your AWS Access Key ID, Secret Access Key, and regionThe GitHub Actions workflow requires an IAM role with the following permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:DescribeRepositories",
"ecr:CreateRepository",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecs:*",
"ec2:*",
"elasticloadbalancing:*",
"logs:*",
"iam:PassRole",
"iam:CreateRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:GetRole",
"iam:ListAttachedRolePolicies"
],
"Resource": "*"
}
]
}AWS_REGION=ap-southeast-1
AWS_ROLE_ARN=arn:aws:iam::YOUR_ACCOUNT:role/GitHubActionsRole
Create environment-specific variable files:
terraform/environments/staging.tfvars
environment = "staging"
aws_region = "ap-southeast-1"
ecs_task_cpu = "256"
ecs_task_memory = "512"
ecs_desired_count = 1
enable_autoscaling = false
log_retention_days = 7
alb_deletion_protection = false
vpc_cidr = "10.0.0.0/16"terraform/environments/production.tfvars
environment = "production"
aws_region = "ap-southeast-1"
ecs_task_cpu = "512"
ecs_task_memory = "1024"
ecs_desired_count = 2
enable_autoscaling = true
log_retention_days = 30
alb_deletion_protection = true
vpc_cidr = "10.1.0.0/16"# Build both services
./build.sh
# Run locally with Docker Compose
docker-compose up -d
# Test the services
curl "http://localhost:8080/service-a/api/health"
curl "http://localhost:8080/service-a/api/greeting?name=John"
curl "http://localhost:8080/service-a/api/status"
curl "http://localhost:8081/service-b/api/health"
curl "http://localhost:8081/service-b/api/hello?name=World"
# Stop local services
docker-compose down# Build Service A
cd service-a
mvn clean package
java -jar target/service-a-0.0.1-SNAPSHOT.jar
# Build Service B
cd service-b
mvn clean package
java -jar target/service-b-0.0.1-SNAPSHOT.jarcd terraform
# Initialize Terraform
terraform init
# Plan staging deployment
terraform plan -var-file="environments/staging.tfvars"
# Apply staging deployment
terraform apply -var-file="environments/staging.tfvars"
# Get ALB URL
terraform output alb_dns_namecd terraform
# Plan production deployment
terraform plan -var-file="environments/production.tfvars"
# Apply production deployment
terraform apply -var-file="environments/production.tfvars"
# Get ALB URL
terraform output alb_dns_name# Deploy to staging
./deploy-env.sh staging
# Deploy to production
./deploy-env.sh productionPush to respective branches to trigger deployment:
- Push to
stagingbranch → deploys to staging environment - Push to
mainbranch → deploys to production environment
After deployment, get the ALB URL:
cd terraform
terraform output alb_dns_nameTest the endpoints:
# Service A greeting endpoint (calls Service B internally)
curl "http://YOUR_ALB_URL/service-a/api/greeting?name=John"
# Service A health check
curl http://YOUR_ALB_URL/service-a/api/health
# Service A status (checks both services)
curl http://YOUR_ALB_URL/service-a/api/status
# Service B hello endpoint directly
curl "http://YOUR_ALB_URL/service-b/api/hello?name=World"
# Service B health check
curl http://YOUR_ALB_URL/service-b/api/healthExpected Responses:
/api/greeting?name=John→ "ServiceA received: Hello John from Service B!"/api/health→ "Service A is healthy!" or "Service B is healthy!"/api/status→ "Service A: OK | Service B: Service B is healthy!"/api/hello?name=World→ "Hello World from Service B!"
# View Service A logs
aws logs tail /ecs/service-a-${ENVIRONMENT} --follow
# View Service B logs
aws logs tail /ecs/service-b-${ENVIRONMENT} --follow# Check ECS services
aws ecs list-services --cluster microservices-${ENVIRONMENT}
# Describe specific service
aws ecs describe-services --cluster microservices-${ENVIRONMENT} --services service-a-${ENVIRONMENT}cd terraform
terraform destroy -var-file="environments/staging.tfvars"cd terraform
terraform destroy -var-file="environments/production.tfvars"# Destroy all environments
cd terraform
# Staging
terraform destroy -var-file="environments/staging.tfvars" -auto-approve
# Production
terraform destroy -var-file="environments/production.tfvars" -auto-approve
# Clean up ECR repositories (if needed)
aws ecr delete-repository --repository-name service-a --force
aws ecr delete-repository --repository-name service-b --forceIf Terraform state is corrupted or you need to force cleanup:
# Remove Terraform state (use with caution)
rm terraform.tfstate*
# Manually delete resources via AWS Console or CLI
aws ecs delete-cluster --cluster microservices-staging
aws ecs delete-cluster --cluster microservices-production- Providers: AWS provider for infrastructure management
- Resources: VPC, ECS, ALB, etc.
- Variables: Environment-specific configurations
- Modules: Reusable infrastructure components
- State: Current infrastructure state tracking
- Use version constraints for providers
- Implement proper tagging strategy
- Use tfvars files for environment configurations
- Store state remotely (S3 + DynamoDB)
- Implement proper IAM permissions
# Format code
terraform fmt
# Validate configuration
terraform validate
# Show current state
terraform show
# List resources
terraform state list
# Import existing resource
terraform import aws_vpc.main vpc-12345678
# Target specific resource
terraform apply -target=aws_ecs_service.service_a-
ECR Login Issues
aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT.dkr.ecr.ap-southeast-1.amazonaws.com -
Service Discovery Issues
- Check security group rules
- Verify service mesh configuration
- Check ECS service logs
-
Terraform State Lock
terraform force-unlock LOCK_ID
-
Port Conflicts
- Service A: 8080
- Service B: 8081
- ALB: 80, 443
For issues or questions, check:
- AWS ECS documentation
- Terraform AWS provider documentation
- CloudWatch logs for application errors
- GitHub Actions workflow logs for CI/CD issues
sudo apt update && sudo apt install terraform
terraform version
terraform init
terraform validate
terraform plan -var-file=environments/staging.tfvars
terraform apply -var-file=environments/staging.tfvars
terraform output alb_dns_name
terraform state list
terraform show
terraform fmt
terraform destroy -var-file=environments/staging.tfvars
terraform workspace new staging
terraform workspace list
terraform workspace select staging
terraform workspace show
# (partial) update command for ECR repositories only
terraform apply -var-file=environments/staging.tfvars -target=aws_ecr_repository.service_a -target=aws_ecr_repository.service_b -auto-approve
Note: Never commit terraform.tfstate files to version control. Use remote state storage for production environments.