Projet 2 : Application Web Hautement Disponible
Contexte
Suite au projet VPC, vous devez maintenant déployer une application web e-commerce. L'application doit supporter des pics de trafic et être résiliente aux pannes.
Exigences
- Haute disponibilité sur 3 AZ
- Auto Scaling basé sur la charge CPU
- Base de données RDS Multi-AZ
- Cache Redis pour les sessions
- CDN pour les assets statiques
- Certificat SSL/TLS
Architecture cible
Structure du projet
projet-webapp/
├─ ─ main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── data.tf
├── alb.tf
├── asg.tf
├── rds.tf
├── elasticache.tf
├── s3.tf
├── cloudfront.tf
├── security-groups.tf
└── terraform.tfvars
Implémentation
data.tf
# AMI Amazon Linux 2023
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Récupérer le certificat ACM
data "aws_acm_certificate" "main" {
count = var.domain_name != "" ? 1 : 0
domain = var.domain_name
statuses = ["ISSUED"]
}
# VPC existant (du projet 1)
data "aws_vpc" "main" {
id = var.vpc_id
}
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [var.vpc_id]
}
filter {
name = "tag:Type"
values = ["public"]
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [var.vpc_id]
}
filter {
name = "tag:Type"
values = ["private"]
}
}
data "aws_subnets" "database" {
filter {
name = "vpc-id"
values = [var.vpc_id]
}
filter {
name = "tag:Type"
values = ["database"]
}
}
security-groups.tf
# Security Group ALB
resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = var.vpc_id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.name_prefix}-alb-sg"
}
}
# Security Group EC2
resource "aws_security_group" "app" {
name = "${local.name_prefix}-app-sg"
description = "Security group for application servers"
vpc_id = var.vpc_id
ingress {
description = "HTTP from ALB"
from_port = var.app_port
to_port = var.app_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.name_prefix}-app-sg"
}
}
# Security Group RDS
resource "aws_security_group" "rds" {
name = "${local.name_prefix}-rds-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from App"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "${local.name_prefix}-rds-sg"
}
}
# Security Group ElastiCache
resource "aws_security_group" "redis" {
name = "${local.name_prefix}-redis-sg"
description = "Security group for ElastiCache Redis"
vpc_id = var.vpc_id
ingress {
description = "Redis from App"
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "${local.name_prefix}-redis-sg"
}
}
alb.tf
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = data.aws_subnets.public.ids
enable_deletion_protection = var.environment == "production"
access_logs {
bucket = aws_s3_bucket.logs.id
prefix = "alb"
enabled = true
}
tags = {
Name = "${local.name_prefix}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${local.name_prefix}-tg"
port = var.app_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "instance"
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = var.health_check_path
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 3
}
stickiness {
type = "lb_cookie"
cookie_duration = 86400
enabled = true
}
tags = {
Name = "${local.name_prefix}-tg"
}
}
# HTTP Listener (redirect to HTTPS)
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# HTTPS Listener
resource "aws_lb_listener" "https" {
count = var.domain_name != "" ? 1 : 0
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = data.aws_acm_certificate.main[0].arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
asg.tf
# Launch Template
resource "aws_launch_template" "app" {
name_prefix = "${local.name_prefix}-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
monitoring {
enabled = true
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh", {
app_port = var.app_port
db_host = aws_db_instance.main.endpoint
db_name = var.db_name
redis_host = aws_elasticache_cluster.redis.cache_nodes[0].address
environment = var.environment
}))
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 20
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${local.name_prefix}-app"
}
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "app" {
name = "${local.name_prefix}-asg"
desired_capacity = var.asg_desired
max_size = var.asg_max
min_size = var.asg_min
vpc_zone_identifier = data.aws_subnets.private.ids
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${local.name_prefix}-app"
propagate_at_launch = true
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Policies
resource "aws_autoscaling_policy" "scale_up" {
name = "${local.name_prefix}-scale-up"
scaling_adjustment = 2
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_autoscaling_policy" "scale_down" {
name = "${local.name_prefix}-scale-down"
scaling_adjustment = -1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.app.name
}
# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
alarm_name = "${local.name_prefix}-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 70
alarm_description = "Scale up when CPU > 70%"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.app.name
}
alarm_actions = [aws_autoscaling_policy.scale_up.arn]
}
resource "aws_cloudwatch_metric_alarm" "cpu_low" {
alarm_name = "${local.name_prefix}-cpu-low"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 20
alarm_description = "Scale down when CPU < 20%"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.app.name
}
alarm_actions = [aws_autoscaling_policy.scale_down.arn]
}
rds.tf
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-db"
engine = "postgres"
engine_version = "15.4"
instance_class = var.db_instance_class
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
multi_az = var.environment == "production"
db_subnet_group_name = var.db_subnet_group_name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = var.environment == "production" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
skip_final_snapshot = var.environment != "production"
final_snapshot_identifier = var.environment == "production" ? "${local.name_prefix}-final-snapshot" : null
deletion_protection = var.environment == "production"
tags = {
Name = "${local.name_prefix}-db"
}
}
elasticache.tf
resource "aws_elasticache_subnet_group" "redis" {
name = "${local.name_prefix}-redis-subnet"
subnet_ids = data.aws_subnets.private.ids
}
resource "aws_elasticache_cluster" "redis" {
cluster_id = "${local.name_prefix}-redis"
engine = "redis"
node_type = var.redis_node_type
num_cache_nodes = 1
parameter_group_name = "default.redis7"
engine_version = "7.0"
port = 6379
subnet_group_name = aws_elasticache_subnet_group.redis.name
security_group_ids = [aws_security_group.redis.id]
snapshot_retention_limit = var.environment == "production" ? 5 : 0
snapshot_window = "03:00-05:00"
tags = {
Name = "${local.name_prefix}-redis"
}
}
outputs.tf
output "alb_dns_name" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
}
output "alb_zone_id" {
description = "Zone ID of the load balancer"
value = aws_lb.main.zone_id
}
output "rds_endpoint" {
description = "Endpoint of RDS instance"
value = aws_db_instance.main.endpoint
}
output "redis_endpoint" {
description = "Endpoint of Redis cluster"
value = aws_elasticache_cluster.redis.cache_nodes[0].address
}
output "cloudfront_domain" {
description = "CloudFront distribution domain"
value = aws_cloudfront_distribution.main.domain_name
}
Déploiement
# Prérequis: Le VPC du projet 1 doit exister
export TF_VAR_vpc_id=$(cd ../projet-vpc && terraform output -raw vpc_id)
export TF_VAR_db_subnet_group_name=$(cd ../projet-vpc && terraform output -raw database_subnet_group_name)
# Déployer
terraform init
terraform plan -out=tfplan
terraform apply tfplan
Tests de charge
# Installer hey (outil de benchmark HTTP)
brew install hey
# Test de charge
hey -n 10000 -c 100 https://$(terraform output -raw alb_dns_name)
# Observer l'auto scaling dans la console AWS
Coûts estimés
| Ressource | Coût mensuel |
|---|---|
| ALB | ~$20 |
| EC2 (3x t3.small) | ~$50 |
| RDS (db.t3.small Multi-AZ) | ~$60 |
| ElastiCache (cache.t3.micro) | ~$15 |
| CloudFront | ~$10 |
| Total | ~$155/mois |