はじめに
こんにちは。SCSKのふくちーぬです。
今回は、Network Load Balancer(NLB)アクセスログをCloudWatch Logsに自動転送する方法をご紹介します。
Network Load Balancer(NLB)のアクセスログは、リスナーがTLSの時のみS3を宛先としてログ保管することができます。一方他の多くのサービスではCloudWatch Logsへの転送をサポートしているため、CloudWatch Logsへログの集約をしている方が多いかと考えます。
そこで今回は、Network Load Balancer(NLB)のアクセスログもCloudWatch Logsに転送することでログの一元的な集約・分析を可能にしていきます。
事前準備
VPC、サブネット、ルートテーブル、インターネットゲートウェイ、ネットワークACL、ACM、ドメインが作成済みであることを確認してください。
アーキテクチャー
- ユーザーは、Network Load Balancer(NLB)を経由でWebサイトにアクセスします。
- Network Load Balancer(NLB)のアクセスログをS3に保存します。
- アクセスログがS3に置かれることをトリガーとして、Lambdaが実行されてCloudWatch Logsに転送します。
本記事ではNetwork Load Balancer(NLB)等の作成をしていますが、ご自身の用途に併せてLambdaとS3のみ利用する等適宜調整してください。
完成したCloudFormationテンプレート
以下のテンプレートを使用して、デプロイします。
AWSTemplateFormatVersion: 2010-09-09 Description: NLB Accesslog push to CWlogs Parameters: ResourceName: Type: String VpcId: Type: String PublicSubnetA: Type: String PublicSubnetC: Type: String AcmArn: Type: String Resources: # ------------------------------------------------------------# # Network Load Balancer # ------------------------------------------------------------# LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: !Sub ${ResourceName}-NLB Subnets: - !Ref PublicSubnetA - !Ref PublicSubnetC Scheme: internet-facing Type: network SecurityGroups: - !Ref NLBSecurityGroup LoadBalancerAttributes: - Key: access_logs.s3.enabled Value: true - Key: access_logs.s3.bucket Value: !Ref MyS3Bucket LoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: TLS DefaultActions: - Type: forward TargetGroupArn: !Ref TargetGroup Certificates: - CertificateArn: !Ref AcmArn TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: !Sub ${ResourceName}-targetgroup VpcId: !Ref VpcId Port: 80 Protocol: TCP TargetType: instance Targets: - Id: !Ref EC2 Port: 80 NLBSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: "0.0.0.0/0" GroupName: !Sub "${ResourceName}-nlb-sg" GroupDescription: !Sub "${ResourceName}-nlb-sg" Tags: - Key: "Name" Value: !Sub "${ResourceName}-nlb-sg" # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# EC2: Type: AWS::EC2::Instance Properties: ImageId: ami-0b193da66bc27147b InstanceType: t2.micro NetworkInterfaces: - AssociatePublicIpAddress: "true" DeviceIndex: "0" SubnetId: !Ref PublicSubnetA GroupSet: - !Ref EC2SecurityGroup UserData: !Base64 | #!/bin/bash yum update -y yum install httpd -y systemctl start httpd systemctl enable httpd touch /var/www/html/index.html echo "Hello,World!" | tee -a /var/www/html/index.html Tags: - Key: Name Value: !Sub "${ResourceName}-ec2" EC2SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !Ref NLBSecurityGroup GroupName: !Sub "${ResourceName}-ec2-sg" GroupDescription: !Sub "${ResourceName}-ec2-sg" Tags: - Key: "Name" Value: !Sub "${ResourceName}-ec2-sg" # ------------------------------------------------------------# # IAM Policy IAM Role # ------------------------------------------------------------# LambdaPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${ResourceName}-lambda-policy Description: IAM Managed Policy with S3 PUT and APIGateway GET Access PolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Action: - 's3:GetObject' Resource: - !Sub "arn:aws:s3:::${ResourceName}-${AWS::AccountId}-bucket/*" LambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${ResourceName}-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !GetAtt LambdaPolicy.PolicyArn # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# MyS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketName: !Sub ${ResourceName}-${AWS::AccountId}-bucket NotificationConfiguration: LambdaConfigurations: - Event: 's3:ObjectCreated:*' Function: !GetAtt PushtoCWlogsFunction.Arn BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref MyS3Bucket PolicyDocument: Version: "2012-10-17" Statement: - Sid: AWSLogDeliveryAclCheck Effect: Allow Principal: Service: delivery.logs.amazonaws.com Action: s3:GetBucketAcl Resource: !Sub "arn:aws:s3:::${ResourceName}-${AWS::AccountId}-bucket" Condition: StringEquals: "aws:SourceAccount": - !Sub ${AWS::AccountId} ArnLike: "aws:SourceArn": - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*" - Sid: AWSLogDeliveryWrite Effect: Allow Principal: Service: delivery.logs.amazonaws.com Action: s3:PutObject Resource: !Sub "arn:aws:s3:::${ResourceName}-${AWS::AccountId}-bucket/AWSLogs/${AWS::AccountId}/*" Condition: StringEquals: "s3:x-amz-acl": "bucket-owner-full-control" "aws:SourceAccount": - !Sub ${AWS::AccountId} ArnLike: "aws:SourceArn": - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*" # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# PushtoCWlogsFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${ResourceName}-lambda-function Role: !GetAtt LambdaRole.Arn Runtime: python3.12 Handler: index.lambda_handler Environment: Variables: LOGGROUPNAME: LGR-NLB-Access-Logs Code: ZipFile: !Sub | import boto3 import time import json import io import gzip import os s3_client = boto3.client('s3') cwlogs_client = boto3.client('logs') #ロググループの指定 LOG_GROUP_NAME = os.environ['LOGGROUPNAME'] #ログフォーマットの変数の指定 fields = ["type","version","time","elb","listener","client:port","destination:port","connection_time","tls_handshake_time", "received_bytes","sent_bytes","incoming_tls_alert","chosen_cert_arn","chosen_cert_serial","tls_cipher","tls_protocol_version", "tls_named_group","domain_name","alpn_fe_protocol","alpn_be_protocol","alpn_client_preference_list","tls_connection_creation_time"] #s3の情報を取得 def parse_event(event): return { "bucket_name": event['Records'][0]['s3']['bucket']['name'], "bucket_key": event['Records'][0]['s3']['object']['key'] } #ログファイルをgzip解凍する def convert_response(response): body = response['Body'].read() body_unzip = gzip.open(io.BytesIO(body)) return body_unzip.read().decode('utf-8') #CWLogsにログを書き込む def put_log(log, LOG_STREAM_NAME): try: response = cwlogs_client.put_log_events( logGroupName = LOG_GROUP_NAME, logStreamName = LOG_STREAM_NAME, logEvents = [ { 'timestamp': int(time.time()) * 1000, 'message' : log }, ] ) return response except cwlogs_client.exceptions.ResourceNotFoundException as e: cwlogs_client.create_log_stream( logGroupName = LOG_GROUP_NAME, logStreamName = LOG_STREAM_NAME) response = put_log(log, LOG_STREAM_NAME) return response except Exception as e: print("error",e) raise #ログをjsonに変換する def transform_json(log_list): data = {} json_data = [] for log_line in log_list: field_item = log_line.split() for field, value in zip(fields, field_item): data[field] = value json_data.append(json.dumps(data)) return json_data def lambda_handler(event, context): bucket_info = parse_event(event) response = s3_client.get_object( Bucket = bucket_info['bucket_name'], Key = bucket_info['bucket_key']) LOG_STREAM_NAME = bucket_info['bucket_key'].split("_")[4] log_text = convert_response(response) log_list = log_text.splitlines() log_data = transform_json(log_list) for log in log_data: response = put_log(log, LOG_STREAM_NAME) return { 'statusCode': 200, } LambdaInvokePermission: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !GetAtt PushtoCWlogsFunction.Arn Action: 'lambda:InvokeFunction' Principal: s3.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' SourceArn: !Sub 'arn:aws:s3:::${ResourceName}-${AWS::AccountId}-bucket' PushtoCWlogsFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${PushtoCWlogsFunction}" NLBAccessLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: LGR-NLB-Access-Logs
Lambdaのコード
Network Load Balancer(NLB)のアクセスログを転送する処理についてPythonファイルを用意しておりますので、ご自身の環境で検証の上ご利用ください。
今回では、CloudFormationテンプレート内にベタ書きしているので特に気にする必要はありません。
import boto3 import time import json import io import gzip import os s3_client = boto3.client('s3') cwlogs_client = boto3.client('logs') #ロググループの指定 LOG_GROUP_NAME = os.environ['LOGGROUPNAME'] #ログフォーマットの変数の指定 fields = ["type","version","time","elb","listener","client:port","destination:port","connection_time","tls_handshake_time", "received_bytes","sent_bytes","incoming_tls_alert","chosen_cert_arn","chosen_cert_serial","tls_cipher","tls_protocol_version", "tls_named_group","domain_name","alpn_fe_protocol","alpn_be_protocol","alpn_client_preference_list","tls_connection_creation_time"] #s3の情報を取得 def parse_event(event): return { "bucket_name": event['Records'][0]['s3']['bucket']['name'], "bucket_key": event['Records'][0]['s3']['object']['key'] } #ログファイルをgzip解凍する def convert_response(response): body = response['Body'].read() body_unzip = gzip.open(io.BytesIO(body)) return body_unzip.read().decode('utf-8') #CWLogsにログを書き込む def put_log(log, LOG_STREAM_NAME): try: response = cwlogs_client.put_log_events( logGroupName = LOG_GROUP_NAME, logStreamName = LOG_STREAM_NAME, logEvents = [ { 'timestamp': int(time.time()) * 1000, 'message' : log }, ] ) return response except cwlogs_client.exceptions.ResourceNotFoundException as e: cwlogs_client.create_log_stream( logGroupName = LOG_GROUP_NAME, logStreamName = LOG_STREAM_NAME) response = put_log(log, LOG_STREAM_NAME) return response except Exception as e: print("error",e) raise #ログをjsonに変換する def transform_json(log_list): data = {} json_data = [] for log_line in log_list: field_item = log_line.split() for field, value in zip(fields, field_item): data[field] = value json_data.append(json.dumps(data)) return json_data def lambda_handler(event, context): bucket_info = parse_event(event) response = s3_client.get_object( Bucket = bucket_info['bucket_name'], Key = bucket_info['bucket_key']) LOG_STREAM_NAME = bucket_info['bucket_key'].split("_")[4] log_text = convert_response(response) log_list = log_text.splitlines() log_data = transform_json(log_list) for log in log_data: response = put_log(log, LOG_STREAM_NAME) return { 'statusCode': 200, }
動作検証
Network Load Balancer(NLB)のDNS名を登録する
カスタムドメインでアクセスできるように、Route53ホストゾーンにてNetwork Load Balancer(NLB)のDNS名のAliasレコードを追加してください。
カスタムドメインでのアクセス
作成されたNetwork Load Balancer(NLB)に向けて、カスタムドメインでアクセスしてください。アクセスを試行することで、アクセスログがS3に保管されます。
そして、S3にオブジェクトがアップロードされたことをトリガーとしてLambdaが起動します。
CloudWatch Logsの確認
Network Load Balancer(NLB)のアクセスログが、JSON形式で格納されていることが分かります。これで、他のサービスと同様の方法でCloudWatch Logs内で分析できます。
最後に
いかがだったでしょうか。
既存のログ監視基盤を利用する等の理由で、AWSサービスのログをCloudWatch Logs内に集約・分析するといった事象が稀に発生します。その際はCloudWatchLogsに転送処理を実施し、引き継ぎCloudWatch Logsをご利用いただければと思います。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥