こしあん
2025-02-11

Auto Scaling Groupのローリング更新を行う


6{icon} {views}


起動テンプレートを更新しただけでは既存インスタンスが残り、Webサーバーの内容がすぐに反映されない場合がある。Auto Scaling Groupのローリング更新機能を使えば、段階的なインスタンス置き換えで最新バージョンを確実に適用できる。

はじめに

  • ALBの背後にあるAuto Scaling Groupの起動テンプレートを更新したときに、以前のインスタンスが残っていてすぐには反映されない
  • この対策は以前のインスタンスを落としてしまうことだが、マネコンからEC2をKillしたり、Auto Scaling Groupから手動で落としたりしなくても、Auto Scaling Groupの「ローリング更新機能」でよしなにできるので試してみた。

やること

  • ALBの背後にAuto Scaling Groupがあり、そこでWebサーバーがホストされている
  • Auto Scaling Groupに紐づいている起動テンプレートのユーザーデータを書き換えたときに、Webサーバーのコンテンツのコンテンツをアップデートする

全体コードは末尾参照。Auto Scaling Groupで使われている起動テンプレートがこうだったとする。こんなようなホームページになる。

resource "aws_launch_template" "web_lt" {
  name_prefix   = "web-lt-"
  image_id      = data.aws_ami.amazon_linux.image_id
  instance_type = "t3.micro"

  # 起動時に適用するユーザーデータ (base64 エンコードも可能)
  user_data = base64encode(<<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y httpd
    systemctl enable httpd
    systemctl start httpd
    echo "<h1>Hello from Auto Scaling Group</h1>" > /var/www/html/index.html
  EOF
  )

  # インスタンスに適用するセキュリティグループやIAMプロファイル
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  iam_instance_profile {
    name = aws_iam_instance_profile.ec2_ssm_profile.name
  }
}

だったとする。このユーザーデータの部分だけ以下のように書き換えてterraform applyとしたとき、どうインスタンスを反映するか?という問題。

  # 起動時に適用するユーザーデータ (base64 エンコードも可能)
  user_data = base64encode(<<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y httpd
    systemctl enable httpd
    systemctl start httpd
    echo "<h1>Hello from Auto Scaling Group Ver.2</h1>" > /var/www/html/index.html
  EOF
  )

Auto Scaling Groupのローリング更新

インスタンスを直接終了しなくてももっと賢いやり方があって、Auto Scaling Groupからインスタンスの置き換えができる。Auto Scaling Groupの「インスタンスの更新」から。

いろんなオプションがある。とりあえずデフォルトでやっている

置き換えが行われている。可用性を多少維持しつつの更新なので、少し時間がかかる。

置き換えが終わったらブラウザから更新。Ver.2に無事置き換えが完了している。

起動テンプレートのデフォルトバージョンは関係ない

起動テンプレートのページを見ると、前のバージョン(1)がデフォルトになっており、terraform apply後のバージョン(2)はデフォルトになっていない。一見ここを切り替えないとAuto Scaling Group側も反映されないかのように思える。

しかし、Auto Scaling Groupの起動テンプレートの設定を見ると、Latestの設定になっている。つまり、起動テンプレートを更新すれば、新しいインスタンスは新しい起動テンプレートで動くようになっている。このケースでブラウザ表示が古いのは、古い起動テンプレートで起動したときのインスタンスが残っているからに他ならない。

よくデバッグしていると「起動テンプレートが古いからなのかな」と思うこともあるが、上手く起動するテンプレートならインスタンスを更新してしまえば理屈上は反映される。

全体コード

変数定義

variable "vpc_id" {
  description = "VPCのID"
  type        = string
}

variable "public_subnet_ids" {
  description = "ALB用のパブリックサブネットのIDリスト"
  type        = list(string)
}

variable "private_subnet_ids" {
  description = "EC2/ASG用のプライベートサブネットのIDリスト"
  type        = list(string)
}

variable "acm_certificate_arn" {
  description = "HTTPSリスナー用のACM証明書ARN"
  type        = string
}

# Route53 ドメイン関係
variable "hosted_zone_name" {
  type        = string
  description = "既存の Route53 Hosted Zone 名 (ex: example.com.)"
}

variable "record_name" {
  type        = string
  description = "レコードのホスト部 (ex: www)"
  default     = "www"
}

alb.tf

# ─────────────────────────────────────────────
# ALB 用セキュリティグループ
# ─────────────────────────────────────────────
resource "aws_security_group" "alb_sg" {
  name   = "alb-sg"
  vpc_id = var.vpc_id

  # インターネットからの HTTP (80) と HTTPS (443) を許可
  ingress {
    description      = "Allow HTTP"
    from_port        = 80
    to_port          = 80
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  ingress {
    description      = "Allow HTTPS"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  # アウトバウンドは全許可
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

# ─────────────────────────────────────────────
# ALB の作成
# ─────────────────────────────────────────────
resource "aws_lb" "alb" {
  name               = "alb-single"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = var.public_subnet_ids

  tags = {
    Name = "alb-single"
  }
}

# ─────────────────────────────────────────────
# ターゲットグループ (ASG のインスタンスへフォワード)
# ─────────────────────────────────────────────
resource "aws_lb_target_group" "tg" {
  name        = "tg-single"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "instance"

  health_check {
    path = "/"
  }
}

# ─────────────────────────────────────────────
# HTTP リスナー (80: HTTP -> HTTPS にリダイレクト)
# ─────────────────────────────────────────────
resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# ─────────────────────────────────────────────
# HTTPS リスナー (443: ALBからターゲットグループへフォワード)
# ─────────────────────────────────────────────
resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = var.acm_certificate_arn
  ssl_policy        = "ELBSecurityPolicy-2016-08"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

# ─────────────────────────────────────────────
# Route 53の設定
# ─────────────────────────────────────────────

data "aws_route53_zone" "main" {
  # 既存のドメインを使う例
  name         = var.hosted_zone_name
  private_zone = false
}

# ヘルスチェック (フェイルオーバーで使う例)
resource "aws_route53_health_check" "primary" {
  # ALB Primary に対する HTTP ヘルスチェック
  type              = "HTTP"
  resource_path     = "/"
  fqdn              = aws_lb.alb.dns_name
  port              = 80
  failure_threshold = 3
  request_interval  = 30
}

# DNSレコード
resource "aws_route53_record" "a_record" {
  zone_id         = data.aws_route53_zone.main.zone_id
  name            = var.record_name  # 例: "www"
  type            = "A"
  set_identifier  = "primary"
  health_check_id = aws_route53_health_check.primary.id

  alias {
    name                   = aws_lb.alb.dns_name
    zone_id                = aws_lb.alb.zone_id
    evaluate_target_health = true
  }

  failover_routing_policy {
    type = "PRIMARY"
  }
}

ec2.tf

resource "aws_security_group" "web_sg" {
  name   = "web-sg"
  vpc_id = var.vpc_id

  # ALB からの 80 番ポートのアクセスを許可
  ingress {
    description     = "Allow HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

# EC2 用 IAM ロール (SSM 用)
resource "aws_iam_role" "ec2_ssm_role" {
  name               = "ec2_ssm_role"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role_policy.json
}

data "aws_iam_policy_document" "ec2_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "ec2_ssm_attach" {
  role       = aws_iam_role.ec2_ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2_ssm_profile" {
  name = "ec2-ssm-profile"
  role = aws_iam_role.ec2_ssm_role.name
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["137112412989"] # Amazon の公式AMI

  filter {
    name   = "name"
    values = ["al2023-ami-2023*-kernel-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_launch_template" "web_lt" {
  name_prefix   = "web-lt-"
  image_id      = data.aws_ami.amazon_linux.image_id
  instance_type = "t3.micro"

  # 起動時に適用するユーザーデータ (base64 エンコードも可能)
  user_data = base64encode(<<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y httpd
    systemctl enable httpd
    systemctl start httpd
    echo "<h1>Hello from Auto Scaling Group</h1>" > /var/www/html/index.html
  EOF
  )

  # インスタンスに適用するセキュリティグループやIAMプロファイル
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  iam_instance_profile {
    name = aws_iam_instance_profile.ec2_ssm_profile.name
  }
}

resource "aws_autoscaling_group" "web_asg" {
  name                      = "web-asg"
  desired_capacity          = 2
  min_size                  = 1
  max_size                  = 3
  vpc_zone_identifier       = var.private_subnet_ids
  health_check_type         = "EC2"
  health_check_grace_period = 300

  # 起動テンプレートの最新バージョンを参照
  launch_template {
    id      = aws_launch_template.web_lt.id
    version = "$Latest"
  }

  # ALB ターゲットグループに自動登録
  target_group_arns = [aws_lb_target_group.tg.arn]

  # オプション: ローリングアップデート用の設定 (Terraform 1.3以降で autoscaling_group_update で定義可能)
  # ※既存インスタンスの更新を行う場合は instance refresh の設定も検討してください

  tag {
    key                 = "Name"
    value               = "web-asg"
    propagate_at_launch = true
  }
}

所感

  • スピード優先でブチッと落としてることも多いけど、正式なやり方はこうみたい
  • Auto Scaling Groupと起動テンプレートのバージョンの関係がはっきりしたのが収穫


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です