Skip to content

Commit 4cc4ae7

Browse files
Copilotlonegunmanb
andcommitted
Add terraform-cli-plan-azure scenario and Azure button on plan docs
Agent-Logs-Url: https://github.com/lonegunmanb/terraform-tutorial/sessions/2e7df53d-7a6e-4759-9041-b48b6124a1ce Co-authored-by: lonegunmanb <2233414+lonegunmanb@users.noreply.github.com>
1 parent 6acab3c commit 4cc4ae7

16 files changed

Lines changed: 1089 additions & 1 deletion

File tree

docs/terraform-cli-plan.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,10 @@ terraform plan -parallelism=5
204204

205205
`-json``-no-color``-input``-lock``-lock-timeout` 等通用参数同样适用于 `terraform plan`。详见 [CLI 基础命令 — 跨命令通用参数](/terraform-cli-basic#跨命令通用参数)
206206

207-
<KillercodaEmbed src="https://killercoda.com/lonegunman-terraform-tutorial/course/terraform-tutorial/terraform-cli-plan" />
207+
<KillercodaEmbed src="https://killercoda.com/lonegunman-terraform-tutorial/course/terraform-tutorial/terraform-cli-plan" title="实验环境(AWS / LocalStack 版)" />
208+
209+
<KillercodaEmbed
210+
src="https://killercoda.com/lonegunman-terraform-tutorial/course/terraform-tutorial/terraform-cli-plan-azure"
211+
title="实验环境(Azure / miniblue 版)"
212+
desc="点击下方按钮在新标签页中打开 Azure 版 Killercoda 实验环境,预装了 Terraform + miniblue(Azure 本地模拟器),所有 plan 子命令与 AWS 版完全一致。"
213+
/>

terraform-tutorial/structure.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
{ "path": "terraform-cli-init" },
2424
{ "path": "terraform-cli-init-azure" },
2525
{ "path": "terraform-cli-plan" },
26+
{ "path": "terraform-cli-plan-azure" },
2627
{ "path": "terraform-cli-apply" },
2728
{ "path": "terraform-cli-apply-azure" },
2829
{ "path": "terraform-cli-destroy" },
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
environment = "dev"
2+
app_name = "myapp"
3+
suffix = "lab"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
miniblue:
3+
# 使用 fork 后的镜像(修复了 ARM 路由大小写敏感与 vnet privateEndpointVNetPolicies 不回传问题,兼容 azurerm v4),版本锁定
4+
image: ghcr.io/lonegunmanb/miniblue:sha-8cc1c25
5+
ports:
6+
- "4566:4566"
7+
- "4567:4567"
8+
deploy:
9+
resources:
10+
limits:
11+
memory: 512M
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
required_providers {
4+
azurerm = {
5+
source = "hashicorp/azurerm"
6+
version = "~> 4.0"
7+
}
8+
}
9+
}
10+
11+
provider "azurerm" {
12+
features {}
13+
14+
# miniblue HTTPS metadata endpoint
15+
metadata_host = "localhost:4567"
16+
resource_provider_registrations = "none"
17+
18+
# miniblue 接受任意凭据
19+
subscription_id = "00000000-0000-0000-0000-000000000000"
20+
tenant_id = "00000000-0000-0000-0000-000000000001"
21+
client_id = "miniblue"
22+
client_secret = "miniblue"
23+
}
24+
25+
variable "environment" {
26+
type = string
27+
default = "dev"
28+
description = "部署环境"
29+
}
30+
31+
variable "app_name" {
32+
type = string
33+
default = "myapp"
34+
description = "应用名称"
35+
}
36+
37+
variable "suffix" {
38+
type = string
39+
default = "lab"
40+
description = "资源名称后缀,用于避免命名冲突"
41+
}
42+
43+
locals {
44+
common_tags = {
45+
Environment = var.environment
46+
App = var.app_name
47+
ManagedBy = "Terraform"
48+
}
49+
}
50+
51+
resource "azurerm_resource_group" "main" {
52+
name = "${var.app_name}-${var.environment}-rg-${var.suffix}"
53+
location = "East US"
54+
tags = local.common_tags
55+
}
56+
57+
resource "azurerm_dns_zone" "app" {
58+
name = "${var.app_name}-${var.environment}-app-${var.suffix}.local"
59+
resource_group_name = azurerm_resource_group.main.name
60+
tags = local.common_tags
61+
}
62+
63+
resource "azurerm_dns_zone" "logs" {
64+
name = "${var.app_name}-${var.environment}-logs-${var.suffix}.local"
65+
resource_group_name = azurerm_resource_group.main.name
66+
tags = local.common_tags
67+
}
68+
69+
resource "azurerm_virtual_network" "net" {
70+
name = "${var.app_name}-${var.environment}-vnet-${var.suffix}"
71+
address_space = ["10.0.0.0/16"]
72+
location = azurerm_resource_group.main.location
73+
resource_group_name = azurerm_resource_group.main.name
74+
tags = local.common_tags
75+
}
76+
77+
output "resource_group" {
78+
value = azurerm_resource_group.main.name
79+
}
80+
81+
output "app_dns_zone" {
82+
value = azurerm_dns_zone.app.name
83+
}
84+
85+
output "logs_dns_zone" {
86+
value = azurerm_dns_zone.logs.name
87+
}
88+
89+
output "vnet" {
90+
value = azurerm_virtual_network.net.name
91+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
environment = "prod"
2+
app_name = "myapp"
3+
suffix = "lab"
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/bin/bash
2+
# ─────────────────────────────────────────────────────────
3+
# setup-common.sh — shared setup functions for Killercoda scenarios
4+
#
5+
# This file is the SINGLE SOURCE OF TRUTH for common setup logic.
6+
# It is copied into each scenario's assets/ directory by:
7+
# npm run sync-setup (or automatically via prebuild)
8+
#
9+
# Usage in background.sh:
10+
# source /root/setup-common.sh
11+
# install_terraform
12+
# install_awscli # optional — AWS CLI v2 + awslocal wrapper
13+
# install_tflint # optional — only in scenarios that need it
14+
# start_localstack
15+
# install_theia_plugin
16+
# finish_setup
17+
# ─────────────────────────────────────────────────────────
18+
19+
TERRAFORM_VERSION="${TERRAFORM_VERSION:-1.14.8}"
20+
TFLINT_VERSION="${TFLINT_VERSION:-v0.61.0}"
21+
TERRAGRUNT_VERSION="${TERRAGRUNT_VERSION:-0.77.5}"
22+
# Note: the miniblue container image tag is pinned in scripts/miniblue-image.mjs
23+
# and propagated into every docker-compose.yml by `npm run sync-miniblue`.
24+
# There is no shell-side miniblue version variable — start_miniblue just runs
25+
# `docker compose up -d` against the pinned compose file.
26+
27+
install_terraform() {
28+
if ! command -v unzip > /dev/null 2>&1; then
29+
apt-get update -qq && apt-get install -y -qq unzip > /dev/null 2>&1
30+
fi
31+
32+
# Install Docker Compose v2 plugin (binary download — apt package not available on Killercoda)
33+
if ! docker compose version > /dev/null 2>&1; then
34+
mkdir -p /usr/local/lib/docker/cli-plugins
35+
curl --connect-timeout 10 --max-time 120 -fsSL \
36+
"https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \
37+
-o /usr/local/lib/docker/cli-plugins/docker-compose
38+
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
39+
fi
40+
41+
curl --connect-timeout 10 --max-time 120 -fsSL \
42+
"https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \
43+
-o /tmp/terraform.zip \
44+
&& unzip -o -q /tmp/terraform.zip -d /usr/local/bin/ \
45+
&& chmod +x /usr/local/bin/terraform \
46+
&& rm -f /tmp/terraform.zip
47+
48+
terraform version || echo "WARNING: terraform install failed"
49+
}
50+
51+
install_tflint() {
52+
curl --connect-timeout 10 --max-time 120 -fsSL \
53+
"https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}/tflint_linux_amd64.zip" \
54+
-o /tmp/tflint.zip \
55+
&& unzip -o -q /tmp/tflint.zip -d /usr/local/bin/ \
56+
&& chmod +x /usr/local/bin/tflint \
57+
&& rm -f /tmp/tflint.zip
58+
59+
tflint --version || echo "WARNING: tflint install failed"
60+
}
61+
62+
install_terragrunt() {
63+
curl --connect-timeout 10 --max-time 120 -fsSL \
64+
"https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" \
65+
-o /usr/local/bin/terragrunt \
66+
&& chmod +x /usr/local/bin/terragrunt
67+
68+
terragrunt --version || echo "WARNING: terragrunt install failed"
69+
}
70+
71+
install_awscli() {
72+
# Install AWS CLI v2 (official binary)
73+
curl --connect-timeout 10 --max-time 120 -fsSL \
74+
"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \
75+
-o /tmp/awscliv2.zip \
76+
&& unzip -o -q /tmp/awscliv2.zip -d /tmp/ \
77+
&& /tmp/aws/install --update > /dev/null 2>&1 \
78+
&& rm -rf /tmp/awscliv2.zip /tmp/aws
79+
80+
aws --version || echo "WARNING: awscli install failed"
81+
82+
# Disable AWS CLI pager globally so output prints directly
83+
mkdir -p /root/.aws
84+
cat > /root/.aws/config <<'AWSCFG'
85+
[default]
86+
region = us-east-1
87+
output = json
88+
cli_pager =
89+
AWSCFG
90+
91+
# Install awscli-local (provides the 'awslocal' command)
92+
pip3 install --break-system-packages awscli-local > /dev/null 2>&1 \
93+
|| {
94+
# Fallback: create a shell wrapper if pip fails
95+
cat > /usr/local/bin/awslocal <<'WRAPPER'
96+
#!/bin/bash
97+
export AWS_PAGER=""
98+
exec aws --endpoint-url=http://localhost:4566 --region us-east-1 "$@"
99+
WRAPPER
100+
chmod +x /usr/local/bin/awslocal
101+
}
102+
103+
awslocal --version || echo "WARNING: awslocal install failed"
104+
}
105+
106+
start_localstack() {
107+
cd /root/workspace
108+
docker compose up -d
109+
110+
echo "Waiting for LocalStack to be ready..."
111+
for i in $(seq 1 60); do
112+
if curl -sf http://localhost:4566/_localstack/health > /dev/null 2>&1; then
113+
echo "LocalStack is ready."
114+
return 0
115+
fi
116+
sleep 2
117+
done
118+
echo "WARNING: LocalStack did not become healthy within 120 seconds"
119+
docker compose logs
120+
}
121+
122+
start_miniblue() {
123+
cd /root/workspace
124+
mkdir -p /root/.miniblue
125+
docker compose up -d
126+
127+
echo "Waiting for miniblue to be ready..."
128+
for i in $(seq 1 60); do
129+
if curl -sf http://localhost:4566/health > /dev/null 2>&1; then
130+
echo "miniblue is ready."
131+
# Trigger an HTTPS request so miniblue generates its self-signed cert.
132+
curl -sk https://localhost:4567/health > /dev/null 2>&1 || true
133+
134+
# Locate cert inside the container and copy to host.
135+
# The miniblue image is distroless (no shell), so we can't `docker exec`
136+
# to probe — `docker cp` itself is sufficient and silently fails on
137+
# missing paths. Try known paths in order.
138+
local cid
139+
cid=$(docker compose ps -q miniblue 2>/dev/null)
140+
for j in $(seq 1 30); do
141+
if [ -n "$cid" ]; then
142+
for p in /home/nonroot/.miniblue/cert.pem /root/.miniblue/cert.pem /app/.miniblue/cert.pem; do
143+
if docker cp "$cid:$p" /root/.miniblue/cert.pem 2>/dev/null; then
144+
break 2
145+
fi
146+
done
147+
fi
148+
sleep 1
149+
done
150+
151+
if [ ! -f /root/.miniblue/cert.pem ]; then
152+
echo "WARNING: failed to copy miniblue cert.pem out of container"
153+
else
154+
chmod 644 /root/.miniblue/cert.pem
155+
fi
156+
157+
# Make SSL_CERT_FILE available for all interactive shells
158+
cat > /etc/profile.d/miniblue.sh <<'PROF'
159+
export SSL_CERT_FILE=/root/.miniblue/cert.pem
160+
PROF
161+
chmod +x /etc/profile.d/miniblue.sh
162+
# Killercoda terminals are non-login interactive shells — they only source
163+
# ~/.bashrc, not /etc/profile.d/*. Append the export there too (idempotent).
164+
if ! grep -q 'SSL_CERT_FILE=/root/.miniblue/cert.pem' /root/.bashrc 2>/dev/null; then
165+
echo 'export SSL_CERT_FILE=/root/.miniblue/cert.pem' >> /root/.bashrc
166+
fi
167+
export SSL_CERT_FILE=/root/.miniblue/cert.pem
168+
return 0
169+
fi
170+
sleep 2
171+
done
172+
echo "WARNING: miniblue did not become healthy within 120 seconds"
173+
docker compose logs
174+
}
175+
176+
finish_setup() {
177+
touch /tmp/.setup-done
178+
}
179+
180+
install_azlocal() {
181+
# azlocal is a standalone Go CLI bundled inside the miniblue image at /azlocal
182+
# (analogous to awslocal for LocalStack). It uses HTTP on port 4566 and needs
183+
# no certificate or az CLI. Requires miniblue to be running.
184+
local cid
185+
cid=$(docker compose ps -q miniblue 2>/dev/null)
186+
if [ -z "$cid" ]; then
187+
cid=$(docker ps -q --filter "name=miniblue" | head -1)
188+
fi
189+
if [ -n "$cid" ]; then
190+
docker cp "$cid:/azlocal" /usr/local/bin/azlocal 2>/dev/null \
191+
&& chmod +x /usr/local/bin/azlocal
192+
fi
193+
azlocal --help > /dev/null 2>&1 || echo "WARNING: azlocal install failed"
194+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# 恭喜完成 terraform plan 实战练习(Azure 版)!
2+
3+
## 知识总结
4+
5+
| 功能 | 命令 / 参数 | 核心场景 |
6+
|------|------------|---------|
7+
| 预览变更 | terraform plan | 日常开发前的必做步骤 |
8+
| 无变更检测 | Plan: 0 to add... | 确认配置与远端一致 |
9+
| 预览全量销毁 | terraform plan -destroy | 执行 destroy 前的安全确认 |
10+
| 对齐带外变更 | terraform plan -refresh-only | 有人在 Terraform 外手动变更了资源 |
11+
| 保存计划文件 | terraform plan -out=tfplan | CI/CD 两步工作流(plan → approve → apply) |
12+
| 查看计划文件 | terraform show tfplan | 以可读格式查看已保存的计划 |
13+
| 定向规划 | terraform plan -target=ADDR | 故障恢复等特殊场景,非常规使用 |
14+
| 强制重建 | terraform plan -replace=ADDR | 替代 terraform taint |
15+
| 命令行变量 | terraform plan -var 'k=v' | 临时覆盖单个变量 |
16+
| 变量文件 | terraform plan -var-file=f.tfvars | 多变量批量注入 |
17+
| 三态退出码 | terraform plan -detailed-exitcode | 脚本化判断是否有变更 |
18+
19+
## Azure 版要点回顾
20+
21+
- 本课程用 [miniblue](https://miniblue.io/) 在本地完整模拟 Azure REST API,无需真实订阅
22+
- azurerm provider 通过 `metadata_host = "localhost:4567"` 指向 miniblue 的 HTTPS 端点
23+
- `azlocal` CLI 走 HTTP 4566,无需证书,类似 `awslocal`,用于校验资源
24+
- 所有 plan 子命令、参数、工作流与 AWS / LocalStack 版完全一致——这正是 Terraform 多云抽象的价值所在
25+
26+
## 重要提醒
27+
28+
- 计划文件(-out 产生的文件)包含完整的变量值(含敏感数据),请视为敏感制品
29+
- -target 是应急手段,不是日常工具
30+
- 在 CI 中推荐组合:terraform plan -out=tfplan -input=false -no-color -detailed-exitcode

0 commit comments

Comments
 (0)