Infrastructure as Code: Provisioning Secure Magento 2 Clusters on AWS Using Terraform
Terraform Project Structure for Magento 2 on AWS
A robust Infrastructure as Code (IaC) strategy for provisioning complex applications like Magento 2 on AWS necessitates a well-organized Terraform project. This structure promotes reusability, maintainability, and scalability. We’ll adopt a modular approach, separating concerns into distinct directories.
Our recommended project layout:
modules/: Contains reusable Terraform modules for common infrastructure components (e.g., VPC, EC2 instances, RDS, ElastiCache, Load Balancers).environments/: Defines environment-specific configurations (e.g., `dev`, `staging`, `prod`). Each environment will leverage modules from themodules/directory and override variables as needed.main.tf: The root Terraform file, typically used to orchestrate module calls for a specific environment.variables.tf: Defines input variables for the root configuration.outputs.tf: Defines output values for the root configuration.providers.tf: Configures the AWS provider and any other necessary providers.
Securing the Network: VPC and Security Groups
A secure Magento 2 deployment begins with a well-defined Virtual Private Cloud (VPC) and granular Security Group rules. We’ll provision a multi-AZ VPC with private subnets for application servers and databases, and public subnets for load balancers.
VPC Module (`modules/vpc/main.tf`)
resource "aws_vpc" "magento_vpc" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-magento-vpc"
}
}
resource "aws_internet_gateway" "magento_igw" {
vpc_id = aws_vpc.magento_vpc.id
tags = {
Name = "${var.environment}-magento-igw"
}
}
resource "aws_subnet" "public_subnet" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.magento_vpc.id
cidr_block = var.public_subnet_cidrs[count.index]
map_public_ip_on_launch = true
availability_zone = data.aws_availability_zones.available.names[count.index % length(data.aws_availability_zones.available.names)]
tags = {
Name = "${var.environment}-magento-public-subnet-${count.index}"
}
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.magento_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.magento_igw.id
}
tags = {
Name = "${var.environment}-magento-public-rt"
}
}
resource "aws_route_table_association" "public_rta" {
count = length(aws_subnet.public_subnet)
subnet_id = aws_subnet.public_subnet[count.index].id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_subnet" "private_subnet" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.magento_vpc.id
cidr_block = var.private_subnet_cidrs[count.index]
map_public_ip_on_launch = false
availability_zone = data.aws_availability_zones.available.names[count.index % length(data.aws_availability_zones.available.names)]
tags = {
Name = "${var.environment}-magento-private-subnet-${count.index}"
}
}
resource "aws_nat_gateway" "magento_nat_gw" {
allocation_id = aws_eip.nat_eip[count.index].id
subnet_id = aws_subnet.public_subnet[count.index].id
count = length(var.public_subnet_cidrs) # One NAT GW per AZ for HA
tags = {
Name = "${var.environment}-magento-nat-gw-${count.index}"
}
depends_on = [aws_internet_gateway.magento_igw]
}
resource "aws_eip" "nat_eip" {
count = length(var.public_subnet_cidrs)
domain = "vpc"
}
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.magento_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.magento_nat_gw[count.index].id
}
count = length(var.private_subnet_cidrs) # One RT per AZ for HA
tags = {
Name = "${var.environment}-magento-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private_rta" {
count = length(aws_subnet.private_subnet)
subnet_id = aws_subnet.private_subnet[count.index].id
route_table_id = aws_route_table.private_rt[count.index % length(aws_route_table.private_rt)].id
}
data "aws_availability_zones" "available" {}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
}
variable "public_subnet_cidrs" {
description = "List of CIDR blocks for public subnets"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "List of CIDR blocks for private subnets"
type = list(string)
}
variable "environment" {
description = "Deployment environment name (e.g., dev, staging, prod)"
type = string
}
Security Group Module (`modules/security_groups/main.tf`)
resource "aws_security_group" "alb_sg" {
name = "${var.environment}-magento-alb-sg"
description = "Allow HTTP and HTTPS traffic to ALB"
vpc_id = var.vpc_id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from anywhere"
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 = "${var.environment}-magento-alb-sg"
}
}
resource "aws_security_group" "app_sg" {
name = "${var.environment}-magento-app-sg"
description = "Allow traffic from ALB and outbound to DB/Cache"
vpc_id = var.vpc_id
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb_sg.id]
}
# Allow SSH for bastion host or specific IPs (restrict this in production)
ingress {
description = "SSH from bastion/admin IPs"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.ssh_cidr_blocks
}
egress {
description = "Allow outbound to RDS"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = [var.db_subnet_cidr] # Restrict to DB subnet
}
egress {
description = "Allow outbound to ElastiCache"
from_port = 6379
to_port = 6379
protocol = "tcp"
cidr_blocks = [var.cache_subnet_cidr] # Restrict to Cache subnet
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-magento-app-sg"
}
}
resource "aws_security_group" "db_sg" {
name = "${var.environment}-magento-db-sg"
description = "Allow traffic from App SG to RDS"
vpc_id = var.vpc_id
ingress {
description = "MySQL from App SG"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-magento-db-sg"
}
}
resource "aws_security_group" "cache_sg" {
name = "${var.environment}-magento-cache-sg"
description = "Allow traffic from App SG to ElastiCache"
vpc_id = var.vpc_id
ingress {
description = "Redis from App SG"
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.app_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-magento-cache-sg"
}
}
variable "vpc_id" {
description = "ID of the VPC"
type = string
}
variable "environment" {
description = "Deployment environment name"
type = string
}
variable "ssh_cidr_blocks" {
description = "List of CIDR blocks allowed for SSH access"
type = list(string)
default = ["0.0.0.0/0"] # WARNING: Restrict this in production!
}
variable "db_subnet_cidr" {
description = "CIDR block of the database subnet"
type = string
}
variable "cache_subnet_cidr" {
description = "CIDR block of the cache subnet"
type = string
}
Provisioning the Application Layer: EC2 Instances and Auto Scaling
Magento 2 applications are typically served by multiple web servers behind a load balancer. We’ll use EC2 instances managed by an Auto Scaling Group (ASG) to ensure high availability and scalability.
EC2 Launch Template Module (`modules/ec2/launch_template.tf`)
resource "aws_launch_template" "magento_lt" {
name_prefix = "${var.environment}-magento-lt-"
image_id = var.ami_id
instance_type = var.instance_type
key_name = var.key_pair_name
vpc_security_group_ids = [var.app_security_group_id]
user_data = base64encode(templatefile("${path.module}/scripts/bootstrap.sh", {
db_host = var.db_host
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
cache_host = var.cache_host
redis_db = var.redis_db
magento_env = var.magento_env
app_env = var.app_env
# Add other necessary variables for bootstrapping
}))
tags {
Name = "${var.environment}-magento-app"
Environment = var.environment
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "magento_asg" {
name_prefix = "${var.environment}-magento-asg-"
desired_capacity = var.desired_capacity
min_size = var.min_size
max_size = var.max_size
vpc_zone_identifier = var.private_subnet_ids
launch_template {
id = aws_launch_template.magento_lt.id
version = "$Latest"
}
# Health check configuration
health_check_type = "ELB"
health_check_grace_period = 300 # Seconds
# Scaling policies (example: scale based on CPU utilization)
resource_limits {
max_size = var.max_size
min_size = var.min_size
desired = var.desired_capacity
}
target_group_arns = [var.target_group_arn] # Associate with ALB Target Group
tags = [
{
key = "Name"
value = "${var.environment}-magento-app"
propagate_at_launch = true
},
{
key = "Environment"
value = var.environment
propagate_at_launch = true
}
]
}
variable "ami_id" {
description = "AMI ID for the EC2 instances"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
}
variable "key_pair_name" {
description = "Name of the EC2 key pair for SSH access"
type = string
}
variable "app_security_group_id" {
description = "ID of the application security group"
type = string
}
variable "private_subnet_ids" {
description = "List of private subnet IDs for the ASG"
type = list(string)
}
variable "db_host" {
description = "Database endpoint"
type = string
}
variable "db_name" {
description = "Database name"
type = string
}
variable "db_user" {
description = "Database username"
type = string
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
variable "cache_host" {
description = "Redis endpoint"
type = string
}
variable "redis_db" {
description = "Redis database index"
type = number
default = 0
}
variable "magento_env" {
description = "Magento environment variable (e.g., 'production', 'developer')"
type = string
default = "production"
}
variable "app_env" {
description = "Application environment variable (e.g., 'prod', 'dev')"
type = string
default = "prod"
}
variable "desired_capacity" {
description = "Desired number of instances in the ASG"
type = number
}
variable "min_size" {
description = "Minimum number of instances in the ASG"
type = number
}
variable "max_size" {
description = "Maximum number of instances in the ASG"
type = number
}
variable "target_group_arn" {
description = "ARN of the ALB Target Group"
type = string
}
Bootstrap Script (`modules/ec2/scripts/bootstrap.sh`)
#!/bin/bash -xe
# Update system packages
sudo apt-get update -y
sudo apt-get upgrade -y
# Install Nginx and PHP (example for Ubuntu 20.04)
sudo apt-get install -y nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl php-zip php-intl redis-server
# Configure PHP-FPM
sudo sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/8.1/fpm/php.ini
sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/8.1/fpm/php.ini
sudo sed -i 's/post_max_size = .*/post_max_size = 128M/' /etc/php/8.1/fpm/php.ini
sudo sed -i 's/;daemonize = .*/daemonize = no/' /etc/php/8.1/fpm/php-fpm.conf
sudo sed -i 's/user = www-data/user = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
sudo sed -i 's/group = www-data/group = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
sudo sed -i 's/listen = \/run\/php\/php8.1-fpm.sock/listen = 127.0.0.1:9000/' /etc/php/8.1/fpm/pool.d/www.conf
# Configure Nginx
sudo systemctl enable nginx
sudo systemctl start nginx
# Magento specific configurations (e.g., virtual host, PHP-FPM socket)
# This is a simplified example. A production setup would involve more complex Nginx configs,
# potentially using a configuration management tool like Ansible or Chef for more advanced deployments.
# Download and configure Magento (This part is highly dependent on your deployment strategy)
# For simplicity, we assume Magento is already deployed to /var/www/html or a similar path.
# You might use a deployment script or CI/CD pipeline to handle Magento installation/updates.
# Example: Set Magento environment variables (if not handled by deployment)
# export MAGENTO_MODE=${magento_env}
# export APP_ENV=${app_env}
# Restart services
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx
# Ensure correct permissions (adjust path as needed)
sudo chown -R nginx:nginx /var/www/html
sudo chmod -R 755 /var/www/html
# Configure Redis for Magento Cache
sudo sed -i 's/^\(databases\:\).*/\1 ${redis_db}/' /etc/redis/redis.conf
sudo sed -i 's/^bind .*/bind 127.0.0.1/' /etc/redis/redis.conf # Bind to localhost for security if accessed via app SG
sudo systemctl restart redis-server
echo "Bootstrap script completed."
Database and Caching Layers: RDS and ElastiCache
Magento 2 relies heavily on a performant database and a robust caching mechanism. We’ll provision a managed AWS RDS for MySQL and an AWS ElastiCache for Redis cluster.
RDS Module (`modules/rds/main.tf`)
resource "aws_db_instance" "magento_db" {
allocated_storage = var.allocated_storage
engine = "mysql"
engine_version = var.db_engine_version
instance_class = var.db_instance_class
identifier = "${var.environment}-magento-db"
username = var.db_username
password = var.db_password
parameter_group_name = "default.mysql8.0" # Adjust based on engine version
skip_final_snapshot = true
publicly_accessible = false
vpc_security_group_ids = [var.db_security_group_id]
db_subnet_group_name = aws_db_subnet_group.magento_db_subnet_group.name
tags = {
Name = "${var.environment}-magento-db"
Environment = var.environment
}
}
resource "aws_db_subnet_group" "magento_db_subnet_group" {
name = "${var.environment}-magento-db-subnet-group"
subnet_ids = var.private_subnet_ids # Place DB in private subnets
tags = {
Name = "${var.environment}-magento-db-subnet-group"
}
}
variable "allocated_storage" {
description = "Allocated storage in GB"
type = number
}
variable "db_engine_version" {
description = "MySQL engine version"
type = string
}
variable "db_instance_class" {
description = "RDS instance class"
type = string
}
variable "db_username" {
description = "Database username"
type = string
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
variable "db_security_group_id" {
description = "ID of the database security group"
type = string
}
variable "private_subnet_ids" {
description = "List of private subnet IDs for the DB subnet group"
type = list(string)
}
variable "environment" {
description = "Deployment environment name"
type = string
}
ElastiCache Module (`modules/elasticache/main.tf`)
resource "aws_elasticache_cluster" "magento_cache" {
cluster_id = "${var.environment}-magento-cache"
engine = "redis"
node_type_ आपको_need_to_specify_a_node_type = var.cache_node_type
num_cache_nodes = var.cache_num_nodes
parameter_group_name = "default.redis7" # Adjust based on engine version
engine_version = var.cache_engine_version
port = 6379
subnet_group_name = aws_elasticache_subnet_group.magento_cache_subnet_group.name
security_group_ids = [var.cache_security_group_id]
automatic_failover_enabled = true # For production, enable this
tags = {
Name = "${var.environment}-magento-cache"
Environment = var.environment
}
}
resource "aws_elasticache_subnet_group" "magento_cache_subnet_group" {
name = "${var.environment}-magento-cache-subnet-group"
subnet_ids = var.private_subnet_ids # Place cache in private subnets
tags = {
Name = "${var.environment}-magento-cache-subnet-group"
}
}
variable "cache_node_type" {
description = "ElastiCache node type"
type = string
}
variable "cache_num_nodes" {
description = "Number of cache nodes"
type = number
}
variable "cache_engine_version" {
description = "Redis engine version"
type = string
}
variable "cache_security_group_id" {
description = "ID of the cache security group"
type = string
}
variable "private_subnet_ids" {
description = "List of private subnet IDs for the cache subnet group"
type = list(string)
}
variable "environment" {
description = "Deployment environment name"
type = string
}
Load Balancing and SSL Termination
An Application Load Balancer (ALB) is crucial for distributing traffic across EC2 instances and handling SSL termination. We’ll configure an ALB with a listener for HTTPS and a default rule to forward traffic to our application instances.
ALB Module (`modules/alb/main.tf`)
resource "aws_lb" "magento_alb" {
name = "${var.environment}-magento-alb"
internal = false
load_balancer_type = "application"
security_groups = [var.alb_security_group_id]
subnets = var.public_subnet_ids
enable_deletion_protection = false # Set to true for production
tags = {
Name = "${var.environment}-magento-alb"
Environment = var.environment
}
}
resource "aws_lb_listener" "https_listener" {
load_balancer_arn = aws_lb.magento_alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08" # Or a more recent policy
certificate_arn = var.ssl_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.magento_tg.arn
}
}
resource "aws_lb_listener" "http_listener" {
load_balancer_arn = aws_lb.magento_alb.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_target_group" "magento_tg" {
name = "${var.environment}-magento-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
interval = 30
path = "/" # Magento health check path
port = "traffic port"
protocol = "HTTP"
matcher = "200" # Expect HTTP 200 OK
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.environment}-magento-tg"
Environment = var.environment
}
}
variable "alb_security_group_id" {
description = "ID of the ALB security group"
type = string
}
variable "public_subnet_ids" {
description = "List of public subnet IDs for the ALB"
type = list(string)
}
variable "ssl_certificate_arn" {
description = "ARN of the SSL certificate for HTTPS"
type = string
}
variable "vpc_id" {
description = "ID of the VPC"
type = string
}
Environment Configuration and Deployment
The environments/ directory houses the specific configurations for each deployment stage. This is where you’ll define variables for your modules.
Production Environment (`environments/prod/main.tf`)
provider "aws" {
region = "us-east-1" # Your preferred region
}
data "aws_availability_zones" "available" {}
locals {
environment = "prod"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"]
db_subnet_cidr = "10.0.101.0/24" # Example, ensure it matches private subnet
cache_subnet_cidr = "10.0.102.0/24" # Example, ensure it matches private subnet
}
module "vpc" {
source = "../../modules/vpc"
vpc_cidr = local.vpc_cidr
public_subnet_cidrs = local.public_subnet_cidrs
private_subnet_cidrs = local.private_subnet_cidrs
environment = local.environment
}
module "security_groups" {
source = "../../modules/security_groups"
vpc_id = module.vpc.aws_vpc.magento_vpc.id
environment = local.environment
ssh_cidr_blocks = ["YOUR_ADMIN_IP/32"] # Restrict SSH access
db_subnet_cidr = local.db_subnet_cidr
cache_subnet_cidr = local.cache_subnet_cidr
}
module "rds" {
source = "../../modules/rds"
allocated_storage = 100 # GB
db_engine_version = "8.0"
db_instance_class = "db.r5.large"
db_username = "magento_user"
db_password = var.prod_db_password # Use Terraform variables for secrets
db_security_group_id = module.security_groups.aws_security_group.db_sg.id
private_subnet_ids = [for subnet in module.vpc.aws_subnet.private_subnet : subnet.id]
environment = local.environment
}
module "elasticache" {
source = "../../modules/elasticache"
cache_node_type = "cache.t3.small"
cache_num_nodes = 2
cache_engine_version = "7.0"
cache_security_group_id = module.security_groups.aws_security_group.cache_sg.id
private_subnet_ids = [for subnet in module.vpc.aws_subnet.private_subnet : subnet.id]
environment = local.environment
}
module "ec2" {
source = "../../modules/ec2"
ami_id = "ami-0abcdef1234567890" # Replace with a suitable AMI ID (e.g., Amazon Linux 2, Ubuntu)
instance_type = "t3.medium"
key_pair_name = "your-ec2-keypair" # Ensure this key pair exists
app_security_group_id = module.security_groups.aws_security_group.app_sg.id
private_subnet_ids = [for subnet in module.vpc.aws_subnet.private_subnet : subnet.id]
db_host = module.rds.aws_db_instance.magento_db.endpoint
db_name = "magento_db"
db_user = module.rds.aws_db_instance.magento_db.username
db_password = var.prod_db_password
cache_host = module.elasticache.aws_elasticache_cluster.magento_cache.cache_nodes[0].address
redis_db = 0
magento_env = "production"
app_env = "prod"
desired_capacity = 2
min_size = 2
max_size = 5
target_group_arn = module.alb.aws_lb_target_group.magento_tg.arn
}
module "alb" {
source = "../../modules/alb"
alb_security_group_id = module.security_groups.aws_security_group.alb_sg.id
public_subnet_ids = [for subnet in module.vpc.aws_subnet.public_subnet : subnet.id]
ssl_certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id" # Replace with your ACM certificate ARN
vpc_id =