Warning
This Terraform configuration provisions real AWS infrastructure that may incur significant costs depending on your usage and configuration. Use at your own risk - you are solely responsible for reviewing, understanding, and monitoring any resources and charges created by this code. Always test in a non-production environment and review the Terraform plan carefully before applying changes.
Production-ready AWS infrastructure for Laravel applications using Terraform. This configuration deploys a complete, scalable infrastructure on AWS with best practices for security, monitoring, and high availability.
- ECS Fargate - Containerized Laravel application with auto-scaling
- Web service (handles HTTP traffic via ALB)
- Queue worker service (processes SQS queue jobs)
- Scheduler service (runs Laravel task scheduler)
- RDS MySQL - Managed database with automated backups
- ElastiCache Redis - Session and cache storage (single-node configuration)
- Application Load Balancer - HTTPS traffic routing with AWS WAF
- S3 - File storage for Laravel filesystem
- SQS - Queue management for Laravel jobs
- CloudWatch - Centralized logging and monitoring
- Route53 - DNS management and health checks
- Meilisearch - Fast, typo-tolerant search engine (optional)
- AWS SES - Email sending capability (optional)
- Client VPN - Secure remote access to VPC (optional)
- Bastion Host - Secure database access (optional)
- CloudTrail - API audit logging (optional)
- Read Replicas - Database read replicas for analytics (optional)
- KMS encryption - All data encrypted at rest
- VPC isolation - Private subnets for application and database
- IAM roles - Least-privilege access controls
- Security groups - Network-level firewalling
- SSL/TLS - HTTPS everywhere with ACM certificates (includes VPN server certificates)
The infrastructure deploys three separate ECS services:
- Web Service - Handles HTTP/HTTPS traffic through the ALB (auto-scales based on traffic)
- Queue Worker Service - Processes Laravel queue jobs from SQS (always runs 1 task)
- Scheduler Service - Runs Laravel's task scheduler (
php artisan schedule:work
) (always runs 1 task)
┌─────────────────────────────────────────────────────────────┐
│ Internet │
└───────────────────────────┬─────────────────────────────────┘
│
┌─────────▼──────────┐
│ Route53 + ACM │
└─────────┬──────────┘
│
┌──────────────────▼───────────────────┐
│ Application Load Balancer (+ WAF) │
└──────────────────┬───────────────────┘
│
┌───────────────────────┴────────────────────────────┐
│ ECS Fargate Cluster │
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Web │ │ Queue │ │Scheduler │ │
│ │ Service │ │ Worker │ │ Service │ │
│ │(Auto-scale)│ │ Service │ │ (1 task) │ │
│ └────────────┘ └──────────┘ └──────────┘ │
└───────┬──────────────┬──────────────┬──────────────┘
│ │ │
┌───────▼────────┐ ┌──▼──────────┐ │
│ ElastiCache │ │ RDS MySQL │ │
│ Redis │ │ (+ Replica) │ │
└────────────────┘ └─────────────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Bastion │ │
│ └────────────┘ │
│ │
┌───────▼─────────────────────────────▼──────┐
│ S3 (Filesystem) + SQS (Queues) │
└────────────────────────────────────────────┘
- AWS Account with appropriate permissions
- Terraform >= 1.0
- AWS CLI configured with credentials
- Domain name registered (can be managed elsewhere)
- Laravel application containerized with Docker
# Navigate to terraform directory
cd terraform
# Copy example configuration
cp environments/example.tfvars environments/production.tfvars
# Edit with your values
vim environments/production.tfvars
Edit production.tfvars
and set these required values:
environment = "production"
domain_name = "yourdomain.com"
app_key = "base64:YOUR_APP_KEY_HERE"
github_org = "your-org"
github_repo = "your-repo"
# Database credentials (use strong random passwords)
app_db_password = "STRONG_RANDOM_PASSWORD"
db_reporting_password = "STRONG_RANDOM_PASSWORD"
# Initialize Terraform
terraform init
# Create workspace (if using workspaces)
terraform workspace new production
# Review the plan
terraform plan -var-file="environments/production.tfvars"
# Deploy infrastructure
terraform apply -var-file="environments/production.tfvars"
After infrastructure is created:
# Build and push your Docker image
docker build -t myapp .
aws ecr get-login-password | docker login --username AWS --password-stdin $(terraform output -raw ecr_repository_url)
docker tag myapp:latest $(terraform output -raw ecr_repository_url):latest
docker push $(terraform output -raw ecr_repository_url):latest
# Force new ECS deployment (updates all three services: web, queue-worker, scheduler)
CLUSTER=$(terraform output -raw ecs_cluster_name)
aws ecs update-service --cluster $CLUSTER --service $(terraform output -raw ecs_service_name) --force-new-deployment
aws ecs update-service --cluster $CLUSTER --service $(terraform output -raw ecs_queue_worker_service_name) --force-new-deployment
aws ecs update-service --cluster $CLUSTER --service $(terraform output -raw ecs_scheduler_service_name) --force-new-deployment
For a basic setup (good for staging/development):
environment = "staging"
domain_name = "staging.example.com"
app_key = "base64:..."
github_org = "your-org"
github_repo = "your-repo"
app_db_password = "..."
db_reporting_password = "..."
# Small instance sizes
container_cpu = 512
container_memory = 1024
desired_count = 1
min_capacity = 1
max_capacity = 4
db_instance_class = "db.t3.micro"
redis_node_type = "cache.t3.micro"
# Disable optional features
enable_meilisearch = false
enable_ses = false
enable_client_vpn = false
enable_bastion = false
enable_cloudtrail = false
Estimated cost: ~$50-100/month
For production with high availability:
environment = "production"
domain_name = "example.com"
app_key = "base64:..."
github_org = "your-org"
github_repo = "your-repo"
app_db_password = "..."
db_reporting_password = "..."
# Larger instance sizes
container_cpu = 2048
container_memory = 4096
desired_count = 3
min_capacity = 2
max_capacity = 10
# Production database settings
db_instance_class = "db.t3.large"
db_allocated_storage = 100
db_max_allocated_storage = 1000
db_multi_az = true
enable_performance_insights = true
enable_deletion_protection = true
db_create_read_replica = true
# Production Redis (note: currently single-node only)
redis_node_type = "cache.t3.medium"
# Enable production features
enable_cloudtrail = true
enable_alb_access_logs = true
log_retention_days = 30
healthcheck_alarm_emails = ["ops@example.com"]
Estimated cost: ~$300-500/month
enable_meilisearch = true
meilisearch_master_key = "YOUR_RANDOM_32_CHAR_KEY"
Laravel configuration:
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_KEY=YOUR_RANDOM_32_CHAR_KEY
enable_ses = true
ses_test_emails = ["test@example.com"] # Required in sandbox mode
Laravel configuration:
MAIL_MAILER=ses
MAIL_FROM_ADDRESS=noreply@yourdomain.com
enable_client_vpn = true
vpn_client_cidr_block = "10.4.0.0/22"
vpn_saml_provider_arn = "arn:aws:iam::ACCOUNT:saml-provider/YourProvider"
Provides secure remote access to your VPC for development and debugging.
Note: VPN server certificates are automatically provisioned for all environments via ACM (even if Client VPN is disabled). This allows you to enable VPN later without additional certificate setup.
enable_bastion = true
ec2_key_name = "my-key-pair"
bastion_allowed_ips = ["203.0.113.0/32"] # Your office IP
Provides secure SSH access to your database:
ssh -i ~/.ssh/my-key.pem ec2-user@bastion-ip
mysql -h rds-endpoint -u app_user -p
db_create_read_replica = true
db_read_replica_instance_class = "db.t3.large"
Laravel will automatically use the read replica for read queries when configured with DB_READ_HOST
.
Three database users are automatically configured:
-
Master User (
admin
)- Full administrative access
- Credentials stored in AWS Secrets Manager
- Used only for infrastructure management
-
Application User (
app_user
by default)- CRUD operations + migrations
- Used by Laravel application
- Created automatically by bastion host
-
Reporting User (
reporting
)- Read-only access
- For BI tools and analytics
- Safe for external reporting tools
Route53 health checks monitor your application endpoint (/up
) every 30 seconds:
healthcheck_alarm_emails = ["ops@example.com", "team@example.com"]
You'll receive email alerts when the application goes down.
All application logs are stored in CloudWatch Logs:
# View recent logs
aws logs tail /ecs/laravel-app-production --follow
# Search logs
aws logs filter-log-events \
--log-group-name /ecs/laravel-app-production \
--filter-pattern "ERROR"
Enable API audit logging:
enable_cloudtrail = true
All AWS API calls are logged to S3 for security auditing.
Configure auto-scaling based on CPU and memory:
desired_count = 3 # Normal load
min_capacity = 2 # Minimum tasks
max_capacity = 10 # Maximum tasks
Tasks auto-scale when CPU > 70% or Memory > 80%.
Adjust CPU and memory per task:
container_cpu = 2048 # 2 vCPU
container_memory = 4096 # 4 GB
Vertical: Change node type:
redis_node_type = "cache.t3.medium" # or cache.r6g.large, etc.
Note: Redis is currently configured as a single-node cluster. Multi-node replication (for high availability) is not yet implemented but can be added in the future using ElastiCache Replication Groups.
Vertical: Change instance class:
db_instance_class = "db.t3.large" # or db.r6g.xlarge, etc.
Storage: Enable auto-scaling:
db_allocated_storage = 100
db_max_allocated_storage = 1000 # Auto-grow to 1TB
- Use
db.t3.micro
(free tier eligible) - Single AZ database (
db_multi_az = false
) - Smaller ECS tasks (512 CPU, 1024 memory)
- Disable optional features (VPN, Meilisearch, CloudTrail)
- Use
filesystem_disk = "local"
instead of S3
- Enable Multi-AZ for high availability
- Use reserved instances for predictable workloads
- Enable storage auto-scaling to avoid over-provisioning
- Use Fargate Spot for non-critical tasks (50% cost savings)
- Enable S3 lifecycle policies for old logs
-
Secrets Management
- Never commit
.tfvars
files with real credentials - Use environment variables or CI/CD secrets
- Rotate passwords regularly
- Never commit
-
Network Security
- All resources in private subnets
- Bastion host required for database access
- Security groups follow least-privilege
-
Encryption
- All data encrypted at rest (RDS, S3, EBS)
- All data encrypted in transit (TLS 1.2+)
- Separate KMS keys per service
-
IAM
- Separate roles for ECS tasks and execution
- No IAM users (use OIDC for CI/CD)
- Regular access reviews
# Check task logs
aws logs tail /ecs/laravel-app-production --follow
# Check task definition
aws ecs describe-task-definition --task-definition laravel-app-production
# Check service events
aws ecs describe-services --cluster laravel-app-production --services laravel-app-production-service
- Check security group allows ECS -> RDS
- Verify SSM parameters are correct
- Test from bastion host
- Check RDS is in
available
state
- Check ALB target health
- Verify Route53 DNS resolves correctly
- Check ACM certificate status
- Review WAF rules (if blocking)
# Update your .tfvars file
vim environments/production.tfvars
# Preview changes
terraform plan -var-file="environments/production.tfvars"
# Apply changes
terraform apply -var-file="environments/production.tfvars"
Automated daily backups with 7-day retention. To restore:
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier myapp-restored \
--db-snapshot-identifier rds:myapp-2024-01-01
- Database: Automated backups + optional Multi-AZ
- Application: Stateless containers, quick redeploy
- Files: S3 with versioning enabled
- Infrastructure: Terraform state in S3 with versioning
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to ECS
run: |
docker build -t myapp .
docker tag myapp:latest $ECR_REPO:latest
docker push $ECR_REPO:latest
aws ecs update-service --cluster myapp-production --service myapp-production-service --force-new-deployment
terraform/
├── main.tf # Root module configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider versions
├── environments/
│ ├── example.tfvars # Template configuration
│ ├── staging.tfvars # Staging environment
│ └── production.tfvars # Production environment
└── modules/
├── bastion/ # Bastion host (optional)
├── cache/ # ElastiCache Redis
├── certificates/ # ACM SSL certificates
├── client_vpn/ # AWS Client VPN (optional)
├── compute/ # ECS Fargate cluster (web + queue-worker + scheduler)
├── configuration/ # SSM parameters
├── container_registry/ # ECR repository
├── database/ # RDS MySQL
├── dns/ # Route53 records
├── email/ # SES configuration (optional)
├── load_balancer/ # ALB + WAF
├── meilisearch/ # Meilisearch (optional)
├── messaging/ # SQS queues
├── monitoring/ # CloudWatch + CloudTrail
├── networking/ # VPC + Security Groups
├── security/ # IAM + KMS
└── storage/ # S3 buckets
The compute module deploys three ECS services:
- Web Service: Handles HTTP/HTTPS requests via the Application Load Balancer. Auto-scales based on CPU and memory utilization.
- Queue Worker Service: Processes Laravel queue jobs from SQS. Runs continuously with 1 task by default.
- Scheduler Service: Runs Laravel's task scheduler (
php artisan schedule:work
). Runs continuously with 1 task.
All three services share the same Docker image from ECR but run different commands based on their role.
This infrastructure template is designed to be a starting point for Laravel applications on AWS. Feel free to customize it for your specific needs.
For issues, questions, or contributions, please open an issue or pull request.
MIT License - use this infrastructure configuration however you'd like!