Infrastructure as Code: Provisioning Secure Laravel Clusters on Google Cloud Using Terraform
Terraform Provider Configuration for Google Cloud
To begin provisioning resources on Google Cloud Platform (GCP) with Terraform, we need to configure the Google Cloud provider. This involves specifying your GCP project ID, region, and potentially authentication credentials. For production environments, it’s highly recommended to use a service account with appropriate IAM roles rather than user credentials.
Create a file named main.tf in your Terraform project directory. This file will house our primary infrastructure definitions.
Here’s a basic provider configuration:
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
variable "gcp_project_id" {
description = "The GCP project ID to deploy resources into."
type = string
}
variable "gcp_region" {
description = "The GCP region to deploy resources into."
type = string
default = "us-central1"
}
# For service account authentication, uncomment and configure:
# variable "gcp_service_account_key_file" {
# description = "Path to the GCP service account key file."
# type = string
# sensitive = true
# }
#
# provider "google" {
# project = var.gcp_project_id
# region = var.gcp_region
# credentials = file(var.gcp_service_account_key_file)
# }
Securing the Laravel Application: VPC and Firewall Rules
A robust network infrastructure is paramount for application security. We’ll define a Virtual Private Cloud (VPC) network and specific firewall rules to control ingress and egress traffic. This ensures only necessary ports are open to the internet, and internal communication is restricted.
We’ll create a custom VPC network to isolate our resources. This is generally preferred over using the default VPC for better control.
resource "google_compute_network" "laravel_vpc" {
name = "laravel-vpc"
auto_create_subnetworks = false # We will define our own subnets
routing_mode = "REGIONAL"
}
resource "google_compute_subnetwork" "laravel_subnet" {
name = "laravel-subnet"
ip_cidr_range = "10.0.1.0/24"
region = var.gcp_region
network = google_compute_network.laravel_vpc.id
}
Next, we’ll implement firewall rules. We’ll allow SSH access for management, HTTP/HTTPS for web traffic, and potentially other ports required by your Laravel application (e.g., database connections if not using Cloud SQL private IP).
# Allow SSH access from anywhere (consider restricting this to specific IPs in production)
resource "google_compute_firewall" "allow_ssh" {
name = "allow-ssh"
network = google_compute_network.laravel_vpc.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["laravel-server"] # Apply this tag to our instances
}
# Allow HTTP and HTTPS traffic from anywhere
resource "google_compute_firewall" "allow_http_https" {
name = "allow-http-https"
network = google_compute_network.laravel_vpc.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["laravel-server"]
}
# Example: Allow internal communication between Laravel servers on a specific port
# resource "google_compute_firewall" "allow_internal_app_port" {
# name = "allow-internal-app-port"
# network = google_compute_network.laravel_vpc.name
# allow {
# protocol = "tcp"
# ports = ["8000"] # Example application port
# }
# source_ranges = ["10.0.1.0/24"] # Our subnet CIDR
# target_tags = ["laravel-server"]
# }
# Deny all other ingress traffic by default (implicit in GCP, but good to be explicit conceptually)
# No explicit resource needed here as GCP denies by default.
Provisioning Compute Instances for Laravel Application Servers
We’ll use Google Compute Engine (GCE) instances to host our Laravel application. For scalability and resilience, we’ll configure a managed instance group (MIG) with an auto-scaling policy and a load balancer.
First, let’s define a startup script that will install necessary software (PHP, web server, Composer, etc.) and deploy our Laravel application. This script will be executed when each instance in the MIG starts.
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
# Update package lists and install essential packages
apt-get update -y
apt-get install -y \
apache2 \
php \
libapache2-mod-php \
php-mysql \
php-mbstring \
php-xml \
php-zip \
php-curl \
php-gd \
php-intl \
composer \
git \
unzip
# Configure Apache to serve from a specific directory (e.g., /var/www/html/laravel_app)
# This assumes your Laravel app will be deployed to this path.
# You might need to adjust Apache's virtual host configuration for production.
a2enmod rewrite
a2dissite 000-default.conf
# Create a new virtual host for Laravel
cat <<EOF > /etc/apache2/sites-available/laravel.conf
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/laravel_app/public
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<Directory /var/www/html/laravel_app/public>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
EOF
a2ensite laravel.conf
systemctl reload apache2
# Deploy Laravel Application (Replace with your actual deployment logic)
# This is a simplified example. In production, you'd likely use Git, CI/CD, or a deployment tool.
# Ensure your application is configured to pull from a Git repository or artifact.
APP_DIR="/var/www/html/laravel_app"
mkdir -p $APP_DIR
cd $APP_DIR
# Example: Clone from a private Git repository (ensure SSH keys are managed securely)
# git clone [email protected]:your-org/your-laravel-repo.git .
# Or pull from a public repo for testing:
git clone https://github.com/laravel/laravel.git .
rm -rf .git # Remove git history if not needed for the deployed app
# Install Composer dependencies
composer install --no-dev --optimize-autoloader
# Set up environment variables (ensure .env file is managed securely)
# For production, use GCP Secret Manager or similar.
# cp .env.example .env
# echo "APP_KEY=$(php artisan key:generate --show)" >> .env
# echo "DB_HOST=your_db_host" >> .env
# echo "DB_DATABASE=your_db_name" >> .env
# echo "DB_USERNAME=your_db_user" >> .env
# echo "DB_PASSWORD=your_db_password" >> .env
# Run Laravel migrations (if applicable and database is accessible)
# php artisan migrate --force
# Set correct file permissions
chown -R www-data:www-data $APP_DIR
chmod -R 755 $APP_DIR/storage
chmod -R 755 $APP_DIR/bootstrap/cache
# Restart Apache
systemctl restart apache2
echo "Laravel application deployed and configured."
Now, let’s define the instance template and the managed instance group.
# Instance Template for Laravel application servers
resource "google_compute_instance_template" "laravel_app_template" {
name_prefix = "laravel-app-template-"
machine_type = "e2-medium" # Choose an appropriate machine type
tags = ["laravel-server", "http-server", "https-server"]
disk {
source_image = "ubuntu-os-cloud/ubuntu-2004-lts" # Or your preferred OS image
auto_delete = true
boot = true
}
network_interface {
subnetwork = google_compute_subnetwork.laravel_subnet.id
# Access config for public IP (for initial setup/testing, consider private IP + NAT for production)
access_config {
// Ephemeral IP
}
}
metadata_startup_script = file("startup-script.sh") # Path to your startup script
service_account {
scopes = ["cloud-platform"] # Grant broad access for simplicity; refine for production
}
lifecycle {
create_before_destroy = true
}
}
# Managed Instance Group (MIG) for auto-scaling
resource "google_compute_instance_group_manager" "laravel_mig" {
name = "laravel-app-mig"
base_instance_name = "laravel-app"
zone = "${var.gcp_region}-a" # Specify a zone within your region
target_size = 2 # Initial number of instances
version {
instance_template = google_compute_instance_template.laravel_app_template.id
name = "v1"
}
# Auto-scaling configuration
auto_healing_policies {
health_check = google_compute_health_check.laravel_app_health_check.id
initial_delay_sec = 300 # Wait 5 minutes before starting health checks
}
update_policy {
type = "PROACTIVE"
minimal_action = "REPLACE"
}
# Optional: Rolling updates configuration
# rolling_update_policy {
# max_unavailable_fixed = 1
# max_surge_fixed = 1
# }
}
# Health Check for Auto-healing
resource "google_compute_health_check" "laravel_app_health_check" {
name = "laravel-app-health-check"
check_interval_sec = 5
timeout_sec = 5
healthy_threshold = 2
unhealthy_threshold = 2
http_health_check {
port = 80
request_path = "/" # Or a specific health check endpoint in your Laravel app
}
}
# Auto-scaler configuration
resource "google_compute_autoscaler" "laravel_autoscaler" {
name = "laravel-app-autoscaler"
zone = google_compute_instance_group_manager.laravel_mig.zone
target = google_compute_instance_group_manager.laravel_mig.id
autoscaling_policy {
max_replicas = 10
min_replicas = 2
cooldown_period = 60
cpu_utilization {
target = 0.6 # Scale up when CPU utilization reaches 60%
}
# You can add other scaling metrics like load balancing serving capacity
# load_balancing_utilization {
# target = 0.8
# }
}
}
Load Balancing for High Availability and SSL Termination
A Global External HTTP(S) Load Balancer is crucial for distributing traffic across your MIG instances, providing high availability and enabling SSL termination. This offloads SSL processing from your application servers.
# Backend Service for the Load Balancer
resource "google_compute_backend_service" "laravel_backend" {
name = "laravel-backend-service"
protocol = "HTTP"
port_name = "http"
timeout_sec = 10
enable_cdn = false
load_balancing_scheme = "EXTERNAL_MANAGED"
backend {
group = google_compute_instance_group_manager.laravel_mig.instance_group
}
health_checks = [google_compute_health_check.laravel_app_health_check.id]
}
# URL Map to route requests to the backend service
resource "google_compute_url_map" "laravel_url_map" {
name = "laravel-url-map"
default_service = google_compute_backend_service.laravel_backend.id
}
# SSL Certificate (Replace with your actual certificate details or use Google-managed certificates)
# For Google-managed certificates, you'll need to create a google_compute_managed_ssl_certificate resource
# and associate it with the target_https_proxy.
# Example for a self-managed certificate:
# resource "google_compute_ssl_certificate" "laravel_ssl_cert" {
# name = "laravel-ssl-cert"
# private_key = file("path/to/your/private.key")
# certificate = file("path/to/your/certificate.crt")
# }
# Target HTTPS Proxy
resource "google_compute_target_https_proxy" "laravel_https_proxy" {
name = "laravel-https-proxy"
url_map = google_compute_url_map.laravel_url_map.id
# ssl_certificates = [google_compute_ssl_certificate.laravel_ssl_cert.id] # Uncomment for self-managed
# For Google-managed certificates:
# ssl_certificates = [google_compute_managed_ssl_certificate.laravel_managed_cert.id]
}
# Global Forwarding Rule for HTTPS traffic
resource "google_compute_global_forwarding_rule" "laravel_https_forwarding_rule" {
name = "laravel-https-forwarding-rule"
ip_protocol = "TCP"
load_balancing_scheme = "EXTERNAL_MANAGED"
port_range = "443"
target = google_compute_target_https_proxy.laravel_https_proxy.id
ip_address = google_compute_global_address.laravel_static_ip.name # Use a static IP
}
# Target HTTP Proxy (for redirecting HTTP to HTTPS)
resource "google_compute_target_http_proxy" "laravel_http_proxy" {
name = "laravel-http-proxy"
url_map = google_compute_url_map.laravel_url_map.id
}
# Global Forwarding Rule for HTTP traffic (redirect to HTTPS)
resource "google_compute_global_forwarding_rule" "laravel_http_forwarding_rule" {
name = "laravel-http-forwarding-rule"
ip_protocol = "TCP"
load_balancing_scheme = "EXTERNAL_MANAGED"
port_range = "80"
target = google_compute_target_http_proxy.laravel_http_proxy.id
ip_address = google_compute_global_address.laravel_static_ip.name # Use the same static IP
}
# Reserve a static global IP address
resource "google_compute_global_address" "laravel_static_ip" {
name = "laravel-static-ip"
}
# If using Google-managed SSL certificates:
# resource "google_compute_managed_ssl_certificate" "laravel_managed_cert" {
# name = "laravel-managed-ssl-cert"
# managed {
# domains = ["your-domain.com"] # Replace with your domain
# }
# }
Database Provisioning with Cloud SQL
For a production Laravel application, a managed database service like Cloud SQL is highly recommended. We’ll provision a PostgreSQL instance and configure it for secure access.
resource "google_sql_database_instance" "laravel_db" {
name = "laravel-db-instance"
region = var.gcp_region
database_version = "POSTGRES_14" # Or your preferred PostgreSQL version
project = var.gcp_project_id
settings {
tier = "db-f1-micro" # Choose an appropriate tier for your needs
ip_configuration {
ipv4_enabled = true # Enable public IP for initial setup/testing
private_network = google_compute_network.laravel_vpc.id # Connect to your VPC
# For enhanced security, disable public IP and use private IP only.
# You would then need to configure VPC peering or Cloud SQL Auth Proxy for access.
}
backup_configuration {
enabled = true
binary_log_enabled = false # Not applicable for PostgreSQL
}
# Uncomment and configure for High Availability
# availability_type = "REGIONAL"
}
# Prevent accidental deletion of the database instance
lifecycle {
prevent_destroy = true
}
}
resource "google_sql_database" "laravel_app_db" {
name = "laravel_app_db"
instance = google_sql_database_instance.laravel_db.name
project = var.gcp_project_id
}
resource "google_sql_user" "laravel_db_user" {
name = "laravel_user"
instance = google_sql_database_instance.laravel_db.name
host = "%" # Allow connection from any host (restrict in production)
password = random_password.db_password.result
project = var.gcp_project_id
}
resource "random_password" "db_password" {
length = 16
special = true
override_special = "_%@"
}
# Output the database password securely
output "db_password" {
description = "The password for the Laravel database user."
value = random_password.db_password.result
sensitive = true
}
Securing Database Access
Directly exposing the database to the internet is a significant security risk. For production, it’s best practice to disable public IP access on the Cloud SQL instance and use the Cloud SQL Auth Proxy or VPC peering for secure connections from your application instances.
If you disable ipv4_enabled on the Cloud SQL instance, you’ll need to ensure your application instances can reach the database via its private IP. This typically involves running the Cloud SQL Auth Proxy as a sidecar container or directly configuring your application to use the private IP and ensuring network connectivity.
# Example of configuring Cloud SQL Auth Proxy in your application's startup script or Dockerfile:
#
# 1. Download the Cloud SQL Auth Proxy binary.
# 2. Run the proxy as a background service, connecting to your database instance.
# ./cloud_sql_proxy -instances=your-gcp-project:your-region:laravel-db-instance=tcp:5432
# 3. Configure your Laravel application's database connection to use '127.0.0.1' and port '5432' (or the port the proxy is listening on).
#
# Alternatively, if your application instances are within the same VPC as the Cloud SQL private IP,
# you might be able to connect directly using the private IP address.
#
# To restrict database user access:
# resource "google_sql_user" "laravel_db_user" {
# name = "laravel_user"
# instance = google_sql_database_instance.laravel_db.name
# host = "your-app-instance-private-ip" # Or a subnet range
# password = random_password.db_password.result
# project = var.gcp_project_id
# }
Deployment Workflow and Considerations
With the Terraform code defined, the deployment workflow is as follows:
- Initialize Terraform:
terraform init - Review the execution plan:
terraform plan -var="gcp_project_id=your-gcp-project-id" - Apply the infrastructure:
terraform apply -var="gcp_project_id=your-gcp-project-id"
Key Considerations for Production:
- Secrets Management: Use GCP Secret Manager for database credentials, API keys, and other sensitive information. Avoid hardcoding secrets in Terraform or startup scripts.
- CI/CD Integration: Integrate Terraform into your CI/CD pipeline (e.g., GitLab CI, GitHub Actions, Cloud Build) for automated, repeatable deployments.
- Monitoring and Logging: Implement robust monitoring (Cloud Monitoring) and logging (Cloud Logging) for your application and infrastructure.
- IAM Roles: Grant the least privilege necessary to service accounts used by Terraform and your application instances.
- Database Backups and Recovery: Ensure your Cloud SQL backup configuration is robust and test your recovery procedures.
- SSL Certificates: For production, use Google-managed SSL certificates for automatic renewal and simplified management.
- Network Security: Further restrict firewall rules to only allow necessary traffic. Consider using Private Google Access for services that don’t require public IPs.
- Application Deployment Strategy: The startup script is a basic example. For zero-downtime deployments, consider using tools like Ansible, Docker, or Kubernetes, and implement blue/green or canary deployment strategies.