部署
所有 Terraform 文件均位于 terraform/ 目录中。克隆仓库并直接部署:
git clone https://github.com/f5-sales-demo/cdn-simulator.gitcd cdn-simulator/terraformcp terraform.tfvars.example terraform.tfvars# 编辑 terraform.tfvars,填入您的 Azure 订阅 ID 和源服务器地址Terraform 配置
Section titled “Terraform 配置”Terraform 文件结构
Section titled “Terraform 文件结构”terraform 目录包含 9 个文件,遵循演示资源标准:
versions.tf— Terraform 及提供程序版本约束(azurerm ~> 4.0,azuread ~> 3.0)providers.tf— Azure RM 与 Azure AD 提供程序配置data.tf— 用于部署者自动解析的 Azure AD 数据源locals.tf— 部署者解析、Azure 云采用框架资源命名、标准标签main.tf— 资源组(命名为rg-cdn-simulator-{environment}-{deployer})variables.tf— 所有输入变量(3 个必填,8 个可选)network.tf— VNet(10.100.0.0/16)、子网、公共 IP、NSG(端口 22/80/443)、NICvm.tf— 通过 templatefile() 使用 cloud-init 的 Ubuntu 24.04 虚拟机outputs.tf— 17 个输出项(15 个标准输出 + 2 个组件专用输出)
variables.tf 定义了 11 个输入变量,分为常规、计算和组件专用三个部分。deployer 标识符从您的 Azure AD 账户自动解析——您只需设置 subscription_id、origin_server 和 origin_host:
# ---------------------------------------------------------# General# ---------------------------------------------------------
variable "subscription_id" { description = "Azure subscription ID" type = string}
variable "deployer" { description = "Override for deployer identifier (auto-resolved from Azure AD if empty). Required for service principal or managed identity authentication." type = string default = ""}
variable "location" { description = "Azure region for all resources" type = string default = "eastus2"}
variable "environment" { description = "Environment label used in resource group naming and tags" type = string default = "lab"}
variable "tags" { description = "Additional tags merged with standard tags (component, environment, deployer, managed_by)" type = map(string) default = {}}
# ---------------------------------------------------------# Compute# ---------------------------------------------------------
variable "vm_size" { description = "Azure VM size — F-series compute-optimized recommended (F4s_v2 for lab, F16s_v2 for load testing, F32s_v2 for production)" type = string default = "Standard_F4s_v2"}
variable "admin_username" { description = "SSH admin username for the VM" type = string default = "azureuser"}
variable "ssh_public_key_path" { description = "Path to the SSH public key file" type = string default = "~/.ssh/id_ed25519.pub"}
variable "disk_size_gb" { description = "OS disk size in GB" type = number default = 30}
# ---------------------------------------------------------# Component-Specific# ---------------------------------------------------------
variable "origin_server" { description = "Origin server URL for cache miss forwarding (e.g., an HTTPS VIP or a direct HTTP origin IP)" type = string}
variable "origin_host" { description = "Origin server host:port for NGINX upstream (no scheme). Use IP:443 for HTTPS or IP:80 for HTTP." type = string}网络基础设施
Section titled “网络基础设施”network.tf 创建 VNet、子网、公共 IP、NSG(端口 22/80/443)以及 NIC:
resource "azurerm_virtual_network" "main" { name = local.name.virtual_network address_space = ["10.100.0.0/16"] location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
tags = azurerm_resource_group.main.tags}
resource "azurerm_subnet" "main" { name = local.name.subnet resource_group_name = azurerm_resource_group.main.name virtual_network_name = azurerm_virtual_network.main.name address_prefixes = ["10.100.1.0/24"]}
resource "azurerm_public_ip" "main" { name = local.name.public_ip location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name allocation_method = "Static" sku = "Standard"
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_security_group" "main" { name = local.name.nsg location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
security_rule { name = "AllowHTTP" priority = 100 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "80" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "AllowHTTPS" priority = 110 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "443" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "AllowSSH" priority = 120 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" source_address_prefix = "*" destination_address_prefix = "*" }
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_interface" "main" { name = local.name.network_interface location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
ip_configuration { name = "internal" subnet_id = azurerm_subnet.main.id private_ip_address_allocation = "Dynamic" public_ip_address_id = azurerm_public_ip.main.id }
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_interface_security_group_association" "main" { network_interface_id = azurerm_network_interface.main.id network_security_group_id = azurerm_network_security_group.main.id}使用 Cloud-Init 的虚拟机
Section titled “使用 Cloud-Init 的虚拟机”vm.tf 创建 Ubuntu 24.04 虚拟机。SSH 公钥路径通过 pathexpand() 展开以处理 ~。Cloud-init 模板接收 origin_server 和 origin_host 变量:
resource "azurerm_linux_virtual_machine" "main" { name = local.name.virtual_machine resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location size = var.vm_size
admin_username = var.admin_username disable_password_authentication = true
admin_ssh_key { username = var.admin_username public_key = file(pathexpand(var.ssh_public_key_path)) }
network_interface_ids = [azurerm_network_interface.main.id]
os_disk { caching = "ReadWrite" storage_account_type = "Premium_LRS" disk_size_gb = var.disk_size_gb }
source_image_reference { publisher = "Canonical" offer = "ubuntu-24_04-lts" sku = "server" version = "latest" }
custom_data = base64encode(templatefile("${path.module}/cloud-init.yaml", { origin_server = var.origin_server origin_host = var.origin_host }))
boot_diagnostics {}
tags = azurerm_resource_group.main.tags}Cloud-Init 配置
Section titled “Cloud-Init 配置”cloud-init.yaml 对虚拟机进行配置,包括内核调优、systemd 限制、NGINX 性能优化配置、128 MB 缓存键区域、上游 keepalive 连接池、gzip 压缩,以及 67+ 个 CDN 厂商请求头。共享辅助库(/usr/local/lib/cloud-init-helpers.sh)提供重试逻辑,并将进度日志记录至 /var/log/cloud-init-progress.log。
Cloud-init 使用 Terraform 模板变量:${origin_server} 和 ${origin_host} 用于上游配置。NGINX 变量(如 ${request_id})在 Terraform templatefile 中需转义为 $${request_id}。
#cloud-configpackage_update: truepackage_upgrade: true
bootcmd: - mkdir -p /var/cache/nginx/cdn - chown www-data:www-data /var/cache/nginx/cdn 2>/dev/null || true
packages: - nginx - irqbalance
write_files: # ── Kernel tuning ────────────────────────────────────────────── - path: /etc/sysctl.d/99-cdn-tuning.conf content: | net.core.somaxconn = 262144 net.core.netdev_max_backlog = 262144 net.ipv4.tcp_max_syn_backlog = 262144 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 1024 65535 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_keepalive_time = 300 net.ipv4.tcp_keepalive_intvl = 15 net.ipv4.tcp_keepalive_probes = 5 net.ipv4.tcp_slow_start_after_idle = 0 net.ipv4.tcp_max_tw_buckets = 8000000 fs.file-max = 8388608 vm.swappiness = 10
# ── Systemd override for NGINX file descriptor limits ────────── - path: /etc/systemd/system/nginx.service.d/override.conf content: | [Service] LimitNOFILE=262144 LimitNPROC=262144
# ── OS-level limits for www-data (NGINX worker user) ─────────── - path: /etc/security/limits.d/99-nginx.conf content: | www-data soft nofile 262144 www-data hard nofile 262144
# ── NGINX main config ────────────────────────────────────────── - path: /etc/nginx/nginx.conf content: | user www-data; worker_processes auto; worker_rlimit_nofile 262144; pid /run/nginx.pid; error_log /var/log/nginx/error.log; include /etc/nginx/modules-enabled/*.conf;
events { use epoll; worker_connections 32768; multi_accept on; accept_mutex off; }
http { sendfile on; tcp_nopush on; tcp_nodelay on; types_hash_max_size 2048; server_tokens off; client_max_body_size 50m;
include /etc/nginx/mime.types; default_type application/octet-stream;
log_format cdn '$remote_addr [$time_local] "$request" $status $body_bytes_sent $upstream_cache_status $request_time'; access_log /var/log/nginx/access.log cdn;
keepalive_timeout 65; keepalive_requests 100000;
proxy_buffering on; proxy_buffer_size 16k; proxy_buffers 128 16k; proxy_busy_buffers_size 256k;
gzip on; gzip_comp_level 4; gzip_min_length 256; gzip_vary on; gzip_proxied any; gzip_types text/plain text/css text/javascript text/xml application/json application/javascript application/xml application/xml+rss application/atom+xml application/ld+json application/manifest+json image/svg+xml;
open_file_cache max=200000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;
include /etc/nginx/conf.d/*.conf; }
# ── CDN edge proxy config ────────────────────────────────────── - path: /etc/nginx/conf.d/cdn-edge.conf content: | proxy_cache_path /var/cache/nginx/cdn levels=1:2 keys_zone=cdn_cache:128m max_size=25g inactive=24h use_temp_path=off;
upstream origin_backend { server ${origin_host}; keepalive 1024; keepalive_timeout 60s; keepalive_requests 100000; }
map $request_id $cdn_ray_id { default "$${request_id}-SJC"; } map $request_id $cdn_azure_ref { default "0$${request_id}AAAAAA"; } map $request_id $cdn_amz_cf_id { default "E1$${request_id}=="; } map $http_user_agent $is_mobile { default "false"; "~*Mobile|Android|iPhone|iPod|BlackBerry|Opera Mini|IEMobile" "true"; } map $http_user_agent $is_tablet { default "false"; "~*iPad|Android(?!.*Mobile)|Tablet|Kindle|PlayBook" "true"; } map $http_user_agent $is_desktop { default "true"; "~*Mobile|Android|iPhone|iPod|BlackBerry|Opera Mini|IEMobile|iPad|Tablet|Kindle|PlayBook" "false"; }
server { listen 80 reuseport; server_name _;
location /health { access_log off; return 200 '{"status":"healthy","component":"cdn-edge","engine":"nginx","vendor_profiles":["akamai","cloudflare","cloudfront","fastly","azure-front-door"]}'; add_header Content-Type application/json; }
location / { proxy_pass https://origin_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_ssl_server_name on; proxy_ssl_name csd.bankexample.com; proxy_ssl_verify off; proxy_read_timeout 180s; proxy_connect_timeout 10s; proxy_send_timeout 15s;
# Standard proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Via "1.1 cdn-simulator"; proxy_set_header Forwarded "for=$remote_addr;proto=$scheme;host=$host"; proxy_set_header CDN-Loop "cdn-simulator";
# Akamai proxy_set_header True-Client-IP $remote_addr; proxy_set_header X-Akamai-Edgescape "georegion=263,country_code=US,region_code=CA,city=SANJOSE,dma=807,pmsa=7400,msa=7362,areacode=408,county=SANTACLARA,fips=06085,lat=37.3353,long=-121.8938,timezone=PST,zip=95113-95196,continent=NA,throughput=vhigh,bw=5000,network=att.net,asnum=7018,network_type=broadband"; proxy_set_header X-Akamai-Device-Characteristics "brand_name=Generic;model_name=Browser;is_mobile=$is_mobile;is_tablet=$is_tablet;is_wireless_device=$is_mobile;device_os=Linux;device_os_version=1.0;resolution_width=1920;resolution_height=1080"; proxy_set_header X-Akamai-Request-ID $request_id;
# Cloudflare proxy_set_header CF-Connecting-IP $remote_addr; proxy_set_header CF-IPCountry "US"; proxy_set_header cf-ipcity "San Jose"; proxy_set_header cf-ipcontinent "NA"; proxy_set_header cf-iplatitude "37.3353"; proxy_set_header cf-iplongitude "-121.8938"; proxy_set_header cf-region "California"; proxy_set_header cf-region-code "CA"; proxy_set_header cf-metro-code "807"; proxy_set_header cf-postal-code "95113"; proxy_set_header cf-timezone "America/Los_Angeles"; proxy_set_header Cf-Ray $cdn_ray_id; proxy_set_header CF-Visitor '{"scheme":"https"}'; proxy_set_header cf-bot-score "85"; proxy_set_header cf-verified-bot "false"; proxy_set_header cf-ja3-hash "e7d705a3286e19ea42f587b344ee6865"; proxy_set_header cf-ja4 "t13d1516h2_8daaf6152771_b0da82dd1658";
# CloudFront proxy_set_header CloudFront-Viewer-Address "$remote_addr:$remote_port"; proxy_set_header CloudFront-Viewer-Country "US"; proxy_set_header CloudFront-Viewer-Country-Name "United States"; proxy_set_header CloudFront-Viewer-Country-Region "CA"; proxy_set_header CloudFront-Viewer-Country-Region-Name "California"; proxy_set_header CloudFront-Viewer-City "San Jose"; proxy_set_header CloudFront-Viewer-Postal-Code "95113"; proxy_set_header CloudFront-Viewer-Latitude "37.33530"; proxy_set_header CloudFront-Viewer-Longitude "-121.89300"; proxy_set_header CloudFront-Viewer-Time-Zone "America/Los_Angeles"; proxy_set_header CloudFront-Viewer-Metro-Code "807"; proxy_set_header CloudFront-Viewer-ASN "7018"; proxy_set_header CloudFront-Viewer-Http-Version "2.0"; proxy_set_header CloudFront-Forwarded-Proto "https"; proxy_set_header CloudFront-Viewer-TLS "TLSv1.3:TLS_AES_128_GCM_SHA256:sessionResumed"; proxy_set_header CloudFront-Viewer-JA3-Fingerprint "e7d705a3286e19ea42f587b344ee6865"; proxy_set_header CloudFront-Is-Desktop-Viewer $is_desktop; proxy_set_header CloudFront-Is-Mobile-Viewer $is_mobile; proxy_set_header CloudFront-Is-Tablet-Viewer $is_tablet; proxy_set_header CloudFront-Is-SmartTV-Viewer "false"; proxy_set_header X-Amz-Cf-Id $cdn_amz_cf_id;
# Fastly proxy_set_header Fastly-Client-IP $remote_addr; proxy_set_header Fastly-SSL "1"; proxy_set_header Fastly-Client "1"; proxy_set_header Fastly-FF "cache-sjc3120-SJC"; proxy_set_header X-Geo-Country-Code "US"; proxy_set_header X-Geo-Country-Code3 "USA"; proxy_set_header X-Geo-Country-Name "United States"; proxy_set_header X-Geo-City "San Jose"; proxy_set_header X-Geo-Region "CA"; proxy_set_header X-Geo-Continent-Code "NA"; proxy_set_header X-Geo-Latitude "37.3353"; proxy_set_header X-Geo-Longitude "-121.8938"; proxy_set_header X-Geo-Postal-Code "95113"; proxy_set_header X-Geo-Metro-Code "807"; proxy_set_header X-Geo-ASN "7018"; proxy_set_header X-Geo-Conn-Speed "broadband"; proxy_set_header X-Geo-Conn-Type "wired";
# Azure Front Door proxy_set_header X-Azure-ClientIP $remote_addr; proxy_set_header X-Azure-SocketIP $remote_addr; proxy_set_header X-Azure-Ref $cdn_azure_ref; proxy_set_header X-Azure-FDID "a0a0a0a0-bbbb-cccc-dddd-e1e1e1e1e1e1"; proxy_set_header X-Azure-RequestChain "hops=1";
# Cache proxy_set_header Host csd.bankexample.com; proxy_cache cdn_cache; proxy_cache_methods GET HEAD; proxy_cache_valid 200 301 302 4h; proxy_cache_valid 404 1m; proxy_cache_key "$scheme$host$request_uri"; proxy_cache_lock on; proxy_cache_lock_age 3s; proxy_cache_lock_timeout 3s; proxy_cache_background_update on; proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504; proxy_ignore_headers Set-Cookie Cache-Control Expires Vary;
proxy_hide_header X-Cache-Status; proxy_hide_header Vary;
add_header X-Cache-Status $upstream_cache_status always; add_header X-CDN-Edge "cdn-simulator" always; add_header X-CDN-POP "SJC" always; add_header X-Served-By "cache-sjc3120-SJC" always; add_header X-Request-ID $request_id always; } }
- path: /etc/nginx/conf.d/default.conf content: ""
- path: /usr/local/lib/cloud-init-helpers.sh permissions: "0644" content: | #!/bin/sh PROGRESS_LOG="/var/log/cloud-init-progress.log" log_phase() { _phase="$1"; shift _msg="$${*:-started}" _ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) printf '[%s] [%s] %s\n' "$_ts" "$_phase" "$_msg" | tee -a "$PROGRESS_LOG" >&2 } retry_cmd() { _max="$1"; _base="$2"; shift 2 _attempt=1 while [ "$_attempt" -le "$_max" ]; do if "$@"; then return 0; fi if [ "$_attempt" -lt "$_max" ]; then _wait=$(( _base * _attempt )) log_phase "retry" "attempt $_attempt/$_max failed ($1) — retrying in $${_wait}s" sleep "$_wait" fi _attempt=$(( _attempt + 1 )) done log_phase "retry" "FAILED after $_max attempts: $1" return 1 }
runcmd: - | . /usr/local/lib/cloud-init-helpers.sh log_phase "init" "cdn-simulator provisioning started" - sysctl -p /etc/sysctl.d/99-cdn-tuning.conf || exit 1 - systemctl daemon-reload - rm -f /etc/nginx/sites-enabled/default - chown -R www-data:www-data /var/cache/nginx/cdn - nginx -t || exit 1 - systemctl enable nginx - systemctl restart nginx - systemctl enable irqbalance - systemctl start irqbalance - | . /usr/local/lib/cloud-init-helpers.sh NIC=$(ip -o link show | awk -F': ' '/state UP/{print $2}' | grep -v lo | head -1) if [ -n "$NIC" ]; then log_phase "nic" "configuring RPS/RFS for $NIC" echo 65536 > /proc/sys/net/core/rps_sock_flow_entries 2>/dev/null || true for i in $(seq 0 $(($(nproc)-1))); do echo 8192 > /sys/class/net/$NIC/queues/rx-$i/rps_flow_cnt 2>/dev/null || true done else log_phase "nic" "no active NIC found — skipping RPS/RFS" fi - | . /usr/local/lib/cloud-init-helpers.sh log_phase "complete" "cdn-simulator provisioned"outputs.tf 按照演示资源标准暴露 17 个输出项——15 个所有演示资源共享的标准输出(deployer、public_ip、private_ip、ssh_command、resource_group_name、vm_name、nsg_name、vnet_name、subnet_id、component、environment、resource_group_id、vm_id、nsg_id、location),以及 2 个组件专用输出(edge_url、health_check_url):
# ---------------------------------------------------------# Standard Outputs (present in every demo resource)# ---------------------------------------------------------
output "deployer" { description = "Resolved deployer identifier" value = local.deployer}
output "resource_group_name" { description = "Name of the resource group" value = azurerm_resource_group.main.name}
output "resource_group_id" { description = "Resource ID of the resource group" value = azurerm_resource_group.main.id}
output "location" { description = "Azure region" value = azurerm_resource_group.main.location}
output "public_ip" { description = "Public IP address of the VM" value = azurerm_public_ip.main.ip_address}
output "private_ip" { description = "Private IP address of the VM" value = azurerm_network_interface.main.private_ip_address}
output "ssh_command" { description = "SSH command to connect to the VM" value = "ssh ${var.admin_username}@${azurerm_public_ip.main.ip_address}"}
output "vm_name" { description = "Name of the virtual machine" value = azurerm_linux_virtual_machine.main.name}
output "vm_id" { description = "Resource ID of the virtual machine" value = azurerm_linux_virtual_machine.main.id}
output "nsg_name" { description = "Name of the network security group" value = azurerm_network_security_group.main.name}
output "nsg_id" { description = "Resource ID of the network security group" value = azurerm_network_security_group.main.id}
output "vnet_name" { description = "Name of the virtual network" value = azurerm_virtual_network.main.name}
output "subnet_id" { description = "Resource ID of the subnet" value = azurerm_subnet.main.id}
output "component" { description = "Component name" value = local.component}
output "environment" { description = "Environment label" value = var.environment}
# ---------------------------------------------------------# Component-Specific Outputs# ---------------------------------------------------------
output "edge_url" { description = "HTTP URL of the CDN edge node" value = "http://${azurerm_public_ip.main.ip_address}"}
output "health_check_url" { description = "Health check endpoint" value = "http://${azurerm_public_ip.main.ip_address}/health"}示例变量文件
Section titled “示例变量文件”将 terraform.tfvars.example 复制为 terraform.tfvars 并填入您的值。.gitignore 已排除 terraform.tfvars 以防止提交凭据:
# Copy this file to terraform.tfvars and fill in your values.# terraform.tfvars is gitignored — never commit real credentials.
# --- Required ---subscription_id = "00000000-0000-0000-0000-000000000000"origin_server = "http://your-origin-ip"origin_host = "your-origin-ip:80"
# --- Optional overrides (defaults shown) ---# deployer = "" # auto-resolved from Azure AD# location = "eastus2"# environment = "lab"# vm_size = "Standard_F4s_v2"# disk_size_gb = 30# admin_username = "azureuser"# ssh_public_key_path = "~/.ssh/id_ed25519.pub"# tags = {}# 初始化 Terraformterraform init
# 查看执行计划terraform plan
# 执行部署terraform apply部署成功后,Terraform 将输出公共 IP、SSH 命令和边缘 URL。
F 系列计算优化型虚拟机适用于此类以 CPU 为主的 NGINX 代理工作负载。默认的 Standard_F4s_v2(4 vCPU,8 GiB)适合实验室和演示使用。如需进行负载测试或生产基准测试,可通过 vm_size 变量覆盖默认配置。Cloud-init 内核调优与 NGINX 配置会随 vCPU 数量自动扩展(worker_processes auto)。
terraform apply 完成后,请等待 2-3 分钟以便 cloud-init 完成 NGINX 的安装与配置。通过以下命令验证健康检查端点:
curl -s "http://$(terraform output -raw public_ip)/health" | jq .预期响应:
{ "status": "healthy", "component": "cdn-edge", "engine": "nginx", "vendor_profiles": [ "akamai", "cloudflare", "cloudfront", "fastly", "azure-front-door" ]}与上游组件对接
Section titled “与上游组件对接”CDN 模拟器需要一个已部署的源服务器。使用源服务器的 Terraform 输出项填充所需变量:
cd ../origin-server/terraformorigin_ip=$(terraform output -raw public_ip)
cd ../../cdn-simulator/terraformcat > terraform.tfvars <<EOFsubscription_id = "your-subscription-id"origin_server = "http://${origin_ip}"origin_host = "${origin_ip}:80"EOF| 必填变量 | 来源 | 格式 |
|---|---|---|
origin_server | 源服务器 public_ip 输出 | http://<ip>(包含协议头) |
origin_host | 源服务器 public_ip 输出 | <ip>:80(不含协议头,需包含端口) |