Auto Scaling Groupのローリング更新を行う
起動テンプレートを更新しただけでは既存インスタンスが残り、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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー