- Prerequisites
- Getting Started
- Important Files
- Understanding the Terraform Code
- Understanding the userdata.tpl file
- Outputs
- Security Considerations
- Cleanup
- Troubleshooting
- Further Enhancements
- Advanced Configuration Options
- Known Issues
- Double-Check
This Terraform project deploys a highly available WordPress website on AWS. It includes the following components:
- Virtual Private Cloud (VPC): A dedicated network for the WordPress deployment.
- Subnets: Public and private subnets for different tiers of the application.
- Internet Gateway: Enables communication with the internet.
- Route Tables: Defines network routes within the VPC.
- Security Groups: Controls inbound and outbound traffic to resources. Includes a dedicated security group for EC2 instances allowing HTTP traffic from the Application Load Balancer (ALB).
- Database Subnet Group: Specifies subnets for the RDS database instance.
- Secrets Manager: Securely stores the database password and WordPress administrator credentials.
- RDS MySQL Database: A managed database instance for WordPress.
- Application Load Balancer (ALB): Distributes traffic across multiple EC2 instances.
- WAFv2 Web ACL: Protects the application from common web exploits.
- Target Group: Defines the set of EC2 instances behind the ALB. Configured with health checks to ensure instance availability.
- Listeners: Configures the ALB to listen on HTTP (redirects to HTTPS) and HTTPS ports.
- Route53 Records: Creates DNS records for the WordPress site, pointing to the ALB.
- ACM Certificate: Provides SSL/TLS encryption for secure communication.
- Launch Template: Defines the configuration for EC2 instances, including the AMI, instance type, security groups, and user data.
- Auto Scaling Group (ASG): Automatically scales the number of EC2 instances based on traffic.
- IAM Roles and Instance Profiles: Provides necessary permissions to EC2 instances.
- CloudWatch Logs: Collects logs for monitoring.
- CloudFront Distribution: Content Delivery Network (CDN) for improved performance (optional, but included).
- Terraform installed.
- AWS CLI installed and configured with appropriate credentials.
- An AWS account.
- A registered domain name (e.g.,
test.example.com
) managed by Route 53. You'll need the Route 53 zone ID for your domain.
-
Clone the repository:
git clone <repository_url> cd <repository_directory>
-
Configure the AWS CLI:
Ensure you have the AWS CLI configured with the necessary credentials and default region:
aws configure
-
Initialize Terraform:
terraform init
-
Set Variables:
Modify the
variables.tf
file or create aterraform.tfvars
file to customize the deployment. Key variables include:aws_region
: The AWS region to deploy to (e.g., "us-east-1"). Important: Ensure that all resources (AMI, key pair, etc.) are available in this region.ami_id
: The AMI ID to use for the EC2 instances. The default is set to an Amazon Linux 2 AMI, but you must verify this AMI ID is valid in the region you are deploying to. AMI IDs are region-specific! Find appropriate AMI in the AWS Marketplace or using the AWS CLI/Console.instance_type
: The EC2 instance type (e.g., "t3.micro"). Considert3.medium
or larger for production workloads.key_name
: The name of your AWS key pair for SSH access (optional). If you omit this, you will not be able to SSH into the instances. Consider using AWS Systems Manager Session Manager instead of SSH for better security.db_name
: The name of the MySQL database.db_user
: The username for the MySQL database.desired_capacity
,max_size
,min_size
: The desired, maximum, and minimum number of EC2 instances in the Auto Scaling Group. Adjust these based on your expected traffic.desired_capacity
should be betweenmin_size
andmax_size
.db_instance_type
: The database instance type (e.g., "db.t3.small"). Choose a larger instance type for production databases (e.g.,db.m5.large
).domain_name
: The full subdomain for your WordPress site (e.g., "wordpress.example.com"). This must match a valid domain name configured in Route 53. Ensure the domain is properly delegated to AWS Name Servers.wp_admin_email
: The email address for the WordPress administrator account. This will be used to create the WordPress admin user.allowed_inbound_cidr_blocks
: A list of CIDR blocks allowed to access the ALB (HTTP/HTTPS). Defaults to0.0.0.0/0
(all IPs), which is highly insecure for production. Restrict this to your specific IP addresses or CIDR blocks.db_allocated_storage
: The amount of storage allocated to the RDS instance in GB.db_skip_final_snapshot
: Whether to skip the final snapshot when destroying the database. Set this tofalse
for production to ensure a backup is created before deletion.db_backup_retention_period
: The number of days to retain automated backups for the RDS instance.db_performance_insights_enabled
: Whether to enable Performance Insights for the RDS instance. Recommended for monitoring database performance.db_performance_insights_retention_period
: The retention period (in days) for Performance Insights data.db_monitoring_interval
: The interval (in seconds) for RDS Enhanced Monitoring. Valid values: 0 (disabled), 10, 30, or 60.
Example
terraform.tfvars
:aws_region = "eu-west-1" ami_id = "ami-xxxxxxxxxxxxxxxxx" # Replace with a valid AMI ID for your region! key_name = "my-key-pair" # Replace with your key pair name domain_name = "wordpress.example.com" db_name = "wordpressdb" db_user = "wordpressuser" db_instance_type = "db.t3.micro" wp_admin_email = "admin@example.com" # Replace with the wordpress admin email allowed_inbound_cidr_blocks = ["192.0.2.0/24", "203.0.113.0/24"] # Replace with your allowed IPs db_skip_final_snapshot = false # Set to false for production to ensure a backup is created db_performance_insights_enabled = true db_monitoring_interval = 60
-
Review the Plan:
terraform plan
-
Apply the Configuration:
terraform apply
Type
yes
to approve the changes. -
Complete WordPress Installation:
- Once the EC2 instances are running, access your WordPress site using the URL provided in the Terraform outputs.
- The WordPress installation will be automated using the credentials stored in AWS Secrets Manager, but you may still need to configure plugins or themes. The administrator username, password, and email are stored securely and retrieved by the EC2 instances during provisioning.
main.tf
: The main Terraform configuration file containing the resource definitions. See the "Understanding the Terraform Code" section below for a detailed breakdown.variables.tf
: Defines the input variables for the project, allowing for customization of the deployment. See the "Setting Variables" section above.outputs.tf
: Defines the output values that will be displayed after the deployment, providing important information such as the ALB URL and database endpoint.userdata.tpl
: A template file used to configure the EC2 instances on launch, including installing WordPress and configuring the database connection. Check/tmp/userdata.log
on the EC2 instance for any errors during setup. This script useswp-cli
to automate the WordPress installation.README.md
: This file.
This section provides a detailed explanation of the resources defined in main.tf
.
-
random_string
: Generates a random string to ensure unique resource names, preventing naming conflicts. This is especially important when deploying multiple instances of the same infrastructure.resource "random_string" "random" { length = 8 special = false upper = false }
-
locals
: Defines local variables used throughout the configuration. Currently, it defines the CIDR block for the VPC.locals { cidr_block = "10.0.0.0/16" }
- Customization: The
cidr_block
can be changed to fit your network requirements. Ensure that it does not conflict with any existing networks.
- Customization: The
-
aws_vpc
: Creates the Virtual Private Cloud (VPC), a logically isolated section of the AWS cloud.resource "aws_vpc" "main" { cidr_block = local.cidr_block tags = { Name = "wordpress-vpc-${random_string.random.result}" } }
- Customization: You can adjust the
cidr_block
and add more tags for organization.
- Customization: You can adjust the
-
aws_subnet
(Public and Private): Creates the subnets within the VPC. Public subnets have a direct route to the internet, while private subnets do not.resource "aws_subnet" "public_subnet_a" { vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(local.cidr_block, 8, 1) availability_zone = "${var.aws_region}a" map_public_ip_on_launch = true tags = { Name = "public-subnet-a-${random_string.random.result}" } }
- Customization: You can adjust the
cidr_block
,availability_zone
, andmap_public_ip_on_launch
settings. Thecidrsubnet
function is used to divide the VPC's CIDR block into smaller subnets.map_public_ip_on_launch
is true for Public subnets, enabling EC2 instances to receive a public IP. Note the numbering used for the cidrsubnet function and ensure that the private and public subnets do not overlap. - Important: The availability zones should correspond to the chosen AWS region.
- Customization: You can adjust the
-
aws_internet_gateway
: Creates an Internet Gateway (IGW), allowing communication between the VPC and the internet.resource "aws_internet_gateway" "gw" { vpc_id = aws_vpc.main.id tags = { Name = "wordpress-igw-${random_string.random.result}" } }
-
aws_route_table
(Public): Creates a route table for the public subnets, routing all traffic to the Internet Gateway.resource "aws_route_table" "public_route_table" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.gw.id } tags = { Name = "public-route-table-${random_string.random.result}" } }
-
aws_route_table_association
: Associates the public subnets with the public route table.resource "aws_route_table_association" "public_subnet_a" { subnet_id = aws_subnet.public_subnet_a.id route_table_id = aws_route_table.public_route_table.id }
-
aws_security_group
: Creates security groups to control inbound and outbound traffic to the resources. Three security groups are defined:ec2_sg
,allow_http_https
, andallow_mysql
.resource "aws_security_group" "ec2_sg" { name = "ec2-wordpress-sg-${random_string.random.result}" description = "Allow HTTP traffic from the ALB" vpc_id = aws_vpc.main.id ingress { from_port = 80 to_port = 80 protocol = "tcp" security_groups = [aws_security_group.allow_http_https.id] # <--- ALLOW FROM THE ALB SG description = "Allow HTTP traffic from the ALB" } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] description = "Allow all outbound traffic" } tags = { Name = "ec2-wordpress-sg-${random_string.random.result}" } }
resource "aws_security_group" "allow_http_https" { name = "allow_http_https-${random_string.random.result}" description = "Allow HTTP and HTTPS traffic" vpc_id = aws_vpc.main.id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = var.allowed_inbound_cidr_blocks # Use a variable for allowed IPs. Defaults to 0.0.0.0/0 in variables.tf description = "Allow HTTP traffic" } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = var.allowed_inbound_cidr_blocks # Use a variable for allowed IPs. Defaults to 0.0.0.0/0 in variables.tf description = "Allow HTTPS traffic" } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] description = "Allow all outbound traffic" } tags = { Name = "allow_http_https-${random_string.random.result}" } }
resource "aws_security_group" "allow_mysql" { name = "allow_mysql-${random_string.random.result}" description = "Allow MySQL traffic from EC2 instances in the allow_http_https SG" vpc_id = aws_vpc.main.id ingress { from_port = 3306 to_port = 3306 protocol = "tcp" security_groups = [aws_security_group.allow_http_https.id] description = "Allow MySQL from app servers (allow_http_https SG)" } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] description = "Allow all outbound traffic" } tags = { Name = "allow_mysql-${random_string.random.result}" } }
-
ec2_sg
: Allows HTTP traffic from the ALB to the EC2 instances. This is crucial for the application to function. The ingress rule specifies that traffic is allowed only from theallow_http_https
security group. -
allow_http_https
: Allows HTTP and HTTPS traffic from the internet (or specified CIDR blocks) to the ALB. Theallowed_inbound_cidr_blocks
variable must be restricted for production environments. The egress rule allows all outbound traffic. -
allow_mysql
: Allows MySQL traffic from the EC2 instances (specifically, from theallow_http_https
security group) to the RDS database. -
Customization: Carefully review and adjust the security group rules to meet your specific security requirements.
-
-
aws_db_subnet_group
: Creates a database subnet group, specifying the subnets where the RDS database instance will be created.resource "aws_db_subnet_group" "db_subnet_group" { name = "db-subnet-group-${random_string.random.result}" subnet_ids = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id] tags = { Name = "DB Subnet Group-${random_string.random.result}" } }
-
aws_secretsmanager_secret
: Creates a secret in AWS Secrets Manager to store the database password. This is a secure way to manage sensitive information. Separate secrets are created for the database password and WordPress admin credentials.resource "aws_secretsmanager_secret" "db_password" { name = "wp-db-pass-${random_string.random.result}" description = "Password for the WordPress database" recovery_window_in_days = 7 }
resource "aws_secretsmanager_secret" "wp_admin_creds" { name = "wp-admin-creds-${random_string.random.result}" description = "WordPress admin credentials" recovery_window_in_days = 7 }
- The
recovery_window_in_days
attribute specifies how many days Secrets Manager waits before permanently deleting a secret that has been marked for deletion.
- The
-
random_password
: Generates a random password for the database.resource "random_password" "db_password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?" }
-
aws_db_instance
: Creates the RDS MySQL database instance.resource "aws_db_instance" "default" { allocated_storage = var.db_allocated_storage engine = "mysql" engine_version = "8.0" instance_class = var.db_instance_type db_name = var.db_name username = var.db_user password = data.aws_secretsmanager_secret_version.db_password.secret_string # Use the secret from Secrets Manager parameter_group_name = aws_db_parameter_group.default.name # Use the custom parameter group db_subnet_group_name = aws_db_subnet_group.db_subnet_group.name vpc_security_group_ids = [aws_security_group.allow_mysql.id] skip_final_snapshot = var.db_skip_final_snapshot final_snapshot_identifier = "wordpress-db-snapshot-${random_string.random.result}" multi_az = true backup_retention_period = var.db_backup_retention_period performance_insights_enabled = var.db_performance_insights_enabled performance_insights_retention_period = var.db_performance_insights_retention_period monitoring_interval = var.db_monitoring_interval monitoring_role_arn = aws_iam_role.rds_monitoring_role.arn storage_type = "gp3" storage_encrypted = true publicly_accessible = false tags = { Name = "wordpress-db-${random_string.random.result}" } lifecycle { create_before_destroy = true } }
- Key Attributes:
allocated_storage
: The amount of storage allocated to the database.instance_class
: The database instance type.db_name
,username
: The database name and username.password
: Retrieved from AWS Secrets Manager.vpc_security_group_ids
: The security groups associated with the database.multi_az
: Enables Multi-AZ deployment for high availability.skip_final_snapshot
: Determines whether a final snapshot is created when the database is destroyed.performance_insights_enabled
,performance_insights_retention_period
: Enables and configures RDS Performance Insights for monitoring.monitoring_interval
,monitoring_role_arn
: Configures RDS Enhanced Monitoring.
- Important: The
lifecycle
block withcreate_before_destroy = true
ensures that a new database instance is created before the old one is destroyed during updates, minimizing downtime. - Customization: Customize the
instance_class
,allocated_storage
,multi_az
,backup_retention_period
,performance_insights_enabled
, andmonitoring_interval
based on your requirements.
- Key Attributes:
-
aws_db_parameter_group
: Creates a custom database parameter group to configure database settings, in this case, slow query logging.resource "aws_db_parameter_group" "default" { name = "wordpress-db-params-${random_string.random.result}" family = "mysql8.0" parameter { name = "slow_query_log" value = "1" } parameter { name = "long_query_time" value = "5" # Log queries taking longer than 5 seconds } description = "Custom parameter group for WordPress" }
- Customization: You can add or modify parameters to fine-tune the database configuration. For example, you can adjust buffer sizes, connection limits, or character sets. Ensure the
family
attribute matches the database engine version.
- Customization: You can add or modify parameters to fine-tune the database configuration. For example, you can adjust buffer sizes, connection limits, or character sets. Ensure the
-
aws_lb
: Creates the Application Load Balancer (ALB) to distribute traffic across the EC2 instances.resource "aws_lb" "main" { name = "wordpress-alb-${random_string.random.result}" internal = false load_balancer_type = "application" security_groups = [aws_security_group.allow_http_https.id] subnets = [aws_subnet.public_subnet_a.id, aws_subnet.public_subnet_b.id] idle_timeout = 60 tags = { Name = "wordpress-alb-${random_string.random.result}" } }
- Key Attributes:
internal
: Specifies whether the ALB is internal or external (internet-facing).load_balancer_type
: Set to "application" for an Application Load Balancer.security_groups
: The security groups associated with the ALB.subnets
: The subnets where the ALB will be created.idle_timeout
: The idle timeout value for connections to the ALB.
- Customization: Adjust the
idle_timeout
based on your application's needs. For WebSocket applications, you'll need to increase this value.
- Key Attributes:
-
aws_wafv2_web_acl
andaws_wafv2_web_acl_association
: Creates a WAFv2 Web ACL and associates it with the ALB to protect against web exploits.resource "aws_wafv2_web_acl" "example" { name = "ExampleWebACL-${random_string.random.result}" description = "A WebACL for WordPress Application" scope = "REGIONAL" default_action { allow {} } visibility_config { cloudwatch_metrics_enabled = false metric_name = "friendly-name-associated-with-the-web-acl" sampled_requests_enabled = false } } resource "aws_wafv2_web_acl_association" "example" { resource_arn = aws_lb.main.arn web_acl_arn = aws_wafv2_web_acl.example.arn }
- Customization: The
default_action
is currently set toallow {}
, which means all requests are allowed. This is NOT recommended for production. You'll need to define rules to block malicious traffic. Consider using AWS Managed Rules for common web exploits. Enablecloudwatch_metrics_enabled
andsampled_requests_enabled
for monitoring.
- Customization: The
-
aws_lb_target_group
: Creates a target group for the ALB, defining the set of EC2 instances that the ALB will distribute traffic to.resource "aws_lb_target_group" "main" { name = "wordpress-tg-${random_string.random.result}" port = 80 protocol = "HTTP" vpc_id = aws_vpc.main.id target_type = "instance" health_check { path = "/" # Change to root path for initial check port = "traffic-port" protocol = "HTTP" matcher = "200-399" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 2 } }
- Key Attributes:
port
: The port on which the EC2 instances are listening.protocol
: The protocol used by the EC2 instances.vpc_id
: The VPC ID.target_type
: The type of target (in this case, "instance").health_check
: Configures health checks to ensure that the EC2 instances are healthy.
- Customization: Adjust the
health_check
settings to match your application's health check endpoint. A200-399
matcher is common for HTTP applications.
- Key Attributes:
-
aws_lb_listener
: Creates listeners for the ALB, configuring it to listen on HTTP (redirects to HTTPS) and HTTPS ports.resource "aws_lb_listener" "https" { load_balancer_arn = aws_lb.main.arn port = 443 protocol = "HTTPS" ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" # Use the latest security policy certificate_arn = aws_acm_certificate.example.arn default_action { type = "forward" target_group_arn = aws_lb_target_group.main.arn } } 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" } } }
- Customization: The
ssl_policy
should be set to the latest security policy. - Important: The HTTP listener redirects all traffic to HTTPS.
- Customization: The
-
data "aws_route53_zone"
: Retrieves information about the Route 53 zone for your domain.data "aws_route53_zone" "selected" { name = "aws.italiadns.net." private_zone = false }
- Customization: Replace
"aws.italiadns.net."
with your registered domain name. - Important: Ensure that the domain is properly delegated to AWS Name Servers.
- Customization: Replace
-
aws_acm_certificate
: Creates an ACM certificate for SSL/TLS encryption.resource "aws_acm_certificate" "example" { domain_name = var.domain_name validation_method = "DNS" tags = { Name = "SSL Certificate-${random_string.random.result}" } lifecycle { create_before_destroy = true } }
- Key Attributes:
domain_name
: The domain name for the certificate.validation_method
: Set to "DNS" for DNS validation.
- Important: The
lifecycle
block withcreate_before_destroy = true
allows recreation of certificate before destroy during updates.
- Key Attributes:
-
aws_route53_record
(DNS Validation and WordPress Site): Creates DNS records in Route 53 for certificate validation and to point the domain name to the ALB.resource "aws_route53_record" "example_validation" { for_each = { for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => dvo } zone_id = data.aws_route53_zone.selected.zone_id name = each.value.resource_record_name type = each.value.resource_record_type records = [each.value.resource_record_value] ttl = 60 } resource "aws_route53_record" "wordpress_site" { zone_id = data.aws_route53_zone.selected.zone_id name = var.domain_name type = "A" alias { name = aws_lb.main.dns_name zone_id = aws_lb.main.zone_id evaluate_target_health = true } }
- Important: The
aws_route53_record.example_validation
record are necessary to validate the ACM certificate. These are created usingfor_each
andaws_acm_certificate.example.domain_validation_options
. Ensure that these records are created correctly and that DNS propagation has completed. - The
aws_route53_record.wordpress_site
creates an A record that points the domain name to the ALB.evaluate_target_health = true
ensures that Route 53 only returns the ALB's IP address if the ALB is healthy.
- Important: The
-
aws_launch_template
: Defines the configuration for the EC2 instances.resource "aws_launch_template" "wordpress_lt" { name_prefix = "wordpress-lt-${random_string.random.result}-" image_id = var.ami_id instance_type = var.instance_type key_name = var.key_name vpc_security_group_ids = [aws_security_group.allow_http_https.id, aws_security_group.ec2_sg.id] # <--- IMPORTANT: Use the correct ec2 sg here. If allow_http_https has outbound access already it may not be necessary to include here. iam_instance_profile { arn = aws_iam_instance_profile.ec2_instance_profile.arn } user_data = base64encode(templatefile("${path.module}/userdata.tpl", { db_name = aws_db_instance.default.db_name db_user = aws_db_instance.default.username db_host = aws_db_instance.default.endpoint domain_name = var.domain_name secrets_wp_admin_arn = aws_secretsmanager_secret.wp_admin_creds.arn secrets_db_password_arn = aws_secretsmanager_secret.db_password.arn db_password = data.aws_secretsmanager_secret_version.db_password.secret_string wp_admin_creds = data.aws_secretsmanager_secret_version.wp_admin_creds.secret_string # Add this line })) tags = { Name = "wordpress-lt-${random_string.random.result}" } lifecycle { create_before_destroy = true } }
- Key Attributes:
image_id
: The AMI ID for the EC2 instances.instance_type
: The EC2 instance type.key_name
: The SSH key pair to use for the EC2 instances.vpc_security_group_ids
: The security groups associated with the EC2 instances. Crucially, both theallow_http_https
andec2_sg
are attached to the instances. This allows outbound traffic from the EC2 instances to the internet and inbound traffic from the ALB.iam_instance_profile
: The IAM instance profile to use for the EC2 instances.user_data
: A script that is executed when the EC2 instance is launched. This script is used to install WordPress and configure the database connection. Thetemplatefile
function is used to render theuserdata.tpl
file with the necessary variables.
- Important: Be very careful about outbound access configured for the
allow_http_https
security group. If all outbound traffic is blocked, you will also need to allow outbound access to the internet using the security group attached to the launch template.
- Key Attributes:
* **`aws_autoscaling_group`:** Creates an Auto Scaling Group (ASG) to automatically scale the number of EC2 instances based on traffic.
```terraform
resource "aws_autoscaling_group" "wordpress_asg" {
name = "wordpress-asg-${random_string.random.result}"
vpc_zone_identifier = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id]
desired_capacity = var.desired_capacity
max_size = var.max_size
min_size = var.min_size
health_check_type = "ELB"
health_check_grace_period = var.default_instance_warmup
target_group_arns = [aws_lb_target_group.main.arn]
launch_template {
id = aws_launch_template.wordpress_lt.id
version = "$Latest" # Or a specific version
}
}
```
* **Key Attributes:**
* `vpc_zone_identifier`: The subnets where the EC2 instances will be launched. **Crucially, these are the *private* subnets.**
* `desired_capacity`, `max_size`, `min_size`: The desired, maximum, and minimum number of EC2 instances.
* `health_check_type`: The type of health check to use. "ELB" indicates that the ALB's health checks will be used.
* `health_check_grace_period`: The amount of time (in seconds) that the ASG will wait before performing health checks on new instances. This allows the instances to boot and configure themselves before being checked. Important to set a reasonable value.
* `launch_template`: Specifies the launch template to use for the EC2 instances.
* **Customization:** Adjust `desired_capacity`, `max_size`, `min_size`, and `health_check_grace_period` based on your application's requirements. Use a specific Launch Template version instead of `$Latest` in production to prevent unexpected changes during ASG scaling events.
* **`aws_autoscaling_policy`:** Creates Auto Scaling policies to automatically scale the number of EC2 instances based on CPU utilization.
```terraform
resource "aws_autoscaling_policy" "scale_up" {
name = "scale-up-${random_string.random.result}"
autoscaling_group_name = aws_autoscaling_group.wordpress_asg.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 70.0
}
}
resource "aws_autoscaling_policy" "scale_down" {
name = "scale-down-${random_string.random.result}"
autoscaling_group_name = aws_autoscaling_group.wordpress_asg.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 30.0
}
}
```
* **Key Attributes:**
* `autoscaling_group_name`: The name of the Auto Scaling Group to which the policy applies.
* `policy_type`: Set to "TargetTrackingScaling" for target tracking policies.
* `target_tracking_configuration`: Configures the target tracking policy.
* `predefined_metric_specification`: Specifies the metric to track.
* `target_value`: The target value for the metric.
* **Customization:** Adjust `target_value` based on your application's performance requirements. You can also use custom metrics and scaling policies. Consider using Step Scaling policies for more granular control.
* **`aws_iam_role` and `aws_iam_instance_profile` (EC2):** Creates an IAM role and instance profile for the EC2 instances, granting them the necessary permissions to access other AWS resources.
```terraform
resource "aws_iam_role" "ec2_role" {
name = "ec2-wordpress-role-${random_string.random.result}"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Principal = {
Service = "ec2.amazonaws.com"
},
Effect = "Allow",
Sid = ""
}
]
})
}
resource "aws_iam_instance_profile" "ec2_instance_profile" {
name = "ec2-wordpress-instance-profile-${random_string.random.result}"
role = aws_iam_role.ec2_role.name
}
```
* **Important:** The `assume_role_policy` allows EC2 instances to assume this role.
* **`aws_iam_role_policy_attachment`:** Attaches the `AmazonSSMManagedInstanceCore` policy to the EC2 IAM role, allowing the instances to be managed by AWS Systems Manager (SSM).
```terraform
resource "aws_iam_role_policy_attachment" "example-attach" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.ec2_role.name
}
```
* **Important**: Enables SSM for remote management.
* **`aws_iam_role_policy` (Secrets Manager):** Creates an IAM policy to allow the EC2 instances to access AWS Secrets Manager to retrieve the database password.
```terraform
resource "aws_iam_role_policy" "secretsmanager_policy" {
name = "secretsmanager-access-${random_string.random.result}"
role = aws_iam_role.ec2_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
Effect = "Allow",
Resource = [
aws_secretsmanager_secret.db_password.arn,
aws_secretsmanager_secret.wp_admin_creds.arn
]
}
]
})
}
```
* **Important:** This policy grants the necessary permissions to retrieve the secrets from Secrets Manager.
* **`aws_iam_role` and `aws_iam_role_policy` (RDS Monitoring):** Creates an IAM role and policy for RDS Enhanced Monitoring, allowing CloudWatch Logs to access RDS metrics.
```terraform
resource "aws_iam_role" "rds_monitoring_role" {
name = "rds-monitoring-role-${random_string.random.result}"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Principal = {
Service = "monitoring.rds.amazonaws.com"
},
Effect = "Allow",
Sid = ""
}
]
})
}
resource "aws_iam_role_policy" "rds_monitoring_policy" {
name = "rds-monitoring-policy-${random_string.random.result}"
role = aws_iam_role.rds_monitoring_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = [
"logs:PutLogEvents",
"logs:GetLogEvents",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:DescribeLogGroups"
],
Effect = "Allow",
Resource = "*"
}
]
})
}
```
* **Important:** This enables Enhanced Monitoring.
* **`aws_cloudwatch_log_group`:** Creates a CloudWatch Log Group to store logs from the EC2 instances.
```terraform
resource "aws_cloudwatch_log_group" "wordpress" {
name = "/aws/ec2/wordpress-${random_string.random.result}"
retention_in_days = 7
}
```
* **Customization:** Adjust the `retention_in_days` based on your logging requirements.
* **`aws_cloudfront_distribution`:** Creates a CloudFront distribution to improve performance by caching static content.
```terraform
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_lb.main.dns_name
origin_id = "LB"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.php"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "LB"
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
tags = {
Environment = "production"
}
}
```
* **Key Attributes:**
* `origin`: Specifies the origin server (in this case, the ALB).
* `origin_protocol_policy` = "https-only" enforces the origin connection over HTTPS.
* `default_cache_behavior`: Configures the default caching behavior.
* `viewer_protocol_policy` = "redirect-to-https" configures redirections from http to https
* `viewer_certificate`: Specifies the SSL/TLS certificate to use.
* **Customization:** Configure caching behavior, allowed HTTP methods, and other settings based on your application's requirements. Consider using an Origin Access Identity (OAI) to restrict direct access to the S3 bucket (if you are serving media from S3).
* **Important:** Setting the `domain_name` for the `origin` directly to the LB may present security risks, as it exposes the internal LB to the internet. It's typically preferable to either serve static assets through an S3 bucket with restricted access, or use Lambda@Edge to authenticate requests to the LB if more dynamic origin interaction is needed.
* **`aws_secretsmanager_secret` (WordPress Admin Credentials):** Creates a secret in AWS Secrets Manager to store the WordPress administrator credentials.
```terraform
resource "aws_secretsmanager_secret" "wp_admin_creds" {
name = "wp-admin-creds-${random_string.random.result}"
description = "WordPress admin credentials"
recovery_window_in_days = 7
}
```
* **`random_password` (WordPress Admin Password):** Generates a random password for the WordPress administrator account.
```terraform
resource "random_password" "wp_admin_password" {
length = 16
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
```
* **`aws_secretsmanager_secret_version` (WordPress Admin Credentials):** Creates a secret in AWS Secrets Manager with username, password and email of WP Admin.
```terraform
resource "aws_secretsmanager_secret_version" "wp_admin_creds" {
secret_id = aws_secretsmanager_secret.wp_admin_creds.id
secret_string = jsonencode({
username = var.wp_admin_username
password = random_password.wp_admin_password.result
email = var.wp_admin_email
})
}
```
## Understanding the `userdata.tpl` file
The `userdata.tpl` file contains a script that is executed when the EC2 instance is launched. This script automates the following tasks:
* Updates the package list and installs necessary tools (Apache, PHP, MySQL client, `wp-cli`, etc.).
* Starts and enables Apache.
* Installs `wp-cli` (WordPress command-line interface).
* Downloads and extracts WordPress.
* Creates the `wp-config.php` file using `wp-cli` and the database credentials from AWS Secrets Manager.
* Installs WordPress using `wp-cli`.
* Configures Apache with a proper Virtual Host.
* Configures SELinux.
* Restarts Apache.
* Configure .htaccess to WordPress
* Enable mod_rewrite
**Important:** This script logs all output to `/tmp/userdata.log`. If you encounter any issues with the WordPress installation, check this file for errors.
**Security:** This script is designed to be a starting point for automating the WordPress installation. For a production environment, you may want to use a configuration management tool such as Ansible or Chef to improve security and manageability.
**Customization:** You can customize the `userdata.tpl` file to install additional software, configure WordPress plugins, or perform other tasks.
## Outputs
The `outputs.tf` file defines the output values that will be displayed after the deployment. These outputs provide important information such as:
* `load_balancer_url`: The URL of the Application Load Balancer (ALB).
* `database_endpoint`: The endpoint of the MySQL database.
* `cloudfront_domain`: The domain name of the CloudFront distribution.
* `cloudfront_test_url`: Test URL for the CloudFront distribution (HTTPS).
* `loadbalancer_test_url`: Test URL for the Load Balancer (HTTP). This will redirect to HTTPS.
* `loadbalancer_https_test_url`: Test URL for the Load Balancer (HTTPS).
* `custom_domain_name`: The custom domain name used for the WordPress site.
## Security Considerations
* **Database and Admin Passwords:** The database and WordPress admin passwords are automatically generated and stored in AWS Secrets Manager. Ensure that the IAM role used by the EC2 instances has the necessary permissions to access Secrets Manager.
* **Security Groups:** The security groups are configured to allow HTTP and HTTPS traffic from the internet and MySQL traffic from the EC2 instances. **Critically, a dedicated security group (`ec2_sg`) allows HTTP traffic from the ALB to the EC2 instances.** Review and adjust the security groups as needed. Consider restricting access to specific IP addresses (using the `allowed_inbound_cidr_blocks` variable).
* **IAM Roles:** The IAM roles grant the EC2 instances the necessary permissions to access other AWS resources. Review and restrict the IAM roles to follow the principle of least privilege.
* **WAFv2:** The WAFv2 Web ACL is a basic configuration. Customize the rules to provide more comprehensive protection against web exploits.
* **RDS Enhanced Monitoring:** Consider enabling Enhanced Monitoring for production environments for deeper database performance insights. If enabling, ensure the IAM role has the required permissions. **Note:** This deployment currently does enable this feature if you set `db_monitoring_interval` variable.
* **Regular Security Updates:** It is your responsibility to keep the underlying operating system (Amazon Linux 2) and WordPress installation up-to-date with the latest security patches. Automated security updates are highly recommended.
* **Restrict Inbound Access:** Restrict the `allowed_inbound_cidr_blocks` to only the necessary IP addresses or CIDR blocks.
* **Final Database Snapshot:** Set `db_skip_final_snapshot` to `false` for production environments to ensure a backup is created before the database is deleted.
* **HTTPS Enforcement:** The ALB is configured to redirect all HTTP traffic to HTTPS, ensuring that all communication is encrypted.
## Cleanup
To destroy the infrastructure created by Terraform:
```bash
terraform destroy
Type yes
to confirm. This will delete all resources created by the Terraform configuration.
- Errors During
terraform apply
: Carefully examine the error messages. Common causes include:- Incorrect AMI ID. Verify the AMI ID is valid in your target AWS region!
- Missing or invalid AWS credentials.
- Insufficient IAM permissions.
- Resource naming conflicts.
- Invalid variable values.
- ACM Certificate Validation Failed: Verify that the Route53 records have been created correctly and that DNS propagation has completed.
- WordPress Site Not Accessible:
- Check the security groups to ensure that HTTP and HTTPS traffic is allowed between the ALB and EC2 instances. Verify that the
ec2_sg
is correctly configured. - Verify that the EC2 instances are running and healthy in the Target Group. A "502 Bad Gateway" error strongly suggests a problem with the security group configuration or the health of the EC2 instances.
- Check the ALB listeners and target group configuration.
- Examine the CloudWatch logs for any errors.
- Ensure that the Route 53 records are correctly configured and have propagated.
- If using CloudFront, ensure that the distribution is enabled and that DNS records point to the CloudFront domain.
- Check the security groups to ensure that HTTP and HTTPS traffic is allowed between the ALB and EC2 instances. Verify that the
- EC2 Instance Configuration Issues:
- SSH into an EC2 instance (if you configured a key pair) and examine the
/tmp/userdata.log
file to identify any errors that occurred during the instance initialization. This is your first stop for troubleshooting EC2 instance setup problems! - Check the Apache error logs for any WordPress-related errors (
/var/log/httpd/error_log
). - Verify that the database connection details in
wp-config.php
are correct.
- SSH into an EC2 instance (if you configured a key pair) and examine the
- Automated WordPress Installation Fails:
- Check the
/tmp/userdata.log
for errors related towp-cli
. - Ensure that the AWS Secrets Manager secret for the WordPress admin credentials is being retrieved correctly.
- Verify that the IAM role for the EC2 instances has permissions to access Secrets Manager.
- Check the
- CloudFront not working properly:
- Verify that your CloudFront distribution points to the correct origin, that is your Load Balancer. Also, verify that you created a Custom Origin Config and that the Origin protocol policy is HTTPS only.
- Verify that there is a DNS A record (alias) that point to the CloudFront distribution.
- You must wait some minutes before the CloudFront distribution can propagate and work properly.
- Centralized Logging: Implement a centralized logging solution using CloudWatch Logs or other log management tools.
- Implement a Backup Strategy: Implement automatic database backups and EC2 instance snapshots.
- CI/CD Pipeline: Create a CI/CD pipeline to automate the deployment process.
- Implement Object Caching: Use Memcached or Redis for improved performance.
- WAFv2 Advanced Configuration: Implement more sophisticated WAF rules using AWS Managed Rules or custom rules.
This section details some advanced configuration options that can be customized to fine-tune the deployment. These options can be set as variables in terraform.tfvars
or overridden on the command line using the -var
flag.
-
Custom AMI: While the project defaults to an Amazon Linux 2 AMI, you can use a custom AMI with pre-installed software or specific configurations. Be sure to update the
ami_id
variable accordingly. When using a custom AMI, ensure it has the AWS SSM agent installed and configured correctly to allow for proper instance management. -
Enhanced Security Group Rules: The default security group rules are relatively permissive for demonstration purposes. For production deployments, consider the following:
- Limit Inbound SSH Access: Restrict inbound SSH access (
key_name
and the related security group rule) to specific IP addresses or CIDR blocks, or remove it entirely and use AWS Systems Manager Session Manager for secure remote access. - Implement a Web Application Firewall (WAF): Customize the WAFv2 rules to protect against specific threats. Consider enabling managed rule groups for common web exploits.
- Network ACLs: Implement Network ACLs (NACLs) on the subnets for an additional layer of security. NACLs are stateless and provide a basic level of traffic filtering.
- TLS Versions and Cipher Suites: Configure the ALB listener to use specific TLS versions and cipher suites to improve security posture. This can be achieved using the
alb.tf
file and modifying the listener resources.
- Limit Inbound SSH Access: Restrict inbound SSH access (
-
Database Configuration:
- RDS Instance Class: The default
db_instance_type
(e.g.,db.t3.micro
) is suitable for testing and development. For production environments, choose a more powerful instance class based on your performance requirements. - RDS Storage: Adjust the
allocated_storage
variable to specify the amount of storage allocated to the RDS instance. - Multi-AZ Deployment: Enable Multi-AZ deployment for high availability by setting the
multi_az
variable totrue
. This will create a standby replica of the database in a different Availability Zone. - Database Encryption: Ensure that RDS encryption is enabled (it's enabled by default in this configuration).
- DB Parameter Groups: For advanced database tuning, create and use custom DB Parameter Groups.
- RDS Instance Class: The default
-
Auto Scaling Group (ASG) Configuration:
- Scaling Policies: Define custom scaling policies for the ASG based on CPU utilization, network traffic, or other metrics. This allows you to automatically scale the number of EC2 instances based on the actual workload. Consider using Predictive Scaling.
- Lifecycle Hooks: Implement lifecycle hooks to perform custom actions during instance launch or termination. This can be useful for tasks such as registering instances with a service discovery system or cleaning up resources when an instance is terminated.
- Instance Refresh: Use Instance Refresh to gracefully roll out new versions of your application to the ASG.
-
CloudFront Customization:
- Cache Policies: Configure custom cache policies for CloudFront to optimize caching behavior. This can significantly improve performance by reducing the load on the origin server.
- Origin Access Identity (OAI): Use an OAI to restrict access to the S3 bucket used for the WordPress media library. This ensures that users can only access the media files through CloudFront.
- Custom Error Pages: Create custom error pages for CloudFront to provide a better user experience in case of errors.
-
Monitoring and Logging:
- CloudWatch Alarms: Create CloudWatch alarms to monitor the health of your infrastructure and receive notifications when issues arise.
- Detailed Monitoring: Enable detailed monitoring on the EC2 instances for more granular performance metrics.
- RDS Performance Insights: Enable RDS Performance Insights for advanced database performance analysis and troubleshooting.
- Centralized Log Aggregation: Use a centralized log aggregation service such as Amazon Elasticsearch Service (now OpenSearch Service) or a third-party solution to collect and analyze logs from all components of the application.
-
WordPress Configuration:
- Object Cache: Implement an object cache such as Memcached or Redis to improve WordPress performance. This can be done by installing the necessary software on the EC2 instances and configuring WordPress to use the cache.
- CDN Integration: Configure WordPress to use the CloudFront CDN for serving static assets. This can be done using a WordPress plugin such as W3 Total Cache or WP Super Cache.
- WordPress Hardening: Implement various WordPress hardening techniques to improve security. This includes disabling file editing, hiding the WordPress version number, and using strong passwords.
- Slow Initial Deployment: The initial deployment can take a considerable amount of time, especially the creation of the RDS instance and the ACM certificate validation.
userdata.tpl
Complexity: Theuserdata.tpl
file can become complex and difficult to manage. Consider using a configuration management tool such as Ansible or Chef to simplify instance configuration.- Database Password Rotation: The database password is automatically generated and stored in Secrets Manager. Implement a mechanism to automatically rotate the database password on a regular basis.
- Security Group Changes Require Replacement: Any time you change the security group rules the underlying resources will need to be recreated, which will cause downtime, so make sure your configurations are correct.
wp-config.php
Hardcoded Paths: The paths used inwp-config.php
may not be compatible with all custom WordPress setups. It's possible you need to adjust them to fit your specific needs.
Before deploying, please review the entire documentation to ensure that all variable values, resource configurations, and instructions remain up-to-date with the current best practices and infrastructure requirements.
This documentation provides a comprehensive overview of the Terraform WordPress deployment on AWS, including prerequisites, getting started instructions, security considerations, troubleshooting tips, further enhancements, contributing guidelines, and advanced configuration options. By following these guidelines, you can successfully deploy and manage a highly available and secure WordPress website on AWS. Remember to always prioritize security best practices and regularly review and update your infrastructure to stay ahead of potential threats.