Repository navigation
[AWS::ECR::Repository] Adding EncryptionConfiguration matching live AES256 default forces destructive Replacement on imported repos #2502
Description
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
EncryptionConfigurationdeclarations 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: trueConfirm 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: AES256Create 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-encryptionResult: 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):
- Fn::GetAtt: and !GetAtt behaves differently during an Update Operation and Import Operation #1045 —
Fn::GetAttsyntax form changes treated as property modifications during Update / Import - [AWS::ElastiCache::ReplicationGroup] - [BUG] - Switching from AuthToken to ACLs (UserGroupIds) forces Replacement (no in-place update), unlike Console/CLI #2356 —
ElastiCache::ReplicationGroupAuthToken→UserGroupIds in-place via Console/CLI but Replacement via CFN
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.
Activity