Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions terraform/azure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
Azure VM + Nginx Terraform Scaffold
===================================

This Terraform configuration deploys a production‑grade baseline on Azure:
- Resource Group
- Virtual Network + subnets (web, optional Bastion)
- Network Security Group with least‑privilege inbound rules
- Public Standard Load Balancer (HTTP, optional HTTPS passthrough)
- Linux VM Scale Set (Ubuntu 22.04) behind the LB
- Cloud‑init to install and configure Nginx + PHP‑FPM
- Optional Azure Bastion for secure SSH access (recommended)

Notes
-----
- This is a scaffold. It provisions compute and networking to run a PHP/Nginx app. It does not include database/storage/caching. Integrate Azure Database for MySQL or Postgres, Azure Files/Disks, Redis, etc., as needed.
- HTTPS is exposed at the LB and Nginx layers (port 443) if enabled, but certificate provisioning/renewal is not included. Consider using Azure Application Gateway + Key Vault or integrate certbot/Key Vault in cloud‑init/CM tooling.
- Remote Terraform state is recommended. Configure the azurerm backend via backend config before `terraform init`.

Prerequisites
-------------
- Terraform >= 1.5
- Azure CLI (`az`) authenticated to your subscription
- An SSH public key

Quick Start
-----------
1) Configure backend (recommended)

Create or reuse a Storage Account and container for state (one‑time):
- Resource Group: rg-tfstate
- Storage Account: sttfstate<unique>
- Container: tfstate

Then either:
- Provide `-backend-config` flags on `terraform init`, or
- Create a `backend.hcl` file:

storage_account_name = "sttfstate<unique>"
container_name = "tfstate"
key = "myapp-dev.tfstate"
resource_group_name = "rg-tfstate"

2) Initialize, plan, and apply

cd terraform/azure
cp terraform.tfvars.example terraform.tfvars
# edit terraform.tfvars to suit your environment

# init (with backend)
terraform init -backend-config=../backend.hcl

# or init without a backend (local state)
# terraform init

terraform plan
terraform apply

3) Access
- Public endpoint:
- IP: output `lb_public_ip`
- FQDN (if `dns_label_prefix` provided): output `lb_fqdn`
- SSH:
- Recommended: use Azure Bastion (set `create_bastion = true`). Open the VMSS instance via the Azure Portal -> Bastion.
- Direct SSH: not recommended; if you must, set `allowed_ssh_cidrs` and access via per‑instance private IP through a jump host or add an inbound NAT pool to the LB.

Configuration
-------------
Edit `terraform.tfvars`:
- name_prefix: short project prefix (e.g. "myapp")
- environment: dev|staging|prod
- location: Azure region (e.g. eastus)
- address_space, web_subnet_cidr: VNet/Subnet CIDRs
- create_bastion: true|false
- ssh_public_key: your SSH public key
- vm_size, instance_count, zones: sizing and HA
- dns_label_prefix: optional public DNS label (produces <label>-<rand>.<region>.cloudapp.azure.com)
- enable_https: expose 443 (certificate management not included)
- tags: map of tags

What gets deployed
------------------
- Standard Public IP with optional DNS label and Standard Load Balancer
- Backend pool + health probes (HTTP 80) and LB rules (80; optional 443 passthrough)
- Ubuntu 22.04 VMSS joined to the backend pool
- Cloud‑init that:
- Installs Nginx + PHP‑FPM and common PHP extensions
- Creates a basic Nginx vhost pointing to /var/www/app/public
- Places a simple index.php health page
- Enables and restarts services

Next steps (app deployment)
---------------------------
- Replace cloud‑init with a stronger provisioning approach:
- Build a custom image (Packer) with Nginx/PHP preinstalled
- Use CM tooling (Ansible/Chef/Puppet) or an Azure VM Extension to deploy your app
- Mount Azure Files or attach managed disks for persistent storage
- Add a managed database (Azure Database for MySQL/Postgres) and secure connectivity
- Add Key Vault for secrets/certificates
- Consider Application Gateway (WAF) for TLS termination and L7 routing

Destroy
-------
terraform destroy
5 changes: 5 additions & 0 deletions terraform/azure/backend.hcl.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Example backend configuration for remote Terraform state in Azure Storage
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstateyourunique"
container_name = "tfstate"
key = "myapp-dev.tfstate"
24 changes: 24 additions & 0 deletions terraform/azure/bastion.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
resource "azurerm_public_ip" "bastion" {
count = var.create_bastion ? 1 : 0
name = "${local.base_name}-bastion-pip"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
tags = local.common_tags
}

resource "azurerm_bastion_host" "bastion" {
count = var.create_bastion ? 1 : 0
name = "${local.base_name}-bastion"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

ip_configuration {
name = "configuration"
subnet_id = azurerm_subnet.bastion[0].id
public_ip_address_id = azurerm_public_ip.bastion[0].id
}

tags = local.common_tags
}
68 changes: 68 additions & 0 deletions terraform/azure/cloud-init.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#cloud-config
package_update: true
package_upgrade: true

packages:
- nginx
- php-fpm
- php-cli
- php-common
- php-curl
- php-gd
- php-mbstring
- php-xml
- php-zip
- php-intl
- php-mysql
- unzip
- git

write_files:
- path: /etc/nginx/sites-available/app.conf
content: |
server {
listen 80;
listen [::]:80;

server_name ${server_name};

root /var/www/app/public;
index index.php index.html index.htm;

access_log /var/log/nginx/app_access.log;
error_log /var/log/nginx/app_error.log;

location / {
try_files $uri /index.php?$query_string;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Ubuntu 22.04 default PHP-FPM socket
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}

location ~* \.(jpg|jpeg|gif|png|css|js|ico|webp|tiff)$ {
expires 30d;
access_log off;
}

client_max_body_size 64M;
}
permissions: "0644"

- path: /var/www/app/public/index.php
content: |
<?php
header('Content-Type: text/plain');
echo "Nginx + PHP-FPM is up.\n";
echo "Server: ${server_name}\n";
permissions: "0644"

runcmd:
- systemctl enable nginx
- systemctl enable php*-fpm
- rm -f /etc/nginx/sites-enabled/default
- ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf
- systemctl restart php*-fpm
- systemctl restart nginx
65 changes: 65 additions & 0 deletions terraform/azure/compute.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
locals {
vmss_name = "${local.base_name}-web-vmss"
}

resource "azurerm_linux_virtual_machine_scale_set" "web" {
name = local.vmss_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku = var.vm_size
instances = var.instance_count
admin_username = var.admin_username
computer_name_prefix = "webvm"

# Use zones if provided (some regions may not support all zones)
zones = length(var.zones) > 0 ? var.zones : null

admin_ssh_key {
username = var.admin_username
public_key = var.ssh_public_key
}

identity {
type = "SystemAssigned"
}

source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}

os_disk {
storage_account_type = "Standard_LRS"
caching = "ReadWrite"
}

upgrade_mode = "Rolling"

# Cloud-init to install and configure Nginx + PHP-FPM
custom_data = base64encode(
templatefile("${path.module}/cloud-init.tpl", {
server_name = trim(var.dns_label_prefix) != "" ? azurerm_public_ip.lb.fqdn : "_"
enable_https = var.enable_https
})
)

network_interface {
name = "${local.vmss_name}-nic"
primary = true

ip_configuration {
name = "internal"
primary = true
subnet_id = azurerm_subnet.web.id
load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.web.id]
}
}

boot_diagnostics {
storage_account_uri = null
}

tags = local.common_tags
}
78 changes: 78 additions & 0 deletions terraform/azure/loadbalancer.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
resource "random_string" "suffix" {
length = 5
upper = false
special = false
}

resource "azurerm_public_ip" "lb" {
name = "${local.base_name}-lb-pip"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"

domain_name_label = trim(var.dns_label_prefix) != "" ? "${var.dns_label_prefix}-${random_string.suffix.result}" : null

tags = local.common_tags
}

resource "azurerm_lb" "web" {
name = "${local.base_name}-lb"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku = "Standard"

frontend_ip_configuration {
name = "PublicFrontend"
public_ip_address_id = azurerm_public_ip.lb.id
}

tags = local.common_tags
}

resource "azurerm_lb_backend_address_pool" "web" {
name = "${local.base_name}-bepool"
loadbalancer_id = azurerm_lb.web.id
}

resource "azurerm_lb_probe" "http" {
name = "http"
resource_group_name = azurerm_resource_group.rg.name
loadbalancer_id = azurerm_lb.web.id
protocol = "Tcp"
port = 80
}

resource "azurerm_lb_rule" "http" {
name = "http"
resource_group_name = azurerm_resource_group.rg.name
loadbalancer_id = azurerm_lb.web.id
protocol = "Tcp"
frontend_port = 80
backend_port = 80
frontend_ip_configuration_name = "PublicFrontend"
backend_address_pool_ids = [azurerm_lb_backend_address_pool.web.id]
probe_id = azurerm_lb_probe.http.id
}

resource "azurerm_lb_probe" "https" {
count = var.enable_https ? 1 : 0
name = "https"
resource_group_name = azurerm_resource_group.rg.name
loadbalancer_id = azurerm_lb.web.id
protocol = "Tcp"
port = 443
}

resource "azurerm_lb_rule" "https" {
count = var.enable_https ? 1 : 0
name = "https"
resource_group_name = azurerm_resource_group.rg.name
loadbalancer_id = azurerm_lb.web.id
protocol = "Tcp"
frontend_port = 443
backend_port = 443
frontend_ip_configuration_name = "PublicFrontend"
backend_address_pool_ids = [azurerm_lb_backend_address_pool.web.id]
probe_id = azurerm_lb_probe.https[0].id
}
23 changes: 23 additions & 0 deletions terraform/azure/network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
resource "azurerm_virtual_network" "vnet" {
name = "${local.base_name}-vnet"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = var.address_space
tags = local.common_tags
}

resource "azurerm_subnet" "web" {
name = "${local.base_name}-web-sn"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.web_subnet_cidr]
}

# Optional subnet for Azure Bastion (required if create_bastion = true)
resource "azurerm_subnet" "bastion" {
count = var.create_bastion ? 1 : 0
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.bastion_subnet_cidr]
}
Loading