Terraform 実践 #3 EC2 とセキュリティグループ — SG チェイニング設計と user_data による起動

読了 5分

前回のネットワークの上に、最初のコンピューティングを載せます。今回作る単一の 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
# 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
# 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 つあります。

  1. プライベートサブネットに置きました。Web サーバーであっても、インターネットと直接接するのは次回の ALB で、サーバー自体はその後ろに隠れるのが 2 層構成の目的です。パッケージのインストール(dnf)ができるのは、前回の NAT のおかげです。
  2. user_data は初回起動時に一度だけ実行されます。そして基礎 #7で見たとおり、user_data の変更はデフォルトでインスタンスの置き換えを引き起こします。「サーバーを直さずに入れ替える」というイミュータブルインフラの方式に合った動作で、次回の Auto Scaling ではこの性質が利点に変わります。
  3. 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 にインスタンス定義を移し、ブラウザで最初のレスポンスを確認します。

X