Skip to content

[AWS::ECR::Repository] Adding EncryptionConfiguration matching live AES256 default forces destructive Replacement on imported repos #2502

Open

Description

@esetnik

Name of the resource

AWS::ECR::Repository

Resource Name

No response

Issue Description

The AWS::ECR::Repository.EncryptionConfiguration property is documented as Update requires: Replacement. AES256 is the AWS default for every ECR repository — every repo created without an explicit encryption block already has EncryptionConfiguration.EncryptionType: AES256 in its live state.

When a repo is imported into a CloudFormation stack without an explicit EncryptionConfiguration declaration in the template, and a user later adds the declaration EncryptionConfiguration: { EncryptionType: AES256 } to match the live state, CloudFormation generates a changeset with Replacement: True. Executing this changeset would destroy the repository (and all image history) and create a new one — even though the live encryption state would be identical before and after.

This is technically consistent with the documented "Update requires: Replacement" contract, but it produces a surprising and destructive outcome for a no-op-equivalent change. Several common workflows hit this wall:

  • Security-tool-driven remediation. Static analyzers (Aikido, Checkov, cfn-nag, tfsec, etc.) flag missing EncryptionConfiguration declarations and demand explicit settings, even when the live default already encrypts at rest. Users cannot resolve the finding without either (a) destroying their repos or (b) suppressing the rule.
  • Drift-resistance hardening. Teams adopting CloudFormation want explicit declarations to lock against drift. For imported repos, this is currently impossible without replacement.
  • Compliance audits. Auditors increasingly want to see encryption explicitly declared in IaC. The current behavior pushes teams toward documentation-only workarounds.

Expected Behavior

When a property declaration is added to an imported resource and the declared value matches the resource's actual live state at import time, CloudFormation should resolve the diff as no-op (Modify with no actual API call) rather than Replacement.

Specifically for AWS::ECR::Repository.EncryptionConfiguration: declaring EncryptionType: AES256 on a repo whose live state is already AES256 should not require replacement.

Observed Behavior

Changeset preview shows Replacement: True on every imported ECR repo when EncryptionConfiguration: { EncryptionType: AES256 } is added to a previously-undeclared template. Executing the changeset would destroy the repos.

Test Cases

Setup: import an existing ECR repository (created with default AES256 encryption) into a CloudFormation stack with this template:

Resources:
  MyRepo:
    Type: AWS::ECR::Repository
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      RepositoryName: my-existing-repo
      ImageTagMutability: MUTABLE
      ImageScanningConfiguration:
        ScanOnPush: true

Confirm import succeeds and the repo's live EncryptionConfiguration.EncryptionType is AES256 (the default).

Reproduce: add EncryptionConfiguration to the template:

Resources:
  MyRepo:
    Type: AWS::ECR::Repository
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      RepositoryName: my-existing-repo
      ImageTagMutability: MUTABLE
      ImageScanningConfiguration:
        ScanOnPush: true
      EncryptionConfiguration:        # Added — matches live state
        EncryptionType: AES256

Create a change set and inspect:

aws cloudformation create-change-set \
  --stack-name my-stack \
  --template-body file://template.yaml \
  --change-set-name add-encryption \
  --change-set-type UPDATE \
  --capabilities CAPABILITY_IAM
aws cloudformation describe-change-set \
  --stack-name my-stack \
  --change-set-name add-encryption

Result: changeset shows Action: Modify, Replacement: True on the repository. Executing it would destroy and recreate the repo.

Other Details

Why this matters:

  • ECR repos contain image history that is not trivially reproducible. Re-pushing all tags from CI is possible but disruptive (multi-minute outages for every consumer pulling during the gap; no atomic swap; lifecycle-policy state lost).
  • AWS does not allow ECR encryption to be turned off — the property only exists to choose between AES256 and KMS-backed. So declaring AES256 on a repo that has AES256 cannot result in a meaningful state change at the AWS API level.
  • The same logical pattern exists for many other resources where a property has an implicit live default and adding the explicit declaration matching that default forces replacement.

Workaround (used to address this in our codebase): remove the resource from the stack via DeletionPolicy: Retain UPDATE, then re-import via IMPORT change-set with the explicit declaration matching live state. CFN's import flow validates declarations against live AWS state (not previous template state), so declared-AES256-against-live-AES256 succeeds without replacement. This works but is operationally heavy: requires two coordinated change-sets per stack, breaks drift detection during the gap, and must be repeated for every imported resource hit by this pattern.

Related issues (same general pattern, different specifics):

Suggested fix scope: at minimum, treat AWS::ECR::Repository.EncryptionConfiguration adds whose value matches the imported live state as Modify with Replacement: False. More broadly, consider this pattern for any property where the imported-state value is known and matches the new declaration.

References

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions