はじめに
この記事は、Terraform の 依存関係(depends_on) と
terraform graph による可視化を使って、
クリスマスツリーを作ってみようという完全に自己満足な試みです。
……そうです。
今年のクリスマスに特に予定はありません。
予定がないなら、せめて Terraform で
依存関係のクリスマスツリーでも作ってやろうと思いました。(?)
Terraform は本来、AWS などのクラウドリソースを
安全かつ再現性高く構築するための IaC ツールですが、
依存関係を明示的に書くことで、リソース同士の関係を
グラフとして出力できる、という面白い一面があります。
今回はその仕組みを使って、
- 依存関係をツリー状に並べる
-
terraform graph+ Graphviz で可視化する - ついでに AWS リソースも「飾り」として配置してみる
という、実用性より楽しさ全振りの記事になっています。
なお、記事中では クリスマスツリーを作るために
実運用では推奨されない書き方(null_resource の多用など)も登場します。
あくまで Terraform の仕組みを理解するための
予定のないクリスマスの有効活用として、
ゆるく読んでもらえたら嬉しいです 🎄
今回書いたコードは Github に上げてます
terraformとは
Terraform は、インフラ構成をコードで管理・構築できるツールです。
AWS や GCP、Azure といったクラウドサービスのリソースを、
管理コンソール上で画面をポチポチ操作して作る代わりに、
コードで定義して一括で作成・変更・削除できます。
terraform のメリット
- インフラ構成をコードとして保存できる
- 再現性がある
- 変更履歴を Git で管理できる
- 手作業による設定のミスを減らせる
といったメリットがあります。
Terraformの特徴
今回のネタにもつながる重要な特徴があります。
- 依存関係を自動で解決する
resource "aws_subnet" "example" {
vpc_id = aws_vpc.example.id
}
このように書くだけで VPC -> Subnet という作成順序(依存関係)を Terraform が自動で判断してくれます。
さらに depends_on を使えば、明示的に依存関係を書くことも可能です。
- グラフ出力機能
リソース同士の依存関係をグラフとして出力する機能があります。
terraform graph
これを Graphviz と組み合わせると、リソースの依存関係と作成順序を矢印付きの図として可視化できます。
TerraformとAWS SDKの違い
Terraform と AWS SDK はどちらもAWSを操作できるという点で似ていますが、目的と考え方が全く違うツールです。
Terraform はインフラの状態を管理するもの。SDK は API を操作するものです。
もう少し詳しく話しましょう。
SDK
SDK は AWS の API をプログラムから呼び出すためのライブラリです。
例えば
- EC2 を起動する
- S3 にファイルをアップロードする
- DynamoDB にデータを挿入する
といった「処理」を記述します。
ec2.run_instances(
ImageId="ami-xxxx",
InstanceType="t3.micro"
)
これは「この処理を実行して、EC2を起動して」という命令型(imperative)な書き方です。
Terraform
一方 Terraform は「最終的にインフラがどうあるべきか」と宣言的(declarative)に書くツールです。
resource "aws_instance" "web" {
ami = "ami-xxxx"
instance_type = "t3.micro"
}
これは「この EC2 が存在していてほしい」と状態を定義しているだけです。
Terraform は現在の状態とコードを比較して
- 足りなければ作る
- 違っていれば直す
- 余っていれば消す
ということを自動で判断します。
状態管理(State)
これが SDK との最大の違いとも言っていい部分です。
Terraform には State(状態ファイル)という概念があります
- どのリソースが
- どの設定で
- どこに存在しているか
これらを Terraform 自身が把握しています
SDKには状態管理の仕組みはありません。
SDKは毎回、
「今どうなってるかは知らないけど、とりあえずAPI叩くわー」
という動きになります。
依存関係の扱い
resource "aws_subnet" "example" {
vpc_id = aws_vpc.example.id
}
- VPC -> Subnet の順で自動作成
- 削除も逆順で安全に実行
依存関係は Terraformが解決します。
vpc_id = create_vpc()
subnet_id = create_subnet(vpc_id)
- 作成順序はすべて自分で制御
- エラー処理も自前
環境
- windows11
- PowerShell
- Terraform CLI
- Graphviz(dot)
- AWS CLIの認証
- VScode
作成前の準備(必要なものだけ)
terraformのインストール
- 公式サイトからダウンロードして解凍
- terraform.exe を置く場所を作る -> 私はC直下にしました
C:\terraform - terraform.exe を移動させる
C:\terraform\terraform.exe
PATHを通す
- windowsキー
- 「環境変数」と入力
- 環境変数を編集
- 「ユーザー環境変数」-> PATH -> 編集
- 新規 -> C:\任意のパス
※私のパスだとC:\terraform
AWS CLIのインストール
公式サイトからインストール(AWSCLIV2.msi)
確認
terraform -v
aws --version
Graphvizをインストール
# インストール
winget install Graphviz.Graphviz
# 確認
dot -v
※必要があれば PATH を通す(terraformの設定と同様)
(Program Files\Graphviz\binとか...dot.exeファイルのあるディレクトリを指定)
IAMユーザーを作る
- AWSマネジメントコンソールにログイン
- IAMサービスを開く
- 左側のメニューからユーザーを選択
- ユーザーの作成をクリック
- ユーザー名は何でもいい(例:terraform-user)
AWSマネジメントコンソールへのアクセスはチェックしない - 権限の設定(ポリシーを直接アタッチする)
AdministratorAccess
※本番環境では絶対NG!! - 作成
アクセスキーの作成
- ユーザーからさっき作ったIAMユーザーをクリック
- セキュリティ認証情報
- アクセスキーを作成
- ユースケース:コマンドラインインターフェース(CLI)
- 作成
※アクセスキーが表示された画面を閉じると二度と見れないためどこかに保存しておくかCSVファイルをダウンロードしておこう!
AWS認証設定(Windows)
aws configure
AWS Access Key ID [None]: さっき作ったアクセスキー
AWS Secret Access Key [None]: さっき作ったシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: json
Terraformでクリスマスツリー(graph)を作成
- 好きな場所に作業用ディレクトリを作成してください
- 作業用のディレクトリの中に
main.tfを作成 - VScodeなりで編集画面を開いてください
※VScodeのターミナルで terraform や dot が使えない場合はPCの再起動や環境変数の設定を確認
main.tf
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
# ★
resource "null_resource" "xmas_tree_star" {}
# 1段目
resource "null_resource" "xmas_tree_1_center" {
depends_on = [null_resource.xmas_tree_star]
}
# 2段目
resource "null_resource" "xmas_tree_2_left" {
depends_on = [null_resource.xmas_tree_1_center]
}
resource "null_resource" "xmas_tree_2_right" {
depends_on = [null_resource.xmas_tree_1_center]
}
# 3段目
resource "null_resource" "xmas_tree_3_left" {
depends_on = [null_resource.xmas_tree_2_left]
}
resource "null_resource" "xmas_tree_3_center" {
depends_on = [
null_resource.xmas_tree_2_left,
null_resource.xmas_tree_2_right
]
}
resource "null_resource" "xmas_tree_3_right" {
depends_on = [null_resource.xmas_tree_2_right]
}
# 4段目
resource "null_resource" "xmas_tree_4_left" {
depends_on = [null_resource.xmas_tree_3_left]
}
resource "null_resource" "xmas_tree_4_midleft" {
depends_on = [
null_resource.xmas_tree_3_left,
null_resource.xmas_tree_3_center
]
}
resource "null_resource" "xmas_tree_4_midright" {
depends_on = [
null_resource.xmas_tree_3_center,
null_resource.xmas_tree_3_right
]
}
resource "null_resource" "xmas_tree_4_right" {
depends_on = [null_resource.xmas_tree_3_right]
}
# 5段目
resource "null_resource" "xmas_tree_5_left" {
depends_on = [null_resource.xmas_tree_4_left]
}
resource "null_resource" "xmas_tree_5_midleft" {
depends_on = [
null_resource.xmas_tree_4_left,
null_resource.xmas_tree_4_midleft
]
}
resource "null_resource" "xmas_tree_5_center" {
depends_on = [
null_resource.xmas_tree_4_midleft,
null_resource.xmas_tree_4_midright
]
}
resource "null_resource" "xmas_tree_5_midright" {
depends_on = [
null_resource.xmas_tree_4_midright,
null_resource.xmas_tree_4_right
]
}
resource "null_resource" "xmas_tree_5_right" {
depends_on = [null_resource.xmas_tree_4_right]
}
# 6段目
resource "null_resource" "xmas_tree_6_1_left" {
depends_on = [null_resource.xmas_tree_5_left]
}
resource "null_resource" "xmas_tree_6_2_midleft" {
depends_on = [
null_resource.xmas_tree_5_left,
null_resource.xmas_tree_5_midleft
]
}
resource "null_resource" "xmas_tree_6_3_center" {
depends_on = [
null_resource.xmas_tree_5_midleft,
null_resource.xmas_tree_5_center
]
}
resource "null_resource" "xmas_tree_6_4_midright" {
depends_on = [
null_resource.xmas_tree_5_center,
null_resource.xmas_tree_5_midright
]
}
resource "null_resource" "xmas_tree_6_5_right" {
depends_on = [
null_resource.xmas_tree_5_midright,
null_resource.xmas_tree_5_right
]
}
resource "null_resource" "xmas_tree_6_6_outerright" {
depends_on = [null_resource.xmas_tree_5_right]
}
# 幹
resource "null_resource" "xmas_tree_trunk" {
depends_on = [
null_resource.xmas_tree_6_1_left,
null_resource.xmas_tree_6_2_midleft,
null_resource.xmas_tree_6_3_center,
null_resource.xmas_tree_6_4_midright,
null_resource.xmas_tree_6_5_right,
null_resource.xmas_tree_6_6_outerright
]
}
Terraformの初期化
terraform init
DOT 形式で依存関係を出力
terraform graph
Graphviz で SVG に変換
$dot = terraform graph
[System.IO.File]::WriteAllText("graph.dot", $dot, (New-Object System.Text.UTF8Encoding($false)))
dot -Kdot -Grankdir=BT -Edir=back -Tsvg graph.dot -o xmas_tree.svg
出力するとこのような画像が出現すると思います。
......え?クリスマスツリーに見えないって?
見える見えないではありません。これは紛れもないクリスマスツリーなのです(断言)
上に☆(star)があって下に幹(trunk)があればそれはもうクリスマスツリーなのです。
AWS 構成にして構築する
ここからは自己満足の世界です。
せっかく Terraform で依存関係(クリスマスツリー)を書いたので、ツリーの飾り付けとして AWS リソースを作成してみます。
※ 見た目(terraform graph)をツリーにするために、実際の依存関係としては不要な depends_on をあえて追加しています。
※ この書き方は実運用では推奨しません(不要な作成順の固定で apply が遅くなったり、構成が読みづらくなるためです)。
構成図
クリスマスツリーを残しつつ main.tf で AWS の設計をしよう
ディレクトリ構成
Terraform-Xmas-Tree/
├─ main.tf
├─ xmas_tree.tf
├─ web_a.html
└─ web_c.html
terraform用のコード
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "random_id" "suffix" {
byte_length = 3
}
# ★(VPC)
resource "aws_vpc" "xmas" {
cidr_block = "10.25.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "xmas-tree-vpc" }
depends_on = [null_resource.xmas_tree_star]
}
# 1段目(Public Subnet)
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.xmas.id
cidr_block = "10.25.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = { Name = "xmas-public-a" }
depends_on = [null_resource.xmas_tree_2_left]
}
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.xmas.id
cidr_block = "10.25.2.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
tags = { Name = "xmas-public-c" }
depends_on = [null_resource.xmas_tree_2_right]
}
# Private Subnet(EC2用)
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.xmas.id
cidr_block = "10.25.101.0/24"
availability_zone = "ap-northeast-1a"
tags = { Name = "xmas-private-a" }
}
resource "aws_subnet" "private_c" {
vpc_id = aws_vpc.xmas.id
cidr_block = "10.25.102.0/24"
availability_zone = "ap-northeast-1c"
tags = { Name = "xmas-private-c" }
}
# 2段目(Internet Gateway + NATGateway)
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.xmas.id
tags = { Name = "xmas-igw" }
}
resource "aws_eip" "nat_a" {
domain = "vpc"
}
resource "aws_eip" "nat_c" {
domain = "vpc"
}
resource "aws_nat_gateway" "nat_a" {
allocation_id = aws_eip.nat_a.id
subnet_id = aws_subnet.public_a.id
depends_on = [aws_internet_gateway.igw]
}
resource "aws_nat_gateway" "nat_c" {
allocation_id = aws_eip.nat_c.id
subnet_id = aws_subnet.public_c.id
depends_on = [aws_internet_gateway.igw]
}
# 3段目(Route Table & Route)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.xmas.id
tags = { Name = "xmas-public-rt" }
depends_on = [null_resource.xmas_tree_4_midleft]
}
resource "aws_route" "public_default" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
depends_on = [null_resource.xmas_tree_4_midright]
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
depends_on = [null_resource.xmas_tree_4_left]
}
resource "aws_route_table_association" "public_c" {
subnet_id = aws_subnet.public_c.id
route_table_id = aws_route_table.public.id
depends_on = [null_resource.xmas_tree_4_right]
}
# Private Route Table(0.0.0.0/0 を NATへ)
resource "aws_route_table" "private_a" {
vpc_id = aws_vpc.xmas.id
tags = { Name = "xmas-private-a-rt" }
}
resource "aws_route" "private_a_default" {
route_table_id = aws_route_table.private_a.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_a.id
}
resource "aws_route_table_association" "private_a" {
subnet_id = aws_subnet.private_a.id
route_table_id = aws_route_table.private_a.id
}
resource "aws_route_table" "private_c" {
vpc_id = aws_vpc.xmas.id
tags = { Name = "xmas-private-c-rt" }
}
resource "aws_route" "private_c_default" {
route_table_id = aws_route_table.private_c.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_c.id
}
resource "aws_route_table_association" "private_c" {
subnet_id = aws_subnet.private_c.id
route_table_id = aws_route_table.private_c.id
}
# 4段目(Security Group)
resource "aws_security_group" "alb" {
name = "xmas-alb-sg"
description = "allow HTTP from internet to ALB"
vpc_id = aws_vpc.xmas.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "xmas-alb-sg" }
depends_on = [null_resource.xmas_tree_5_center]
}
resource "aws_security_group" "web" {
name = "xmas-web-sg"
description = "allow HTTP from ALB to EC2"
vpc_id = aws_vpc.xmas.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "HTTP from ALB"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "xmas-web-sg" }
depends_on = [null_resource.xmas_tree_5_center]
}
# 5段目(AMI)
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64*"]
}
}
# 6段目(EC2)
locals {
web_a_html = templatefile("${path.module}/web_a.html", { az = "ap-northeast-1a" })
web_c_html = templatefile("${path.module}/web_c.html", { az = "ap-northeast-1c" })
}
resource "aws_instance" "web_a" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.private_a.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
dnf -y install httpd
systemctl enable --now httpd
echo '${base64encode(local.web_a_html)}' | base64 -d > /var/www/html/index.html
EOF
tags = { Name = "xmas-web-a" }
depends_on = [null_resource.xmas_tree_6_1_left]
}
resource "aws_instance" "web_c" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.private_c.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
dnf -y install httpd
systemctl enable --now httpd
echo '${base64encode(local.web_c_html)}' | base64 -d > /var/www/html/index.html
EOF
tags = { Name = "xmas-web-c" }
depends_on = [null_resource.xmas_tree_6_6_outerright]
}
# 幹(ALB)
resource "aws_lb_target_group" "xmas" {
name = "xmas-tg-${random_id.suffix.hex}"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.xmas.id
health_check {
path = "/"
}
depends_on = [null_resource.xmas_tree_6_3_center]
}
resource "aws_lb" "xmas_trunk" {
name = "xmas-trunk-${random_id.suffix.hex}"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [aws_subnet.public_a.id, aws_subnet.public_c.id]
depends_on = [null_resource.xmas_tree_trunk]
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.xmas_trunk.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.xmas.arn
}
depends_on = [null_resource.xmas_tree_trunk]
}
resource "aws_lb_target_group_attachment" "web_a" {
target_group_arn = aws_lb_target_group.xmas.arn
target_id = aws_instance.web_a.id
port = 80
}
resource "aws_lb_target_group_attachment" "web_c" {
target_group_arn = aws_lb_target_group.xmas.arn
target_id = aws_instance.web_c.id
port = 80
}
output "xmas_url" {
value = "http://${aws_lb.xmas_trunk.dns_name}"
}
# ★
resource "null_resource" "xmas_tree_star" {}
# 1段目
resource "null_resource" "xmas_tree_1_center" {
depends_on = [null_resource.xmas_tree_star]
}
# 2段目
resource "null_resource" "xmas_tree_2_left" {
depends_on = [null_resource.xmas_tree_1_center]
}
resource "null_resource" "xmas_tree_2_right" {
depends_on = [null_resource.xmas_tree_1_center]
}
# 3段目
resource "null_resource" "xmas_tree_3_left" {
depends_on = [null_resource.xmas_tree_2_left]
}
resource "null_resource" "xmas_tree_3_center" {
depends_on = [
null_resource.xmas_tree_2_left,
null_resource.xmas_tree_2_right
]
}
resource "null_resource" "xmas_tree_3_right" {
depends_on = [null_resource.xmas_tree_2_right]
}
# 4段目
resource "null_resource" "xmas_tree_4_left" {
depends_on = [null_resource.xmas_tree_3_left]
}
resource "null_resource" "xmas_tree_4_midleft" {
depends_on = [
null_resource.xmas_tree_3_left,
null_resource.xmas_tree_3_center
]
}
resource "null_resource" "xmas_tree_4_midright" {
depends_on = [
null_resource.xmas_tree_3_center,
null_resource.xmas_tree_3_right
]
}
resource "null_resource" "xmas_tree_4_right" {
depends_on = [null_resource.xmas_tree_3_right]
}
# 5段目
resource "null_resource" "xmas_tree_5_left" {
depends_on = [null_resource.xmas_tree_4_left]
}
resource "null_resource" "xmas_tree_5_midleft" {
depends_on = [
null_resource.xmas_tree_4_left,
null_resource.xmas_tree_4_midleft
]
}
resource "null_resource" "xmas_tree_5_center" {
depends_on = [
null_resource.xmas_tree_4_midleft,
null_resource.xmas_tree_4_midright
]
}
resource "null_resource" "xmas_tree_5_midright" {
depends_on = [
null_resource.xmas_tree_4_midright,
null_resource.xmas_tree_4_right
]
}
resource "null_resource" "xmas_tree_5_right" {
depends_on = [null_resource.xmas_tree_4_right]
}
# 6段目
resource "null_resource" "xmas_tree_6_1_left" {
depends_on = [null_resource.xmas_tree_5_left]
}
resource "null_resource" "xmas_tree_6_2_midleft" {
depends_on = [
null_resource.xmas_tree_5_left,
null_resource.xmas_tree_5_midleft
]
}
resource "null_resource" "xmas_tree_6_3_center" {
depends_on = [
null_resource.xmas_tree_5_midleft,
null_resource.xmas_tree_5_center
]
}
resource "null_resource" "xmas_tree_6_4_midright" {
depends_on = [
null_resource.xmas_tree_5_center,
null_resource.xmas_tree_5_midright
]
}
resource "null_resource" "xmas_tree_6_5_right" {
depends_on = [
null_resource.xmas_tree_5_midright,
null_resource.xmas_tree_5_right
]
}
resource "null_resource" "xmas_tree_6_6_outerright" {
depends_on = [null_resource.xmas_tree_5_right]
}
# 幹
resource "null_resource" "xmas_tree_trunk" {
depends_on = [
null_resource.xmas_tree_6_1_left,
null_resource.xmas_tree_6_2_midleft,
null_resource.xmas_tree_6_3_center,
null_resource.xmas_tree_6_4_midright,
null_resource.xmas_tree_6_5_right,
null_resource.xmas_tree_6_6_outerright
]
}
表示用のhtmlを書いておく
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Xmas Tree - ap-northeast-1a</title>
<style>
:root {
--bg1: #070b1a;
--bg2: #0b2a2a;
--tree: #0fb36b;
--tree2: #0a8a54;
--star: #ffd54a;
--trunk: #7a4a21;
--snow: #e8f6ff;
--card: rgba(255, 255, 255, 0.07);
--border: rgba(255, 255, 255, 0.12);
--text: #eaf2ff;
--muted: rgba(234, 242, 255, 0.75);
--glow: rgba(255, 213, 74, 0.35);
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
"Noto Sans JP", sans-serif;
color: var(--text);
background: radial-gradient(
1200px 600px at 20% 10%,
rgba(79, 186, 255, 0.2),
transparent 60%
),
radial-gradient(
900px 500px at 80% 30%,
rgba(21, 255, 180, 0.12),
transparent 55%
),
linear-gradient(160deg, var(--bg1), var(--bg2));
overflow: hidden;
}
/* 雪 */
.snow {
position: fixed;
inset: 0;
pointer-events: none;
background-image: radial-gradient(
circle at 10% 20%,
rgba(255, 255, 255, 0.7) 0 1px,
transparent 2px
),
radial-gradient(
circle at 30% 80%,
rgba(255, 255, 255, 0.6) 0 1px,
transparent 2px
),
radial-gradient(
circle at 70% 30%,
rgba(255, 255, 255, 0.6) 0 1px,
transparent 2px
),
radial-gradient(
circle at 90% 70%,
rgba(255, 255, 255, 0.5) 0 1px,
transparent 2px
);
background-size: 180px 220px;
animation: drift 14s linear infinite;
opacity: 0.6;
filter: blur(0.2px);
}
@keyframes drift {
from {
transform: translateY(-40px);
}
to {
transform: translateY(40px);
}
}
.card {
width: min(860px, 92vw);
border: 1px solid var(--border);
background: var(--card);
backdrop-filter: blur(10px);
border-radius: 22px;
padding: 26px 24px 18px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
position: relative;
}
.top {
display: flex;
gap: 18px;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 18px;
}
h1 {
margin: 0;
font-size: 22px;
letter-spacing: 0.3px;
}
.meta {
margin-top: 6px;
color: var(--muted);
font-size: 13px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(0, 0, 0, 0.18);
font-size: 13px;
color: var(--muted);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #38ffb2;
box-shadow: 0 0 18px rgba(56, 255, 178, 0.45);
}
/* ツリー */
.scene {
display: grid;
place-items: center;
padding: 12px 0 18px;
}
.tree-wrap {
position: relative;
width: 320px;
height: 420px;
display: grid;
place-items: center;
}
.star {
position: absolute;
top: 18px;
width: 32px;
height: 32px;
background: var(--star);
transform: rotate(35deg);
box-shadow: 0 0 26px var(--glow);
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 92%,
50% 72%,
21% 92%,
32% 57%,
2% 35%,
39% 35%
);
animation: twinkle 1.8s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
filter: brightness(1);
transform: translateY(0) rotate(35deg);
}
50% {
filter: brightness(1.25);
transform: translateY(-2px) rotate(35deg);
}
}
.layer {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 150px solid transparent;
border-right: 150px solid transparent;
border-bottom: 120px solid var(--tree);
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.25));
}
.layer.l1 {
top: 54px;
border-left-width: 90px;
border-right-width: 90px;
border-bottom-width: 86px;
background: none;
border-bottom-color: var(--tree);
}
.layer.l2 {
top: 112px;
border-left-width: 115px;
border-right-width: 115px;
border-bottom-width: 98px;
border-bottom-color: var(--tree2);
}
.layer.l3 {
top: 176px;
border-left-width: 140px;
border-right-width: 140px;
border-bottom-width: 112px;
border-bottom-color: var(--tree);
}
.layer.l4 {
top: 248px;
border-left-width: 160px;
border-right-width: 160px;
border-bottom-width: 120px;
border-bottom-color: var(--tree2);
}
.trunk {
position: absolute;
bottom: 32px;
width: 52px;
height: 64px;
background: linear-gradient(180deg, #8b5a2b, var(--trunk));
border-radius: 10px;
box-shadow: 0 12px 16px rgba(0, 0, 0, 0.25);
}
/* オーナメント */
.orn {
position: absolute;
width: 12px;
height: 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 16px rgba(255, 255, 255, 0.25);
animation: blink 2.6s ease-in-out infinite;
}
@keyframes blink {
0%,
100% {
opacity: 0.75;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.12);
}
}
.orn.r {
background: #ff5a7a;
box-shadow: 0 0 18px rgba(255, 90, 122, 0.35);
}
.orn.b {
background: #53a6ff;
box-shadow: 0 0 18px rgba(83, 166, 255, 0.35);
}
.orn.y {
background: #ffd54a;
box-shadow: 0 0 18px rgba(255, 213, 74, 0.35);
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas,
"Cascadia Mono", monospace;
color: rgba(234, 242, 255, 0.9);
}
</style>
</head>
<body>
<div class="snow"></div>
<main class="card">
<div class="top">
<div>
<h1>🎄 Terraform Xmas Tree</h1>
<div class="meta">Served by <code>ap-northeast-1a</code></div>
</div>
<div class="pill"><span class="dot"></span> ALB Target: web_a</div>
</div>
<section class="scene">
<div class="tree-wrap" aria-label="Christmas tree">
<div class="star"></div>
<div class="layer l1"></div>
<div class="layer l2"></div>
<div class="layer l3"></div>
<div class="layer l4"></div>
<div class="trunk"></div>
<!-- ornaments -->
<div class="orn r" style="top: 118px; left: 140px"></div>
<div
class="orn b"
style="top: 138px; left: 190px; animation-delay: 0.3s"
></div>
<div
class="orn y"
style="top: 202px; left: 120px; animation-delay: 0.5s"
></div>
<div
class="orn r"
style="top: 222px; left: 210px; animation-delay: 0.9s"
></div>
<div
class="orn b"
style="top: 266px; left: 150px; animation-delay: 1.2s"
></div>
<div
class="orn y"
style="top: 292px; left: 210px; animation-delay: 1.6s"
></div>
</div>
</section>
</main>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Xmas Tree - ap-northeast-1c</title>
<style>
:root {
--bg1: #070b1a;
--bg2: #0b2a2a;
--tree: #0fb36b;
--tree2: #0a8a54;
--star: #ffd54a;
--trunk: #7a4a21;
--snow: #e8f6ff;
--card: rgba(255, 255, 255, 0.07);
--border: rgba(255, 255, 255, 0.12);
--text: #eaf2ff;
--muted: rgba(234, 242, 255, 0.75);
--glow: rgba(255, 213, 74, 0.35);
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
"Noto Sans JP", sans-serif;
color: var(--text);
background: radial-gradient(
1200px 600px at 20% 10%,
rgba(79, 186, 255, 0.2),
transparent 60%
),
radial-gradient(
900px 500px at 80% 30%,
rgba(21, 255, 180, 0.12),
transparent 55%
),
linear-gradient(160deg, var(--bg1), var(--bg2));
overflow: hidden;
}
/* 雪 */
.snow {
position: fixed;
inset: 0;
pointer-events: none;
background-image: radial-gradient(
circle at 10% 20%,
rgba(255, 255, 255, 0.7) 0 1px,
transparent 2px
),
radial-gradient(
circle at 30% 80%,
rgba(255, 255, 255, 0.6) 0 1px,
transparent 2px
),
radial-gradient(
circle at 70% 30%,
rgba(255, 255, 255, 0.6) 0 1px,
transparent 2px
),
radial-gradient(
circle at 90% 70%,
rgba(255, 255, 255, 0.5) 0 1px,
transparent 2px
);
background-size: 180px 220px;
animation: drift 14s linear infinite;
opacity: 0.6;
filter: blur(0.2px);
}
@keyframes drift {
from {
transform: translateY(-40px);
}
to {
transform: translateY(40px);
}
}
.card {
width: min(860px, 92vw);
border: 1px solid var(--border);
background: var(--card);
backdrop-filter: blur(10px);
border-radius: 22px;
padding: 26px 24px 18px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
position: relative;
}
.top {
display: flex;
gap: 18px;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 18px;
}
h1 {
margin: 0;
font-size: 22px;
letter-spacing: 0.3px;
}
.meta {
margin-top: 6px;
color: var(--muted);
font-size: 13px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(0, 0, 0, 0.18);
font-size: 13px;
color: var(--muted);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #38ffb2;
box-shadow: 0 0 18px rgba(56, 255, 178, 0.45);
}
/* ツリー */
.scene {
display: grid;
place-items: center;
padding: 12px 0 18px;
}
.tree-wrap {
position: relative;
width: 320px;
height: 420px;
display: grid;
place-items: center;
}
.star {
position: absolute;
top: 18px;
width: 32px;
height: 32px;
background: var(--star);
transform: rotate(35deg);
box-shadow: 0 0 26px var(--glow);
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 92%,
50% 72%,
21% 92%,
32% 57%,
2% 35%,
39% 35%
);
animation: twinkle 1.8s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
filter: brightness(1);
transform: translateY(0) rotate(35deg);
}
50% {
filter: brightness(1.25);
transform: translateY(-2px) rotate(35deg);
}
}
.layer {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 150px solid transparent;
border-right: 150px solid transparent;
border-bottom: 120px solid var(--tree);
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.25));
}
.layer.l1 {
top: 54px;
border-left-width: 90px;
border-right-width: 90px;
border-bottom-width: 86px;
background: none;
border-bottom-color: var(--tree);
}
.layer.l2 {
top: 112px;
border-left-width: 115px;
border-right-width: 115px;
border-bottom-width: 98px;
border-bottom-color: var(--tree2);
}
.layer.l3 {
top: 176px;
border-left-width: 140px;
border-right-width: 140px;
border-bottom-width: 112px;
border-bottom-color: var(--tree);
}
.layer.l4 {
top: 248px;
border-left-width: 160px;
border-right-width: 160px;
border-bottom-width: 120px;
border-bottom-color: var(--tree2);
}
.trunk {
position: absolute;
bottom: 32px;
width: 52px;
height: 64px;
background: linear-gradient(180deg, #8b5a2b, var(--trunk));
border-radius: 10px;
box-shadow: 0 12px 16px rgba(0, 0, 0, 0.25);
}
/* オーナメント */
.orn {
position: absolute;
width: 12px;
height: 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 16px rgba(255, 255, 255, 0.25);
animation: blink 2.6s ease-in-out infinite;
}
@keyframes blink {
0%,
100% {
opacity: 0.75;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.12);
}
}
.orn.r {
background: #ff5a7a;
box-shadow: 0 0 18px rgba(255, 90, 122, 0.35);
}
.orn.b {
background: #53a6ff;
box-shadow: 0 0 18px rgba(83, 166, 255, 0.35);
}
.orn.y {
background: #ffd54a;
box-shadow: 0 0 18px rgba(255, 213, 74, 0.35);
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas,
"Cascadia Mono", monospace;
color: rgba(234, 242, 255, 0.9);
}
</style>
</head>
<body>
<div class="snow"></div>
<main class="card">
<div class="top">
<div>
<h1>🎄 Terraform Xmas Tree</h1>
<div class="meta">Served by <code>ap-northeast-1c</code></div>
</div>
<div class="pill"><span class="dot"></span> ALB Target: web_c</div>
</div>
<section class="scene">
<div class="tree-wrap" aria-label="Christmas tree">
<div class="star"></div>
<div class="layer l1"></div>
<div class="layer l2"></div>
<div class="layer l3"></div>
<div class="layer l4"></div>
<div class="trunk"></div>
<!-- ornaments -->
<div class="orn r" style="top: 118px; left: 140px"></div>
<div
class="orn b"
style="top: 138px; left: 190px; animation-delay: 0.3s"
></div>
<div
class="orn y"
style="top: 202px; left: 120px; animation-delay: 0.5s"
></div>
<div
class="orn r"
style="top: 222px; left: 210px; animation-delay: 0.9s"
></div>
<div
class="orn b"
style="top: 266px; left: 150px; animation-delay: 1.2s"
></div>
<div
class="orn y"
style="top: 292px; left: 210px; animation-delay: 1.6s"
></div>
</div>
</section>
</main>
</body>
</html>
※この記事では terraform graph をツリーっぽく見せるために null_resource を大量に使っています。
実運用ではこの見た目目的の依存関係は不要で、リソース間参照(vpc_id = ... など)だけで十分です。
ただし、ケースによっては depends_on を使うこと自体はあります(今回の用途は見た目用)。
出来上がるサービス
- VPC:1
- Public Subnet:2(ap-northeast-1a / 1c)
- Private Subnet:2(ap-northeast-1a / 1c)
- Internet Gateway:1
- NAT Gateway:2(各AZに1つ)
- Route Table:
- Public:1(0.0.0.0/0 → IGW)
- Private:2(0.0.0.0/0 → NAT Gateway)
- Security Group:2(ALB / EC2)
- EC2:2
- ALB:1
- Target Group:1
- Listener:1
実行
成形&チェック
# 自動整形
terraform fmt -recursive
# Terraform プロジェクトを初期化
terraform init
# Terraformの設定ファイルの構文が正しいかチェック
terraform validate
実行内容の確認
terraform plan
Terraform の変更を AWS に適用
terraform apply
# Enter a valueが表示されたらyesと入力
Webで確認
terraform applyの処理が終了後
xmas_url = "http://xxxxxxx.ap-northeast-1.elb.amazonaws.com"と表示されるのでリンクを開いてみよう!
クリスマスツリーがある!!すごい!!
これで外出することなくクリスマスツリーが見れますね(?)
一応コンソール側からも確認
ちゃんと全部立ち上がっててなにより(アドレスやインスタンスIDは隠してます)
EC2
ALB
NATGateway
terraformの削除
NATGatewayを2つ使っているのでそのままにしておくと超課金されてしまいます。
クリスマスツリーを見ることができたら削除しておきましょう
terraform destroy
終わりに
この記事でやりたかったのは 「Terraform の依存関係ってこうやって見えるんだ」 を、ちゃんと手触りで理解することでした。
depends_on は普段あまり意識しないかもしれませんが、Terraform が「何を先に作って、何を後に作るか」を判断する上で重要な仕組みです。
今回はそれを クリスマスツリーという形で無理やり可視化してみました。
完全にネタ記事ではあるんですが、、、
-
terraform graphの出力は “依存関係の答え合わせ” に使える - 依存関係を意識すると「なぜその順番で作られるのか」が理解しやすくなる
- 実運用で
depends_onを増やしすぎると逆に読みづらくなる(今回がまさにそれ)
みたいな、割とちゃんとした学びもありました。
あと、今回の「クリスマスツリーに飾り付け」パート(AWS構築パート)は NAT Gateway がガチで課金されるので、見終わったら terraform destroy を忘れずに。
AWSの課金額で年末に泣くのは避けたいですし、
クリスマスツリーは無料で見たいですからね(切実)。
来年のクリスマスは予定があるかもしれませんが、もし無かったらまた Terraform で何か作ります。
そのときは お正月も近いことですし依存関係で鏡餅とか…(?)
それでは、皆さんよいクリスマスを…🎄





