Skip to content

(apigateway): LogGroupLogDestination emits log group ARN with trailing :*, causing permanent Stage CFN drift #38796

Description

@rgoltz

Describe the bug

LogGroupLogDestination sets the REST API Stage property AccessLogSetting.DestinationArn to the log group logGroupArn, which carries a trailing :*. API Gateway stores this ARN without the :*. As a result, CloudFormation drift detection reports the Stage as MODIFIED on every drift run, even though nothing was changed and access logging works correctly.

CloudFormation Drift detection is only useful if IN_SYNC is the normal, trusted state. A resource that is permanently MODIFIED trains operators to ignore drift, which then hides real drift. Many teams also gate deploys, audits, or compliance checks on a clean drift status, so a permanent false positive weakens those gates. "Just ignore it" or "add it to an allow list" does not scale across stacks and environments, and it suppresses real drift on the same property. The goal is a clean IN_SYNC state without manual suppression.

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Library Version

No response

Expected Behavior

LogGroupLogDestination sets AccessLogSetting.DestinationArn to the log group ARN without the trailing :*, so the synthesized template matches what API Gateway stores and drift detection reports the Stage as IN_SYNC.

Current Behavior

The synthesized DestinationArn points at the log group Arn attribute, which includes the :*:

{
  "AccessLogSetting": {
    "DestinationArn": {
      "Fn::GetAtt": ["AccessLogs8B620ECA", "Arn"]
    }
  }
}

At deploy time this resolves to an ARN with a trailing :*:

arn:aws:logs:eu-central-1:111122223333:log-group:/aws/apigateway/demo/accessLogs:*

API Gateway stores the ARN without the :*. Reading it back from the service:

aws apigateway get-stage \
  --rest-api-id <rest-api-id> \
  --stage-name prod \
  --query 'accessLogSettings.destinationArn' \
  --output text
arn:aws:logs:eu-central-1:111122223333:log-group:/aws/apigateway/demo/accessLogs

CloudFormation drift detection then reports the Stage as MODIFIED with a single property difference:

aws cloudformation describe-stack-resource-drifts \
  --stack-name <stack-name> \
  --stack-resource-drift-status-filters MODIFIED
{
  "LogicalResourceId": "ApiDeploymentStageprod...",
  "ResourceType": "AWS::ApiGateway::Stage",
  "PropertyDifferences": [
    {
      "PropertyPath": "/AccessLogSetting/DestinationArn",
      "ExpectedValue": "arn:aws:logs:eu-central-1:111122223333:log-group:/aws/apigateway/demo/accessLogs:*",
      "ActualValue": "arn:aws:logs:eu-central-1:111122223333:log-group:/aws/apigateway/demo/accessLogs",
      "DifferenceType": "NOT_EQUAL"
    }
  ],
  "StackResourceDriftStatus": "MODIFIED"
}

The only difference is the trailing :*. This repeats on every drift run and affects every stack that enables access logging this way.

Reproduction Steps

Below you can find a sample CDK code in TypeScript. After synth, inspect the Stage in the generated template.

import { App, Stack } from 'aws-cdk-lib';
import { RestApi, LogGroupLogDestination, AccessLogFormat, MethodLoggingLevel } from 'aws-cdk-lib/aws-apigateway';
import { LogGroup } from 'aws-cdk-lib/aws-logs';

const app = new App();
const stack = new Stack(app, 'DefaultStack');

const logGroup = new LogGroup(stack, 'AccessLogs', {
  logGroupName: '/aws/apigateway/demo/accessLogs',
});

new RestApi(stack, 'Api', {
  deployOptions: {
    accessLogDestination: new LogGroupLogDestination(logGroup),
    accessLogFormat: AccessLogFormat.jsonWithStandardFields(),
    loggingLevel: MethodLoggingLevel.ERROR,
  },
}).root.addMethod('GET');

app.synth();

In the synthesized DefaultStack template, the AWS::ApiGateway::Stage resource has AccessLogSetting.DestinationArn set to { "Fn::GetAtt": ["AccessLogs...", "Arn"] }, which resolves with the trailing :*. Deploy the stack and run drift detection to see the Stage reported as MODIFIED.

Possible Solution

The :* on LogGroup.logGroupArn is intended and should not be changed globally, because IAM policies rely on it. The relevant source confirms this.

LogGroupLogDestination.bind() (packages/aws-cdk-lib/aws-apigateway/lib/access-log.ts) returns the ARN unchanged:

public bind(_stage: IStageRef): AccessLogDestinationConfig {
  return {
    destinationArn: this.logGroup.logGroupRef.logGroupArn,
  };
}

logGroupArn is documented to include the :* (packages/aws-cdk-lib/aws-logs/lib/log-group.ts):

/**
 * The ARN of this log group, with ':*' appended
 *
 * @attribute
 */
readonly logGroupArn: string;

The same file documents the IAM reason for the :* in grant():

// A LogGroup ARN out of CloudFormation already includes a ':*' at the end to
// include the log streams under the group.

So the fix belongs in LogGroupLogDestination, which should use the log group ARN without the :* for this destination. CDK already knows how to build the :*-free form: in log-group.ts, fromLogGroupName builds the ARN with formatArn(... ArnFormat.COLON_RESOURCE_NAME) from the name, and fromLogGroupArn starts by stripping the suffix with logGroupArn.replace(/:\*$/, ''). The destination could reuse the same approach.

An alternative or additional fix is on the CloudFormation side: give the AWS::ApiGateway::Stage resource type a propertyTransform for AccessLogSetting.DestinationArn so the server side normalization is not reported as drift. See Preventing false drift detection results for resource types. That page documents an equivalent case already solved this way: AWS::Route53::HostedZone uses a propertyTransform for a trailing . on the Name property, which is structurally the same problem as the trailing :* here. Note that this would be a change to the AWS owned resource type schema, so it can only be done by AWS, not by CDK or the user. Its advantage is that it would fix existing stacks without any template change or redeploy.

Before, I would create a PR, I'd like to check the following question with you:

  • (1) Preferred fix location: normalize the ARN inside LogGroupLogDestination, or add a propertyTransform for AWS::ApiGateway::Stage, or both. The propertyTransform route fixes drift for existing stacks without any template change, which is the cleanest path to IN_SYNC.

  • (2) If the fix is in LogGroupLogDestination, the synthesized template changes from Fn::GetAtt ... Arn to a name based ARN, so existing stacks will see a diff on the next cdk diff. The diff is an in place property update, not a resource replacement, and the value stored in API Gateway does not change. Should this go behind a feature flag so existing users do not get an unexpected diff on a plain version upgrade? This question is independent of how small the code change is: the need for a feature flag depends only on whether the synthesized template changes for existing stacks, not on the size of the fix.

I am happy to open a PR once there is agreement on the fix location and on whether a feature flag is wanted.

Additional Information/Context

Workaround using a custom IAccessLogDestination that rebuilds the ARN from the log group name. The :* is only added when the Arn attribute is resolved at deploy time, so trimming the string at synth time does not work; rebuilding from the name avoids the :* entirely.

import { Stack, ArnFormat } from 'aws-cdk-lib';
import { IAccessLogDestination, AccessLogDestinationConfig } from 'aws-cdk-lib/aws-apigateway';
import { LogGroup } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

class LogGroupLogDestinationWithoutWildcard implements IAccessLogDestination {
  private readonly destinationArn: string;

  constructor(scope: Construct, logGroup: LogGroup) {
    this.destinationArn = Stack.of(scope).formatArn({
      service: 'logs',
      resource: 'log-group',
      resourceName: logGroup.logGroupName,
      arnFormat: ArnFormat.COLON_RESOURCE_NAME,
    });
  }

  bind(): AccessLogDestinationConfig {
    return { destinationArn: this.destinationArn };
  }
}

With the workaround the synthesized DestinationArn no longer contains the :*, and drift detection reports the Stage as IN_SYNC:

{
  "AccessLogSetting": {
    "DestinationArn": {
      "Fn::Join": [
        "",
        [
          "arn:", { "Ref": "AWS::Partition" },
          ":logs:", { "Ref": "AWS::Region" },
          ":", { "Ref": "AWS::AccountId" },
          ":log-group:", { "Ref": "AccessLogs8B620ECA" }
        ]
      ]
    }
  }
}

Related: aws-cdk #18253 "(logs): Log Group ARN has extra :*" describes the same underlying behavior with a WAF example, and suggests the same formatArn(... COLON_RESOURCE_NAME) approach. It notes that it may be better to normalize the ARN in the L2 constructs that consume the log group ARN rather than changing logGroupArn globally. This issue applies that idea to LogGroupLogDestination and the API Gateway Stage drift specifically. The same fix pattern could later be reused for other consumers of the log group ARN, such as the WAF logging configuration in #18253.

AWS CDK Library version (aws-cdk-lib)

2.268.0

AWS CDK CLI version

2.1140.0

Node.js Version

v24.20.0

OS

Linux

Language

TypeScript

Language Version

TypeScript (7.0.2)

Other information

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/aws-apigatewayRelated to Amazon API GatewaybugThis issue is a bug.effort/mediumMedium work item – several days of effortp1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions