[AWS CDK] CloudFrontの標準ログV2をL1 Constructで設定してみたかった
カスタムリソースを使わずにCloudFrontの標準ログV2の設定をしたい
こんにちは、のんピ(@non____97)です。
皆さんはカスタムリソースを使わずにCloudFrontの標準ログV2の設定をしたいなと思ったことはありますか? 私はあります。
以下記事でカスタムリソースを使ってCloudFrontの標準ログV2を設定する方法を紹介しました。
こちらの記事を書いてから気づいたのですが、L1 Constructでも標準ログV2の設定ができるようです。
使用するL1 Constructはaws_logs.CfnDeliverySourceやaws_logs.CfnDeliveryDestinationなどと、サービスがCloudWatch Logsなのがポイントです。
実際に試してみたので紹介します。
いきなりまとめ
- CfnDeliveryの
s3SuffixPath
は冪等性がないため、カスタムリソースを使うのが良さそう- 新規作成時は自動的に
AWSLogs/{account-id}/CloudFront/
が付与されるが、更新時は付与されない - カスタムリソースを使用せずにリソースが新規作成されるのか、更新されるのかをAWS CDKのコード内で判断する方法が無いように思える
- 新規作成時は自動的に
やってみた
検証環境
検証環境は全てAWS CDKでデプロイしました。使用したコードは以下GitHubリポジトリに保存しています。
こちらのベースとなったコードの詳細な説明は以下記事をご覧ください。
L1 Constructを用いた標準ログV2の設定
L1 Constructを用いた標準ログV2の設定の箇所を抜粋すると以下のとおりです。
// CloudFront Standard Log V2
if (props.enableLogAnalytics.includes("cloudFrontStandardLogV2")) {
// Remove CloudFront Standard Log Legacy
const cfnDistribution = this.distribution.node
.defaultChild as cdk.aws_cloudfront.CfnDistribution;
cfnDistribution.addPropertyDeletionOverride("DistributionConfig.Logging");
const logPrefix = this.getStandardLogV2Prefix(props.logFilePrefix);
props.cloudFrontAccessLogBucketConstruct.bucket.addToResourcePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ["s3:PutObject"],
effect: cdk.aws_iam.Effect.ALLOW,
principals: [
new cdk.aws_iam.ServicePrincipal("delivery.logs.amazonaws.com"),
],
resources: [
`${props.cloudFrontAccessLogBucketConstruct.bucket.bucketArn}/${logPrefix.awsLogObjectPrefix}*`,
],
conditions: {
StringEquals: {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceAccount": cdk.Stack.of(this).account,
},
ArnLike: {
"aws:SourceArn": `arn:aws:logs:${cdk.Stack.of(this).region}:${
cdk.Stack.of(this).account
}:delivery-source:cf-${this.distribution.distributionId}`,
},
},
})
);
const cloudFrontStandardLogDeliverySourceName = `cf-${this.distribution.distributionId}`;
const cloudFrontStandardLogDeliveryDestinationName = `cf-${this.distribution.distributionId}-s3`;
const cloudFrontStandardLogDeliverySource =
new cdk.aws_logs.CfnDeliverySource(
this,
"CloudFrontStandardLogDeliverySource",
{
name: cloudFrontStandardLogDeliverySourceName,
resourceArn: this.distribution.distributionArn,
logType: "ACCESS_LOGS",
}
);
const cloudFrontStandardLogDeliveryDestination =
new cdk.aws_logs.CfnDeliveryDestination(
this,
"CloudFrontStandardLogDeliveryDestination",
{
name: cloudFrontStandardLogDeliveryDestinationName,
outputFormat: "parquet",
destinationResourceArn:
props.cloudFrontAccessLogBucketConstruct.bucket.bucketArn,
}
);
new cdk.custom_resources.AwsCustomResource(
this,
"CloudFrontStandardLogDelivery",
{
logRetention: cdk.aws_logs.RetentionDays.ONE_WEEK,
serviceTimeout: cdk.Duration.seconds(180),
timeout: cdk.Duration.seconds(120),
installLatestAwsSdk: true,
onCreate: {
action: "createDelivery",
parameters: {
deliverySourceName: cloudFrontStandardLogDeliverySource.name,
deliveryDestinationArn:
cloudFrontStandardLogDeliveryDestination.attrArn,
s3EnableHiveCompatiblePath: false,
s3DeliveryConfiguration: {
enableHiveCompatiblePath: false,
suffixPath: logPrefix.logPrefix.split(
logPrefix.awsLogObjectPrefix
)[1],
},
},
physicalResourceId:
cdk.custom_resources.PhysicalResourceId.fromResponse(
"delivery.id"
),
service: "CloudWatchLogs",
},
onUpdate: {
action: "updateDeliveryConfiguration",
parameters: {
id: new cdk.custom_resources.PhysicalResourceIdReference(),
s3EnableHiveCompatiblePath: false,
s3DeliveryConfiguration: {
enableHiveCompatiblePath: true,
suffixPath: logPrefix.logPrefix,
},
},
service: "CloudWatchLogs",
},
onDelete: {
action: "deleteDelivery",
parameters: {
id: new cdk.custom_resources.PhysicalResourceIdReference(),
},
service: "CloudWatchLogs",
},
policy: cdk.custom_resources.AwsCustomResourcePolicy.fromStatements([
new cdk.aws_iam.PolicyStatement({
actions: [
"logs:CreateDelivery",
"logs:DeleteDelivery",
"logs:UpdateDeliveryConfiguration",
],
resources: ["*"],
}),
]),
}
);
}
はい、お気づきになられたと思いますがcdk.aws_logs.CfnDelivery
を使用せずにカスタムリソースを使っています。
これはCfnDelivery
もとい、AWS::Logs::Delivery
のS3SuffixPath
に冪等性がないためです。
前回の記事で消化しているように、CreateDelivery
でプレフィックスを指定する/しないに関わらずAWSLogs/{account-id}/CloudFront/
が必ず付与されます。一方、更新処理であるUpdateDeliveryConfiguration
をする際はAWSLogs/{account-id}/CloudFront/
が付与されません。
AWS公式ドキュメントには、プレフィックスの指定有無でAWSLogs/{account-id}/CloudFront/
が付与されるように紹介されていますが、記事投稿のタイミングで私が確認したところ、プレフィックスを指定した場合でも付与されました。
If you specified a prefix for your S3 bucket, your logs appear under that path. If you don't specify a prefix, CloudFront will automatically append the AWSLogs/<account-ID>/CloudFront prefix for you.
Example: Bucket with a prefix
If you specify the following bucket name with a prefix: amzn-s3-demo-bucket.s3.amazonaws.com/MyLogPrefix
Your logs will appear under the following path: amzn-s3-demo-bucket.s3.amazonaws.com/MyLogPrefix/logs
Example: Bucket without a prefix
If you specify the bucket name only: amzn-s3-demo-bucket.s3.amazonaws.com
Your logs will appear under the following path: amzn-s3-demo-bucket.s3.amazonaws.com/AWSLogs/123456789012/CloudFront/logs
これによって何が起きるのかというと、リソースの作成時と更新時でプレフィックスを変更していなくとも、プレフィックスが変更されてしまいます。
例えばプレフィックスに{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
した場合、リソースが作成されるタイミングではAWSLogs/{account-id}/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
が設定され、リソースが更新されるタイミングでは{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
となります。
- リソースの新規作成後
- リソースの更新後
では、「最初からAWSLogs/{account-id}/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
をプレフィックスとして指定すれば良いのでは」となるところです、これはProvided suffixPath contains reference(s) to invalid fields. Please consult documentation for a list of valid fields for log type.
とエラーになります。どうやら{account-id}
はユーザーからの入力としては受け付けないようです。
また、AWSLogs/<AWSアカウントID>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
をプレフィックスとして指定すると、リソース新規作成時はAWSLogs/{account-id}/CloudFront/AWSLogs/<AWSアカウントID>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
となってしまいます。
そこで考えるのがリソースの新規作成と更新処理とでS3SuffixPath
に指定する値を変更することです。
例えば、リソースの新規作成時にはAWSLogs/{account-id}/CloudFront/
が補完されるため{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
を指定し、リソースの更新時にはAWSLogs/{account-id}/CloudFront/
が付与されないため、AWSLogs/<AWSアカウントID>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
を指定するといった具合です。
肝心なリソースの新規作成なのか更新処理なのかを判断する方法ですが、AWS CDKのコード上でのみ判定を行うとなるとカスタムリソースを使うしかないと考えています。(良い方法があれば教えてください)
cdk.out/manifest.json
やcdk.out/WebsiteStack.template.json
から、判定する方法も試してみたのですが、これらファイルはcdk deploy
だけではなく、cdk diff
やcdk synth
などシンセサイズのタイミングでも更新が走るため、不適切です。
例えば、以下のようにConstructパスが現時点のcdk.out/manifest.json
内に存在するか否かで判定する方法は以下のとおりです。
const cloudFrontStandardLogDelivery = new cdk.aws_logs.CfnDelivery(
this,
"CloudFrontStandardLogDelivery",
{
deliverySourceName: cloudFrontStandardLogDeliverySource.name,
deliveryDestinationArn:
cloudFrontStandardLogDeliveryDestination.attrArn,
s3EnableHiveCompatiblePath: false,
s3SuffixPath: logPrefix.logPrefix.split(
logPrefix.awsLogObjectPrefix
)[1],
}
);
// Constructパスが現時点の`cdk.out/manifest.json`内に存在する = 既にリソースが存在する = 更新処理と判断し、プレフィックス内の AWSLogs/<AWSアカウントID>/CloudFront がある状態で渡す
if (this.resourceExists(cloudFrontStandardLogDelivery)) {
cloudFrontStandardLogDelivery.s3SuffixPath = logPrefix.logPrefix;
}
console.log(
`cloudFrontStandardLogDelivery.s3SuffixPath : ${cloudFrontStandardLogDelivery.s3SuffixPath}`
);
}
}
private resourceExists(resource: cdk.CfnResource): boolean {
try {
const manifestPath = path.join("cdk.out", "manifest.json");
if (!fs.existsSync(manifestPath)) {
console.log(
"manifest.json does not exist. This might be the first deployment."
);
return false;
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const resourcePath = resource.cfnOptions?.metadata?.["aws:cdk:path"];
if (!resourcePath) {
console.log("Resource path not found in metadata");
return false;
}
const resourceMetadata =
manifest?.artifacts?.WebsiteStack?.metadata?.[`/${resourcePath}`];
return resourceMetadata !== undefined;
} catch (error) {
console.log("Error checking resource existence:", error);
return false;
}
}
こちらのコードでCloudFrontStandardLogDelivery
が存在しない状態でcdk diff
を叩くと1回目は確かに意図したプレフィックスを渡していますが、ただし、2回目からはまだCloudFrontStandardLogDelivery
がデプロイされていないにも関わらず、AWSLogs/<AWSアカウントID>/CloudFront/
が付与されてしまっています。
px cdk diff --no-change-set
Bundling asset WebsiteStack/ContentsDeliveryConstruct/RewriteToWebpLambdaEdge/Code/Stage...
cdk.out/bundling-temp-a0b0205c5ae59be4b27b17777568e9cab4257b303b38ab098d49bca58284c947/index.mjs 731b
⚡ Done in 9ms
cloudFrontStandardLogDelivery.s3SuffixPath : {DistributionId}/{yyyy}/{MM}/{dd}/{HH}
Stack WebsiteStack
Resources
[+] AWS::Logs::Delivery ContentsDeliveryConstruct/CloudFrontStandardLogDelivery ContentsDeliveryConstructCloudFrontStandardLogDelivery2C76C0AA
✨ Number of stacks with differences: 1
> (feature/logging-v2 →⚡=) npx cdk diff --no-change-set
Bundling asset WebsiteStack/ContentsDeliveryConstruct/RewriteToWebpLambdaEdge/Code/Stage...
cdk.out/bundling-temp-a0b0205c5ae59be4b27b17777568e9cab4257b303b38ab098d49bca58284c947/index.mjs 731b
⚡ Done in 11ms
update
cloudFrontStandardLogDelivery.s3SuffixPath : AWSLogs/<AWSアカウントID>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
Stack WebsiteStack
Resources
[+] AWS::Logs::Delivery ContentsDeliveryConstruct/CloudFrontStandardLogDelivery ContentsDeliveryConstructCloudFrontStandardLogDelivery2C76C0AA
✨ Number of stacks with differences: 1
他にもリソースの物理IDをCfnOutput
として出力するように設定しておき、リソースの作成時にFn.ImportValue
でCfnOutput
の結果をインポートしてConditions
で空かどうかで判断することも考えたのですが、Cannot use Fn::ImportValue in Conditions.
とエラーになったため実現できませんでした。
その他にも方法あるかも知れませんが、個人的にはCfnDelivery
をそのまま使用するのは難しいと考えます。
一方、カスタムリソースではCreate
、Update
、Delete
のリクエストタイプごとに処理を振り分けることが可能です。
動作確認
カスタムリソースを使用する方式で動作確認をします。
まず、リソース新規作成時です。プレフィックスには{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
を指定しました。
はい、自動的にAWSLogs/{account-id}/CloudFront/
が付与され、AWSLogs/{account-id}/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
となっていますね。
続いて更新です。Constructに渡すプレフィックスは変更せずに、enableHiveCompatiblePath
をfalse
からtrue
に変更します。
onUpdate: {
action: "updateDeliveryConfiguration",
parameters: {
id: new cdk.custom_resources.PhysicalResourceIdReference(),
s3EnableHiveCompatiblePath: false,
s3DeliveryConfiguration: {
+ enableHiveCompatiblePath: true,
suffixPath: logPrefix.logPrefix,
},
},
physicalResourceId:
cdk.custom_resources.PhysicalResourceId.fromResponse(
"delivery.id"
),
service: "CloudWatchLogs",
},
こちらでcdk deploy
すると、Apache Hive互換プレフィックスが有効になったとともに、AWSLogs/{account-id}/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
がAWSLogs/<AWSアカウントID>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}
正しく、リソースの新規作成時と更新時とで処理を振り分けられていますね。
リソースの新規作成と更新とで渡されたパラメーターがそのまま使われるのかどうか異なる
CloudFrontの標準ログV2をL1 Constructで設定してみようとしてみました。
結果としては、CfnDeliveryについては、s3SuffixPath
に冪等性がないため、カスタムリソースを使うのが良いと考えます。
個人的にはあまりよろしくない挙動だと思うので、APIの動きが見直されることを願っています。
また、カスタムリソースを使用せずにリソースの新規作成、更新を判定する方法をご存知の方がいらっしゃれば教えてください。
この記事が誰かの助けになれば幸いです。
以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!