Route 53 ResolverのクエリログをGrafana Cloudに送りたい
概要
Route 53 ResolverのクエリログはAmazon Data Firehoseに出力できます。
Amazon Data FirehoseはHTTPエンドポイントにデータを配信できるので、Grafana Lokiにクエリログを送ることができます。
本記事では、VPC LambdaでDNSクエリログを発生させ、Grafana Cloud上のLokiに送付してみます。

今回の構成
前提
Grafana CloudのLokiに書き込むために以下の準備をします。
LokiのHTTPエンドポイントとユーザーID確認
Grafana Cloudで以下のURLを参考にHTTPエンドポイントとユーザーを確認します。
HTTPエンドポイントは記載されている値を加工して使います。
| 項目 | 設定値 |
|---|---|
| 記載されている値 | https://logs-prod-012.grafana.net |
| HTTPエンドポイント | https://aws-logs-prod-012.grafana.net/aws-logs/api/v1/push |
ユーザーIDは、Grafana Cloudの画面上でUserと表示されている6桁の値です。(2025/12/19時点)
例:345450
ログの書き込み権限設定
Grafana CloudのLokiにログを書き込むためのトークンを以下のURLを参考に取得します。
用意したCFnテンプレート
今回のテスト環境は、VPC LambdaでDNSクエリを発生させ、VPC内のRoute 53 Resolverに問い合わせを実施します。
このRoute 53 ResolverのクエリログをAmazon Data Firehoseに送付する構成のため、VPC Lambda自体はインターネットへの疎通が不要であり、NAT GatewayやInternet Gatewayを作っていません。
CFnテンプレートのデプロイ時には以下のパラメータを設定する必要があります。
| キー | バリュー | 備考 |
|---|---|---|
| HTTPEndpointURL | LokiのHTTPエンドポイント | 例:https://aws-logs-prod-012.grafana.net/aws-logs/api/v1/push |
| LogsInstanceID | ユーザーID |
LokiのHTTPエンドポイントとユーザー確認で確認した値。 例:345450 |
| LogsWriteToken | ログ書き込み権限のトークン | ログの書き込み権限設定で取得したトークン |
CFnテンプレート全体
CFnテンプレート全体
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Route 53 Resolver Query Logs to Firehose to HTTP Endpoint (minimal)'
Parameters:
HTTPEndpointURL:
Type: String
Description: 'HTTP endpoint URL to send logs to'
LogsInstanceID:
Description: Grafana Loki instance ID.
Type: String
LogsWriteToken:
Description: Grafana Cloud token used to write to Loki.
Type: String
NoEcho: true
BackupBucketName:
Type: String
Description: 'S3 bucket for Firehose backup (required by AWS)'
Default: 'route53-resolver-backup'
TestDomains:
Type: CommaDelimitedList
Default: 'example.com,example.net,example.org'
Description: 'Comma-separated domains to resolve for test queries'
ScheduleExpression:
Type: String
Default: 'rate(5 minutes)'
Description: 'EventBridge schedule for periodic query tests'
VPCCidr:
Type: String
Default: '10.0.0.0/16'
Description: 'CIDR for the new VPC'
PrivateSubnetCidr:
Type: String
Default: '10.0.10.0/24'
Description: 'CIDR for private subnet'
Resources:
# ==================== IAM Roles ====================
FirehoseRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: firehose.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: FirehoseS3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 's3:PutObject'
- 's3:GetObject'
- 's3:ListBucket'
Resource:
- !GetAtt BackupBucket.Arn
- !Sub '${BackupBucket.Arn}/*'
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: LambdaLogs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
# ==================== VPC (private-only, no NAT/IGW) ====================
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: route53-resolver-vpc
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PrivateSubnetCidr
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: route53-resolver-private-a
# ==================== Security Group for Lambda ====================
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Egress-only security group for resolver test Lambda'
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
# ==================== Backup S3 Bucket ====================
BackupBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${BackupBucketName}-${AWS::AccountId}'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# ==================== VPC Lambda for Resolver Query Tests ===================
QueryTesterFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: route53-resolver-query-tester
Runtime: python3.12
Architectures:
- arm64
Handler: index.handler
MemorySize: 128
Timeout: 30
Role: !GetAtt LambdaExecutionRole.Arn
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet
Environment:
Variables:
DOMAINS: !Join [',', !Ref TestDomains]
Code:
ZipFile: |
import os
import socket
def handler(event, context):
domains = os.environ.get('DOMAINS', 'example.com').split(',')
results = []
for name in domains:
name = name.strip()
if not name:
continue
try:
socket.getaddrinfo(name, 80)
results.append({'domain': name, 'status': 'ok'})
except Exception as exc: # noqa: BLE001
results.append({'domain': name, 'status': 'error', 'error': str(exc)})
return {'results': results}
QueryTesterSchedule:
Type: AWS::Events::Rule
Properties:
Name: route53-resolver-query-tester-schedule
ScheduleExpression: !Ref ScheduleExpression
State: ENABLED
Targets:
- Arn: !GetAtt QueryTesterFunction.Arn
Id: QueryTesterTarget
QueryTesterInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref QueryTesterFunction
Action: 'lambda:InvokeFunction'
Principal: events.amazonaws.com
SourceArn: !GetAtt QueryTesterSchedule.Arn
# ==================== Data Firehose ====================
QueryLogDeliveryStream:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
DeliveryStreamName: route53-resolver-to-http
DeliveryStreamType: DirectPut
HttpEndpointDestinationConfiguration:
EndpointConfiguration:
Url: !Ref HTTPEndpointURL
AccessKey: !Sub '${LogsInstanceID}:${LogsWriteToken}'
Name: HTTPEndpoint
BufferingHints:
SizeInMBs: 5
IntervalInSeconds: 60
RetryOptions:
DurationInSeconds: 3600
RoleARN: !GetAtt FirehoseRole.Arn
S3BackupMode: FailedDataOnly
S3Configuration:
BucketARN: !GetAtt BackupBucket.Arn
RoleARN: !GetAtt FirehoseRole.Arn
# ==================== Route 53 Resolver Query Logging ====================
QueryLoggingConfig:
Type: AWS::Route53Resolver::ResolverQueryLoggingConfig
Properties:
DestinationArn: !GetAtt QueryLogDeliveryStream.Arn
Name: route53-resolver-query-logs
QueryLoggingConfigAssociation:
Type: AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation
Properties:
ResolverQueryLogConfigId: !GetAtt QueryLoggingConfig.Id
ResourceId: !Ref VPC
Outputs:
DeliveryStreamArn:
Description: 'Kinesis Data Firehose Delivery Stream ARN'
Value: !GetAtt QueryLogDeliveryStream.Arn
DeliveryStreamName:
Description: 'Kinesis Data Firehose Delivery Stream Name'
Value: !Ref QueryLogDeliveryStream
QueryLogConfigId:
Description: 'Route 53 Resolver Query Logging Config ID'
Value: !GetAtt QueryLoggingConfig.Id
BackupBucketName:
Description: 'S3 Backup Bucket Name'
Value: !Ref BackupBucket
QueryTesterFunctionName:
Description: 'Lambda function for periodic resolver query tests'
Value: !Ref QueryTesterFunction
VpcId:
Description: 'Created VPC ID'
Value: !Ref VPC
PrivateSubnetId:
Description: 'Created Private Subnet ID'
Value: !Ref PrivateSubnet
CFnテンプレートのデプロイ
以下のようにパラメータを指定してCFnテンプレートをデプロイします。
CFnでIAMロールもデプロイするために --capabilities CAPABILITY_NAMED_IAM を指定しています。
--parameter-overrides に指定しているパラメータは例示のため、環境に応じた値に修正が必要です。
aws cloudformation deploy \
--template-file route53-resolver-firehose.yaml \
--stack-name route53-resolver-firehose \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
HTTPEndpointURL=https://aws-logs-prod-XXX.grafana.net/aws-logs/api/v1/push \
LogsInstanceID=123456 \
LogsWriteToken=glc_eyJ***** #ログ書き込み権限のトークン
Grafana Cloudでの確認
Grafana CloudのExploreでLogを確認できました。
オブザーバビリティーの可視化にGrafana Cloudを使っている場合でも、Route 53 Resolverのクエリログも結構簡単に集約できそうです。

Grafana CloudのExplore
Discussion