I checked the health check logs supported by ALB by configuring them with CloudFormation

I checked the health check logs supported by ALB by configuring them with CloudFormation

I verified the "health check log" feature of ALB that was added in the November 2025 update. I built an environment using CloudFormation templates to separately store access logs and health check logs, and examined the actual contents of the health check logs output to S3.
2025.11.23

This page has been translated by machine translation. View original

On November 21, 2025, there was an upgrade that made "health check logs" available for Application Load Balancer (ALB).

https://aws.amazon.com/jp/about-aws/whats-new/2025/11/application-load-balancer-health-check-logs/

Have you ever struggled to investigate why an ALB target became "Unhealthy"? Even when trying to check the target application logs, there were often difficult cases where requests weren't reaching the target, or the instance had already been terminated by Auto Scaling, making root cause identification challenging.

This time, I had the opportunity to build a test environment using CloudFormation and check the health check logs output to S3, which I'd like to share with you.

Test Environment

I created an ALB with health check logging enabled. For targets, I set up 2 EC2 instances, with one intentionally configured to fail health checks (web server not started).

Architecture Diagram

AWS Architecture

S3 Bucket Policy

I used a bucket policy to allow ALB logs to be written.

  LogsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref LogsBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSLogDeliveryWrite
            Effect: Allow
            Principal:
              Service: logdelivery.elasticloadbalancing.amazonaws.com
            Action: s3:PutObject
            Resource:
              # Permission for access logs path
              - !Sub '${LogsBucket.Arn}/access-logs/AWSLogs/${AWS::AccountId}/*'
              # Permission for health check logs path
              - !Sub '${LogsBucket.Arn}/health-check-logs/AWSLogs/${AWS::AccountId}/*'

https://dev.classmethod.jp/articles/alb-access-log-s3-bucket-policy-tips/

ALB

I added the new health check log configuration (health_check_logs) in LoadBalancerAttributes.

  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        # --- Health Check Logs Settings ---
        - Key: health_check_logs.s3.enabled
          Value: 'true'
        - Key: health_check_logs.s3.bucket
          Value: !Ref LogsBucket
        - Key: health_check_logs.s3.prefix
          Value: 'health-check-logs'

CloudFormation

Test Environment Creation Template

AWSTemplateFormatVersion: '2010-09-09'
Description: 'ALB Access & Health Check Logs (Separated Prefixes) Test Environment'

Parameters:
  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64

  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Default VPC ID

  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: At least 2 public subnets in different AZs

Resources:
  # S3 Bucket for Logs
  LogsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'alb-logs-${AWS::AccountId}-${AWS::Region}'
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # S3 Bucket Policy
  LogsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref LogsBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSLogDeliveryWrite
            Effect: Allow
            Principal:
              Service: logdelivery.elasticloadbalancing.amazonaws.com
            Action: s3:PutObject
            Resource:
              # アクセスログ用のパス許可
              - !Sub '${LogsBucket.Arn}/access-logs/AWSLogs/${AWS::AccountId}/*'
              # ヘルスチェックログ用のパス許可
              - !Sub '${LogsBucket.Arn}/health-check-logs/AWSLogs/${AWS::AccountId}/*'

  # Security Group for ALB
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ALB
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  # Security Group for EC2
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EC2 instances
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSecurityGroup

  # IAM Role for EC2
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EC2Role

  # EC2 Instance 1 (Healthy)
  EC2Instance1:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t4g.nano
      IamInstanceProfile: !Ref EC2InstanceProfile
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: '0'
          GroupSet: 
            - !Ref EC2SecurityGroup
          SubnetId: !Select [0, !Ref SubnetIds]
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>Healthy Instance 1 (ARM)</h1>" > /var/www/html/index.html
      Tags:
        - Key: Name
          Value: ALB-Target-Healthy

  # EC2 Instance 2 (Unhealthy)
  EC2Instance2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t4g.nano
      IamInstanceProfile: !Ref EC2InstanceProfile
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: '0'
          GroupSet: 
            - !Ref EC2SecurityGroup
          SubnetId: !Select [1, !Ref SubnetIds]
      Tags:
        - Key: Name
          Value: ALB-Target-Unhealthy

  # Target Group
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: alb-logs-test-tg
      Port: 80
      Protocol: HTTP
      VpcId: !Ref VpcId
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      Targets:
        - Id: !Ref EC2Instance1
        - Id: !Ref EC2Instance2

  # Application Load Balancer
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    DependsOn: LogsBucketPolicy
    Properties:
      Name: alb-logs-test
      Type: application
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets: !Ref SubnetIds
      LoadBalancerAttributes:
        # --- Access Logs Settings ---
        - Key: access_logs.s3.enabled
          Value: 'true'
        - Key: access_logs.s3.bucket
          Value: !Ref LogsBucket
        - Key: access_logs.s3.prefix
          Value: 'access-logs'  # フォルダ分け: access-logs/

        # --- Health Check Logs Settings ---
        - Key: health_check_logs.s3.enabled
          Value: 'true'
        - Key: health_check_logs.s3.bucket
          Value: !Ref LogsBucket
        - Key: health_check_logs.s3.prefix
          Value: 'health-check-logs' # フォルダ分け: health-check-logs/

  # Listener
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

Outputs:
  ALBDNSName:
    Description: ALB DNS Name
    Value: !GetAtt ApplicationLoadBalancer.DNSName
  S3BucketName:
    Description: S3 Bucket for Logs
    Value: !Ref LogsBucket

Operational Check

After deployment, I confirmed connectivity to the ALB.

# Set ALB DNS name as a variable
ALB_DNS="<alb-name>-<random-id>.<region>.elb.amazonaws.com"
# Check connectivity
curl http://${ALB_DNS}

Confirming Health Check Log Output

After waiting a few minutes, I checked the S3 destination for health check logs.
Logs were generated under the health-check-logs prefix.

BUCKET_NAME="alb-logs-<account-id>-<region>"
aws s3 ls s3://${BUCKET_NAME}/health-check-logs/ 

Hierarchy image

s3://alb-logs-123456789012-ap-northeast-1/
├── access-logs/AWSLogs/123456789012/elasticloadbalancing/ap-northeast-1/2025/11/22/
└── health-check-logs/AWSLogs/123456789012/elasticloadbalancing/ap-northeast-1/2025/11/22/

※Both health check logs and traditional access logs are output to the same S3 bucket with different prefixes.

I confirmed that health check logs were output in the filename format health_check_log_<account-id>_elasticloadbalancing_<region>_<load-balancer-name>.<load-balancer-id>_<timestamp>_<ip-address>_<random-string>.log.gz.

2025-11-22 15:35:08        362 health-check-logs/AWSLogs/<account-id>/elasticloadbalancing/<region>/2025/11/22/health_check_log_<account-id>_elasticloadbalancing_<region>_app.<alb-name>.<alb-id>_20251122T1535Z_<ip-1>_<random>.log.gz
2025-11-22 15:35:08        358 health-check-logs/AWSLogs/<account-id>/elasticloadbalancing/<region>/2025/11/22/health_check_log_<account-id>_elasticloadbalancing_<region>_app.<alb-name>.<alb-id>_20251122T1535Z_<ip-2>_<random>.log.gz
2025-11-22 15:40:08        357 health-check-logs/AWSLogs/<account-id>/elasticloadbalancing/<region>/2025/11/22/health_check_log_<account-id>_elasticloadbalancing_<region>_app.<alb-name>.<alb-id>_20251122T1540Z_<ip-1>_<random>.log.gz
2025-11-22 15:40:08        357 health-check-logs/AWSLogs/<account-id>/elasticloadbalancing/<region>/2025/11/22/health_check_log_<account-id>_elasticloadbalancing_<region>_app.<alb-name>.<alb-id>_20251122T1540Z_<ip-2>_<random>.log.gz

I downloaded and checked the content of the log files.

# Download and extract log
aws s3 cp s3://<bucket-name>/path/to/your/log.gz .
gzip -d log.gz
cat log

The health check results (success and failure) for both EC2 instances were recorded.

> download: s3://<bucket-name>/health-check-logs/AWSLogs/<account-id>/elasticloadbalancing/<region>/YYYY/MM/DD/health_check_log_<account-id>_elasticloadbalancing_<region>_<alb-name>.<alb-id>_<timestamp>_<ip>_<random>.log.gz to /tmp/hc.log.gz

http 2025-11-22T15:35:10.504741Z 0.001469625 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:35:10.504211Z 0.002958522 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:35:40.535012Z 0.001567201 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:35:40.534548Z 0.002960985 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:36:10.547468Z 0.00154518 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:36:10.547032Z 0.002790372 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:36:40.578086Z 0.001601312 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:36:40.577634Z 0.002930846 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:37:10.608677Z 0.001456432 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:37:10.608229Z 0.00288121 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:37:40.635578Z 0.001517185 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:37:40.635025Z 0.003032974 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:38:10.666143Z 0.001580969 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:38:10.665657Z 0.003204206 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:38:40.695716Z 0.001406524 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:38:40.695266Z 0.00285128 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:39:10.716992Z 0.001626345 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:39:10.716548Z 0.002873815 172.31.x.x:80 <target-group-name> FAIL 502 TargetError
http 2025-11-22T15:39:40.739579Z 0.001600648 172.31.x.x:80 <target-group-name> PASS 200 -
http 2025-11-22T15:39:40.739106Z 0.00284948 172.31.x.x:80 <target-group-name> FAIL 502 TargetError

Log format

Item Value Description
Protocol http Health check protocol
Timestamp 2025-11-22T... Execution time
Processing time 0.001469625 Health check response time
Target 172.31.x.x:80 Target IP address and port
Target Group alb-logs-test-tg Target group name
Status PASS Health check result (PASS/FAIL)
HTTP Code 200 Response code from target
Reason - Failure reason (hyphen when successful)

Summary

Previously, when ALB health checks failed and targets became "Unhealthy," investigations relied on target-side application logs or access logs. However, in cases where resources were exhausted preventing log creation, or instances were replaced by Auto Scaling, it was often difficult to find clues.

With the newly supported health check logs, we can now track the "root cause of failure" from the ALB's perspective in detail. In this test, the TargetError (502) due to the stopped web server was clearly recorded, allowing us to immediately determine that the problem was with the instance's internal components (or application response) rather than the network path.

These health check logs can be valuable for environments with concerns about network connectivity to targets, or for critical workloads where downtime is not allowed and detailed investigation for prevention is required. Please give it a try.

Share this article

FacebookHatena blogX

Related articles