NLB access logs can now be directly forwarded to CloudWatch Logs

NLB access logs can now be directly forwarded to CloudWatch Logs

NLB logs now support Vended Logs, enabling direct delivery to CloudWatch Logs and Firehose. Structured JSON logs simplify queries, and Live Tail allows real-time log investigation.
2025.11.24

This page has been translated by machine translation. View original

On November 12, 2025, there was an update to Network Load Balancer (NLB) access logs.

Until now, the standard practice for NLB access logs was to "output to S3, wait a few minutes, then analyze with Athena." With this update, direct delivery to CloudWatch Logs, S3, and Kinesis Data Firehose is now supported, similar to CloudFront's standard access logs v2.

Amazon CloudWatch supports logs for Network Load Balancer access logs | AWS What's New

This enables real-time log monitoring using CloudWatch Logs Live Tail, fast query processing with CloudWatch Logs Insights, and the use of structured logs in JSON format.

In this article, I'll introduce my experience testing this new feature (Logs Delivery API) by building a test environment with CloudFormation that delivers logs to four destinations: CloudWatch Logs (JSON/TSV), S3, and Kinesis Data Firehose.

Test Environment

Taking advantage of the specification that allows up to 10 deliveries per delivery source, I verified the following formats simultaneously:

  1. CloudWatch Logs (JSON)
    • Recommended setting. Outputs as structured logs, eliminating the need for parsing when searching with CloudWatch Logs Insights.
  2. CloudWatch Logs (Plain)
    • Traditional TSV (tab-separated) format. Useful when migrating without changing existing log monitoring infrastructure.
  3. Amazon S3 (Plain)
    • Compatible with traditional access log format (GZIP compressed).
  4. Amazon S3 (JSON)
    • New format. Stored in JSONL (newline-delimited JSON) format.
  5. Amazon S3 (W3C)
    • New format. W3C Extended Log File Format.
  6. Amazon S3 (Parquet)
    • New format. Stored in Apache Parquet format. Expect reduced Athena scanning costs and improved performance.
  7. Amazon Kinesis Data Firehose
    • Used for forwarding to third-party products like Splunk or Datadog, or for pre-processing (ETL) before data lake storage.

Architecture Diagram

NLB Access Log Test Configuration

CloudFormation Template

Here's the template used for testing.
Deploying this template will automatically provision the NLB, test EC2, and the seven log delivery patterns mentioned above.

AWS::Logs::Delivery uses DependsOn for sequential processing to avoid errors caused by parallel execution.

  TSVDelivery:
    Type: AWS::Logs::Delivery
    DependsOn: JSONDelivery
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt TSVDeliveryDestination.Arn
  S3DeliveryJSON:
    Type: AWS::Logs::Delivery
    DependsOn: TSVDelivery
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt S3DeliveryDestinationJSON.Arn

Deployment Timeline

Test Environment Creation Template (Click to expand)
  • To use TLS listener, create a certificate with ACM in advance and specify the certificate ARN as a parameter.
  • Assumes operation in a Default VPC environment.

AWSTemplateFormatVersion: '2010-09-09'
Description: 'NLB with all logging destinations (CloudWatch JSON/TSV, S3 x4 formats, Firehose) - Default VPC'

Parameters:
  CertificateArn:
    Type: String
    Description: ARN of the ACM Certificate for HTTPS listener

  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: VPC ID for resources

  Subnet1:
    Type: AWS::EC2::Subnet::Id
    Description: First subnet for NLB

  Subnet2:
    Type: AWS::EC2::Subnet::Id
    Description: Second subnet for NLB

Resources:
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP traffic
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  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

  TestEC2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64}}'
      InstanceType: t4g.nano
      IamInstanceProfile: !Ref EC2InstanceProfile
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<h1>NLB Logging Test - $(hostname)</h1>" > /var/www/html/index.html

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub '${AWS::StackName}-tg'
      Port: 80
      Protocol: TCP
      VpcId: !Ref VpcId
      HealthCheckEnabled: true
      HealthCheckProtocol: TCP
      Targets:
        - Id: !Ref TestEC2
          Port: 80

  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub '${AWS::StackName}-nlb'
      Type: network
      Scheme: internet-facing
      Subnets:
        - !Ref Subnet1
        - !Ref Subnet2

  TLSListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref NetworkLoadBalancer
      Port: 443
      Protocol: TLS
      Certificates:
        - CertificateArn: !Ref CertificateArn
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06

  DeliverySource:
    Type: AWS::Logs::DeliverySource
    Properties:
      Name: !Sub '${AWS::StackName}-source'
      ResourceArn: !Ref NetworkLoadBalancer
      LogType: NLB_ACCESS_LOGS

  NLBLogGroupPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: !Sub '${AWS::StackName}-policy'
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "AllowLogDelivery",
              "Effect": "Allow",
              "Principal": {
                "Service": "delivery.logs.amazonaws.com"
              },
              "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              "Resource": [
                "${JSONLogGroup.Arn}",
                "${TSVLogGroup.Arn}"
              ],
              "Condition": {
                "StringEquals": {
                  "aws:SourceAccount": "${AWS::AccountId}"
                }
              }
            }
          ]
        }

  # 1. CloudWatch Logs (JSON)
  JSONLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/vendedlogs/nlb/${AWS::StackName}/json'
      RetentionInDays: 3

  JSONDeliveryDestination:
    Type: AWS::Logs::DeliveryDestination
    Properties:
      Name: !Sub '${AWS::StackName}-json'
      OutputFormat: json
      DestinationResourceArn: !GetAtt JSONLogGroup.Arn

  JSONDelivery:
    Type: AWS::Logs::Delivery
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt JSONDeliveryDestination.Arn

  # 2. CloudWatch Logs (TSV)
  TSVLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/vendedlogs/nlb/${AWS::StackName}/tsv'
      RetentionInDays: 3

  TSVDeliveryDestination:
    Type: AWS::Logs::DeliveryDestination
    Properties:
      Name: !Sub '${AWS::StackName}-tsv'
      OutputFormat: plain
      DestinationResourceArn: !GetAtt TSVLogGroup.Arn

  TSVDelivery:
    Type: AWS::Logs::Delivery
    DependsOn: JSONDelivery
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt TSVDeliveryDestination.Arn

  S3LogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'nlb-logs-${AWS::StackName}-${AWS::AccountId}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldLogs
            Status: Enabled
            ExpirationInDays: 7

  S3LogBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3LogBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSLogDeliveryWrite
            Effect: Allow
            Principal:
              Service: delivery.logs.amazonaws.com
            Action: s3:PutObject
            Resource: !Sub '${S3LogBucket.Arn}/*'
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control
                aws:SourceAccount: !Ref AWS::AccountId
          - Sid: AWSLogDeliveryAclCheck
            Effect: Allow
            Principal:
              Service: delivery.logs.amazonaws.com
            Action: s3:GetBucketAcl
            Resource: !GetAtt S3LogBucket.Arn
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId

  # 3. S3 (JSON)
  S3DeliveryDestinationJSON:
    Type: AWS::Logs::DeliveryDestination
    DependsOn: S3LogBucketPolicy
    Properties:
      Name: !Sub '${AWS::StackName}-s3-json'
      OutputFormat: json
      DestinationResourceArn: !Sub '${S3LogBucket.Arn}/json'

  S3DeliveryJSON:
    Type: AWS::Logs::Delivery
    DependsOn: TSVDelivery
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt S3DeliveryDestinationJSON.Arn

  # 4. S3 (W3C)
  S3DeliveryDestinationW3C:
    Type: AWS::Logs::DeliveryDestination
    DependsOn: S3LogBucketPolicy
    Properties:
      Name: !Sub '${AWS::StackName}-s3-w3c'
      OutputFormat: w3c
      DestinationResourceArn: !Sub '${S3LogBucket.Arn}/w3c'

  S3DeliveryW3C:
    Type: AWS::Logs::Delivery
    DependsOn: S3DeliveryJSON
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt S3DeliveryDestinationW3C.Arn

  # 5. S3 (Parquet)
  S3DeliveryDestinationParquet:
    Type: AWS::Logs::DeliveryDestination
    DependsOn: S3LogBucketPolicy
    Properties:
      Name: !Sub '${AWS::StackName}-s3-parquet'
      OutputFormat: parquet
      DestinationResourceArn: !Sub '${S3LogBucket.Arn}/parquet'

  S3DeliveryParquet:
    Type: AWS::Logs::Delivery
    DependsOn: S3DeliveryW3C
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt S3DeliveryDestinationParquet.Arn

  # 6. S3 (Plain)
  S3DeliveryDestinationPlain:
    Type: AWS::Logs::DeliveryDestination
    DependsOn: S3LogBucketPolicy
    Properties:
      Name: !Sub '${AWS::StackName}-s3-plain'
      OutputFormat: plain
      DestinationResourceArn: !Sub '${S3LogBucket.Arn}/plain'

  S3DeliveryPlain:
    Type: AWS::Logs::Delivery
    DependsOn: S3DeliveryParquet
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt S3DeliveryDestinationPlain.Arn

  # 7. Firehose
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: FirehoseS3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:ListBucket
                Resource:
                  - !GetAtt S3LogBucket.Arn
                  - !Sub '${S3LogBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - logs:PutLogEvents
                Resource: '*'

  FirehoseLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/kinesisfirehose/${AWS::StackName}'
      RetentionInDays: 1

  FirehoseLogStream:
    Type: AWS::Logs::LogStream
    Properties:
      LogGroupName: !Ref FirehoseLogGroup
      LogStreamName: S3Delivery

  DeliveryStream:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub '${AWS::StackName}-firehose'
      DeliveryStreamType: DirectPut
      S3DestinationConfiguration:
        BucketARN: !GetAtt S3LogBucket.Arn
        Prefix: firehose/
        ErrorOutputPrefix: firehose-errors/
        RoleARN: !GetAtt FirehoseRole.Arn
        BufferingHints:
          IntervalInSeconds: 60
          SizeInMBs: 1
        CompressionFormat: GZIP
        CloudWatchLoggingOptions:
          Enabled: true
          LogGroupName: !Ref FirehoseLogGroup
          LogStreamName: !Ref FirehoseLogStream

  FirehoseDeliveryDestination:
    Type: AWS::Logs::DeliveryDestination
    Properties:
      Name: !Sub '${AWS::StackName}-firehose'
      OutputFormat: json
      DestinationResourceArn: !GetAtt DeliveryStream.Arn

  FirehoseDelivery:
    Type: AWS::Logs::Delivery
    DependsOn: S3DeliveryPlain
    Properties:
      DeliverySourceName: !Ref DeliverySource
      DeliveryDestinationArn: !GetAtt FirehoseDeliveryDestination.Arn

Outputs:
  NLBDNSName:
    Description: NLB DNS Name
    Value: !GetAtt NetworkLoadBalancer.DNSName

  TestCommand:
    Description: Command to generate traffic
    Value: !Sub 'curl -k https://${NetworkLoadBalancer.DNSName}'

  JSONLogGroup:
    Description: CloudWatch Logs (JSON)
    Value: !Ref JSONLogGroup

  TSVLogGroup:
    Description: CloudWatch Logs (TSV)
    Value: !Ref TSVLogGroup

  S3Bucket:
    Description: S3 Bucket (json/, w3c/, parquet/, plain/ prefixes)
    Value: !Ref S3LogBucket

  FirehoseStream:
    Description: Kinesis Data Firehose
    Value: !Ref DeliveryStream

Verification

Using the CloudFormation stack we built, I verified the new NLB log delivery functionality.
After deployment, you can confirm the configured log destinations (CloudWatch Logs, S3, Firehose) from the "Integrations" tab in the NLB console.

NLB Integration Logging

Generating Traffic

I sent curl requests from a test client to the NLB and checked the log output.

for i in {1..10}; do curl -k -s https://nlb-test.elb.ap-northeast-1.amazonaws.com && echo " - Request $i"; sleep 1; done

1. Delivery to CloudWatch Logs

JSON format

When the output format is omitted (or set to json), logs are output in structured JSON format.
This eliminates the need to write complex parse commands when querying with CloudWatch Logs Insights, significantly improving analysis efficiency.

{
    "id": "nlb/app/net/nlb-test/xxxxxxxxxxxxxxxx",
    "timestamp": "2025-11-24T08:00:00.123Z",
    "version": "1.0",
    "type": "tcp",
    "listener_arn": "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:listener/net/nlb-test/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx",
    "client_ip": "203.0.113.10",
    "client_port": 54321,
    "target_ip": "10.0.1.50",
    "target_port": 80,
    "connection_status": "success",
    "bytes_received": 150,
    "bytes_sent": 500
}

Plain format (TSV compatible)

By setting the output format to plain with a field separator of \t (tab), logs are output in the traditional space-separated (TSV compatible) format.

Comparing the TSV logs output with the new feature and the conventional S3 output (legacy method), I confirmed that the field count and order match. This is useful when you want to minimize the impact on existing log analysis infrastructure.

Current TSV log (field breakdown)

 1: tls
 2: 2.0
 3: 2025-11-23T18:23:09
 4: net/nlb-auto-tokyo/xxxxxxxxxxxxxxxx
 5: xxxxxxxxxxxxxxxx
 6: 203.0.113.10:57510
 7: 172.31.xx.xx:443
 8: 752
 9: 379
10: 238
...

Reference: Legacy log (field breakdown)

 1: tls
 2: 2.0
 3: 2020-04-01T08:51:42
 4: net/my-network-loadbalancer/xxxxxxxxxxxxxxxx
 5: xxxxxxxxxxxxxxxx
 6: 72.21.xx.xx:51341
 7: 172.100.xx.xx:443
 ...

2. Delivery to Amazon S3

For S3 delivery, I verified output in different formats for each specified prefix in the bucket.

Plain format

Logs in the same format as traditional access logs were output in GZIP compressed format.

S3 URI
s3://nlb-logs-123456789012/plain/AWSLogs/.../xxxxxxxx.log.gz

Log sample

tls 2.0 2025-11-24T07:00:20 net/nlb-test/xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx ...

JSON format

Saved as newline-delimited "JSONL" format, compressed with GZIP.
The JSON structure of each line was equivalent to the content stored in CloudWatch Logs.

S3 URI
s3://nlb-logs-123456789012/json/AWSLogs/.../xxxxxxxx.log.gz

W3C format

In this test configuration, I confirmed that the output was the same as the plain specification with tab separators.

S3 URI
s3://nlb-logs-123456789012/w3c/AWSLogs/.../xxxxxxxx.log.gz

Parquet format

Output in Apache Parquet format. Using S3 Select to run queries, I was able to retrieve structured data equivalent to the JSON configuration.
Cost reduction and performance improvements can be expected with analytics tools that support Parquet format, such as Athena.

S3 URI
s3://nlb-logs-123456789012/parquet/AWSLogs/.../xxxxxxxx.log.parquet

S3 Select query result sample

{
  "type": "tls",
  "version": "2.0",
  "time": "2025-11-24T07:00:20",
  "elb": "net/nlb-test/xxxxxxxxxxxxxxxx",
  ...
}

3. Delivery to Kinesis Data Firehose

I used Firehose with default settings. I confirmed that files saved to the S3 destination were GZIP compressed files with newline-delimited JSON (JSONL). This enables real-time log transfer to third-party monitoring platforms like Splunk or Datadog.

S3 URI
s3://nlb-logs-123456789012/firehose/2025/11/24/07/nlb-test-firehose-1-2025-11-24-07-01-04-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.gz

Consider using Firehose if you need ETL processing in addition to long-term access log storage, such as filtering logs to keep, extracting log items, optimizing storage formats, and managing partitions.

Important Considerations

When using the new method (Logs Delivery API), log delivery charges (Vended Logs fees) vary depending on the destination.

Delivery to S3 is free (unless Parquet conversion is performed), but please note that usage-based charges apply when delivering to CloudWatch Logs or Firehose.

Use Cases for "Legacy Settings"

The traditional "legacy settings" remain a valid option if "S3 is sufficient for logs and conversion to JSON or Parquet format is unnecessary," or if you want to keep your CloudFormation description simple.

  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      # ...
      LoadBalancerAttributes:
        # Traditional settings (free delivery)
        - Key: access_logs.s3.enabled
          Value: 'true'
        - Key: access_logs.s3.bucket
          Value: !Ref LogsBucket

Summary

Now that NLB logs can be sent to CloudWatch Logs, we can expect a dramatic improvement in initial incident investigation (such as checking errors with Live Tail).

For regular use, enable log output to S3 only with any output format.
For periods when real-time log investigation is necessary, enabling CloudWatch Logs output temporarily can provide cost-effective usage.

Consider the balance between cost and convenience when selecting the appropriate log output destination for your requirements.

Share this article

FacebookHatena blogX

Related articles