• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Infrastructure as Code: Provisioning Secure PHP Clusters on AWS Using Terraform

Infrastructure as Code: Provisioning Secure PHP Clusters on AWS Using Terraform

Terraform: The Foundation for Secure PHP Cluster Deployment

Provisioning robust and secure infrastructure for PHP applications on AWS demands a systematic and repeatable approach. Infrastructure as Code (IaC) tools like Terraform are indispensable for achieving this. This post details a production-ready Terraform configuration for deploying a secure, scalable PHP cluster, focusing on best practices for networking, security groups, and resource management.

Core Terraform Configuration: VPC, Subnets, and Security Groups

We begin by defining the foundational network infrastructure. A Virtual Private Cloud (VPC) provides network isolation, and subnets within Availability Zones (AZs) ensure high availability. Crucially, security groups act as virtual firewalls, controlling inbound and outbound traffic to our instances.

Here’s the core Terraform code for setting up the VPC, public and private subnets across multiple AZs, and essential security groups:

VPC and Subnet Definition

# main.tf

# Configure AWS Provider
provider "aws" {
  region = "us-east-1" # Replace with your desired region
}

# VPC
resource "aws_vpc" "php_cluster_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "php-cluster-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.php_cluster_vpc.id

  tags = {
    Name = "php-cluster-igw"
  }
}

# Public Subnet (for NAT Gateways and potentially Bastion Hosts)
resource "aws_subnet" "public_subnet_az1" {
  vpc_id                  = aws_vpc.php_cluster_vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a" # Replace with your AZ
  map_public_ip_on_launch = true

  tags = {
    Name = "php-cluster-public-az1"
  }
}

resource "aws_subnet" "public_subnet_az2" {
  vpc_id                  = aws_vpc.php_cluster_vpc.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "us-east-1b" # Replace with your AZ
  map_public_ip_on_launch = true

  tags = {
    Name = "php-cluster-public-az2"
  }
}

# Private Subnet (for application servers)
resource "aws_subnet" "private_subnet_az1" {
  vpc_id            = aws_vpc.php_cluster_vpc.id
  cidr_block        = "10.0.101.0/24"
  availability_zone = "us-east-1a" # Replace with your AZ

  tags = {
    Name = "php-cluster-private-az1"
  }
}

resource "aws_subnet" "private_subnet_az2" {
  vpc_id            = aws_vpc.php_cluster_vpc.id
  cidr_block        = "10.0.102.0/24"
  availability_zone = "us-east-1b" # Replace with your AZ

  tags = {
    Name = "php-cluster-private-az2"
  }
}

# Route Table for Public Subnets
resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.php_cluster_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "php-cluster-public-rt"
  }
}

# Associate Public Subnets with Public Route Table
resource "aws_route_table_association" "public_assoc_az1" {
  subnet_id      = aws_subnet.public_subnet_az1.id
  route_table_id = aws_route_table.public_rt.id
}

resource "aws_route_table_association" "public_assoc_az2" {
  subnet_id      = aws_subnet.public_subnet_az2.id
  route_table_id = aws_route_table.public_rt.id
}

# NAT Gateway and EIP for Private Subnet Outbound Access
resource "aws_eip" "nat_eip_az1" {
  domain = "vpc"
}

resource "aws_nat_gateway" "nat_gw_az1" {
  allocation_id = aws_eip.nat_eip_az1.id
  subnet_id     = aws_subnet.public_subnet_az1.id

  tags = {
    Name = "php-cluster-nat-gw-az1"
  }

  depends_on = [aws_internet_gateway.gw]
}

resource "aws_eip" "nat_eip_az2" {
  domain = "vpc"
}

resource "aws_nat_gateway" "nat_gw_az2" {
  allocation_id = aws_eip.nat_eip_az2.id
  subnet_id     = aws_subnet.public_subnet_az2.id

  tags = {
    Name = "php-cluster-nat-gw-az2"
  }

  depends_on = [aws_internet_gateway.gw]
}

# Route Table for Private Subnets
resource "aws_route_table" "private_rt_az1" {
  vpc_id = aws_vpc.php_cluster_vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw_az1.id
  }

  tags = {
    Name = "php-cluster-private-rt-az1"
  }
}

resource "aws_route_table" "private_rt_az2" {
  vpc_id = aws_vpc.php_cluster_vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw_az2.id
  }

  tags = {
    Name = "php-cluster-private-rt-az2"
  }
}

# Associate Private Subnets with Private Route Tables
resource "aws_route_table_association" "private_assoc_az1" {
  subnet_id      = aws_subnet.private_subnet_az1.id
  route_table_id = aws_route_table.private_rt_az1.id
}

resource "aws_route_table_association" "private_assoc_az2" {
  subnet_id      = aws_subnet.private_subnet_az2.id
  route_table_id = aws_route_table.private_rt_az2.id
}

Security Group Definitions

# security_groups.tf

# Security Group for Load Balancer (e.g., ALB)
resource "aws_security_group" "alb_sg" {
  name        = "php-cluster-alb-sg"
  description = "Allow HTTP/HTTPS inbound traffic to ALB"
  vpc_id      = aws_vpc.php_cluster_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 = "php-cluster-alb-sg"
  }
}

# Security Group for PHP Application Servers
resource "aws_security_group" "app_sg" {
  name        = "php-cluster-app-sg"
  description = "Allow traffic from ALB and internal communication"
  vpc_id      = aws_vpc.php_cluster_vpc.id

  # Allow traffic from the ALB
  ingress {
    description     = "HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }

  # Allow SSH from a specific bastion host or internal network (highly recommended)
  # For simplicity, this example allows SSH from a specific CIDR block.
  # In production, restrict this to your management network or bastion host SG.
  ingress {
    description = "SSH from management network"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR_MANAGEMENT_IP_CIDR"] # e.g., "203.0.113.0/24"
  }

  # Allow internal communication between app servers (e.g., for clustering, caching)
  # Adjust ports as needed for your PHP application's dependencies (e.g., Redis, Memcached)
  ingress {
    description = "Internal app communication"
    from_port   = 0 # Or specific ports like 6379 for Redis
    to_port     = 65535
    protocol    = "tcp"
    self        = true # Allows traffic from instances within this security group
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"] # Allow outbound access for updates, external APIs, etc.
  }

  tags = {
    Name = "php-cluster-app-sg"
  }
}

# Security Group for Database (e.g., RDS)
resource "aws_security_group" "db_sg" {
  name        = "php-cluster-db-sg"
  description = "Allow traffic from App Servers to Database"
  vpc_id      = aws_vpc.php_cluster_vpc.id

  # Allow database port from application servers
  # Replace 3306 with your database's port (e.g., 5432 for PostgreSQL)
  ingress {
    description     = "MySQL/MariaDB from App Servers"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.app_sg.id]
  }

  # Egress is often not strictly necessary for RDS if it's not initiating outbound connections
  # but can be useful for specific scenarios.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "php-cluster-db-sg"
  }
}

Important Security Considerations:

  • Replace YOUR_MANAGEMENT_IP_CIDR with your actual IP address range for SSH access. Never expose SSH to the entire internet (0.0.0.0/0).
  • The self = true rule in the app security group is crucial for inter-instance communication.
  • Database security group rules should be as restrictive as possible, only allowing access from the application security group on the specific database port.

Provisioning Application Servers (EC2 Instances)

Now, let’s define the EC2 instances that will host our PHP application. We’ll use an Auto Scaling Group (ASG) to manage a fleet of instances, ensuring scalability and high availability. User data scripts will be used to configure the instances upon launch.

EC2 Launch Template and Auto Scaling Group

# ec2.tf

# Data source to get the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# User Data Script for PHP Application Server Configuration
locals {
  user_data_script = <<-EOF
    #!/bin/bash
    # Update packages
    yum update -y

    # Install Apache, PHP, and common modules
    yum install -y httpd php php-cli php-mysqlnd php-gd php-xml php-mbstring php-curl

    # Enable and start Apache
    systemctl enable httpd
    systemctl start httpd

    # Configure Apache to serve from a specific directory (e.g., /var/www/html/myapp)
    # You would typically deploy your application code here via other means (e.g., CodeDeploy, S3 sync)
    echo "

Hello from PHP Cluster Instance

" > /var/www/html/index.html # Example: Install Composer curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer # Example: Configure PHP settings (optional) # sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php.ini # sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php.ini # Add any other necessary configurations here (e.g., database credentials, cache setup) EOF } # EC2 Launch Template resource "aws_launch_template" "php_app_lt" { name_prefix = "php-app-lt-" image_id = data.aws_ami.amazon_linux_2.id instance_type = "t3.medium" # Choose an appropriate instance type key_name = "your-ssh-key-pair-name" # Replace with your EC2 key pair name # Attach the application security group vpc_security_group_ids = [aws_security_group.app_sg.id] # Assign to private subnets subnet_id = aws_subnet.private_subnet_az1.id # ASG will handle distribution across AZs user_data = base64encode(local.user_data_script) # IAM instance profile for accessing other AWS services (e.g., S3, Secrets Manager) # iam_instance_profile { # name = aws_iam_instance_profile.app_profile.name # } # Add EBS volume configuration if needed # block_device_mappings { # device_name = "/dev/xvda" # ebs { # volume_size = 30 # volume_type = "gp3" # } # } tags = { Name = "php-app-server" } lifecycle { create_before_destroy = true } } # Auto Scaling Group resource "aws_autoscaling_group" "php_app_asg" { name_prefix = "php-app-asg-" desired_capacity = 2 min_size = 1 max_size = 5 vpc_zone_identifier = [aws_subnet.private_subnet_az1.id, aws_subnet.private_subnet_az2.id] # Distribute across private subnets launch_template { id = aws_launch_template.php_app_lt.id version = "$Latest" } # Health check configuration health_check_type = "ELB" # Or EC2 if not using ELB health_check_grace_period = 300 # Seconds # Attach to ALB Target Group (defined later) # target_group_arns = [aws_lb_target_group.php_app_tg.arn] # Tags for instances launched by ASG tags = [ { key = "Name" value = "php-app-server" propagate_at_launch = true }, { key = "Environment" value = "production" propagate_at_launch = true } ] lifecycle { create_before_destroy = true } }

Notes on EC2 Configuration:

  • Replace your-ssh-key-pair-name with the name of your existing EC2 key pair.
  • The user_data script installs Apache, PHP, and essential modules. For production, consider using a more robust deployment strategy (e.g., AWS CodeDeploy, Ansible, or baking custom AMIs) to manage application code and configurations.
  • The iam_instance_profile is commented out but essential if your PHP application needs to interact with other AWS services. You’ll need to define an IAM role and instance profile.
  • The health_check_type should align with your load balancing strategy. If using an Application Load Balancer (ALB), ELB is appropriate.

Load Balancing with Application Load Balancer (ALB)

An ALB distributes incoming traffic across your EC2 instances, enhancing availability and fault tolerance. It also handles SSL termination, offloading this task from your application servers.

# alb.tf

# Application Load Balancer
resource "aws_lb" "php_alb" {
  name               = "php-cluster-alb"
  internal           = false # Set to true for internal-only ALB
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = [aws_subnet.public_subnet_az1.id, aws_subnet.public_subnet_az2.id] # ALB resides in public subnets

  enable_deletion_protection = false # Set to true in production

  tags = {
    Name = "php-cluster-alb"
  }
}

# ALB Listener for HTTP (redirect to HTTPS)
resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.php_alb.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# ALB Listener for HTTPS
resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.php_alb.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08" # Choose an appropriate SSL policy
  certificate_arn   = "arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id" # Replace with your ACM certificate ARN

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.php_app_tg.arn
  }
}

# Target Group for PHP Application Servers
resource "aws_lb_target_group" "php_app_tg" {
  name     = "php-app-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.php_cluster_vpc.id

  health_check {
    path                = "/" # Health check endpoint for your application
    protocol            = "HTTP"
    matcher             = "200" # Expected HTTP status code
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }

  tags = {
    Name = "php-app-tg"
  }
}

# Associate ASG with Target Group
resource "aws_autoscaling_attachment" "php_app_asg_attachment" {
  autoscaling_group_name = aws_autoscaling_group.php_app_asg.name
  lb_target_group_arn    = aws_lb_target_group.php_app_tg.arn
}

Key ALB Configurations:

  • Replace arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id with your AWS Certificate Manager (ACM) ARN for SSL/TLS.
  • The health_check.path should point to a URL in your application that returns a 200 OK status code when the application is healthy.
  • Ensure your app_sg allows inbound traffic on port 80 from the ALB’s security group.

Database Provisioning (RDS Example)

For a production PHP cluster, a managed database service like AWS RDS is highly recommended. This example shows provisioning a MySQL RDS instance.

# rds.tf

# RDS Subnet Group
resource "aws_db_subnet_group" "php_db_subnet_group" {
  name       = "php-db-subnet-group"
  subnet_ids = [aws_subnet.private_subnet_az1.id, aws_subnet.private_subnet_az2.id] # Place RDS in private subnets

  tags = {
    Name = "php-db-subnet-group"
  }
}

# RDS Instance (MySQL Example)
resource "aws_db_instance" "php_rds_instance" {
  allocated_storage    = 20
  engine               = "mysql"
  engine_version       = "8.0" # Specify your desired MySQL version
  instance_class       = "db.t3.medium" # Choose an appropriate instance class
  db_name              = "php_app_db"
  username             = "admin"
  password             = "YOUR_SECURE_PASSWORD" # Use AWS Secrets Manager or Parameter Store for production
  parameter_group_name = "default.mysql8.0"
  db_subnet_group_name = aws_db_subnet_group.php_db_subnet_group.name
  vpc_security_group_ids = [aws_security_group.db_sg.id]
  skip_final_snapshot  = true # Set to false in production
  publicly_accessible  = false # Must be false for security

  tags = {
    Name = "php-rds-instance"
  }
}

Critical RDS Security:

  • Never set publicly_accessible = true. RDS instances should reside in private subnets and be accessed only by your application servers.
  • Replace YOUR_SECURE_PASSWORD with a strong, unique password. For production, integrate with AWS Secrets Manager or AWS Systems Manager Parameter Store to manage database credentials securely.
  • Ensure the db_sg is correctly configured to allow inbound traffic from the app_sg on the database port.

Deployment Workflow and Best Practices

With the Terraform code defined, the deployment process is straightforward:

  1. Initialize Terraform: Run terraform init in your project directory to download necessary providers.
  2. Plan Changes: Execute terraform plan to review the infrastructure changes Terraform will make.
  3. Apply Changes: Run terraform apply to provision the resources on AWS. Confirm with ‘yes’ when prompted.
  4. Destroy Resources: When no longer needed, use terraform destroy to tear down all provisioned infrastructure, avoiding ongoing costs.

Security Hardening and Further Enhancements

  • IAM Roles: Utilize IAM roles with least-privilege permissions for EC2 instances instead of hardcoding access keys.
  • Secrets Management: Integrate with AWS Secrets Manager or Parameter Store for database credentials, API keys, and other sensitive information.
  • Logging and Monitoring: Configure CloudWatch Logs and Metrics for your EC2 instances, ALB, and RDS. Set up alarms for critical metrics.
  • Patch Management: Implement a robust patch management strategy for your EC2 instances (e.g., using AWS Systems Manager Patch Manager).
  • Immutable Infrastructure: Consider building custom AMIs with your application code pre-installed and updated, then deploying new instances from these AMIs rather than configuring existing ones.
  • Bastion Host: For enhanced security, deploy a bastion host in a public subnet to act as a secure jump point for SSH access to your private instances.
  • WAF Integration: Integrate AWS WAF with your ALB to protect against common web exploits.

This comprehensive Terraform configuration provides a secure, scalable, and highly available foundation for your PHP applications on AWS. By adhering to IaC principles and prioritizing security best practices, you can confidently manage and deploy your infrastructure.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability
  • Scala Pekko vs. Go Goroutines: Actor Model vs. CSP for Event-Driven Reactive Systems
  • Java Loom Virtual Threads vs. Go Goroutines: Under-the-Hood Scheduler and Thread Overhead Comparison

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (584)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (806)
  • PHP (5)
  • PHP Development (21)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (19)
  • Ruby on Rails (1)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (7)
  • WordPress Theme Development (357)

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (806)
  • Debugging & Troubleshooting (584)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala