콘텐츠로 이동

배포

모든 Terraform 파일은 terraform/ 디렉토리에 있습니다. 저장소를 클론하고 직접 배포하세요:

Terminal window
git clone https://github.com/f5-sales-demo/traffic-generator.git
cd traffic-generator/terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your Azure subscription ID and target FQDN

terraform 디렉토리에는 데모 리소스 표준을 따르는 9개의 파일이 포함되어 있습니다:

  • versions.tf — Terraform 및 프로바이더 버전 제약 (azurerm ~> 4.0, azuread ~> 3.0)
  • providers.tf — Azure RM 및 Azure AD 프로바이더 구성
  • data.tf — 배포자 자동 해결을 위한 Azure AD 데이터 소스
  • locals.tf — 배포자 해결, Azure Cloud Adoption Framework 리소스 명명, 표준 태그
  • main.tf — 리소스 그룹 (rg-traffic-generator-{environment}-{deployer} 형식으로 명명)
  • variables.tf — Azure AD를 통한 배포자 자동 해결이 포함된 모든 입력 변수 (필수 2개, 선택 9개)
  • network.tf — VNet (10.201.0.0/16), 서브넷, 공용 IP, NSG (포트 22 SSH), NIC
  • vm.tf — templatefile()을 통한 cloud-init이 포함된 Ubuntu 24.04 VM
  • 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개의 구성 요소별 출력 (target_fqdn, status_check)

main.tf는 Azure 프로바이더를 구성합니다:

resource "azurerm_resource_group" "main" {
name = local.name.resource_group
location = var.location
tags = local.tags
}

variables.tf는 모든 구성 가능한 매개변수를 정의합니다. vm_size의 기본값은 Standard_F16s_v2 (16 vCPU 컴퓨팅 최적화)입니다 — 스케일링 지침은 VM 크기 조정을 참조하세요:

# ---------------------------------------------------------
# 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 (F16s_v2: 16 vCPU compute-optimized, validated by benchmark)"
type = string
default = "Standard_F16s_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 = 64
}
# ---------------------------------------------------------
# Component-Specific
# ---------------------------------------------------------
variable "target_fqdn" {
description = "FQDN of the F5 XC load balancer to target"
type = string
}
variable "target_origin_ip" {
description = "Direct origin IP for bypass testing"
type = string
default = ""
}
variable "tool_tier" {
description = "Tool installation tier: standard (default) or full (includes ZAP, Metasploit)"
type = string
default = "standard"
validation {
condition = contains(["standard", "full"], var.tool_tier)
error_message = "tool_tier must be \"standard\" or \"full\"."
}
}

network.tf는 VNet, 서브넷, 공용 IP, NSG (포트 22 SSH), 그리고 NIC를 생성합니다:

resource "azurerm_virtual_network" "main" {
name = local.name.virtual_network
address_space = ["10.201.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.201.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 = "AllowSSH"
priority = 100
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
}

vm.tf는 Ubuntu 24.04 VM을 생성하고 대상 및 도구 변수와 함께 cloud-init 템플릿을 전달합니다:

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", {
target_fqdn = var.target_fqdn
target_origin_ip = var.target_origin_ip
tool_tier = var.tool_tier
}))
tags = azurerm_resource_group.main.tags
}

cloud-init.yaml은 4라운드의 오리진 부하 테스트를 포함한 48시간 이상의 지속적인 부하 테스트에서 검증된 프로덕션 구성입니다. 재시도 로직이 있는 헬퍼 라이브러리, /var/log/cloud-init-progress.log에 대한 진행 상황 로깅, 9개의 프로비저닝 단계, 그리고 조건부 상태 보고(status.json에서 ready/degraded)를 사용합니다. 다음을 프로비저닝합니다:

  • 커널 튜닝somaxconn=131072, tcp_max_tw_buckets=4000000, tcp_fin_timeout=5, 64 MiB 소켓 버퍼, file-max=4194304
  • NIC 최적화 — 모든 코어에 걸친 RPS/RFS, ethtool을 통한 최대화된 링 버퍼, THP=always
  • APT 패키지 — nikto, nmap, masscan, sqlmap, hydra, medusa, ncrack, tshark, hping3, tcpdump, netcat, ngrep, iperf3, mtr, sslscan, socat, dirb, whatweb, wrk, hey, vegeta, ethtool
  • GitHub 릴리스 — nuclei, dalfox, ffuf, gobuster, feroxbuster, subfinder, httpx, amass
  • Python 패키지 — mitmproxy, sslyze, dnsrecon, fierce, theHarvester, playwright, scapy, impacket, arjun
  • Node.js 20.x — 봇 시뮬레이션을 위한 스텔스 플러그인이 포함된 Puppeteer
  • 브라우저 자동화 — 헤드리스 브라우저 테스트를 위한 Playwright Chromium
  • Git 클론 — testssl.sh, recon-ng, spiderfoot, SecLists, waf-bypass, PayloadsAllTheThings
  • 스위트 배포 — 이 저장소에서 /opt/traffic-generator/suites/로 클론된 19개의 트래픽 스위트
  • 구성 파일TARGET_FQDN, TARGET_ORIGIN_IP, 그리고 CRAPI_PORT가 포함된 config.env

cloud-init은 Terraform 템플릿 변수를 사용합니다: ${target_fqdn}, ${target_origin_ip}, ${tool_tier}. $NPROC와 같은 셸 변수는 Terraform templatefile에서 $$NPROC로 이스케이프됩니다.

#cloud-config
package_update: true
package_upgrade: true
packages:
# System essentials
- ca-certificates
- curl
- gnupg
- lsb-release
- jq
- git
- unzip
- build-essential
- python3
- python3-pip
- python3-venv
- python3-dev
- libffi-dev
- libssl-dev
- libpcap-dev
# Network analysis
- nmap
- masscan
- tshark
- hping3
- socat
- tcpdump
- netcat-openbsd
- ngrep
- iperf3
- mtr-tiny
- whois
- dnsutils
# Web scanners
- nikto
- sqlmap
- dirb
- whatweb
- sslscan
# Password/credential testing
- hydra
- medusa
- ncrack
- john
- hashcat
# Forensics/analysis
- binwalk
- strace
- ltrace
# Performance monitoring
- sysstat
- htop
- iotop
- ethtool
# Load testing
- apache2-utils
- wrk
# Headless Chrome dependencies
- fonts-noto-color-emoji
- fonts-liberation
- libnss3
- libatk-bridge2.0-dev
- libdrm2
- libxkbcommon0
- libgbm1
- libasound2-dev
write_files:
- path: /usr/local/bin/ghlatest
permissions: '0755'
content: |
#!/bin/sh
set -e
REPO="$1"
if [ -z "$REPO" ]; then echo "Usage: ghlatest owner/repo" >&2; exit 1; fi
R=0; M=3
while [ "$R" -lt "$M" ]; do
RESP=$(curl -fsSL -w "\n%%{http_code}" "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null) || true
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | sed '$d')
if [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then
echo "WARN: ghlatest rate-limited ($HTTP_CODE) for $REPO" >&2; exit 1
fi
V=$(echo "$BODY" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/')
if [ -n "$V" ]; then echo "$V"; exit 0; fi
R=$((R + 1)); sleep $((R * 3))
done
echo "ERROR: ghlatest failed for $REPO" >&2; exit 1
- path: /etc/sysctl.d/99-traffic-generator.conf
content: |
net.core.somaxconn = 131072
net.ipv4.tcp_max_syn_backlog = 131072
net.core.netdev_max_backlog = 131072
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 5
net.ipv4.tcp_max_tw_buckets = 4000000
net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 3
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_window_scaling = 1
fs.file-max = 4194304
- path: /etc/security/limits.d/99-traffic-generator.conf
content: |
* soft nofile 524288
* hard nofile 524288
root soft nofile 524288
root hard nofile 524288
* soft nproc 65535
* hard nproc 65535
- path: /etc/profile.d/traffic-generator.sh
permissions: '0644'
content: |
export NODE_PATH=/usr/lib/node_modules
export PATH=$PATH:/usr/local/bin
- path: /usr/local/bin/tgen-profile
permissions: '0755'
content: |
#!/bin/bash
set -uo pipefail
report() {
echo "================================================================"
echo " TRAFFIC GENERATOR PLATFORM PROFILE — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "================================================================"
echo ""
echo "--- CPU ---"
echo "Cores: $(nproc)"
uptime
echo ""
echo "--- Memory ---"
free -h
echo ""
echo "--- Disk I/O ---"
iostat -x 1 1 2>/dev/null | grep -A20 'Device' || echo "iostat not available"
echo ""
echo "--- Network connections ---"
ss -s
echo ""
echo "--- TCP state distribution ---"
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
echo ""
echo "--- TIME_WAIT count ---"
ss -tan state time-wait | wc -l
echo ""
echo "--- Ephemeral port usage ---"
RANGE=$(sysctl -n net.ipv4.ip_local_port_range)
LOW=$(echo "$RANGE" | awk '{print $1}')
HIGH=$(echo "$RANGE" | awk '{print $2}')
USED=$(ss -tan | awk -v low="$LOW" -v high="$HIGH" '{split($4,a,":"); p=a[length(a)]; if(p>=low && p<=high) count++} END {print count+0}')
TOTAL=$((HIGH - LOW))
echo " Range: $LOW-$HIGH ($TOTAL ports)"
echo " Used: $USED ($((USED * 100 / TOTAL))%)"
echo ""
echo "--- Open file descriptors ---"
echo " System: $(awk '{print $1}' /proc/sys/fs/file-nr) / $(cat /proc/sys/fs/file-max)"
echo " Limit: $(ulimit -n) (soft) / $(ulimit -Hn) (hard)"
echo ""
echo "--- Kernel TCP tuning ---"
for k in net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.ipv4.tcp_fin_timeout net.ipv4.tcp_max_tw_buckets net.core.rmem_max net.core.wmem_max; do
printf " %-40s %s\n" "$k" "$(sysctl -n $k 2>/dev/null || echo N/A)"
done
}
if [ "$${1:-}" = "--watch" ]; then
SECS="$${2:-10}"
while true; do clear; report; sleep "$SECS"; done
else
report
fi
- path: /opt/traffic-generator/config.env
content: |
TARGET_FQDN=${target_fqdn}
TARGET_ORIGIN_IP=${target_origin_ip}
TARGET_PROTOCOL=http
CRAPI_PORT=8888
NODE_PATH=/usr/lib/node_modules
- path: /opt/traffic-generator/status.json
content: |
{"status":"provisioning","tool_tier":"${tool_tier}"}
- 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
}
fetch_url() {
_url="$1"; _out="$2"; _desc="$${3:-$1}"
log_phase "fetch" "$_desc"
retry_cmd 4 5 curl -fsSL --connect-timeout 15 --max-time 300 -o "$_out" "$_url"
}
install_packages() {
log_phase "apt" "installing: $*"
retry_cmd 3 10 apt-get install -y -o DPkg::Lock::Timeout=60 "$@"
}
clone_repo() {
_url="$1"; _dest="$2"; _depth="$${3:-1}"
log_phase "git" "cloning $_url -> $_dest"
retry_cmd 3 10 git clone --depth "$_depth" --single-branch "$_url" "$_dest"
}
wait_for_http() {
_url="$1"; _max="$2"; _desc="$${3:-$_url}"
log_phase "health" "waiting for $_desc (max $${_max}s)"
_elapsed=0
while [ "$_elapsed" -lt "$_max" ]; do
if curl -sf --max-time 5 "$_url" >/dev/null 2>&1; then
log_phase "health" "$_desc ready after $${_elapsed}s"
return 0
fi
sleep 5; _elapsed=$(( _elapsed + 5 ))
done
log_phase "health" "TIMEOUT: $_desc not ready after $${_max}s"
return 1
}
runcmd:
# --- Phase 0: Kernel tuning, NIC optimization, system limits ---
- sysctl --system
- systemctl daemon-reload
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase0" "kernel tuning and NIC optimization"
# RPS/RFS: distribute NIC softirqs across all cores
NCPU=$(nproc)
RPS_MASK=$(printf '%x' $(( (1 << NCPU) - 1 )))
NIC=$(ip -o link show | awk -F': ' '/state UP/ && !/lo/{print $2; exit}')
if [ -n "$NIC" ]; then
RFS_ENTRIES=$((32768 * NCPU))
echo "$RFS_ENTRIES" > /proc/sys/net/core/rps_sock_flow_entries
for rxq in /sys/class/net/"$NIC"/queues/rx-*/rps_cpus; do
echo "$RPS_MASK" > "$rxq"
done
for rxq in /sys/class/net/"$NIC"/queues/rx-*/rps_flow_cnt; do
RXQ_COUNT=$(ls -d /sys/class/net/"$NIC"/queues/rx-* | wc -l)
echo "$((RFS_ENTRIES / RXQ_COUNT))" > "$rxq"
done
# Maximize NIC ring buffers
if command -v ethtool >/dev/null 2>&1; then
RX_MAX=$(ethtool -g "$NIC" 2>/dev/null | awk '/Pre-set.*:/,/^$/{if(/RX:/){print $2; exit}}')
TX_MAX=$(ethtool -g "$NIC" 2>/dev/null | awk '/Pre-set.*:/,/^$/{if(/TX:/){print $2; exit}}')
[ -n "$RX_MAX" ] && [ -n "$TX_MAX" ] && ethtool -G "$NIC" rx "$RX_MAX" tx "$TX_MAX" 2>/dev/null
fi
echo "NIC tuning: RPS mask=$RPS_MASK on $NIC"
fi
# Transparent Huge Pages for large connection pools
echo "always" > /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true
# --- Phase 1: Node.js 24 ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase1" "installing Node.js 24"
retry_cmd 3 10 sh -c 'curl -fsSL https://deb.nodesource.com/setup_24.x | bash -'
install_packages nodejs
# --- Phase 2: Python toolchain (uv) ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase2" "installing Python uv"
retry_cmd 4 5 sh -c 'curl -fsSL https://astral.sh/uv/install.sh | sh'
cp /root/.local/bin/uv /usr/local/bin/uv || true
cp /root/.local/bin/uvx /usr/local/bin/uvx || true
# --- Phase 3: Security binaries (Go tools via ghlatest) ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase3" "installing security binaries"
export DPKG_ARCH=$(dpkg --print-architecture)
export PATH=/usr/local/bin:$PATH
echo "Installing nuclei..."
NUCLEI_VER=$(ghlatest projectdiscovery/nuclei)
fetch_url "https://github.com/projectdiscovery/nuclei/releases/download/v$${NUCLEI_VER}/nuclei_$${NUCLEI_VER}_linux_$${DPKG_ARCH}.zip" /tmp/nuclei.zip "nuclei"
unzip -oq /tmp/nuclei.zip nuclei -d /usr/local/bin && rm /tmp/nuclei.zip
echo "Installing subfinder..."
SUBFINDER_VER=$(ghlatest projectdiscovery/subfinder)
fetch_url "https://github.com/projectdiscovery/subfinder/releases/download/v$${SUBFINDER_VER}/subfinder_$${SUBFINDER_VER}_linux_$${DPKG_ARCH}.zip" /tmp/subfinder.zip "subfinder"
unzip -oq /tmp/subfinder.zip subfinder -d /usr/local/bin && rm /tmp/subfinder.zip
echo "Installing httpx..."
HTTPX_VER=$(ghlatest projectdiscovery/httpx)
fetch_url "https://github.com/projectdiscovery/httpx/releases/download/v$${HTTPX_VER}/httpx_$${HTTPX_VER}_linux_$${DPKG_ARCH}.zip" /tmp/httpx.zip "httpx"
unzip -oq /tmp/httpx.zip httpx -d /usr/local/bin && rm /tmp/httpx.zip
echo "Installing ffuf..."
FFUF_VER=$(ghlatest ffuf/ffuf)
retry_cmd 4 5 sh -c 'curl -fsSL "https://github.com/ffuf/ffuf/releases/download/v$${FFUF_VER}/ffuf_$${FFUF_VER}_linux_$${DPKG_ARCH}.tar.gz" | tar -xz -C /usr/local/bin ffuf'
echo "Installing gobuster..."
GOBUSTER_VER=$(ghlatest OJ/gobuster)
retry_cmd 4 5 sh -c 'curl -fsSL "https://github.com/OJ/gobuster/releases/download/v$${GOBUSTER_VER}/gobuster_Linux_$(uname -m).tar.gz" | tar -xz -C /usr/local/bin gobuster'
echo "Installing feroxbuster..."
FEROX_VER=$(ghlatest epi052/feroxbuster)
fetch_url "https://github.com/epi052/feroxbuster/releases/download/v$${FEROX_VER}/feroxbuster_$${DPKG_ARCH}.deb.zip" /tmp/ferox.zip "feroxbuster"
unzip -oq /tmp/ferox.zip -d /tmp && dpkg -i /tmp/feroxbuster_*.deb && rm -f /tmp/ferox.zip /tmp/feroxbuster_*.deb
echo "Installing dalfox..."
DALFOX_VER=$(ghlatest hahwul/dalfox)
fetch_url "https://github.com/hahwul/dalfox/releases/download/v$${DALFOX_VER}/dalfox-linux-$${DPKG_ARCH}.tar.gz" /tmp/dalfox.tar.gz "dalfox"
tar -xzf /tmp/dalfox.tar.gz -C /tmp && mv /tmp/dalfox-linux-$${DPKG_ARCH} /usr/local/bin/dalfox && chmod +x /usr/local/bin/dalfox && rm /tmp/dalfox.tar.gz
echo "Installing amass..."
AMASS_VER=$(ghlatest owasp-amass/amass)
fetch_url "https://github.com/owasp-amass/amass/releases/download/v$${AMASS_VER}/amass_linux_$${DPKG_ARCH}.tar.gz" /tmp/amass.tar.gz "amass"
mkdir -p /tmp/amass && tar -xzf /tmp/amass.tar.gz -C /tmp/amass && cp /tmp/amass/amass_linux_$${DPKG_ARCH}/amass /usr/local/bin/ && rm -rf /tmp/amass /tmp/amass.tar.gz
echo "Phase 3 complete"
# --- Phase 3b: High-performance load testing tools ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase3b" "installing load testing tools"
export DPKG_ARCH=$(dpkg --print-architecture)
export PATH=/usr/local/bin:$PATH
echo "Installing hey (via go install — no prebuilt binaries available)..."
install_packages golang-go
mkdir -p /opt/go/bin
chown -R azureuser:azureuser /opt/go
retry_cmd 3 15 sh -c 'GOPATH=/opt/go go install github.com/rakyll/hey@latest'
cp /opt/go/bin/hey /usr/local/bin/hey
echo "Installing gotestwaf..."
retry_cmd 3 15 sh -c 'GOPATH=/opt/go go install github.com/wallarm/gotestwaf/cmd/gotestwaf@latest'
cp /opt/go/bin/gotestwaf /usr/local/bin/gotestwaf
mkdir -p /opt/gotestwaf
cp -r /opt/go/pkg/mod/github.com/wallarm/gotestwaf@*/config.yaml /opt/gotestwaf/ 2>/dev/null || true
cp -r /opt/go/pkg/mod/github.com/wallarm/gotestwaf@*/testcases /opt/gotestwaf/ 2>/dev/null || true
chown -R azureuser:azureuser /opt/gotestwaf
echo "Installing vegeta..."
VEGETA_VER=$(ghlatest tsenart/vegeta)
retry_cmd 4 5 sh -c 'curl -fsSL "https://github.com/tsenart/vegeta/releases/download/v$${VEGETA_VER}/vegeta_$${VEGETA_VER}_linux_$${DPKG_ARCH}.tar.gz" | tar -xz -C /usr/local/bin vegeta'
echo "Phase 3b complete"
# --- Phase 4: Python security tools ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase4" "installing Python security tools"
retry_cmd 3 10 pip install --retries 3 --break-system-packages scapy impacket arjun hashid pwntools
retry_cmd 3 10 pip install --retries 3 --break-system-packages --ignore-installed typing_extensions mitmproxy sslyze
retry_cmd 3 10 pip install --retries 3 --break-system-packages wfuzz
# --- Phase 5: Browser automation ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase5" "installing browser automation tools"
retry_cmd 3 15 npm install -g playwright puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
- |
. /usr/local/lib/cloud-init-helpers.sh
retry_cmd 3 30 npx --yes playwright install chromium
retry_cmd 3 30 sh -c 'su - azureuser -c "export NODE_PATH=/usr/lib/node_modules && npx --yes playwright install chromium"'
npx --yes playwright install-deps chromium 2>/dev/null || true
# --- Phase 6: Git-cloned tools ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase6" "cloning tool repositories"
clone_repo "https://github.com/drwetter/testssl.sh.git" /opt/testssl.sh
ln -sf /opt/testssl.sh/testssl.sh /usr/local/bin/testssl
clone_repo "https://github.com/danielmiessler/SecLists.git" /opt/SecLists
clone_repo "https://github.com/lanmaster53/recon-ng.git" /opt/recon-ng
clone_repo "https://github.com/smicallef/spiderfoot.git" /opt/spiderfoot
clone_repo "https://github.com/nemesida-waf/waf-bypass.git" /opt/waf-bypass
clone_repo "https://github.com/swisskyrepo/PayloadsAllTheThings.git" /opt/PayloadsAllTheThings
# --- Phase 7: ZAP (standard tier) + heavy frameworks (full tier) ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase7" "installing ZAP and optional tools"
export PATH=/usr/local/bin:$PATH
echo "Installing ZAP..."
ZAP_VER=$(ghlatest zaproxy/zaproxy)
retry_cmd 4 5 sh -c 'curl -fsSL "https://github.com/zaproxy/zaproxy/releases/download/v$${ZAP_VER}/ZAP_$${ZAP_VER}_Linux.tar.gz" | tar -xz -C /opt'
mv /opt/ZAP_$${ZAP_VER} /opt/zaproxy 2>/dev/null || true
printf '#!/bin/sh\nexec /opt/zaproxy/zap.sh "$@"\n' > /usr/local/bin/zap && chmod +x /usr/local/bin/zap
if [ "${tool_tier}" = "full" ]; then
echo "Installing Metasploit (full tier)..."
curl -fsSL https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall
chmod +x /tmp/msfinstall && /tmp/msfinstall
fi
# --- Phase 8: Clone traffic-generator suites ---
- mkdir -p /opt/traffic-generator/suites /opt/traffic-generator/results
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase8" "cloning traffic generator suites"
clone_repo "https://github.com/f5-sales-demo/traffic-generator.git" /opt/traffic-generator/repo 1 || log_phase "warning" "traffic-generator repo clone failed — suites will be from cloud-init"
- |
if [ -d /opt/traffic-generator/repo/suites ]; then
cp -r /opt/traffic-generator/repo/suites/* /opt/traffic-generator/suites/
chmod -R +x /opt/traffic-generator/suites/
fi
- chown -R azureuser:azureuser /opt/traffic-generator
# --- Phase 9: Smoke test and status update ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "phase9" "running smoke test"
export NODE_PATH=/usr/lib/node_modules
export PATH=/usr/local/bin:$PATH
PASS=0; FAIL=0
for tool in nmap nikto sqlmap nuclei dalfox ffuf gobuster feroxbuster subfinder httpx sslscan hydra john hashcat masscan hping3 tshark wrk hey vegeta ab zap testssl arjun wfuzz gotestwaf node npx playwright; do
if command -v "$tool" >/dev/null 2>&1; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "MISSING: $tool"; fi
done
if [ "$FAIL" -eq 0 ]; then STATUS="ready"; else STATUS="degraded"; fi
log_phase "phase9" "smoke test complete: $PASS passed, $FAIL failed"
printf '{"status":"%s","tool_tier":"${tool_tier}","tools_pass":%d,"tools_fail":%d,"timestamp":"%s"}\n' \
"$STATUS" "$PASS" "$FAIL" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /opt/traffic-generator/status.json
# --- Completion ---
- |
. /usr/local/lib/cloud-init-helpers.sh
log_phase "complete" "traffic-generator provisioned"

outputs.tf는 공용/사설 IP, SSH 명령, 리소스 식별자, 그리고 구성 요소별 값(target_fqdn, status_check)을 포함한 17개의 출력을 노출합니다:

# ---------------------------------------------------------
# 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 "target_fqdn" {
description = "Target FQDN the traffic generator is configured to attack"
value = var.target_fqdn
}
output "status_check" {
description = "SSH command to check provisioning status"
value = "ssh ${var.admin_username}@${azurerm_public_ip.main.ip_address} cat /opt/traffic-generator/status.json"
}

terraform.tfvars.exampleterraform.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"
target_fqdn = "demo.example.com"
# --- Optional overrides (defaults shown) ---
# deployer = "" # auto-resolved from Azure AD
# location = "eastus2"
# environment = "lab"
# vm_size = "Standard_F16s_v2"
# disk_size_gb = 64
# admin_username = "azureuser"
# ssh_public_key_path = "~/.ssh/id_ed25519.pub"
# tags = {}
# target_origin_ip = ""
# tool_tier = "standard"

subscription_id를 Azure 구독 ID(az account show --query id -o tsv에서 확인)로 교체하고 target_fqdn을 F5 XC 로드 밸런서 도메인으로 교체하세요.

Terminal window
terraform init
terraform plan
terraform apply

Terraform은 성공적인 배포 후 공용 IP, SSH 명령, 그리고 대상 FQDN을 출력합니다.

F 시리즈 컴퓨팅 최적화 VM은 이 CPU 바운드 트래픽 생성 워크로드에 권장됩니다. cloud-init 커널 튜닝 및 도구 동시성은 vCPU 수에 따라 자동으로 확장됩니다.

VM SKUvCPURAM네트워크사용 사례
Standard_F4s_v248 GiB10 Gbps가벼운 랩/데모
Standard_F8s_v2816 GiB12.5 Gbps표준 데모
Standard_F16s_v21632 GiB12.5 Gbps부하 테스트 (기본값)
Standard_F32s_v23264 GiB16 Gbps고부하 벤치마킹
Standard_D16s_v31664 GiB12.5 GbpsRAM 집약적 워크로드

Cloud-init 프로비저닝은 표준 티어의 경우 15-20분, 전체 티어의 경우 약 25분이 소요됩니다. VM은 즉시 SSH를 통해 접근할 수 있지만, cloud-init이 완료될 때까지 도구가 설치 중입니다.

VM에 SSH로 접속하고 프로비저닝이 완료될 때까지 기다립니다:

Terminal window
ssh azureuser@$(terraform output -raw public_ip)
cloud-init status --wait

완료 시 예상 출력:

status: done

cloud-init 스크립트는 완료 시 상태 파일을 작성합니다:

Terminal window
cat /opt/traffic-generator/status.json

예상 출력:

{
"status": "ready",
"tier": "standard",
"target_fqdn": "demo.example.com",
"completed": "2026-04-25T14:30:00Z",
"tools_installed": true
}

각 중요 도구가 PATH에서 사용 가능한지 확인하기 위해 검증 루프를 실행합니다:

Terminal window
for tool in nikto sqlmap nuclei dalfox ffuf gobuster feroxbuster nmap masscan \
hydra medusa ncrack sslscan tshark hping3 mitmproxy sslyze subfinder httpx \
amass arjun mtr whatweb dirb socat netcat ngrep iperf3 whois dig wrk hey; do
if command -v "$tool" > /dev/null 2>&1; then
echo "OK: $tool"
else
echo "MISSING: $tool"
fi
done

러너와 스위트가 설치되어 있는지 확인합니다:

Terminal window
ls /opt/traffic-generator/suites/runner.sh
ls /opt/traffic-generator/suites/*/
/opt/traffic-generator/suites/runner.sh web-app-attacks --dry-run
출력설명예시
deployer해결된 배포자 식별자jsmith
public_ip트래픽 생성기 VM의 공용 IP 주소20.12.78.200
private_ipVM의 사설 IP 주소10.201.0.4
ssh_commandVM에 연결하기 위한 SSH 명령ssh azureuser@20.12.78.200
resource_group_name리소스 그룹 이름rg-traffic-generator-dev-jsmith
resource_group_id리소스 그룹 Azure 리소스 ID/subscriptions/.../resourceGroups/rg-traffic-generator-dev-jsmith
vm_name가상 머신 이름vm-traffic-generator-dev-jsmith
vm_id가상 머신 Azure 리소스 ID/subscriptions/.../virtualMachines/vm-traffic-generator-dev-jsmith
nsg_name네트워크 보안 그룹 이름nsg-traffic-generator-dev-jsmith
nsg_id네트워크 보안 그룹 Azure 리소스 ID/subscriptions/.../networkSecurityGroups/nsg-traffic-generator-dev-jsmith
vnet_name가상 네트워크 이름vnet-traffic-generator-dev-jsmith
subnet_id서브넷 Azure 리소스 ID/subscriptions/.../subnets/snet-traffic-generator-dev-jsmith
component구성 요소 식별자traffic-generator
environment배포 환경dev
locationAzure 리전eastus2
target_fqdn트래픽 스위트에 구성된 대상 FQDNdemo.example.com
status_check프로비저닝 상태 확인 명령ssh azureuser@20.12.78.200 cat /opt/traffic-generator/status.json

출력 값 조회:

Terminal window
terraform output -raw public_ip
terraform output -raw ssh_command

트래픽 생성기는 오리진 서버를 앞단에서 처리하는 F5 XC HTTP 로드 밸런서를 대상으로 합니다. 먼저 오리진 서버를 배포하고 F5 XC를 구성한 다음, target_fqdn을 로드 밸런서의 FQDN으로 설정하세요:

Terminal window
# After deploying origin-server and creating an F5 XC HTTP load balancer:
cat > terraform.tfvars <<EOF
subscription_id = "your-subscription-id"
target_fqdn = "your-xc-load-balancer.example.com"
target_origin_ip = "$(cd ../../origin-server/terraform && terraform output -raw public_ip)"
EOF
필수 변수소스설명
target_fqdnF5 XC 콘솔오리진 서버를 보호하는 HTTP 로드 밸런서의 FQDN
target_origin_ip오리진 서버 public_ip 출력우회 테스트를 위한 직접 오리진 IP (선택 사항)

전체 도구 참조는 도구 카탈로그로, 트래픽 생성 시작은 스위트로 이동하세요.