Infrastructure as Code: Provisioning Secure Magento 2 Clusters on Google Cloud Using Terraform
Terraform Project Structure for Magento 2 on GCP
A robust Infrastructure as Code (IaC) strategy for deploying complex applications like Magento 2 on Google Cloud Platform (GCP) necessitates a well-organized Terraform project. This structure facilitates maintainability, reusability, and collaboration. We’ll adopt a modular approach, separating concerns for networking, compute, databases, and application-specific configurations.
Our root module will orchestrate the deployment, calling sub-modules for distinct infrastructure components. This promotes a clear separation of concerns and allows for easier management of individual parts of the infrastructure.
Networking Module: VPC, Subnets, and Firewall Rules
Securely isolating your Magento 2 cluster begins with a well-defined Virtual Private Cloud (VPC) network. We’ll define custom subnets for different tiers (e.g., public-facing web servers, private application servers, database servers) and implement strict firewall rules to control ingress and egress traffic.
The following Terraform configuration outlines the creation of a custom VPC, a public subnet for load balancers and bastion hosts, and a private subnet for Magento application servers and the database. We’ll also define essential firewall rules for SSH, HTTP/S, and internal communication.
modules/gcp/networking/main.tf
# GCP VPC Network
resource "google_compute_network" "magento_vpc" {
name = "${var.project_name}-vpc"
auto_create_subnetworks = false
routing_mode = "REGIONAL"
}
# Public Subnet for Load Balancers and Bastion Hosts
resource "google_compute_subnetwork" "public_subnet" {
name = "${var.project_name}-public-subnet"
ip_cidr_range = "10.0.1.0/24"
region = var.region
network = google_compute_network.magento_vpc.id
private_ip_google_access = true # Allows instances to reach Google APIs without external IPs
}
# Private Subnet for Magento Application Servers and Database
resource "google_compute_subnetwork" "private_subnet" {
name = "${var.project_name}-private-subnet"
ip_cidr_range = "10.0.2.0/24"
region = var.region
network = google_compute_network.magento_vpc.id
private_ip_google_access = true
}
# Firewall Rule: Allow SSH from Bastion Host (or specific IP range)
resource "google_compute_firewall" "allow_ssh" {
name = "${var.project_name}-allow-ssh"
network = google_compute_network.magento_vpc.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["10.0.1.0/24"] # Restrict SSH to the public subnet
target_tags = ["magento-server"]
}
# Firewall Rule: Allow HTTP/HTTPS from Load Balancer
resource "google_compute_firewall" "allow_http_https" {
name = "${var.project_name}-allow-http-https"
network = google_compute_network.magento_vpc.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"] # Allow from anywhere, LB will handle further restrictions
target_tags = ["magento-web"]
}
# Firewall Rule: Allow internal communication between Magento components
resource "google_compute_firewall" "allow_internal" {
name = "${var.project_name}-allow-internal"
network = google_compute_network.magento_vpc.name
allow {
protocol = "tcp"
ports = ["0-65535"]
}
allow {
protocol = "udp"
ports = ["0-65535"]
}
allow {
protocol = "icmp"
}
source_ranges = ["10.0.1.0/24", "10.0.2.0/24"] # Allow communication within VPC subnets
target_tags = ["magento-server", "magento-web", "magento-db"]
}
# Firewall Rule: Deny all other ingress traffic by default
resource "google_compute_firewall" "deny_all_ingress" {
name = "${var.project_name}-deny-all-ingress"
network = google_compute_network.magento_vpc.name
deny {
protocol = "tcp"
ports = ["0-65535"]
}
deny {
protocol = "udp"
ports = ["0-65535"]
}
deny {
protocol = "icmp"
}
source_ranges = ["0.0.0.0/0"]
# No target_tags means it applies to all instances, effectively a default deny
}
# Output VPC and Subnet IDs for use in other modules
output "vpc_id" {
description = "The ID of the VPC network."
value = google_compute_network.magento_vpc.id
}
output "public_subnet_id" {
description = "The ID of the public subnet."
value = google_compute_subnetwork.public_subnet.id
}
output "private_subnet_id" {
description = "The ID of the private subnet."
value = google_compute_subnetwork.private_subnet.id
}
modules/gcp/networking/variables.tf
variable "project_name" {
description = "The name of the project, used for naming resources."
type = string
}
variable "region" {
description = "The GCP region for deploying resources."
type = string
}
Compute Module: Magento Application Servers (GCE Instances)
We’ll provision a managed instance group (MIG) for our Magento application servers. This provides scalability, high availability, and automated healing. Each instance will be configured with a hardened OS image, necessary software packages, and a startup script to join the cluster.
For security, instances will reside in the private subnet, accessible only via internal IP addresses. SSH access will be proxied through a bastion host or managed via Identity-Aware Proxy (IAP).
modules/gcp/compute/main.tf
# Managed Instance Group for Magento Application Servers
resource "google_compute_instance_group_manager" "magento_app_mig" {
name = "${var.project_name}-app-mig"
base_instance_name = "${var.project_name}-app"
zone = var.zone
target_size = var.instance_count
version {
instance_template = google_compute_instance_template.magento_app_template.id
}
auto_healing_policies {
health_check = google_compute_health_check.app_health_check.id
initial_delay_sec = 300 # Wait 5 minutes before health checking
}
update_policy {
type = "PROACTIVE"
minimal_action = "REPLACE"
max_unavailable = 1
max_surge = 1
}
}
# Instance Template for Magento Application Servers
resource "google_compute_instance_template" "magento_app_template" {
name_prefix = "${var.project_name}-app-template-"
machine_type = var.machine_type
tags = ["magento-server", "magento-app"]
disk {
source_image = "projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20230913" # Or a custom hardened image
auto_delete = true
boot = true
}
network_interface {
subnetwork = var.private_subnet_id
network_ip = "" # Assign internal IP automatically
access_config {
# Ephemeral public IP is not needed for instances in private subnet
}
}
metadata = {
# User data for cloud-init or startup scripts
startup-script = file("${path.module}/scripts/startup.sh")
}
service_account {
scopes = ["cloud-platform"] # Grant broad access for simplicity, refine in production
}
scheduling {
automatic_restart = true
on_host_maintenance = "MIGRATE"
}
lifecycle {
create_before_destroy = true
}
}
# Health Check for Application Servers
resource "google_compute_health_check" "app_health_check" {
name = "${var.project_name}-app-hc"
check_interval_sec = 30
timeout_sec = 5
healthy_threshold = 2
unhealthy_threshold = 3
http_health_check {
port = 80
request_path = "/healthz" # A simple health check endpoint on your app
}
}
# Output MIG name
output "app_mig_name" {
description = "The name of the Magento application MIG."
value = google_compute_instance_group_manager.magento_app_mig.name
}
modules/gcp/compute/variables.tf
variable "project_name" {
description = "The name of the project, used for naming resources."
type = string
}
variable "zone" {
description = "The GCP zone for deploying resources."
type = string
}
variable "instance_count" {
description = "The desired number of application instances."
type = number
default = 2
}
variable "machine_type" {
description = "The machine type for application instances."
type = string
default = "e2-medium"
}
variable "private_subnet_id" {
description = "The ID of the private subnet to deploy instances into."
type = string
}
modules/gcp/compute/scripts/startup.sh
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
# Update package lists and install necessary packages
apt-get update -y
apt-get install -y \
nginx \
php-fpm \
php-mysql \
php-gd \
php-xml \
php-mbstring \
php-curl \
php-zip \
php-intl \
redis-server \
unzip \
git
# Configure Nginx (basic example, will be managed by a dedicated webserver module later)
cat <<EOF > /etc/nginx/sites-available/magento
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html/public; # Magento's public directory
index index.php index.html index.htm;
server_name _; # Catch-all
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version as needed
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~* /(composer\.json|composer\.lock|\.env|\.htaccess|var/|\.git/|\.svn/|media/downloadable/|\bin/|\setup/|\update/|\magento) {
deny all;
}
location /healthz {
access_log off;
return 200 "OK";
}
}
EOF
ln -sf /etc/nginx/sites-available/magento /etc/nginx/sites-enabled/magento
rm /etc/nginx/sites-enabled/default # Remove default Nginx config
systemctl restart nginx
systemctl enable nginx
# Configure PHP-FPM (adjust settings for production)
sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/7.4/fpm/php.ini # Adjust PHP version
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/7.4/fpm/php.ini
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/7.4/fpm/php.ini
sed -i 's/max_execution_time = .*/max_execution_time = 300/' /etc/php/7.4/fpm/php.ini
sed -i 's/;daemonize = yes/daemonize = no/' /etc/php/7.4/fpm/php-fpm.conf # For systemd
systemctl restart php7.4-fpm # Adjust PHP version
systemctl enable php7.4-fpm
# Configure Redis (basic example)
systemctl start redis-server
systemctl enable redis-server
# Placeholder for Magento installation/configuration steps
# This would typically involve:
# 1. Downloading Magento
# 2. Running composer install
# 3. Setting up database connection
# 4. Running Magento setup commands (setup:install, setup:upgrade, cache:flush)
# 5. Deploying static content
# 6. Setting file permissions
echo "Magento application server setup complete."
Database Module: Cloud SQL for MySQL
For the Magento database, we’ll leverage Google Cloud SQL for MySQL. This managed service offers high availability, automated backups, and patching, significantly reducing operational overhead. We’ll configure it for optimal performance and security, placing it in the private subnet.
modules/gcp/database/main.tf
# Google Cloud SQL Instance for Magento Database
resource "google_sql_database_instance" "magento_db" {
name = "${var.project_name}-db"
region = var.region
database_version = "MYSQL_8_0" # Or your preferred MySQL version
settings {
tier = var.db_tier
ip_configuration {
ipv4_enabled = false # Private IP only
private_network = var.vpc_self_link
require_ssl = true
}
backup_configuration {
enabled = true
binary_log_enabled = true
point_in_time_recovery_enabled = true
}
location_preference {
zone = var.zone
}
# High Availability configuration
availability_type = "REGIONAL"
# Uncomment and configure for read replicas if needed
# replica_configuration {
# failover_target = true
# }
}
# Prevent accidental deletion
deletion_protection = true
}
# Magento Database
resource "google_sql_database" "magento_database" {
name = "magento_db"
instance = google_sql_database_instance.magento_db.name
charset = "utf8mb4"
collation = "utf8mb4_unicode_ci"
}
# Magento Database User
resource "google_sql_user" "magento_user" {
name = "magento_user"
instance = google_sql_database_instance.magento_db.name
host = "%" # Allow connections from any host within the VPC
password = random_password.db_password.result
}
# Generate a random password for the database user
resource "random_password" "db_password" {
length = 32
special = true
override_special = "_%@"
}
# Output database connection details
output "db_instance_name" {
description = "The name of the Cloud SQL instance."
value = google_sql_database_instance.magento_db.name
}
output "db_name" {
description = "The name of the Magento database."
value = google_sql_database.magento_database.name
}
output "db_user" {
description = "The username for the Magento database."
value = google_sql_user.magento_user.name
}
output "db_password" {
description = "The password for the Magento database user."
value = google_sql_user.magento_user.password
sensitive = true
}
output "db_private_ip_address" {
description = "The private IP address of the Cloud SQL instance."
value = google_sql_database_instance.magento_db.private_ip_address
}
modules/gcp/database/variables.tf
variable "project_name" {
description = "The name of the project, used for naming resources."
type = string
}
variable "region" {
description = "The GCP region for deploying resources."
type = string
}
variable "zone" {
description = "The GCP zone for deploying resources."
type = string
}
variable "vpc_self_link" {
description = "The self-link of the VPC network to connect the Cloud SQL instance to."
type = string
}
variable "db_tier" {
description = "The machine type for the Cloud SQL instance."
type = string
default = "db-custom-2-7680" # Example custom tier: 2 vCPU, 7.68 GB RAM
}
Load Balancing and Web Server Module
A robust load balancing strategy is crucial for distributing traffic and ensuring high availability. We’ll use a GCP Network Load Balancer (NLB) or HTTP(S) Load Balancer to distribute traffic to our Magento application servers. This module will also manage the Nginx configuration on the web-facing instances.
modules/gcp/webserver/main.tf
# Google Cloud HTTP(S) Load Balancer
resource "google_compute_backend_service" "magento_backend" {
name = "${var.project_name}-backend-service"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
enable_cdn = false
load_balancing_scheme = "EXTERNAL"
backend {
group = var.app_mig_self_link # Link to the MIG
balancing_mode = "UTILIZATION"
capacity_scaler = 1.0
}
health_checks = [google_compute_health_check.app_health_check.id]
# Enable session affinity if needed for specific Magento functionalities
# session_affinity = "CLIENT_IP"
}
# URL Map for the Load Balancer
resource "google_compute_url_map" "magento_url_map" {
name = "${var.project_name}-url-map"
default_service = google_compute_backend_service.magento_backend.id
}
# Target HTTP Proxy
resource "google_compute_target_http_proxy" "magento_http_proxy" {
name = "${var.project_name}-http-proxy"
url_map = google_compute_url_map.magento_url_map.id
}
# Global Forwarding Rule for HTTP
resource "google_compute_global_forwarding_rule" "magento_http_forwarding_rule" {
name = "${var.project_name}-http-forwarding-rule"
ip_protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
port_range = "80"
target = google_compute_target_http_proxy.magento_http_proxy.id
ip_address = google_compute_global_address.lb_ip.address
}
# Static IP Address for the Load Balancer
resource "google_compute_global_address" "lb_ip" {
name = "${var.project_name}-lb-ip"
}
# --- HTTPS Configuration (Optional but Recommended) ---
# You would typically manage SSL certificates using Google-managed certificates
# or by uploading your own. This example omits detailed cert management for brevity.
# resource "google_compute_target_https_proxy" "magento_https_proxy" {
# name = "${var.project_name}-https-proxy"
# url_map = google_compute_url_map.magento_url_map.id
# ssl_certificates = [google_compute_managed_ssl_certificate.magento_cert.id]
# }
# resource "google_compute_global_forwarding_rule" "magento_https_forwarding_rule" {
# name = "${var.project_name}-https-forwarding-rule"
# ip_protocol = "TCP"
# load_balancing_scheme = "EXTERNAL"
# port_range = "443"
# target = google_compute_target_https_proxy.magento_https_proxy.id
# ip_address = google_compute_global_address.lb_ip.address
# }
# resource "google_compute_managed_ssl_certificate" "magento_cert" {
# name = "${var.project_name}-magento-cert"
# managed {
# domains = ["your-magento-domain.com"] # Replace with your domain
# }
# }
# Output Load Balancer IP
output "load_balancer_ip" {
description = "The static IP address of the load balancer."
value = google_compute_global_address.lb_ip.address
}
modules/gcp/webserver/variables.tf
variable "project_name" {
description = "The name of the project, used for naming resources."
type = string
}
variable "app_mig_self_link" {
description = "The self-link of the application instance group manager."
type = string
}
variable "app_health_check_id" {
description = "The ID of the application health check."
type = string
}
Root Module: Orchestration
The root module ties everything together. It defines the GCP provider, declares variables, and calls the sub-modules to provision the complete Magento 2 cluster. This is where you’ll specify your project ID, region, zone, and other top-level configurations.
main.tf (Root Module)
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "google" {
project = var.gcp_project_id
region = var.region
}
# Networking Module
module "networking" {
source = "./modules/gcp/networking"
project_name = var.project_name
region = var.region
}
# Compute Module
module "compute" {
source = "./modules/gcp/compute"
project_name = var.project_name
zone = var.zone
instance_count = var.app_instance_count
machine_type = var.app_machine_type
private_subnet_id = module.networking.private_subnet_id
}
# Database Module
module "database" {
source = "./modules/gcp/database"
project_name = var.project_name
region = var.region
zone = var.zone
vpc_self_link = module.networking.vpc_id
db_tier = var.db_tier
}
# Web Server / Load Balancer Module
module "webserver" {
source = "./modules/gcp/webserver"
project_name = var.project_name
app_mig_self_link = google_compute_instance_group_manager.magento_app_mig.self_link # Reference from compute module
app_health_check_id = google_compute_health_check.app_health_check.id # Reference from compute module
}
# Outputs
output "load_balancer_ip" {
description = "The static IP address of the load balancer."
value = module.webserver.load_balancer_ip
}
output "db_instance_name" {
description = "The name of the Cloud SQL instance."
value = module.database.db_instance_name
}
output "db_name" {
description = "The name of the Magento database."
value = module.database.db_name
}
output "db_user" {
description = "The username for the Magento database."
value = module.database.db_user
}
output "db_password" {
description = "The password for the Magento database user."
value = module.database.db_password
sensitive = true
}
variables.tf (Root Module)
variable "gcp_project_id" {
description = "Your GCP project ID."
type = string
}
variable "project_name" {
description = "A short name for the project, used for resource naming."
type = string
default = "magento-prod"
}
variable "region" {
description = "The GCP region for deploying resources."
type = string
default = "us-central1"
}
variable "zone" {
description = "The GCP zone for deploying resources."
type = string
default = "us-central1-a"
}
variable "app_instance_count" {
description = "The desired number of application instances."
type = number
default = 3
}
variable "app_machine_type" {
description = "The machine type for application instances."
type = string
default = "e2-standard-2"
}
variable "db_tier" {
description = "The machine type for the Cloud SQL instance."
type = string
default = "db-custom-4-15360" # Example custom tier: 4 vCPU, 15.36 GB RAM
}
terraform.tfvars.example (Root Module)
gcp_project_id = "your-gcp-project-id" # project_name = "my-magento-cluster" # Optional: override default # region = "europe-west2" # Optional: override default # zone = "europe-west2-b" # Optional: override default # app_instance_count = 5 # Optional: override default # app_machine_type = "n2-standard-4" # Optional: override default # db_tier = "db-standard-1" # Optional: override default
Deployment and Management Workflow
With the Terraform code structured and defined, the deployment process is streamlined:
- Initialize Terraform: Navigate to your root Terraform directory and run
terraform init. This downloads the necessary provider plugins. - Review Plan: Execute
terraform plan -var-file="terraform.tfvars.example"to see the exact resources Terraform will create, modify, or destroy. Carefully review this output. - Apply Configuration: Run
terraform apply -var-file="terraform.tfvars.example"to provision the infrastructure. Confirm with ‘yes’ when prompted. - Magento Deployment: After infrastructure provisioning, you’ll need to deploy your Magento application code. This can be automated using CI/CD pipelines (e.g., Cloud Build, Jenkins) that trigger after Terraform apply, or by manually SSHing into the bastion host (if configured) and executing deployment scripts. The startup scripts in the compute module provide a basic setup, but a full Magento deployment is a separate, complex process.
- Access Magento: Once deployed, access your Magento store via the load balancer’s IP address outputted by Terraform.
- Destroy Infrastructure: When no longer needed, run
terraform destroy -var-file="terraform.tfvars.example"to tear down all provisioned resources and avoid ongoing costs.
Security Considerations and Enhancements
The provided configuration offers a foundational secure setup. For production environments, consider these enhancements:
- Hardened OS Images: Use custom-built, hardened OS images for your instances, minimizing attack surfaces.
- Secrets Management: Integrate with GCP Secret Manager for database credentials, API keys, and other sensitive information instead of hardcoding or relying solely on Terraform outputs.
- IAM Roles: Refine service account scopes to grant only the minimum necessary permissions.
- Network Security: Implement more granular firewall rules. Consider using Private Google Access for all outbound traffic to Google APIs.
- Web Application Firewall (WAF): Deploy Cloud Armor or a third-party WAF in front of the load balancer for protection against common web exploits.
- Bastion Host: For enhanced security, provision a dedicated bastion host in the public subnet, restricting SSH access to it, and then allowing SSH from the bastion to the private instances.
- Logging and Monitoring: Configure comprehensive logging (Cloud Logging) and monitoring (Cloud Monitoring) for all resources.
- VPC Service Controls: Implement VPC Service Controls to create security perimeters around your GCP resources.
- Database Security: Regularly rotate database passwords and enforce SSL/TLS connections.
By leveraging Terraform and GCP’s robust infrastructure services, you can provision a secure, scalable, and highly available Magento 2 cluster with a well-defined and repeatable process.