Terraform 実践 #3 EC2 とセキュリティグループ — SG チェイニング設計と user_data による起動
前回のネットワークの上に、最初のコンピューティングを載せます。今回作る単一の EC2 インスタンスは最終形ではなく、次回 Auto Scaling グループに昇格させる前の検証段階です。それでも今回には実践の核心が 1 つ入っています。セキュリティグループをどう設計するかです。コンソール時代の「とりあえず開けて後で整理しよう」で積み上がったセキュリティグループがどんな姿になるかを経験した方なら、コードで最初から構造を作る今回がその答えになるはずです。
セキュリティグループのチェイニング — CIDR ではなく SG を参照します #
原則は 1 つです。トラフィックが流れる道筋に沿って、セキュリティグループがセキュリティグループを参照するようにすることです。インターネット → ALB → EC2 という流れなら、こうなります。
- ALB SG: インバウンド 80・443 をインターネット(0.0.0.0/0)に開放
- EC2 SG: インバウンド 80 を ALB SG からのみ許可
EC2 側に CIDR を書かないのがポイントです。「Web サーバーは ALB から来るトラフィックだけを受ける」という意図がそのままコードに残り、サブネットのレンジが変わってもルールを修正する必要がありません。次回作る ALB の SG を先に定義しておきます。
# security.tf(新規ファイル)
resource "aws_security_group" "alb" {
name_prefix = "myapp-${var.env}-alb-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group" "web" {
name_prefix = "myapp-${var.env}-web-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id] # チェイニング
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}基礎 #7で学んだ 2 つが、さっそく実戦に出てきました。セキュリティグループは名前が一意でなければならず、ルールの変更が置き換えを引き起こす場合があるため、name_prefix(ランダムなサフィックス)と create_before_destroy をセットで掛けて、置き換えが起きても空白が生まれないようにしました。この組み合わせはセキュリティグループの定番パターンとして覚えておく価値があります。
SSH ポートがない理由 #
気づいたかもしれませんが、22 番ポートを開けるルールがありません。ミスではなく設計です。今の標準的な接続手段は、SSH キーと開いたポートの代わりに SSM Session Manager です。ポートを 1 つも開けず、キーファイルを配布せず、接続の記録が残ります。動作にはインスタンスに IAM ロールが必要で、それは #7 のテーマなので、今回のインスタンスはいったん「ポートが閉じた状態」のままにして、#7 で接続まで完成させます。急ぎでデバッグが必要になったら、そのとき一時的なルールを追加して後で消せばよく、その変更さえ plan に残るのがコード管理の利点です。
AMI の照会と user_data #
基礎 #4のデータソースで最新の AL2023 AMI を照会し、起動スクリプトで nginx まで立ち上げます。
# compute.tf(新規ファイル)
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.private["ap-northeast-2a"].id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
dnf install -y nginx
echo "myapp ${var.env} - $(hostname)" > /usr/share/nginx/html/index.html
systemctl enable --now nginx
EOF
tags = { Name = "myapp-${var.env}-web" }
}整理しておく決定が 3 つあります。
- プライベートサブネットに置きました。Web サーバーであっても、インターネットと直接接するのは次回の ALB で、サーバー自体はその後ろに隠れるのが 2 層構成の目的です。パッケージのインストール(dnf)ができるのは、前回の NAT のおかげです。
- user_data は初回起動時に一度だけ実行されます。そして基礎 #7で見たとおり、user_data の変更はデフォルトでインスタンスの置き換えを引き起こします。「サーバーを直さずに入れ替える」というイミュータブルインフラの方式に合った動作で、次回の Auto Scaling ではこの性質が利点に変わります。
- key_name がありません。SSH を使わないと決めたので、キーペアも作りません。
検証 — まだ接続する道がありません #
apply するとインスタンスは起動しますが、プライベートサブネットにあるため、ブラウザで確認する方法がまだありません。この中途半端な状態は意図した中間段階です。コンソールでインスタンスのステータスチェック(2/2 checks)が通ること、そして terraform state show aws_instance.web でプライベート IP が割り当てられていることまで見れば、今回の確認としては十分です。接続の確認は、ALB ができる次回にブラウザで行います。
まとめ #
今回扱った内容です。
- セキュリティグループは CIDR の代わりにセキュリティグループ参照でチェイニングします。「ALB から来るトラフィックだけ」という意図がコードに残ります
- セキュリティグループには name_prefix と create_before_destroy をセットで掛けます。置き換えが起きても空白がありません
- SSH ポートは開けません。接続は #7 で SSM Session Manager によって完成させます
- インスタンスはプライベートサブネットに置き、user_data で起動時に Web サーバーを構成します。user_data の変更が置き換えになる性質は、次回利点になります
- プライベートなインスタンスはまだ接続手段のない中間状態で、次回の ALB がその入口になります
次回(#4 ALB と Auto Scaling)では、この単一インスタンスをロードバランサーの後ろの Auto Scaling グループに昇格させます。launch template にインスタンス定義を移し、ブラウザで最初のレスポンスを確認します。