7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「なんかヘルスチェック落ちてるんだが」がくれたギフト🎁

Last updated at Posted at 2025-12-02

この記事は Medley(メドレー) Advent Calendar 2025 3日目の記事です🎄

はじめに

どうも、株式会社メドレーのSREエンジニアの森川です⚙️

先日、新規のインフラを構築しておりまして、ALB(Application Load Balancer)とターゲットグループにてヘルスチェックが通らないという状況に陥りました。調査を進める中で得られた知見を書き留めておこうと思います。

また、この機会に普段あまり意識しない「いいヘルスチェックのコマンド」についても考えてみました。

ヘルスチェック落ちてるんだが

発見の経緯

ALB・ターゲットグループ・Fargateを構築した際、ターゲットグループのヘルスチェックが unhealthy になっていることに気が付きました。

  • ECSコンテナヘルスチェックは通っている
  • ECS exec でコンテナにアクセスし、プロセス確認も問題なし

Fargate側では問題がなさそうです。

次に「ALB↔Fargate間の接続の問題か?」と疑い、調査を進めました。
すると、ALBのセキュリティグループでegressルールに何も定義していないことを発見しました。

なぜegressが必要なのか

ALBのヘルスチェックは、ALBからターゲットに対してHTTPリクエストを送信することで行われます。

つまり、ALBはターゲットへの「アウトバウンド通信」を行うため、egressルールでターゲットへの通信を許可する必要があります。

検証の構成

記事執筆にあたって同様の状況を作って検証を行ってみました。

構成は以下の通りです。

インターネット
     │
     ▼
┌─────────┐
│   ALB   │  ← セキュリティグループ: ALB用
└────┬────┘    - ingress: 80/tcp (0.0.0.0/0)
     │         - egress: ??? (ここを検証)
     ▼
┌─────────┐
│  Nginx  │  ← セキュリティグループ: EC2用
│  (EC2)  │    - ingress: 80/tcp (ALB SGから)
└─────────┘    ※ Nginxを使用することでアクセスログが見やすい

検証環境の構築

以下の terraform コードから構築が可能です。

検証構成 terraform コード

前提

  • DefaultのVPC、Subnetが存在すること
  • profile = "your-aws-profile" は自分の環境に合わせて変更してください
  • Nginxのログはわかりやすいようにjson形式での出力にしています
# =============================================================================
# ALB ヘルスチェック検証用 Terraform(単一ファイル版)
# 検証手順:
#   1. enable_alb_egress = false で apply → ヘルスチェック失敗を確認
#   2. enable_alb_egress = true  で apply → ヘルスチェック成功を確認
# =============================================================================

terraform {
  required_version = ">= 1.0.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = "your-aws-profile" # 自分のプロファイルに変更
}

locals {
  name = "alb-hc-test"
}

variable "enable_alb_egress" {
  type    = bool
  default = false  # まず失敗を確認、その後 true にして成功を確認
}

# -----------------------------------------------------------------------------
# Data Sources(Default VPC を使用)
# -----------------------------------------------------------------------------
data "aws_vpc" "default" {
  default = true
}

data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
  filter {
    name   = "default-for-az"
    values = ["true"]
  }
}

data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

# -----------------------------------------------------------------------------
# Security Groups
# -----------------------------------------------------------------------------

# ALB用 SG
resource "aws_security_group" "alb" {
  name        = "${local.name}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = data.aws_vpc.default.id

  ingress {
    description = "HTTP from internet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group_rule" "alb_egress_to_ec2" {
  count = var.enable_alb_egress ? 1 : 0

  type                     = "egress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  security_group_id        = aws_security_group.alb.id
  source_security_group_id = aws_security_group.ec2.id
  description              = "Allow HTTP to EC2 instances"
}

# EC2用 SG
resource "aws_security_group" "ec2" {
  name        = "${local.name}-ec2-sg"
  description = "Security group for EC2 instances"
  vpc_id      = data.aws_vpc.default.id

  ingress {
    description     = "HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
  egress {
    description = "All outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# -----------------------------------------------------------------------------
# IAM Role(SSM接続用)
# -----------------------------------------------------------------------------
resource "aws_iam_role" "ec2" {
  name = "${local.name}-ec2-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

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

resource "aws_iam_instance_profile" "ec2" {
  name = "${local.name}-ec2-profile"
  role = aws_iam_role.ec2.name
}

# -----------------------------------------------------------------------------
# EC2(Nginx)
# -----------------------------------------------------------------------------
resource "aws_instance" "nginx" {
  ami                         = data.aws_ami.al2023.id
  instance_type               = "t3.micro"
  subnet_id                   = tolist(data.aws_subnets.default.ids)[0]
  vpc_security_group_ids      = [aws_security_group.ec2.id]
  iam_instance_profile        = aws_iam_instance_profile.ec2.name
  associate_public_ip_address = true

  user_data = <<-EOF
#!/bin/bash
dnf install -y nginx

# JSON形式のログフォーマットを追加
cat > /tmp/log_format.conf << 'LOGFMT'
    log_format json escape=json '{"time": "$time_iso8601", "msec": "$msec", "remote_addr": "$remote_addr", "server_addr": "$server_addr", "server_port": "$server_port", "x_forwarded_for": "$http_x_forwarded_for", "x_forwarded_port": "$http_x_forwarded_port", "x_forwarded_proto": "$http_x_forwarded_proto", "host": "$host", "request": "$request", "request_method": "$request_method", "request_uri": "$request_uri", "status": $status, "request_length": $request_length, "body_bytes_sent": $body_bytes_sent, "bytes_sent": $bytes_sent, "request_time": $request_time, "user_agent": "$http_user_agent", "http_connection": "$http_connection", "http_accept": "$http_accept", "connection": "$connection", "connection_requests": "$connection_requests", "tcpinfo_rtt": "$tcpinfo_rtt", "tcpinfo_rttvar": "$tcpinfo_rttvar"}';
LOGFMT
sed -i '/http {/r /tmp/log_format.conf' /etc/nginx/nginx.conf

cat > /etc/nginx/conf.d/default.conf << 'CONF'
server {
    listen 80;
    access_log /var/log/nginx/access.log json;
    location / { return 200 'OK'; }
}
CONF
systemctl enable --now nginx
EOF

  tags = { Name = "${local.name}-nginx" }
}

# -----------------------------------------------------------------------------
# ALB + Target Group
# -----------------------------------------------------------------------------
resource "aws_lb" "main" {
  name               = "${local.name}-alb"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = slice(tolist(data.aws_subnets.default.ids), 0, 2)
}

resource "aws_lb_target_group" "main" {
  name     = "${local.name}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = data.aws_vpc.default.id

  health_check {
    path                = "/"
    matcher             = "200"
    interval            = 10
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_target_group_attachment" "main" {
  target_group_arn = aws_lb_target_group.main.arn
  target_id        = aws_instance.nginx.id
  port             = 80
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

検証の結果

ALBのSGにegressルールがない場合

ヘルスチェック失敗

  • ターゲットグループのステータスが unhealthy のまま
  • Nginxのアクセスログにリクエストが記録されない

ALBのSGにegressルールがある場合

ヘルスチェック成功

  • ターゲットグループのステータスが healthy
  • Nginxのアクセスログにリクエストが記録される

ALBのヘルスチェックアクセスを調べる

ALBのヘルスチェックのアクセスログを確認してみましょう。

作成したNginxインスタンスに入り、アクセスログを取得しました。

sh-5.2$ sudo tail -f /var/log/nginx/access.log | jq .

{
  "time": "2025-12-03T10:00:00+00:00",
  "msec": "1764673200.000",
  "remote_addr": "192.168.10.50",
  "server_addr": "192.168.20.100",
  "server_port": "80",
  "x_forwarded_for": "",
  "x_forwarded_port": "",
  "x_forwarded_proto": "",
  "host": "192.168.20.100",
  "request": "GET / HTTP/1.1",
  "request_method": "GET",
  "request_uri": "/",
  "status": 200,
  "request_length": 128,
  "body_bytes_sent": 2,
  "bytes_sent": 184,
  "request_time": 0.000,
  "user_agent": "ELB-HealthChecker/2.0",
  "http_connection": "close",
  "http_accept": "",
  "connection": "214",
  "connection_requests": "1",
  "tcpinfo_rtt": "686",
  "tcpinfo_rttvar": "343"
}

このログからいくつかのことがわかりました。

ALBはターゲットに直接HTTPリクエストを送っている

"remote_addr": "192.168.10.50",  // ALBのENI(プライベートIP)
"server_addr": "192.168.20.100",  // ターゲットのプライベートIP
"host": "192.168.20.100",         // HostヘッダーもIP直指定

ALBはターゲットのプライベートIPに対して直接HTTPリクエストを送信しています。これがegressルールが必要な理由というわけですね。

X-Forwarded-* ヘッダーが付かない

"x_forwarded_for": "",
"x_forwarded_port": "",
"x_forwarded_proto": ""

通常のリクエストでは付与される X-Forwarded-* ヘッダーですが、ヘルスチェックでは空になっていました。ALB内部から直接送信されていることがわかります。

Keep-Aliveなし

"http_connection": "close",
"connection_requests": "1"

Connection: close が指定されており、毎回TCPコネクションを切断しています。ヘルスチェックは軽量に設計されているようです。

User-Agent

"user_agent": "ELB-HealthChecker/2.0"

ELB-HealthChecker/2.0 という専用のUser-Agentが設定されています。このUser-Agentを利用することで、ログからヘルスチェックリクエストを識別・除外するような運用も可能です。

TCP RTT(往復遅延)

"tcpinfo_rtt": "686",       // 0.7ms
"tcpinfo_rttvar": "343"     // ばらつき約0.3ms

同一VPC内なのでRTTは1ms未満。ネットワーク遅延はほぼ無視できるレベルです。

教訓

Terraformで作るSGはegressも忘れずに

マネジメントコンソールでセキュリティグループを作成すると、アウトバウンドルールに 0.0.0.0/0 がデフォルトで設定されます。

しかし、Terraformの aws_security_group リソースでは、egressブロックを明示的に定義しないとルールが何もない状態になります。

ingressに意識が向きがちで、egressを忘れやすいので注意。

ヘルスチェックが落ちたときの切り分け

確認ポイント チェック内容
アプリ側 プロセスは起動してる?
ポートはLISTENしてる?
ターゲットグループ設定 ヘルスチェックのパス・ポートは正しい?
SG(ターゲット側) ALBからのingressは許可してる?
SG(ALB側) ターゲットへのegressは許可してる?
(※ 今回のハマりどころ)

いいヘルスチェックってなんだろう

せっかくなので、ヘルスチェックについてもう少し考えてみます。

ヘルスチェックって curl localhost でいいんだっけ?
専用のエンドポイント作った方がいい?
そもそも何をもってHealthyとするのか。

考え出すと意外と沼が深かったりしますよね。

結論をつけるとしたら、「アプリケーションの特性によって最適解は変わる」 になりそうです。

これは逃げでしょうか。いいえ、真理です(強気)

ALBヘルスチェックのパターンで考える

ここでは ALB → TargetGroup → Fargate(Nginx → App) の構成を想定して、ALB・ターゲットグループのヘルスチェックという条件で考えてみます。

      ヘルスチェック
          │     ┌─────────────────────┐
          │     │  Fargate            │
┌─────┐   ↓     │  ┌───────┐  ┌─────┐ │
│ ALB │────────→│─→│ Nginx │─→│ App │ │
└─────┘         │  └───────┘  └─────┘ │
                └─────────────────────┘

パターン1: シンプルな疎通確認

ルートパス(/)やポートへの疎通を確認するシンプルなパターンです。

以下のようなコマンドが挙げられます。

curl -f http://localhost:80/
wget -q --spider http://localhost:80/
nc -z localhost 80
項目 内容
メリット ・シンプルで分かりやすい
・アプリ側の実装不要で既存ページがそのまま使える
デメリット ・DBやキャッシュの状態までは確認できない
・SSRだとレンダリング処理が毎回走るので重い
・パスだけでは一般アクセスと区別がつかずログのノイズになる
用途 ・開発環境
・静的サイト

パターン2: ヘルスチェック専用エンドポイント

ヘルスチェック専用のパスやAPIを用意するパターンです。

curl -f http://localhost:80/health
wget -q --spider http://localhost:80/healthcheck
項目 内容
メリット ・実装次第でDB・Redisなど依存サービスの状態まで確認可能
・SSRのような重い処理を避けられる
・パスでヘルスチェックと判別できるのでログ除外しやすい
デメリット ・専用パスorAPIの実装が必要
・プロキシ側で返す場合、Appが落ちていても気付けない
用途 ・DBやキャッシュに依存するアプリ
・「本当に動いてるか」を確認したい場合

コンテナのヘルスチェックのパターンで考える

次にECSやDockerのコンテナヘルスチェックで使えるパターンも考えてみます。

                     ヘルスチェック
            ┌─────────│───────│─────┐
            │Fargate  ↓       ↓     │
┌─────┐     │    ┌───────┐  ┌─────┐ │
│ ALB │────→│───→│ Nginx │─→│ App │ │
└─────┘     │    └───────┘  └─────┘ │
            └───────────────────────┘

パターン1: プロセス存在確認

プロセスが起動しているかを確認するパターンです。

よく見る ps aux | grep よりも pgrep がシンプルでおすすめです。(最近覚えた)

# よくやっちゃうやつ
ps aux | grep 'node server.js' | grep -v grep

# おすすめ
pgrep -f 'node server.js'
項目 内容
メリット ・HTTPサーバーが不要でも使える
pgrep なら終了コードで判定可能
デメリット ・プロセスがいても応答できるとは限らない
・部分一致で意図しないプロセスにマッチする可能性
用途 ・バックグラウンドワーカー
・非HTTPサービス

パターン2: ソケットファイルの存在確認

Nginx ↔ App 間でUnixソケット通信をしている場合に使えるパターンです。

test コマンドの -S オプションでソケットファイルかどうかを確認できます。

test -S /tmp/sockets/puma.sock
項目 内容
メリット ・HTTPリクエストのオーバーヘッドがない
・プロセス起動を直接確認できる
デメリット ・ソケットファイルがあっても接続できるとは限らない
・ソケット通信を使う構成限定
用途 ・Railsアプリ(Unicorn/Puma)
・Nginx + Gunicorn など

パターン3: 複合チェック

複数の条件を組み合わせて、より確実なヘルスチェックを実現するパターンです。

# ダブルチェック(プロセス存在 & HTTP応答)
pgrep -f 'node server.js' && curl -sf http://localhost:80/health
項目 内容
メリット ・複数の観点で確認できる
・「プロセスはいるけど応答しない」を検知可能
デメリット ・コマンドが複雑になる
・チェック時間が長くなる
用途 ・高可用性が求められる環境
・確実に動作確認したい場合

他にもヘルスチェックに相応しいコマンドはあるかと思います。

これいいよ!というのがありましたら是非教えてください 🤲

まとめ

「なんかヘルスチェック落ちてるんだが」という稀によくある状況を発端に、普段あまり意識しないヘルスチェックについて改めて考えるよい機会となりました。

今回の学びとしてまとめると以下になります。

  1. Terraformで作成したセキュリティグループにはegressルールを忘れるな
    • ALBはターゲットに対してHTTPリクエストを送信するため、アウトバウンド通信が必要
    • マネコンと違い、Terraformではegressを明示的に定義しないとルールがない状態になる
  2. ヘルスチェックは目的に応じて使い分ける
    • 非HTTPサービスには pgrep が便利
    • 複合チェックで信頼性を高めるのもあり

ちょっとしたトラブルから深堀りしてみると、新しい発見があって楽しいですね。

さて、明日12/4(木)は弊社 山田(@saku1335) の記事です!お楽しみに🎅🏼

We’re hiring!

メドレーでは各種エンジニアを絶賛募集中です!
カジュアル面談いつでもWelcomeですので、どうぞお気軽にお問い合わせください🎄✨️

7
0
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
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?