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'
テンプレートの特徴:
- 本番対応設計: 環境別設定、アラーム、DLQ
- セキュリティ強化: 最小権限、暗号化、条件付きアクセス
- 運用性向上: タグ付け、ライフサイクル、監視
- コスト最適化: 環境別リソース制限、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ライセンスキーの取得:
- NewRelic管理画面にログイン
- Account settings → API keys
- 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 でのデプロイ:
- CloudFormation コンソールを開く
- 「スタックの作成」 → 「新しいリソースを使用」
-
テンプレートの指定:
- 「テンプレートファイルのアップロード」を選択
-
autofirehose-template.yamlをアップロード
-
スタックの詳細を指定:
- スタック名:
autofirehose-infrastructure-dev - パラメータを入力(上記参照)
- スタック名:
-
スタックオプションの設定:
- タグ:
Environment=dev,Purpose=AutoFirehose
- タグ:
-
確認:
- 「AWS CloudFormation によって IAM リソースが作成される場合があることを承認します」にチェック
- 作成実行
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."
期待される結果:
- Lambda実行ログ: "Processing event: TagLogGroup" が表示
-
Firehose作成:
autofirehose-test-autofirehose-demo-*が作成 - サブスクリプションフィルター: 対象ロググループに設定済み
- 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 自動化 監視 オブザーバビリティ インフラ