Aller au contenu principal

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

RessourceCoû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

← Projet 1 | Projet 3: Cluster EKS →