Projet 1 : VPC Multi-AZ Production
Contexte
Vous êtes DevOps Engineer dans une startup fintech. Votre mission est de créer l'infrastructure réseau de base qui hébergera tous les services de l'entreprise.
Exigences
- Haute disponibilité sur 3 zones de disponibilité
- Séparation des réseaux publics et privés
- Accès Internet sortant pour les subnets privés
- VPC Flow Logs pour la conformité
- Endpoints VPC pour les services AWS
Architecture cible
Structure du projet
projet-vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── vpc.tf
├── subnets.tf
├── nat.tf
├── routes.tf
├── endpoints.tf
├── flow-logs.tf
└── terraform.tfvars
Implémentation
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
variables.tf
variable "region" {
description = "AWS Region"
type = string
default = "eu-west-1"
}
variable "project_name" {
description = "Project name for resource naming"
type = string
}
variable "environment" {
description = "Environment (dev, staging, production)"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "VPC CIDR must be a valid CIDR block."
}
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway for private subnets"
type = bool
default = true
}
variable "single_nat_gateway" {
description = "Use single NAT Gateway (cost saving for non-prod)"
type = bool
default = false
}
variable "enable_flow_logs" {
description = "Enable VPC Flow Logs"
type = bool
default = true
}
variable "enable_vpc_endpoints" {
description = "Enable VPC Endpoints for AWS services"
type = bool
default = true
}
variable "tags" {
description = "Additional tags for resources"
type = map(string)
default = {}
}
main.tf
provider "aws" {
region = var.region
default_tags {
tags = merge(var.tags, {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
})
}
}
locals {
name_prefix = "${var.project_name}-${var.environment}"
az_count = length(var.availability_zones)
# Calcul des CIDR pour les subnets
public_subnets = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 1)
]
private_subnets = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 11)
]
database_subnets = [
for i in range(local.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + 21)
]
# Nombre de NAT Gateways
nat_gateway_count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : local.az_count) : 0
}
vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${local.name_prefix}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-igw"
}
}
subnets.tf
# Public Subnets
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnets[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${local.name_prefix}-public-${count.index + 1}"
Type = "public"
"kubernetes.io/role/elb" = "1"
}
}
# Private Subnets (Application)
resource "aws_subnet" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.private_subnets[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${local.name_prefix}-private-${count.index + 1}"
Type = "private"
"kubernetes.io/role/internal-elb" = "1"
}
}
# Database Subnets
resource "aws_subnet" "database" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.database_subnets[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${local.name_prefix}-database-${count.index + 1}"
Type = "database"
}
}
# Database Subnet Group (pour RDS)
resource "aws_db_subnet_group" "main" {
name = "${local.name_prefix}-db-subnet-group"
subnet_ids = aws_subnet.database[*].id
tags = {
Name = "${local.name_prefix}-db-subnet-group"
}
}
nat.tf
# Elastic IPs pour NAT Gateways
resource "aws_eip" "nat" {
count = local.nat_gateway_count
domain = "vpc"
tags = {
Name = "${local.name_prefix}-nat-eip-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
# NAT Gateways
resource "aws_nat_gateway" "main" {
count = local.nat_gateway_count
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${local.name_prefix}-nat-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
routes.tf
# Route Table Public
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${local.name_prefix}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = local.az_count
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Route Tables Private
resource "aws_route_table" "private" {
count = local.nat_gateway_count > 0 ? local.az_count : 0
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-private-rt-${count.index + 1}"
}
}
resource "aws_route" "private_nat" {
count = local.nat_gateway_count > 0 ? local.az_count : 0
route_table_id = aws_route_table.private[count.index].id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = var.single_nat_gateway ? aws_nat_gateway.main[0].id : aws_nat_gateway.main[count.index].id
}
resource "aws_route_table_association" "private" {
count = local.nat_gateway_count > 0 ? local.az_count : 0
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# Route Table Database (pas d'accès Internet)
resource "aws_route_table" "database" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-database-rt"
}
}
resource "aws_route_table_association" "database" {
count = local.az_count
subnet_id = aws_subnet.database[count.index].id
route_table_id = aws_route_table.database.id
}
endpoints.tf
# VPC Endpoints pour les services AWS
resource "aws_vpc_endpoint" "s3" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = concat(
[aws_route_table.public.id],
aws_route_table.private[*].id,
[aws_route_table.database.id]
)
tags = {
Name = "${local.name_prefix}-s3-endpoint"
}
}
resource "aws_vpc_endpoint" "dynamodb" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = concat(
[aws_route_table.public.id],
aws_route_table.private[*].id
)
tags = {
Name = "${local.name_prefix}-dynamodb-endpoint"
}
}
# Interface Endpoints (pour ECR, Secrets Manager, etc.)
resource "aws_security_group" "vpc_endpoints" {
count = var.enable_vpc_endpoints ? 1 : 0
name = "${local.name_prefix}-vpc-endpoints-sg"
description = "Security group for VPC endpoints"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.name_prefix}-vpc-endpoints-sg"
}
}
resource "aws_vpc_endpoint" "ecr_api" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints[0].id]
private_dns_enabled = true
tags = {
Name = "${local.name_prefix}-ecr-api-endpoint"
}
}
resource "aws_vpc_endpoint" "ecr_dkr" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints[0].id]
private_dns_enabled = true
tags = {
Name = "${local.name_prefix}-ecr-dkr-endpoint"
}
}
resource "aws_vpc_endpoint" "secretsmanager" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints[0].id]
private_dns_enabled = true
tags = {
Name = "${local.name_prefix}-secretsmanager-endpoint"
}
}
flow-logs.tf
# CloudWatch Log Group pour Flow Logs
resource "aws_cloudwatch_log_group" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc/${local.name_prefix}-flow-logs"
retention_in_days = 30
tags = {
Name = "${local.name_prefix}-flow-logs"
}
}
# IAM Role pour Flow Logs
resource "aws_iam_role" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "${local.name_prefix}-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "${local.name_prefix}-flow-logs-policy"
role = aws_iam_role.flow_logs[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Effect = "Allow"
Resource = "*"
}
]
})
}
# VPC Flow Logs
resource "aws_flow_log" "main" {
count = var.enable_flow_logs ? 1 : 0
iam_role_arn = aws_iam_role.flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.flow_logs[0].arn
traffic_type = "ALL"
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-flow-log"
}
}
outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "database_subnet_ids" {
description = "IDs of database subnets"
value = aws_subnet.database[*].id
}
output "database_subnet_group_name" {
description = "Name of the database subnet group"
value = aws_db_subnet_group.main.name
}
output "nat_gateway_ips" {
description = "Public IPs of NAT Gateways"
value = aws_eip.nat[*].public_ip
}
output "availability_zones" {
description = "Availability zones used"
value = var.availability_zones
}
terraform.tfvars
project_name = "fintech"
environment = "production"
region = "eu-west-1"
vpc_cidr = "10.0.0.0/16"
availability_zones = [
"eu-west-1a",
"eu-west-1b",
"eu-west-1c"
]
enable_nat_gateway = true
single_nat_gateway = false # HA: un NAT par AZ
enable_flow_logs = true
enable_vpc_endpoints = true
tags = {
CostCenter = "infrastructure"
Team = "platform"
}
Déploiement
# 1. Initialiser
terraform init
# 2. Valider
terraform validate
# 3. Prévisualiser
terraform plan -out=tfplan
# 4. Appliquer
terraform apply tfplan
# 5. Vérifier les outputs
terraform output
Tests de validation
# Vérifier le VPC
aws ec2 describe-vpcs --vpc-ids $(terraform output -raw vpc_id)
# Vérifier les subnets
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)"
# Tester la connectivité NAT (lancer une instance dans un subnet privé)
# et vérifier qu'elle peut accéder à Internet