0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudWatchからNewRelicへのログ連携を半自動化する「AutoFirehose」の実装

はじめに

CloudWatchからNewRelicへのログ連携作業、手動でやっていませんか?

複数のAWSアカウント(30以上)でNewRelicを運用している中で、ログ連携の手動作業が大きなボトルネックになっていました。1件あたり10-15分かかる作業を、タグ付与だけで30秒に短縮できる「AutoFirehose」という仕組みを開発したので、実装方法と効果を共有します。

この記事で学べること:

  • CloudWatchログの自動NewRelic連携
  • EventBridge + Lambda + Firehoseの実装パターン
  • 本番運用可能なCloudFormationテンプレート
  • コスト最適化とセキュリティ考慮事項

対象読者:

  • NewRelicでログ監視を行っている方
  • AWS運用の自動化を検討している方
  • 複数アカウント環境でのログ管理に課題を感じている方

前提条件・必要な準備

実装前に必要な環境:

AWS環境

# 必要なサービスの確認
aws cloudtrail describe-trails  # CloudTrail有効化必須
aws sts get-caller-identity     # 適切なIAM権限

NewRelic環境

  • NewRelicアカウント
  • Ingest License Key(40文字)の取得
  • ログ取り込み制限の確認

必要な権限

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:*",
        "lambda:*",
        "events:*",
        "firehose:*",
        "logs:*",
        "iam:*",
        "s3:*",
        "ssm:*"
      ],
      "Resource": "*"
    }
  ]
}

課題:手動ログ連携の限界と影響

従来の作業フロー

具体的な問題点と影響

項目 詳細
作業時間 1件あたり10-15分
対象アカウント 30以上
設定漏れリスク 手動作業によるミス
スケーラビリティ 新サービス増加に追従困難

AutoFirehose:解決策の全体像

コンセプト

「タグを付けるだけで自動的にNewRelicへログ連携」

この単純な操作で、複雑な設定作業を完全自動化します。

今回はautofirehose:enabledというタグをロググループに付与した場合の例をご紹介します。

システムアーキテクチャ

技術スタック

コンポーネント 役割 選定理由
CloudTrail API呼び出し検知 全AWSアクションを確実にキャプチャ
EventBridge イベントルーティング 柔軟なフィルタリングとスケーラビリティ
Lambda 自動化ロジック サーバーレスで運用コスト最小
Kinesis Data Firehose ログ配信 NewRelic連携の標準的な方法
S3 バックアップストレージ 配信失敗時のデータ保護

実装詳細:段階的な構築手順

Phase 1: EventBridge設定

CloudTrailイベントの検知設定:

設定ルール例:

{
  "Rules": [
    {
      "Name": "AutoFirehoseRule",
      "Description": "CloudWatch Logs作成・タグ付与イベントを検知",
      "EventPattern": {
        "source": ["aws.logs"],
        "detail-type": ["AWS API Call via CloudTrail"],
        "detail": {
          "eventSource": ["logs.amazonaws.com"],
          "eventName": ["CreateLogGroup", "TagLogGroup"]
        }
      },
      "Targets": [
        {
          "Id": "AutoFirehoseLambda",
          "Arn": "arn:aws:lambda:REGION:ACCOUNT:function:autofirehose-handler"
        }
      ]
    }
  ]
}

設定コマンド例:

# EventBridgeルール作成
aws events put-rule \
  --name AutoFirehoseRule \
  --event-pattern file://event-pattern.json \
  --description "AutoFirehose trigger rule"

# Lambda関数をターゲットに追加
aws events put-targets \
  --rule AutoFirehoseRule \
  --targets Id=1,Arn=arn:aws:lambda:us-east-1:123456789012:function:autofirehose-handler

Phase 2: Lambda関数実装

設定コード例:

コード(クリックして展開)
import boto3
import json
import logging
import os
from botocore.exceptions import ClientError

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    """
    AutoFirehose メイン処理
    CloudTrailイベントを受信してAutoFirehoseを設定
    
    Args:
        event: EventBridgeからのCloudTrailイベント
        context: Lambda実行コンテキスト
    
    Returns:
        dict: 処理結果
    """
    try:
        # CloudTrailイベント解析
        detail = event.get('detail', {})
        event_name = detail.get('eventName')
        request_params = detail.get('requestParameters', {})
        
        logger.info(f"Processing event: {event_name}")
        logger.info(f"Event detail: {json.dumps(detail, default=str)}")
        
        # ロググループ名取得
        log_group_name = request_params.get('logGroupName')
        if not log_group_name:
            logger.warning("logGroupName not found in event")
            return {"status": "skipped", "reason": "no_log_group_name"}
        
        # タグ確認(重要:EventBridgeでフィルタリングしないため)
        if not check_autofirehose_tag(log_group_name):
            logger.info(f"autofirehose:enabled tag not found for {log_group_name}")
            return {"status": "skipped", "reason": "tag_not_found"}
        
        # 冪等性チェック(重複実行防止)
        if check_existing_firehose(log_group_name):
            logger.info(f"DataFirehose already exists for {log_group_name}")
            return {"status": "already_configured"}
        
        # DataFirehose作成
        firehose_name = create_firehose_delivery_stream(log_group_name)
        
        # サブスクリプションフィルター設定
        setup_subscription_filter(log_group_name, firehose_name)
        
        logger.info(f"AutoFirehose setup completed for {log_group_name}")
        return {
            "status": "success", 
            "firehose_name": firehose_name,
            "log_group_name": log_group_name
        }
        
    except Exception as e:
        logger.error(f"Error in AutoFirehose: {str(e)}", exc_info=True)
        # 本番環境では適切なエラーハンドリングを実装
        # 例:SNS通知、CloudWatchアラーム等
        raise

def check_autofirehose_tag(log_group_name):
    """
    ロググループのautofirehose:enabledタグを確認
    
    Args:
        log_group_name (str): 確認対象のロググループ名
    
    Returns:
        bool: タグが存在し、値がtrueの場合True
    """
    try:
        logs_client = boto3.client('logs')
        
        # ロググループの詳細取得
        response = logs_client.describe_log_groups(
            logGroupNamePrefix=log_group_name,
            limit=1
        )
        
        if not response.get('logGroups'):
            logger.warning(f"Log group {log_group_name} not found")
            return False
        
        log_group = response['logGroups'][0]
        
        # 完全一致確認(プレフィックス検索のため)
        if log_group['logGroupName'] != log_group_name:
            logger.warning(f"Exact match not found for {log_group_name}")
            return False
        
        # タグ情報取得(新しいAPI使用)
        try:
            tags_response = logs_client.list_tags_for_resource(
                resourceArn=log_group.get('arn')
            )
            tags = tags_response.get('tags', {})
            
            # autofirehose:enabled タグの確認
            tag_value = tags.get('autofirehose:enabled', '').lower()
            return tag_value == 'true'
            
        except ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'ResourceNotFoundException':
                logger.info(f"No tags found for {log_group_name}")
                return False
            elif error_code == 'AccessDeniedException':
                logger.error(f"Access denied when checking tags for {log_group_name}")
                return False
            raise
            
    except Exception as e:
        logger.error(f"Error checking tags for {log_group_name}: {str(e)}")
        return False

def check_existing_firehose(log_group_name):
    """
    既存DataFirehose確認(冪等性保証)
    
    Args:
        log_group_name (str): ロググループ名
    
    Returns:
        bool: 既存のFirehoseが存在する場合True
    """
    try:
        firehose_client = boto3.client('firehose')
        delivery_stream_name = generate_firehose_name(log_group_name)
        
        response = firehose_client.describe_delivery_stream(
            DeliveryStreamName=delivery_stream_name
        )
        
        # ストリームが存在し、アクティブ状態の場合
        status = response['DeliveryStreamDescription']['DeliveryStreamStatus']
        logger.info(f"Existing Firehose status: {status}")
        return status in ['ACTIVE', 'CREATING']
        
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code == 'ResourceNotFoundException':
            return False
        logger.error(f"Error checking existing firehose: {str(e)}")
        raise

def create_firehose_delivery_stream(log_group_name):
    """
    DataFirehose作成(NewRelic向け)
    
    Args:
        log_group_name (str): ロググループ名
    
    Returns:
        str: 作成されたFirehose配信ストリーム名
    """
    try:
        firehose_client = boto3.client('firehose')
        delivery_stream_name = generate_firehose_name(log_group_name)
        
        # 環境変数から設定取得(CloudFormationで設定)
        account_id = os.environ.get('AWS_ACCOUNT_ID', 
                                   boto3.client('sts').get_caller_identity()['Account'])
        region = os.environ.get('AWS_REGION', 
                               boto3.Session().region_name)
        backup_bucket = os.environ.get('BACKUP_BUCKET')
        firehose_role_arn = os.environ.get('FIREHOSE_ROLE_ARN')
        
        # NewRelic設定(正しい構造)
        http_endpoint_config = {
            'EndpointConfiguration': {
                'Url': get_newrelic_endpoint_url(),
                'Name': 'NewRelic',
                'AccessKey': get_newrelic_license_key()
            },
            'RoleARN': firehose_role_arn or f'arn:aws:iam::{account_id}:role/firehose-delivery-role',
            'RequestConfiguration': {
                'ContentEncoding': 'GZIP',
                'CommonAttributes': {
                    'logtype': 'cloudwatch-logs',
                    'source': 'aws-firehose',
                    'environment': os.environ.get('ENVIRONMENT', 'prod')
                }
            },
            'BufferingHints': {
                'SizeInMBs': int(os.environ.get('BUFFER_SIZE_MB', '1')),
                'IntervalInSeconds': int(os.environ.get('BUFFER_INTERVAL_SEC', '60'))
            },
            'RetryOptions': {
                'DurationInSeconds': 3600
            },
            'S3BackupMode': 'FailedDataOnly',
            'S3Configuration': {
                'RoleARN': firehose_role_arn or f'arn:aws:iam::{account_id}:role/firehose-delivery-role',
                'BucketARN': f'arn:aws:s3:::{backup_bucket or f"autofirehose-backup-{account_id}"}',
                'Prefix': f'failed-logs/{log_group_name.replace("/", "-").lstrip("-")}/',
                'BufferingHints': {
                    'SizeInMBs': 5,
                    'IntervalInSeconds': 300
                },
                'CompressionFormat': 'GZIP'
            }
        }
        
        response = firehose_client.create_delivery_stream(
            DeliveryStreamName=delivery_stream_name,
            DeliveryStreamType='DirectPut',
            HttpEndpointDestinationConfiguration=http_endpoint_config
        )
        
        logger.info(f"Created Firehose delivery stream: {delivery_stream_name}")
        return delivery_stream_name
        
    except Exception as e:
        logger.error(f"Error creating Firehose delivery stream: {str(e)}")
        raise

def setup_subscription_filter(log_group_name, firehose_name):
    """
    サブスクリプションフィルター設定
    
    Args:
        log_group_name (str): ロググループ名
        firehose_name (str): Firehose配信ストリーム名
    """
    try:
        logs_client = boto3.client('logs')
        
        # 環境変数から設定取得
        account_id = os.environ.get('AWS_ACCOUNT_ID', 
                                   boto3.client('sts').get_caller_identity()['Account'])
        region = os.environ.get('AWS_REGION', 
                               boto3.Session().region_name)
        cwl_role_arn = os.environ.get('CWL_ROLE_ARN')
        
        # 正しいFirehose ARN形式
        firehose_arn = f"arn:aws:firehose:{region}:{account_id}:deliverystream/{firehose_name}"
        
        # 既存のサブスクリプションフィルター確認
        existing_filters = logs_client.describe_subscription_filters(
            logGroupName=log_group_name
        )
        
        filter_name = f"autofirehose-{firehose_name}"
        
        # 既存フィルターがある場合はスキップ
        for filter_info in existing_filters.get('subscriptionFilters', []):
            if filter_info['filterName'] == filter_name:
                logger.info(f"Subscription filter already exists: {filter_name}")
                return
        
        logs_client.put_subscription_filter(
            logGroupName=log_group_name,
            filterName=filter_name,
            filterPattern="",  # 全ログを対象(必要に応じて調整)
            destinationArn=firehose_arn,
            roleArn=cwl_role_arn or f"arn:aws:iam::{account_id}:role/CWLtoKinesisFirehoseRole"
        )
        
        logger.info(f"Created subscription filter for {log_group_name}")
        
    except Exception as e:
        logger.error(f"Error creating subscription filter: {str(e)}")
        raise

def generate_firehose_name(log_group_name):
    """
    FireHose名生成(命名規則に従う)
    
    Args:
        log_group_name (str): ロググループ名
    
    Returns:
        str: 生成されたFirehose名
    """
    # ロググループ名から安全な名前を生成
    safe_name = log_group_name.replace('/', '-').replace('_', '-').lstrip('-')
    
    # 先頭に文字を追加(数字で始まることを防ぐ)
    if safe_name and safe_name[0].isdigit():
        safe_name = f"lg-{safe_name}"
    
    # 64文字制限(Firehoseの制限)
    firehose_name = f"autofirehose-{safe_name}"[:64]
    
    # 末尾のハイフンを削除
    return firehose_name.rstrip('-')

def get_newrelic_license_key():
    """
    NewRelicライセンスキー取得(SSM Parameter Store)
    
    Returns:
        str: NewRelicライセンスキー
    """
    try:
        ssm_client = boto3.client('ssm')
        parameter_name = os.environ.get('NEWRELIC_LICENSE_PARAM', 
                                       '/newrelic/license-key')
        
        response = ssm_client.get_parameter(
            Name=parameter_name,
            WithDecryption=True
        )
        return response['Parameter']['Value']
        
    except Exception as e:
        logger.error(f"Error getting NewRelic license key: {str(e)}")
        raise

def get_newrelic_endpoint_url():
    """
    NewRelicエンドポイントURL取得(リージョン対応)
    
    Returns:
        str: NewRelicログAPIエンドポイント
    """
    # 環境変数で指定可能(デフォルトはUS)
    endpoint = os.environ.get('NEWRELIC_ENDPOINT', 'https://log-api.newrelic.com/log/v1')
    
    # リージョン別エンドポイント(必要に応じて拡張)
    region_endpoints = {
        'eu': 'https://log-api.eu.newrelic.com/log/v1',
        'us': 'https://log-api.newrelic.com/log/v1'
    }
    
    newrelic_region = os.environ.get('NEWRELIC_REGION', 'us')
    return region_endpoints.get(newrelic_region, endpoint)

⚠️ 運用時の注意点:

  • Lambda関数のタイムアウトは300秒に設定
  • 同時実行数制限を考慮(デフォルト1000)
  • CloudWatchログでの動作監視が重要

Phase 3: CloudFormationテンプレート

設定テンプレート例:

テンプレート(クリックして展開)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AutoFirehose Infrastructure - CloudWatch Logs to NewRelic automation'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "NewRelic Configuration"
        Parameters:
          - NewRelicLicenseKey
          - NewRelicRegion
      - Label:
          default: "Environment Configuration"
        Parameters:
          - Environment
          - BufferSizeMB
          - BufferIntervalSec
    ParameterLabels:
      NewRelicLicenseKey:
        default: "NewRelic Ingest License Key"
      NewRelicRegion:
        default: "NewRelic Region (us/eu)"

Parameters:
  NewRelicLicenseKey:
    Type: String
    NoEcho: true
    Description: 'NewRelic Ingest License Key (40 characters)'
    MinLength: 32
    MaxLength: 40
    AllowedPattern: '^[a-zA-Z0-9]+$'
    ConstraintDescription: 'Must be a valid NewRelic license key'
  
  NewRelicRegion:
    Type: String
    Default: 'us'
    AllowedValues: ['us', 'eu']
    Description: 'NewRelic region for log endpoint'
  
  Environment:
    Type: String
    Default: 'dev'
    AllowedValues: ['dev', 'staging', 'prod']
    Description: 'Environment name for resource naming and tagging'
  
  BufferSizeMB:
    Type: Number
    Default: 1
    MinValue: 1
    MaxValue: 128
    Description: 'Firehose buffer size in MB (1-128)'
  
  BufferIntervalSec:
    Type: Number
    Default: 60
    MinValue: 60
    MaxValue: 900
    Description: 'Firehose buffer interval in seconds (60-900)'

Conditions:
  IsProdEnvironment: !Equals [!Ref Environment, 'prod']

Resources:
  # S3バケット(Firehoseバックアップ用)
  AutoFirehoseBackupBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'autofirehose-backup-${AWS::AccountId}-${Environment}'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldBackups
            Status: Enabled
            ExpirationInDays: !If [IsProdEnvironment, 90, 30]
          - Id: TransitionToIA
            Status: Enabled
            Transition:
              StorageClass: STANDARD_IA
              TransitionInDays: 30
      NotificationConfiguration:
        CloudWatchConfigurations:
          - Event: s3:ObjectCreated:*
            CloudWatchConfiguration:
              LogGroupName: !Sub '/aws/s3/${AWS::StackName}-backup-notifications'
      Tags:
        - Key: Environment
          Value: !Ref Environment
        - Key: Purpose
          Value: AutoFirehose-Backup

  # Lambda実行ロール
  AutoFirehoseLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'AutoFirehoseLambdaRole-${Environment}-${AWS::Region}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AutoFirehosePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Firehose操作権限(最小権限)
              - Effect: Allow
                Action:
                  - firehose:CreateDeliveryStream
                  - firehose:DescribeDeliveryStream
                  - firehose:ListDeliveryStreams
                Resource: !Sub 'arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/autofirehose-*'
              # CloudWatch Logs操作権限
              - Effect: Allow
                Action:
                  - logs:DescribeLogGroups
                  - logs:ListTagsForResource
                  - logs:PutSubscriptionFilter
                  - logs:DescribeSubscriptionFilters
                Resource: 
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*'
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:destination:*'
              # SSM Parameter Store読み取り
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource: !Ref NewRelicLicenseKeyParameter
              # STS(アカウントID取得用)
              - Effect: Allow
                Action:
                  - sts:GetCallerIdentity
                Resource: '*'
      Tags:
        - Key: Environment
          Value: !Ref Environment

  # Firehose配信ロール
  FirehoseDeliveryRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'firehose-delivery-role-${Environment}-${AWS::Region}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                'sts:ExternalId': !Ref 'AWS::AccountId'
      Policies:
        - PolicyName: FirehoseDeliveryPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # S3バックアップ権限
              - Effect: Allow
                Action:
                  - s3:AbortMultipartUpload
                  - s3:GetBucketLocation
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:ListBucketMultipartUploads
                  - s3:PutObject
                Resource:
                  - !GetAtt AutoFirehoseBackupBucket.Arn
                  - !Sub '${AutoFirehoseBackupBucket.Arn}/*'
              # CloudWatch Logs権限
              - Effect: Allow
                Action:
                  - logs:PutLogEvents
                Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/*'

  # CloudWatch Logs → Firehose ロール
  CWLtoFirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'CWLtoKinesisFirehoseRole-${Environment}-${AWS::Region}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CWLtoFirehosePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - firehose:PutRecord
                  - firehose:PutRecordBatch
                Resource: !Sub 'arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/autofirehose-*'

  # Lambda関数
  AutoFirehoseLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub 'autofirehose-handler-${Environment}'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt AutoFirehoseLambdaRole.Arn
      Timeout: 300
      MemorySize: 256
      ReservedConcurrencyLimit: !If [IsProdEnvironment, 100, 10]
      Environment:
        Variables:
          ENVIRONMENT: !Ref Environment
          BACKUP_BUCKET: !Ref AutoFirehoseBackupBucket
          FIREHOSE_ROLE_ARN: !GetAtt FirehoseDeliveryRole.Arn
          CWL_ROLE_ARN: !GetAtt CWLtoFirehoseRole.Arn
          NEWRELIC_LICENSE_PARAM: !Ref NewRelicLicenseKeyParameter
          NEWRELIC_REGION: !Ref NewRelicRegion
          BUFFER_SIZE_MB: !Ref BufferSizeMB
          BUFFER_INTERVAL_SEC: !Ref BufferIntervalSec
      DeadLetterQueue:
        TargetArn: !GetAtt AutoFirehoseDLQ.Arn
      Code:
        ZipFile: |
          # 本番環境では上記の完全なPythonコードを使用
          # デプロイ後にupdate-function-codeで更新
          import json
          def lambda_handler(event, context):
              return {"status": "placeholder", "message": "Update function code after deployment"}
      Tags:
        - Key: Environment
          Value: !Ref Environment

  # Dead Letter Queue
  AutoFirehoseDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub 'autofirehose-dlq-${Environment}'
      MessageRetentionPeriod: 1209600  # 14 days
      Tags:
        - Key: Environment
          Value: !Ref Environment

  # EventBridgeルール
  AutoFirehoseEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub 'AutoFirehoseRule-${Environment}'
      Description: 'Trigger AutoFirehose on CloudWatch Logs events'
      EventPattern:
        source: ["aws.logs"]
        detail-type: ["AWS API Call via CloudTrail"]
        detail:
          eventSource: ["logs.amazonaws.com"]
          eventName: ["CreateLogGroup", "TagLogGroup"]
      State: ENABLED
      Targets:
        - Arn: !GetAtt AutoFirehoseLambda.Arn
          Id: AutoFirehoseLambdaTarget
          RetryPolicy:
            MaximumRetryAttempts: 3
          DeadLetterConfig:
            Arn: !GetAtt AutoFirehoseDLQ.Arn

  # Lambda実行権限
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref AutoFirehoseLambda
      Principal: events.amazonaws.com
      SourceArn: !GetAtt AutoFirehoseEventRule.Arn

  # NewRelicライセンスキー保存
  NewRelicLicenseKeyParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub '/newrelic/license-key-${Environment}'
      Type: SecureString
      Value: !Ref NewRelicLicenseKey
      Description: !Sub 'NewRelic Ingest License Key for AutoFirehose (${Environment})'
      Tags:
        Environment: !Ref Environment
        Purpose: AutoFirehose

  # CloudWatch Alarms
  LambdaErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProdEnvironment
    Properties:
      AlarmName: !Sub 'AutoFirehose-Lambda-Errors-${Environment}'
      AlarmDescription: 'AutoFirehose Lambda function errors'
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref AutoFirehoseLambda
      TreatMissingData: notBreaching

  LambdaDurationAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProdEnvironment
    Properties:
      AlarmName: !Sub 'AutoFirehose-Lambda-Duration-${Environment}'
      AlarmDescription: 'AutoFirehose Lambda function duration'
      MetricName: Duration
      Namespace: AWS/Lambda
      Statistic: Average
      Period: 300
      EvaluationPeriods: 2
      Threshold: 30000  # 30 seconds
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref AutoFirehoseLambda

Outputs:
  LambdaFunctionArn:
    Description: 'AutoFirehose Lambda Function ARN'
    Value: !GetAtt AutoFirehoseLambda.Arn
    Export:
      Name: !Sub '${AWS::StackName}-LambdaArn'
  
  S3BackupBucket:
    Description: 'S3 Backup Bucket for failed deliveries'
    Value: !Ref AutoFirehoseBackupBucket
    Export:
      Name: !Sub '${AWS::StackName}-BackupBucket'
  
  FirehoseRoleArn:
    Description: 'Firehose Delivery Role ARN'
    Value: !GetAtt FirehoseDeliveryRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-FirehoseRole'
  
  CWLRoleArn:
    Description: 'CloudWatch Logs to Firehose Role ARN'
    Value: !GetAtt CWLtoFirehoseRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-CWLRole'
  
  EventRuleArn:
    Description: 'EventBridge Rule ARN'
    Value: !GetAtt AutoFirehoseEventRule.Arn
    Export:
      Name: !Sub '${AWS::StackName}-EventRule'

テンプレートの特徴:

  1. 本番対応設計: 環境別設定、アラーム、DLQ
  2. セキュリティ強化: 最小権限、暗号化、条件付きアクセス
  3. 運用性向上: タグ付け、ライフサイクル、監視
  4. コスト最適化: 環境別リソース制限、S3ライフサイクル

実装手順:ステップバイステップガイド

Step 1: 事前準備と環境確認

必要な情報の収集:

# 1. AWSアカウント情報確認
aws sts get-caller-identity
# 出力例: {"UserId": "...", "Account": "123456789012", "Arn": "..."}

# 2. CloudTrail状態確認(必須)
aws cloudtrail describe-trails --query 'trailList[*].[Name,IsMultiRegionTrail,IncludeGlobalServiceEvents]' --output table
# CloudTrailが無い場合は作成が必要

# 3. 現在のリージョン確認
aws configure get region
# または
echo $AWS_DEFAULT_REGION

# 4. IAM権限確認
aws iam simulate-principal-policy \
  --policy-source-arn $(aws sts get-caller-identity --query Arn --output text) \
  --action-names cloudformation:CreateStack \
  --resource-arns "*"

NewRelicライセンスキーの取得:

  1. NewRelic管理画面にログイン
  2. Account settings → API keys
  3. Ingest - License をコピー(40文字の英数字)

⚠️ 重要な確認事項:

  • CloudTrailが有効でない場合、イベント検知できません
  • NewRelicの月間ログ取り込み制限を確認してください
  • 本実装により以下のAWS料金が発生します:
    • Lambda実行料金
    • Firehose配信料金
    • S3ストレージ料金
    • EventBridge料金

Step 2: CloudFormationデプロイ

パラメータファイルの作成:

# parameters.json を作成
cat > autofirehose-params.json << 'EOF'
[
  {
    "ParameterKey": "NewRelicLicenseKey",
    "ParameterValue": "YOUR_40_CHAR_NEWRELIC_LICENSE_KEY"
  },
  {
    "ParameterKey": "NewRelicRegion",
    "ParameterValue": "us"
  },
  {
    "ParameterKey": "Environment",
    "ParameterValue": "dev"
  },
  {
    "ParameterKey": "BufferSizeMB",
    "ParameterValue": "1"
  },
  {
    "ParameterKey": "BufferIntervalSec",
    "ParameterValue": "60"
  }
]
EOF

# セキュリティ:ファイル権限を制限
chmod 600 autofirehose-params.json

AWS CLI でのデプロイ:

# 1. テンプレートの検証
aws cloudformation validate-template \
  --template-body file://autofirehose-template.yaml

# 2. スタック作成
aws cloudformation create-stack \
  --stack-name autofirehose-infrastructure-dev \
  --template-body file://autofirehose-template.yaml \
  --parameters file://autofirehose-params.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --tags Key=Environment,Value=dev Key=Purpose,Value=AutoFirehose

# 3. デプロイ進行状況の監視
aws cloudformation describe-stack-events \
  --stack-name autofirehose-infrastructure-dev \
  --query 'StackEvents[?ResourceStatus==`CREATE_IN_PROGRESS`].[Timestamp,ResourceType,ResourceStatus]' \
  --output table

# 4. 完了確認(5-10分程度)
aws cloudformation wait stack-create-complete \
  --stack-name autofirehose-infrastructure-dev

# 5. 出力値の確認
aws cloudformation describe-stacks \
  --stack-name autofirehose-infrastructure-dev \
  --query 'Stacks[0].Outputs' \
  --output table

AWS Management Console でのデプロイ:

  1. CloudFormation コンソールを開く
  2. 「スタックの作成」「新しいリソースを使用」
  3. テンプレートの指定:
    • 「テンプレートファイルのアップロード」を選択
    • autofirehose-template.yaml をアップロード
  4. スタックの詳細を指定:
    • スタック名: autofirehose-infrastructure-dev
    • パラメータを入力(上記参照)
  5. スタックオプションの設定:
    • タグ: Environment=dev, Purpose=AutoFirehose
  6. 確認:
    • 「AWS CloudFormation によって IAM リソースが作成される場合があることを承認します」にチェック
  7. 作成実行

Step 3: Lambda関数コードの更新

コードをデプロイ:

# 1. Lambda関数コードファイルの作成
# 上記のPython完全版コードを index.py として保存

# 2. デプロイパッケージ作成
zip -r autofirehose-function.zip index.py

# 3. Lambda関数コード更新
aws lambda update-function-code \
  --function-name autofirehose-handler-dev \
  --zip-file fileb://autofirehose-function.zip

# 4. 更新確認
aws lambda get-function \
  --function-name autofirehose-handler-dev \
  --query 'Configuration.[FunctionName,Runtime,Handler,LastModified]' \
  --output table

# 5. 環境変数確認
aws lambda get-function-configuration \
  --function-name autofirehose-handler-dev \
  --query 'Environment.Variables' \
  --output table

Step 4: 動作確認とテスト

段階的テスト手順:

# 1. インフラの確認
echo "=== CloudFormation Stack Status ==="
aws cloudformation describe-stacks \
  --stack-name autofirehose-infrastructure-dev \
  --query 'Stacks[0].[StackStatus,CreationTime]' \
  --output table

# 2. Lambda関数の確認
echo "=== Lambda Function Status ==="
aws lambda get-function \
  --function-name autofirehose-handler-dev \
  --query 'Configuration.[State,LastUpdateStatus]' \
  --output table

# 3. EventBridge ルールの確認
echo "=== EventBridge Rule Status ==="
aws events describe-rule \
  --name AutoFirehoseRule-dev \
  --query '[Name,State,EventPattern]' \
  --output table

# 4. テスト用ロググループ作成
echo "=== Creating Test Log Group ==="
aws logs create-log-group \
  --log-group-name "/test/autofirehose-demo-$(date +%s)"

# 5. タグ付与(AutoFirehose起動トリガー)
LOG_GROUP_NAME="/test/autofirehose-demo-$(date +%s)"
aws logs create-log-group --log-group-name "$LOG_GROUP_NAME"

aws logs tag-log-group \
  --log-group-name "$LOG_GROUP_NAME" \
  --tags autofirehose:enabled=true

echo "Test log group created: $LOG_GROUP_NAME"

# 6. Lambda実行ログ確認(2-3分後)
echo "=== Checking Lambda Execution Logs ==="
sleep 180  # 3分待機

aws logs filter-log-events \
  --log-group-name "/aws/lambda/autofirehose-handler-dev" \
  --start-time $(date -d '5 minutes ago' +%s)000 \
  --filter-pattern "Processing event" \
  --query 'events[*].[eventId,message]' \
  --output table

# 7. Firehose作成確認
echo "=== Checking Created Firehose Streams ==="
aws firehose list-delivery-streams \
  --limit 10 \
  --query 'DeliveryStreamNames[?contains(@, `autofirehose`)]' \
  --output table

# 8. サブスクリプションフィルター確認
echo "=== Checking Subscription Filters ==="
aws logs describe-subscription-filters \
  --log-group-name "$LOG_GROUP_NAME" \
  --query 'subscriptionFilters[*].[filterName,destinationArn]' \
  --output table

# 9. テストログ送信
echo "=== Sending Test Log ==="
aws logs put-log-events \
  --log-group-name "$LOG_GROUP_NAME" \
  --log-stream-name "test-stream" \
  --log-events timestamp=$(date +%s)000,message="AutoFirehose test log message"

echo "Test completed. Check NewRelic for log delivery in 2-3 minutes."

期待される結果:

  1. Lambda実行ログ: "Processing event: TagLogGroup" が表示
  2. Firehose作成: autofirehose-test-autofirehose-demo-* が作成
  3. サブスクリプションフィルター: 対象ロググループに設定済み
  4. NewRelic: 2-3分後にログが表示

使用方法:日常運用での活用

基本的な使い方

新しいサービスでの利用:

# 1. 通常通りロググループ作成
aws logs create-log-group \
  --log-group-name "/aws/lambda/my-new-service"

# 2. AutoFirehoseタグを付与(これだけで自動連携開始)
aws logs tag-log-group \
  --log-group-name "/aws/lambda/my-new-service" \
  --tags autofirehose:enabled=true

# 3. 動作確認(オプション)
aws logs describe-subscription-filters \
  --log-group-name "/aws/lambda/my-new-service"

Terraform での利用:

resource "aws_cloudwatch_log_group" "application" {
  name              = "/aws/lambda/my-application"
  retention_in_days = 14

  tags = {
    Environment           = "production"
    "autofirehose:enabled" = "true"  # この1行で自動連携
  }
}

# Lambda関数作成時に同時設定
resource "aws_lambda_function" "app" {
  filename         = "app.zip"
  function_name    = "my-application"
  role            = aws_iam_role.lambda_role.arn
  handler         = "index.handler"
  runtime         = "python3.11"

  depends_on = [
    aws_cloudwatch_log_group.application,  # ログ自動連携を保証
  ]
}

CDK での利用:

import * as logs from 'aws-cdk-lib/aws-logs';
import * as lambda from 'aws-cdk-lib/aws-lambda';

// ログ自動連携付きのLambda関数
const logGroup = new logs.LogGroup(this, 'AppLogGroup', {
  logGroupName: '/aws/lambda/my-app',
  retention: logs.RetentionDays.TWO_WEEKS,
});

// AutoFirehoseタグ付与
cdk.Tags.of(logGroup).add('autofirehose:enabled', 'true');

const lambdaFunction = new lambda.Function(this, 'MyApp', {
  runtime: lambda.Runtime.PYTHON_3_11,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  logGroup: logGroup,  // 明示的にログ関連付け
});

効果・結果:定量的・定性的な改善

定量的効果

項目 導入前 導入後
設定時間 10-15分/件 30秒/件
運用工数 月25時間 月1時間
設定漏れ 月1-2件 0件
新サービス対応 手動依頼→作業 即日自動(非インフラチームも対応可能)

定性的効果

🎯 開発チームの自律性向上

Before:

開発者 → インフラチーム依頼 → 手動設定 → 確認 → 完了
(所要時間: 1-2日)

After:

開発者 → タグ付与 → 自動設定完了
(所要時間: 30秒)

🛡️ 監視網羅性の向上

  • 設定漏れゼロ: 手動作業によるミスを完全排除
  • 一貫性確保: 全アカウント・全サービスで統一された設定

🚀 開発速度の向上

  • ボトルネック解消: インフラチーム待ちの解消
  • CI/CD統合: デプロイパイプラインに組み込み可能
  • スケーラビリティ: サービス数増加に自動対応

📊 運用品質の向上

  • 標準化: 設定の統一とベストプラクティス適用
  • 可視性: 全ログの一元管理

トラブルシューティング:よくある問題と解決方法

問題1: CloudTrailイベントが検知されない

症状:

# タグ付与してもLambdaが実行されない
aws logs tag-log-group --log-group-name "/test/demo" --tags autofirehose:enabled=true
# → Lambda実行ログに何も表示されない

原因と解決方法:

# 1. CloudTrail状態確認
aws cloudtrail describe-trails \
  --query 'trailList[*].[Name,IsLogging,IncludeGlobalServiceEvents]' \
  --output table

# CloudTrailが無効の場合
aws cloudtrail start-logging --name YOUR_TRAIL_NAME

# CloudTrailが存在しない場合
aws cloudtrail create-trail \
  --name autofirehose-trail \
  --s3-bucket-name your-cloudtrail-bucket \
  --include-global-service-events \
  --is-multi-region-trail

# 2. EventBridge ルール確認
aws events describe-rule --name AutoFirehoseRule-dev
# State が ENABLED であることを確認

# 3. Lambda権限確認
aws lambda get-policy --function-name autofirehose-handler-dev
# EventBridgeからの実行権限があることを確認

問題2: Lambda関数がタイムアウトする

症状:

Task timed out after 300.00 seconds

解決方法:

# 1. タイムアウト時間を延長
aws lambda update-function-configuration \
  --function-name autofirehose-handler-dev \
  --timeout 600

# 2. メモリサイズを増加(処理速度向上)
aws lambda update-function-configuration \
  --function-name autofirehose-handler-dev \
  --memory-size 512

# 3. 同時実行数制限確認
aws lambda get-function-concurrency \
  --function-name autofirehose-handler-dev

# 4. CloudWatchログで詳細確認
aws logs filter-log-events \
  --log-group-name "/aws/lambda/autofirehose-handler-dev" \
  --filter-pattern "Task timed out" \
  --start-time $(date -d '1 hour ago' +%s)000

問題3: NewRelic接続エラー

症状:

Error creating Firehose delivery stream: An error occurred (InvalidArgumentException)

診断と解決:

# 1. ライセンスキー確認
aws ssm get-parameter \
  --name "/newrelic/license-key-dev" \
  --with-decryption \
  --query 'Parameter.Value' \
  --output text | wc -c
# 40文字であることを確認

# 2. NewRelicエンドポイント確認
# US: https://log-api.newrelic.com/log/v1
# EU: https://log-api.eu.newrelic.com/log/v1

# 3. 手動テスト
curl -X POST "https://log-api.newrelic.com/log/v1" \
  -H "Content-Type: application/json" \
  -H "Api-Key: YOUR_LICENSE_KEY" \
  -d '{"message":"test","timestamp":1640995200000}'

# 4. Firehose IAMロール確認
aws iam get-role-policy \
  --role-name firehose-delivery-role-dev \
  --policy-name FirehoseDeliveryPolicy

問題4: S3バックアップにログが蓄積される

症状:
NewRelicにログが届かず、S3バックアップバケットにファイルが蓄積

診断手順:

# 1. S3バックアップ状況確認
aws s3 ls s3://autofirehose-backup-ACCOUNT-dev/failed-logs/ --recursive

# 2. Firehose配信状況確認
aws firehose describe-delivery-stream \
  --delivery-stream-name autofirehose-test-demo \
  --query 'DeliveryStreamDescription.Destinations[0].HttpEndpointDestinationDescription'

# 3. CloudWatchメトリクス確認
aws cloudwatch get-metric-statistics \
  --namespace AWS/KinesisFirehose \
  --metric-name DeliveryToHttpEndpoint.Records \
  --dimensions Name=DeliveryStreamName,Value=autofirehose-test-demo \
  --start-time $(date -d '1 hour ago' --iso-8601) \
  --end-time $(date --iso-8601) \
  --period 300 \
  --statistics Sum

# 4. エラーログ確認
aws logs filter-log-events \
  --log-group-name "/aws/kinesisfirehose/autofirehose-test-demo" \
  --filter-pattern "ERROR" \
  --start-time $(date -d '1 hour ago' +%s)000

問題5: 大量ログによるコスト増加

症状:
予想以上のAWS料金請求

コスト最適化:

# 1. 現在のコスト確認
aws ce get-cost-and-usage \
  --time-period Start=2024-01-01,End=2024-01-31 \
  --granularity MONTHLY \
  --metrics BlendedCost \
  --group-by Type=DIMENSION,Key=SERVICE

# 2. Firehose使用量確認
aws cloudwatch get-metric-statistics \
  --namespace AWS/KinesisFirehose \
  --metric-name IncomingRecords \
  --start-time $(date -d '7 days ago' --iso-8601) \
  --end-time $(date --iso-8601) \
  --period 86400 \
  --statistics Sum

# 3. 不要なロググループの除外
# 高頻度・大容量ログの特定
aws logs describe-log-groups \
  --query 'logGroups[?storedBytes>`10000000`].[logGroupName,storedBytes]' \
  --output table

# 4. バッファサイズ最適化
aws lambda update-function-configuration \
  --function-name autofirehose-handler-dev \
  --environment Variables='{
    "BUFFER_SIZE_MB":"5",
    "BUFFER_INTERVAL_SEC":"300"
  }'

問題6: 権限エラー

症状:

AccessDenied: User is not authorized to perform: firehose:CreateDeliveryStream

権限確認と修正:

# 1. 現在の権限確認
aws iam simulate-principal-policy \
  --policy-source-arn $(aws sts get-caller-identity --query Arn --output text) \
  --action-names firehose:CreateDeliveryStream \
  --resource-arns "*"

# 2. Lambda実行ロールの権限確認
aws iam list-attached-role-policies \
  --role-name AutoFirehoseLambdaRole-dev

# 3. 必要に応じて権限追加
aws iam attach-role-policy \
  --role-name AutoFirehoseLambdaRole-dev \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

デバッグ用コマンド集

包括的な状態確認スクリプト:

#!/bin/bash
# AutoFirehose 診断スクリプト

echo "=== AutoFirehose Diagnostic Report ==="
echo "Generated: $(date)"
echo

# 基本情報
echo "1. Basic Information"
echo "Account ID: $(aws sts get-caller-identity --query Account --output text)"
echo "Region: $(aws configure get region)"
echo

# CloudFormation
echo "2. CloudFormation Stack"
aws cloudformation describe-stacks \
  --stack-name autofirehose-infrastructure-dev \
  --query 'Stacks[0].[StackStatus,CreationTime]' \
  --output table 2>/dev/null || echo "Stack not found"
echo

# Lambda Function
echo "3. Lambda Function"
aws lambda get-function \
  --function-name autofirehose-handler-dev \
  --query 'Configuration.[State,LastUpdateStatus,Runtime,Timeout]' \
  --output table 2>/dev/null || echo "Function not found"
echo

# EventBridge Rule
echo "4. EventBridge Rule"
aws events describe-rule \
  --name AutoFirehoseRule-dev \
  --query '[State,EventPattern]' \
  --output table 2>/dev/null || echo "Rule not found"
echo

# Recent Lambda Executions
echo "5. Recent Lambda Executions (last 1 hour)"
aws logs filter-log-events \
  --log-group-name "/aws/lambda/autofirehose-handler-dev" \
  --start-time $(date -d '1 hour ago' +%s)000 \
  --filter-pattern "Processing event" \
  --query 'events[*].[eventId,message]' \
  --output table 2>/dev/null || echo "No recent executions"
echo

# Firehose Streams
echo "6. AutoFirehose Delivery Streams"
aws firehose list-delivery-streams \
  --query 'DeliveryStreamNames[?contains(@, `autofirehose`)]' \
  --output table 2>/dev/null || echo "No streams found"
echo

echo "=== End of Report ==="

このスクリプトを実行することで、AutoFirehoseの全体的な状態を迅速に把握できます。

まとめ

AutoFirehoseにより、CloudWatchからNewRelicへのログ連携を半自動化できました。

技術的なポイント:

  • CloudTrailイベント駆動による即座な反応
  • タグベースの直感的な制御
  • StackSetsによるマルチアカウント対応

運用面での効果:

  • 96%の時間短縮(10-15分 → 30秒)
  • ヒューマンエラーの排除(完全ではないが)
  • 開発チームの自律性向上

同様の課題を抱えている方の参考になれば幸いです。NewRelicでのログ監視をより効率的に活用していきましょう!


関連タグ:
AWS NewRelic CloudWatch DataFirehose Lambda EventBridge 自動化 監視 オブザーバビリティ インフラ

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?