3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリスマスなので Terraform の依存関係でクリスマスツリーを作ってみた

Last updated at Posted at 2025-12-24

はじめに

この記事は、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 を使えば、明示的に依存関係を書くことも可能です。

  • グラフ出力機能

リソース同士の依存関係をグラフとして出力する機能があります。

bash
terraform graph

これを Graphviz と組み合わせると、リソースの依存関係作成順序矢印付きの図として可視化できます。

TerraformとAWS SDKの違い

Terraform と AWS SDK はどちらもAWSを操作できるという点で似ていますが、目的と考え方が全く違うツールです。
Terraform はインフラの状態を管理するもの。SDK は API を操作するものです。
もう少し詳しく話しましょう。

SDK

SDK は AWS の API をプログラムから呼び出すためのライブラリです。
例えば

  • EC2 を起動する
  • S3 にファイルをアップロードする
  • DynamoDB にデータを挿入する

といった「処理」を記述します。

python
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叩くわー」
という動きになります。

依存関係の扱い

Terraform
resource "aws_subnet" "example" {
  vpc_id = aws_vpc.example.id
}
  • VPC -> Subnet の順で自動作成
  • 削除も逆順で安全に実行

依存関係は Terraformが解決します。

SDK
vpc_id = create_vpc()
subnet_id = create_subnet(vpc_id)
  • 作成順序はすべて自分で制御
  • エラー処理も自前

環境

  • windows11
  • PowerShell
  • Terraform CLI
  • Graphviz(dot)
  • AWS CLIの認証
  • VScode

作成前の準備(必要なものだけ)

terraformのインストール

  1. 公式サイトからダウンロードして解凍
  2. terraform.exe を置く場所を作る -> 私はC直下にしました
    C:\terraform
  3. terraform.exe を移動させる
    C:\terraform\terraform.exe

PATHを通す

  1. windowsキー
  2. 「環境変数」と入力
  3. 環境変数を編集
  4. 「ユーザー環境変数」-> PATH -> 編集
  5. 新規 -> C:\任意のパス
    ※私のパスだとC:\terraform

AWS CLIのインストール

公式サイトからインストール(AWSCLIV2.msi)

確認

powershell
terraform -v
aws --version

Graphvizをインストール

# インストール
winget install Graphviz.Graphviz
# 確認
dot -v

※必要があれば PATH を通す(terraformの設定と同様)
(Program Files\Graphviz\binとか...dot.exeファイルのあるディレクトリを指定)

IAMユーザーを作る

  1. AWSマネジメントコンソールにログイン
  2. IAMサービスを開く
  3. 左側のメニューからユーザーを選択
  4. ユーザーの作成をクリック
  5. ユーザー名は何でもいい(例:terraform-user)
    AWSマネジメントコンソールへのアクセスはチェックしない
  6. 権限の設定(ポリシーを直接アタッチする)
    AdministratorAccess
    ※本番環境では絶対NG!!
  7. 作成

アクセスキーの作成

  1. ユーザーからさっき作ったIAMユーザーをクリック
  2. セキュリティ認証情報
  3. アクセスキーを作成
  4. ユースケース:コマンドラインインターフェース(CLI)
  5. 作成

※アクセスキーが表示された画面を閉じると二度と見れないためどこかに保存しておくかCSVファイルをダウンロードしておこう!

AWS認証設定(Windows)

powershell
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)を作成

  1. 好きな場所に作業用ディレクトリを作成してください
  2. 作業用のディレクトリの中にmain.tfを作成
  3. VScodeなりで編集画面を開いてください
    ※VScodeのターミナルで terraform や dot が使えない場合はPCの再起動や環境変数の設定を確認

main.tf

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の初期化

bash
terraform init

DOT 形式で依存関係を出力

bash
terraform graph

Graphviz で SVG に変換

bash
$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)があればそれはもうクリスマスツリーなのです。

image.png

AWS 構成にして構築する

ここからは自己満足の世界です。
せっかく Terraform で依存関係(クリスマスツリー)を書いたので、ツリーの飾り付けとして AWS リソースを作成してみます。
※ 見た目(terraform graph)をツリーにするために、実際の依存関係としては不要な depends_on をあえて追加しています。
※ この書き方は実運用では推奨しません(不要な作成順の固定で apply が遅くなったり、構成が読みづらくなるためです)。

構成図

クリスマスツリー.drawio.png

クリスマスツリーを残しつつ main.tf で AWS の設計をしよう

ディレクトリ構成

Terraform-Xmas-Tree/
├─ main.tf
├─ xmas_tree.tf
├─ web_a.html
└─ web_c.html

terraform用のコード

main.tf
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}"
}
xmas_tree.tf
# ★
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を書いておく

web_a.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>
web_c.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

実行

成形&チェック

powershell
# 自動整形
terraform fmt -recursive
# Terraform プロジェクトを初期化
terraform init
# Terraformの設定ファイルの構文が正しいかチェック
terraform validate

実行内容の確認

powershell
terraform plan

Terraform の変更を AWS に適用

powershell
terraform apply
# Enter a valueが表示されたらyesと入力

Webで確認

terraform applyの処理が終了後
xmas_url = "http://xxxxxxx.ap-northeast-1.elb.amazonaws.com"と表示されるのでリンクを開いてみよう!

クリスマスツリーがある!!すごい!!
これで外出することなくクリスマスツリーが見れますね(?)

スクリーンショット 2025-12-25 035929.png

一応コンソール側からも確認

ちゃんと全部立ち上がっててなにより(アドレスやインスタンスIDは隠してます)

EC2

スクリーンショット 2025-12-25 031717.png

ALB

スクリーンショット 2025-12-25 031758.png

NATGateway

スクリーンショット 2025-12-25 031828.png

terraformの削除

NATGatewayを2つ使っているのでそのままにしておくと超課金されてしまいます。
クリスマスツリーを見ることができたら削除しておきましょう

powershell
terraform destroy

終わりに

この記事でやりたかったのは 「Terraform の依存関係ってこうやって見えるんだ」 を、ちゃんと手触りで理解することでした。
depends_on は普段あまり意識しないかもしれませんが、Terraform が「何を先に作って、何を後に作るか」を判断する上で重要な仕組みです。

今回はそれを クリスマスツリーという形で無理やり可視化してみました。
完全にネタ記事ではあるんですが、、、

  • terraform graph の出力は “依存関係の答え合わせ” に使える
  • 依存関係を意識すると「なぜその順番で作られるのか」が理解しやすくなる
  • 実運用で depends_on を増やしすぎると逆に読みづらくなる(今回がまさにそれ)

みたいな、割とちゃんとした学びもありました。

あと、今回の「クリスマスツリーに飾り付け」パート(AWS構築パート)は NAT Gateway がガチで課金されるので、見終わったら terraform destroy を忘れずに。

AWSの課金額で年末に泣くのは避けたいですし、
クリスマスツリーは無料で見たいですからね(切実)。

来年のクリスマスは予定があるかもしれませんが、もし無かったらまた Terraform で何か作ります。
そのときは お正月も近いことですし依存関係で鏡餅とか…(?)

それでは、皆さんよいクリスマスを…🎄

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?