Copilot stack layering: app, StackSet, env, workload and addons¶
Source: the archived AWS Copilot CLI repository (Apache-2.0), commit a0dbe689. Template paths are
relative to internal/pkg/template/templates/. Go paths are relative to the repository root. Anything
not confirmed in the source is marked (UNCONFIRMED). Statements about CloudFormation or AWS
defaults are labelled as such, because they come from AWS behaviour, not Copilot code.
Copilot templates are Go text/template files, so there are two layers of conditionality:
-
Render-time (
{{ if … }}): whether a resource appears in the deployed template at all. ecsodus must read the deployed template (GetTemplate), never re-render it. -
CloudFormation
Condition:: whether a resource in the deployed template actually exists. In the env stack these conditions are driven by parameters that the env-controller mutates at runtime.
app account (tools account) per env region/account
┌──────────────────────────────┐ ┌───────────────────────────────────────────┐
│ <app>-infrastructure-roles │ StackSet │ StackSet-<app>-infrastructure-<id> │
│ (templates/app/app.yml) ├──────────► │ (templates/app/cf.yml): KMS, bucket, ECR │
│ AppHostedZone, roles │ └───────────────────────────────────────────┘
└──────────────────────────────┘ ┌───────────────────────────────────────────┐
│ <app>-<env> (environment/cf.yml) │
│ VPC, cluster, ALB?, NAT?, EFS?, certs │
│ └ AddonsStack (env addons, optional) │
└───────────▲───────────────────────────────┘
│ UpdateStack (env-controller)
┌───────────┴───────────────────────────────┐
│ <app>-<env>-<svc> (workloads/services/…) │
│ service, taskdef, TG, rules, custom res. │
│ └ AddonsStack (workload addons, optional)│
└───────────────────────────────────────────┘
1. App stack: templates/app/app.yml¶
-
Stack name:
<app>-infrastructure-roles(NameForAppStack,internal/pkg/deploy/cloudformation/stack/name.go). -
Tags:
copilot-application=<app>merged with the app'sAdditionalTags(AppStackConfig.Tags). -
Parameters:
AdminRoleName,ExecutionRoleName,DNSDelegationRoleName,AppDNSDelegatedAccounts,AppDomainName,AppDomainHostedZoneID,AppName. -
Condition:
DelegateDNS: !Not [!Equals [ !Ref AppDomainName, "" ]].
| Logical ID | Type | Condition | Notes |
|---|---|---|---|
AdministrationRole |
AWS::IAM::Role |
— | RoleName: <app>-adminrole. StackSet admin role. |
ExecutionRole |
AWS::IAM::Role |
— | RoleName: <app>-executionrole. StackSet execution role. |
DNSDelegationRole |
AWS::IAM::Role |
DelegateDNS |
RoleName: <app>-DNSDelegationRole (deploy.DNSDelegationRoleName). Env accounts assume it to write into AppHostedZone/the root zone. |
AppHostedZone |
AWS::Route53::HostedZone |
DelegateDNS |
<app>.<domain>. Holds the env NS delegations that dns-delegation.js writes out of band, plus alias and validation records. |
AppDomainDelegationRecordSet |
AWS::Route53::RecordSet |
DelegateDNS |
NS record <app>.<domain>. in the customer's root zone AppDomainHostedZoneID, TTL 900. |
No resource has a DeletionPolicy. CloudFormation behaviour: deleting a hosted zone fails if it
still contains records other than SOA/NS, so an AppHostedZone that still holds out-of-band records
blocks the stack delete rather than losing them.
2. App StackSet: templates/app/cf.yml¶
-
StackSet name:
<app>-infrastructure(NameForAppStackSet). Description: "ECS CLI Application Resources (ECR repos, KMS keys, S3 buckets)". -
Admin/execution roles:
<app>-adminrole/<app>-executionrole. -
Tags: the same as the app stack (
WithTags(toMap(appConfig.Tags()))ininternal/pkg/deploy/cloudformation/app.go). -
Instances: one per env region/account. Copilot finds them through
InstanceSummaries(ListStackInstances) and describes them byStackID. It does not use a name pattern. CloudFormation names instance stacksStackSet-<stackset-name>-<uuid>(UNCONFIRMED: CloudFormation behaviour, not in Copilot source).
| Logical ID | Type | Notes |
|---|---|---|
KMSKey |
AWS::KMS::Key |
EnableKeyRotation: true. Its policy allows the tools account and every env account. Output KMSKeyARN, exported as <app>-ArtifactKey. |
PipelineBuiltArtifactBucket |
AWS::S3::Bucket |
Versioned, SSE-KMS with KMSKey, BucketOwnerEnforced. The lifecycle expires local-assets/ after 30 days. Output PipelineBucket. Holds addon templates, env files and custom-resource zips. |
PipelineBuiltArtifactBucketPolicy |
AWS::S3::BucketPolicy |
Allows account principals; denies non-KMS puts and non-TLS access. |
ECRRepo<Workload> |
AWS::ECR::Repository |
One per workload with WithECR. RepositoryName: <app>/<workload>, tagged copilot-service=<workload>. Output ECRRepo<Workload>. |
No resource has a DeletionPolicy. CloudFormation defaults:
- a KMS key is scheduled for deletion (default 30-day window)
- a bucket delete fails if it is not empty
- an ECR repository delete fails if it holds images, because
EmptyOnDeleteis not set
(UNCONFIRMED: AWS defaults.) Copilot's own copilot app delete empties both before deleting
the StackSet: cleanUpRegionalResources in internal/pkg/deploy/cloudformation/app.go calls
s3.EmptyBucket and ecr.ClearRepository("<app>/<svc>"). That is client-side, not a custom
resource.
3. Environment stack: templates/environment/cf.yml + environment/partials/*¶
- Stack name:
<app>-<env>(NameForEnv). -
Tags:
copilot-application,copilot-environment, plus the app's tags (Env.Tagsinstack/env.go). -
Bootstrap:
environment/bootstrap-cf.ymluses the same stack name, and its only content isbootstrap-resources(the two roles below). -
Parameters:
AppName,EnvironmentName,ALBWorkloads,InternalALBWorkloads,EFSWorkloads,NATWorkloads,AppRunnerPrivateWorkloads,ToolsAccountPrincipalARN,AppDNSName,AppDNSDelegationRole,Aliases,CreateHTTPSListener,CreateInternalHTTPSListener,ServiceDiscoveryEndpoint.
The six env-controller-managed parameters are ALBWorkloads, EFSWorkloads, NATWorkloads,
InternalALBWorkloads, Aliases and AppRunnerPrivateWorkloads (template.AvailableEnvFeatures()
in internal/pkg/template/env.go). On copilot env deploy, transformEnvControllerParameters
(stack/env.go) keeps their previous values. The *Workloads parameters are comma-separated
workload names. Aliases is a JSON map of workload to a list of aliases, or "".
3.1 Conditions (quoted from environment/cf.yml)¶
Conditions:
CreateALB:
!Not [!Equals [ !Ref ALBWorkloads, "" ]]
CreateInternalALB:
!Not [!Equals [ !Ref InternalALBWorkloads, "" ]]
DelegateDNS:
!Not [!Equals [ !Ref AppDNSName, "" ]]
ExportHTTPSListener: !And
- !Condition CreateALB
- !Equals [ !Ref CreateHTTPSListener, true ]
ExportInternalHTTPSListener: !And
- !Condition CreateInternalALB
- !Equals [ !Ref CreateInternalHTTPSListener, true ]
CreateEFS:
!Not [!Equals [ !Ref EFSWorkloads, ""]]
CreateNATGateways:
!Not [!Equals [ !Ref NATWorkloads, ""]]
CreateAppRunnerVPCEndpoint:
!Not [!Equals [ !Ref AppRunnerPrivateWorkloads, ""]]
ManagedAliases: !And
- !Condition DelegateDNS
- !Not [!Equals [ !Ref Aliases, "" ]]
- !Condition CreateALB
3.2 Every env resource, grouped by condition¶
{n} means one resource per subnet index (1-based) or per extra imported certificate. The
"render" column gives the Go-template gate that decides whether the resource is in the template
at all.
Unconditional (always exist if rendered):
| Logical ID | Type | Render gate | Notes |
|---|---|---|---|
CloudformationExecutionRole |
AWS::IAM::Role |
always (bootstrap-resources) |
DeletionPolicy: Retain in source. RoleName: <app>-<env>-CFNExecutionRole. The partial's comment says the definition "must be immediately followed with DeletionPolicy: Retain" (#1533). |
EnvironmentManagerRole |
AWS::IAM::Role |
always | DeletionPolicy: Retain in source. RoleName: <app>-<env>-EnvManagerRole. |
ServiceDiscoveryNamespace |
AWS::ServiceDiscovery::PrivateDnsNamespace |
always | Name: !Ref ServiceDiscoveryEndpoint (<env>.<app>.local, or <app>.local for envs created before v1.5). |
Cluster |
AWS::ECS::Cluster |
always | FARGATE/FARGATE_SPOT. |
EnvironmentSecurityGroup |
AWS::EC2::SecurityGroup |
always | |
EnvironmentSecurityGroupIngressFromSelf |
AWS::EC2::SecurityGroupIngress |
always | |
LogResourcePolicy |
AWS::Logs::ResourcePolicy |
always | <app>-<env>-LogResourcePolicy. |
VPC, PublicRouteTable, DefaultPublicRoute, InternetGateway, InternetGatewayAttachment, PublicSubnet{n}, PrivateSubnet{n}, PublicSubnet{n}RouteTableAssociation |
EC2 | not .VPCConfig.Imported (vpc-resources.yml) |
Private subnets have no route-table association unless NAT is on. |
ELBAccessLogsBucket |
AWS::S3::Bucket |
.PublicHTTPConfig.ELBAccessLogs.ShouldCreateBucket |
Versioned. No CFN condition: it survives CreateALB turning false. |
ELBAccessLogsBucketCleanerAction |
Custom::BucketCleanerFunction |
same | Its Delete empties the bucket (see copilot-custom-resources.md). |
BucketCleanerFunction, ELBAccessLogsBucketCleanerRole |
Lambda, IAM | same | |
CloudFrontOriginAccessControl |
AWS::CloudFront::OriginAccessControl |
.CDNConfig.Static |
|
VpcFlowLogGroup, FlowLog, FlowLogRole |
Logs, EC2, IAM | .VPCConfig.FlowLogs |
|
AddonsStack |
AWS::CloudFormation::Stack |
.Addons |
Env addons. Parameters App, Env + extra params. |
CreateALB (ALBWorkloads non-empty):
PublicHTTPLoadBalancerSecurityGroup, EnvironmentHTTPSecurityGroupIngressFromPublicALB,
PublicLoadBalancer, DefaultHTTPTargetGroup, HTTPListener, ELBAccessLogsBucketPolicy
(render: ShouldCreateBucket).
With .CDNConfig, the following are also gated on CreateALB, and the first three are rendered
only if or .CDNConfig.ImportedCertificate .DelegateDNS:
UniqueJSONValuesFunctionRoleUniqueJSONValuesFunctionUniqueAliasesAction(Custom::UniqueJSONValuesFunction)CloudFrontDistribution
ExportHTTPSListener (CreateALB and CreateHTTPSListener == true):
PublicHTTPSLoadBalancerSecurityGroup, EnvironmentHTTPSSecurityGroupIngressFromPublicALB,
HTTPSListener (certificate = !Ref HTTPSCert, or the first imported cert ARN),
HTTPSImportCertificate{n} (AWS::ElasticLoadBalancingV2::ListenerCertificate, one per extra
imported cert).
CreateInternalALB (InternalALBWorkloads non-empty):
-
InternalLoadBalancerSecurityGroup,EnvironmentSecurityGroupIngressFromInternalALB,InternalALBIngressFromEnvironmentSecurityGroup -
InternalLoadBalancerSecurityGroupIngressFromHttp(render:.VPCConfig.AllowVPCIngress) InternalLoadBalancer,DefaultInternalHTTPTargetGroup,InternalHTTPListenerInternalWorkloadsHostedZone(AWS::Route53::HostedZone<env>.<app>.internal, private to the VPC; render:not .PrivateHTTPConfig.ImportedCertARNs)
ExportInternalHTTPSListener (CreateInternalALB and CreateInternalHTTPSListener == true):
InternalLoadBalancerSecurityGroupIngressFromHttps (render: AllowVPCIngress),
InternalHTTPSListener, InternalHTTPSImportCertificate{n}.
CreateEFS (EFSWorkloads non-empty):
FileSystem (AWS::EFS::FileSystem, encrypted, BackupPolicy: ENABLED, IA after 30 days),
EFSSecurityGroup, EFSSecurityGroupIngressFromEnvironment, MountTarget{n} (one per private
subnet).
CreateNATGateways (NATWorkloads non-empty; render: not .VPCConfig.Imported,
nat-gateways.yml):
NatGateway{n}Attachment (AWS::EC2::EIP), NatGateway{n} (in PublicSubnet{n}),
PrivateRouteTable{n}, PrivateRoute{n} (0.0.0.0/0 → NAT), PrivateRouteTable{n}Association.
There is one of each per private subnet.
CreateAppRunnerVPCEndpoint (AppRunnerPrivateWorkloads non-empty; render:
not .VPCConfig.Imported, ar-vpc-connector.yml):
AppRunnerVpcEndpointSecurityGroup, AppRunnerVpcEndpointSecurityGroupIngressFromEnvironment,
AppRunnerVpcEndpoint.
DelegateDNS (AppDNSName non-empty; all rendered only when
not .PublicHTTPConfig.ImportedCertARNs):
CustomResourceRole,EnvironmentHostedZone(<env>.<app>.<domain>)CertificateValidationFunction,DNSDelegationFunctionDelegateDNSAction(Custom::DNSDelegationFunction)HTTPSCert(Custom::CertificateValidationFunction)- with
.CDNConfig:CertificateReplicatorFunctionandCertificateReplicator(Custom::CertificateReplicatorFunction)
ManagedAliases (DelegateDNS, Aliases non-empty and CreateALB; same render gate):
CustomDomainFunction, CustomDomainAction (Custom::CustomDomainFunction).
Condition-gated outputs:
-
CreateALB:PublicLoadBalancerDNSName,PublicLoadBalancerFullName,PublicLoadBalancerHostedZone,HTTPListenerArn,DefaultHTTPTargetGroupArn,CloudFrontDomainName,PublicALBAccessible -
ExportHTTPSListener:HTTPSListenerArn -
CreateInternalALB: theInternalLoadBalancer*outputs,InternalWorkloadsHostedZone/Name,InternalHTTPListenerArn,InternalLoadBalancerSecurityGroup -
ExportInternalHTTPSListener:InternalHTTPSListenerArn CreateEFS:ManagedFileSystemIDCreateNATGateways:PrivateRouteTableIDsDelegateDNS:EnvironmentHostedZone,EnvironmentSubdomainCreateAppRunnerVPCEndpoint:AppRunnerVpcEndpointId
Most outputs are exported as ${AWS::StackName}-<Name>, for example <app>-<env>-ClusterId,
-VpcId, -PrivateSubnets, -HostedZone, -SubDomain and -FilesystemID. Workload and addon
templates import these, which creates a CloudFormation dependency: the env stack cannot be deleted
while an importer exists. EnabledFeatures concatenates all six managed parameters so that a
parameter-only change still updates the stack.
3.3 What the env-controller can delete¶
When a workload stack is deleted, EnvControllerAction's Delete removes the workload from every
*Workloads parameter and from Aliases, then updates <app>-<env> with UsePreviousTemplate.
If it was the last workload using a feature, every resource gated on that feature's condition (3.2)
is deleted by CloudFormation. This covers:
- the ALB and listeners
- the NAT gateways and EIPs
- the internal ALB and
InternalWorkloadsHostedZone - the App Runner endpoint
- the EFS
FileSystemand its data
It also deletes CustomDomainAction, whose own Delete handler then removes the alias A records.
An Aliases change additionally reissues HTTPSCert, and the old cert is deleted. None of these
env resources has a DeletionPolicy in source. Only the two bootstrap roles do.
4. Workload stacks¶
- Stack name:
<app>-<env>-<svc>, truncated to 128 characters (NameForWorkload). -
Tags:
copilot-application,copilot-environment,copilot-service, plus the app's tags (stack/workload.go). -
Deletion policies: no workload template or partial contains
DeletionPolicy(grep ofworkloads/).
4.1 Load Balanced Web Service: workloads/services/lb-web/cf.yml¶
CFN conditions: IsGovCloud, HasAssociatedDomain (DNSDelegated == true), HasAddons
(AddonsTemplateURL != ""), HasEnvFile, HasLoggingEnvFile, HasEnvFileFor<Sidecar>.
| Logical ID | Type | Render gate / condition | Source partial |
|---|---|---|---|
LogGroup |
AWS::Logs::LogGroup |
always | loggroup.yml. Name /copilot/<app>-<env>-<svc>, RetentionInDays: !Ref LogRetention. |
TaskDefinition |
AWS::ECS::TaskDefinition |
always | |
ExecutionRole, TaskRole |
AWS::IAM::Role |
always | executionrole.yml, taskrole.yml |
DiscoveryService |
AWS::ServiceDiscovery::Service |
always | servicediscovery.yml |
DynamicDesiredCountAction |
Custom::DynamicDesiredCountFunction |
.Autoscaling |
autoscaling.yml |
DynamicDesiredCountFunction, DynamicDesiredCountFunctionRole, AutoScalingRole, AutoScalingTarget |
Lambda, IAM, AppAutoScaling | .Autoscaling |
|
AutoScalingPolicyECSServiceAverageCPUUtilization / …MemoryUtilization / AutoScalingPolicyALBSumRequestCountPerTarget / AutoScalingPolicyALBAverageResponseTime |
AWS::ApplicationAutoScaling::ScalingPolicy |
per manifest field | |
CPURollbackAlarm, MemoryRollbackAlarm |
AWS::CloudWatch::Alarm |
per manifest | rollback-alarms.yml |
EnvControllerAction |
Custom::EnvControllerFunction |
always | env-controller.yml. Parameters = ALBWorkloads (if the ALB is enabled and not imported), Aliases (if not an imported ALB), NATWorkloads (private placement), EFSWorkloads (managed EFS). Aliases: property only with ALB aliases and no imported ALB. |
EnvControllerFunction, EnvControllerRole |
Lambda, IAM | always | |
Service |
AWS::ECS::Service |
always | DependsOn the listener rules / NLB listeners. DesiredCount = !GetAtt DynamicDesiredCountAction.DesiredCount with autoscaling (not on Spot), else !Ref TaskCount. |
TargetGroup{i} (or TargetGroupForImportedALB{i}) |
AWS::ElasticLoadBalancingV2::TargetGroup |
.ALBListener |
alb.yml |
RulePriorityFunction, RulePriorityFunctionRole |
Lambda, IAM | .ALBListener |
alb.yml |
HTTP only: HTTPRulePriorityAction, HTTPListenerRule{i} |
Custom::RulePriorityFunction, ListenerRule |
.ALBListener, not HTTPS |
http-listener.yml. The listener comes from !GetAtt EnvControllerAction.HTTPListenerArn. |
HTTPS: HTTPSRulePriorityAction, HTTPRuleWithDomainPriorityAction, HTTPSListenerRule{i}, HTTPListenerRuleWithDomain{i} (HTTP→HTTPS redirect) |
Custom, ListenerRule | .ALBListener.IsHTTPS |
https-listener.yml |
HTTPS without aliases: LoadBalancerDNSAlias |
AWS::Route53::RecordSetGroup |
not .ALBListener.Aliases |
A-alias <svc>.<env>.<app>.<domain> in the env zone (import <app>-<env>-HostedZone) → public ALB. CFN-owned. |
HTTPS with aliases that have hosted_zone: LoadBalancerDNSAlias<HostedZoneID> |
AWS::Route53::RecordSetGroup |
per zone in HostedZoneAliases |
CFN-owned. Aliases without hosted_zone get their A records from the env CustomDomainAction instead (out of band). |
Imported ALB: HTTPSRulePriorityAction/HTTPSListenerRuleForImportedALB{i}, HTTPRedirectRulePriorityAction/HTTPListenerRedirectRuleForImportedALB{i}, or HTTPRulePriorityAction/HTTPListenerRuleForImportedALB{i}, LoadBalancerDNSAlias<zone>, EnvironmentSecurityGroupIngressFromImportedALB{i} |
various | .ImportedALB |
imported-alb-resources.yml |
NLB: PublicNetworkLoadBalancerV2, NetworkLoadBalancerSecurityGroup, EnvironmentSecurityGroupIngressFromNetworkLoadBalancerSecurityGroup, NLBListener{i}, NetworkLoadBalancerTargetGroup{i}, NLBDNSAlias (HasAssociatedDomain) or NLBCustomDomainAction/Function/Role (HasAssociatedDomain), NLBCertValidatorAction/Function/Role (HasAssociatedDomain, .NLB.CertificateRequired) |
various | .NLB |
nlb.yml (NLB is blocked in ecsodus v0.1) |
AccessPoint |
AWS::EFS::AccessPoint |
.Storage.ManagedVolumeInfo |
efs-access-point.yml (on the env FileSystem) |
AddonsStack |
AWS::CloudFormation::Stack |
Condition: HasAddons |
addons.yml. DependsOn: EnvControllerAction when the workload requests any env feature. Parameters App, Env, Name + extras. TemplateURL: !Ref AddonsTemplateURL. |
<Topic>SNSTopic, <Topic>SNSTopicPolicy |
SNS | .Publish |
publish.yml |
Delete order (CloudFormation reverse-dependency behaviour). The listener rules take their
listener ARN from !GetAtt EnvControllerAction.*, and AddonsStack depends on it. So
Service, the rules, the target groups and the addons are deleted first, and
EnvControllerAction's Delete runs last, triggering the env update in §3.3.
4.2 Backend Service: workloads/services/backend/cf.yml¶
CFN conditions: IsGovCloud, HasAddons, HasEnvFile, HasLoggingEnvFile,
HasEnvFileFor<Sidecar>, ExposePort (TargetPort != -1).
The resources are the same as LBWS, with these differences:
- no NLB, and no
Aliasespassed to the env-controller -
EnvControllerActionParameters=InternalALBWorkloads(ALB enabled and not imported),NATWorkloads,EFSWorkloads -
Servicehas an explicitDependsOn: EnvControllerAction(plus the rules).ServiceRegistriesuses!If [ExposePort, …]. -
With
.ALBListener:TargetGroup{i},RulePriorityFunction/Role, and the listener rules. The listener ARN comes fromEnvControllerAction.InternalHTTPListenerArn/InternalHTTPSListenerArn. -
LoadBalancerInternalDNSAlias(AWS::Route53::RecordSetGroup) inEnvControllerAction.InternalWorkloadsHostedZone:<svc>.<env>.<app>.internal→ the internal ALB. This is rendered inhttp-listener.ymlfor Backend Service. -
HTTPS-with-alias records point at
InternalLoadBalancerHostedZone/DNSName.
4.3 Other workload kinds (all blocked in ecsodus v0.1)¶
-
Worker (
services/worker/cf.yml): the env-controller, and the backlog calculator when.Autoscaling.QueueDelayis set. -
Scheduled Job (
jobs/scheduled-job/cf.yml): the env-controller. -
RDWS (
services/rd-web/cf.yml): the env-controller andCustomDomainAction(custom-domain-app-runner.js). -
Static Site (
services/static-site/cf.yml):Bucket(noDeletionPolicy),TriggerStateMachineAction,CustomDomainAction,CertificateValidatorAction.
5. Addons: templates/addons/*¶
These are the templates that copilot storage init writes into the user's workspace
(copilot/<svc>/addons/ or copilot/environments/addons/). Users may have edited them, so
ecsodus must read the deployed nested-stack template and not assume these contents. Workload
addons are a nested AddonsStack in the workload stack. Env addons are a nested AddonsStack in
the env stack. The child stack name is CloudFormation-generated (<parent>-AddonsStack-<suffix>)
(UNCONFIRMED: CloudFormation behaviour).
5.1 DeletionPolicy / UpdateReplacePolicy present in source¶
The only policy in any addon template is DeletionPolicy: Retain on the S3 bucket policy
({{Name}}BucketPolicy in s3/cf.yml and s3/env/cf.yml). No addon template sets
UpdateReplacePolicy anywhere.
| Template | Resource (logical ID) | Type | DeletionPolicy in source | Default that applies (CloudFormation/AWS behaviour) |
|---|---|---|---|---|
aurora/serverlessv2.yml (workload) |
<C>DBSubnetGroup |
AWS::RDS::DBSubnetGroup |
none | Delete |
<C>SecurityGroup, <C>DBClusterSecurityGroup |
AWS::EC2::SecurityGroup |
none | Delete | |
<C>AuroraSecret |
AWS::SecretsManager::Secret |
none | Delete. The secret is deleted without a recovery window (PLAN §1; UNCONFIRMED against AWS docs in this pass). | |
<C>DBClusterParameterGroup (unless .ParameterGroup) |
AWS::RDS::DBClusterParameterGroup |
none | Delete | |
<C>DBCluster |
AWS::RDS::DBCluster |
none | Snapshot: CloudFormation takes a final cluster snapshot and deletes the cluster. | |
<C>DBWriterInstance |
AWS::RDS::DBInstance (has DBClusterIdentifier) |
none | Delete (the Snapshot default applies only to DB instances without DBClusterIdentifier) |
|
<C>SecretAuroraClusterAttachment |
AWS::SecretsManager::SecretTargetAttachment |
none | Delete | |
aurora/cf.yml (Serverless v1, EngineMode: serverless) |
the same set, no DBInstance | none | Cluster: Snapshot; the rest: Delete | |
aurora/rdws/*.yml |
the same, plus AWS::IAM::ManagedPolicy |
none | as above | |
aurora/env/serverlessv2.yml, aurora/env/rdws/* |
the same (env-level), WorkloadSecurityGroup, SecurityGroupIngress |
none | as above | |
ddb/cf.yml (workload), ddb/env/cf.yml |
<Name> table (TableName: ${App}-${Env}-${Name}-<table> for workload, ${App}-${Env}-<table> for env), <Name>AccessPolicy |
AWS::DynamoDB::Table, AWS::IAM::ManagedPolicy |
none | Delete: the table and all items are deleted. The template sets no DeletionProtectionEnabled and no PITR, so nothing survives. |
s3/cf.yml, s3/env/cf.yml |
<Name>Bucket |
AWS::S3::Bucket (versioned; noncurrent versions expire after 30 days) |
none | Delete. It fails with BucketNotEmpty if any object or version exists, so the stack ends DELETE_FAILED and the bucket survives. |
<Name>BucketPolicy |
AWS::S3::BucketPolicy |
Retain | — | |
<Name>AccessPolicy |
AWS::IAM::ManagedPolicy |
none | Delete |
Templates also set none of: DeletionProtection, BackupRetentionPeriod or
SnapshotIdentifier on the Aurora clusters (so RDS defaults apply; retention is 1 day
(UNCONFIRMED: RDS default)), or PITR on DynamoDB.
5.2 Final-snapshot handling¶
-
Aurora
DBClusterwith no policy: on stack delete, or when the resource is removed from the template, CloudFormation deletes the cluster after taking a final snapshot. The snapshot sits outside any stack, is kept indefinitely, and keeps incurring storage cost. The DB instance in the cluster is deleted with no snapshot of its own. Automated backups follow the RDS delete defaults and are not kept by CloudFormation (UNCONFIRMED: AWS behaviour). -
On replacement: the default
UpdateReplacePolicyforDBClusteris (UNCONFIRMED), and Copilot sets none. The retain patch setsUpdateReplacePolicy: Retainexplicitly, so the question is moot for ecsodus. -
Under the ecsodus retain patch:
DeletionPolicy: Retainon theDBClustermeans no final snapshot is taken on teardown, because the cluster itself is kept and imported. Runbook step 2 takes a manual snapshot and turns on deletion protection before any patch. -
Secret: the retain patch keeps
<C>AuroraSecret. Without it, stack deletion would remove the credentials the retained cluster still uses. -
Other stateful defaults (CloudFormation behaviour):
-
the env EFS
FileSystemhas no policy, so its Delete destroys the data. AWS Backup recovery points fromBackupPolicy: ENABLEDmay survive in the backup vault (UNCONFIRMED). -
LogGroups are deleted along with their log data. - the StackSet
KMSKeyis scheduled for deletion (default window 30 days) (UNCONFIRMED: AWS default).
-
6. Naming, tags and SSM metadata¶
6.1 Stack names (internal/pkg/deploy/cloudformation/stack/name.go)¶
| Stack | Pattern |
|---|---|
| App stack | <app>-infrastructure-roles |
| App StackSet | <app>-infrastructure (instance stacks: CloudFormation-generated, StackSet-<app>-infrastructure-<uuid>, UNCONFIRMED) |
| Env stack | <app>-<env> |
| Workload stack | <app>-<env>-<svc>, truncated to 128 chars |
| Addons nested stack | child of the env or workload stack, logical ID AddonsStack (template.AddonsStackLogicalID) |
| One-off task | task-<name> |
| Pipeline | pipeline-<app>-<pipeline> (legacy: <pipeline>) |
6.2 Tags (internal/pkg/deploy/deploy.go)¶
AppTagKey = "copilot-application", EnvTagKey = "copilot-environment",
ServiceTagKey = "copilot-service", PipelineTagKey = "copilot-pipeline",
TaskTagKey = "copilot-task".
| Object | Tags |
|---|---|
| App stack, StackSet | copilot-application + app Tags |
| Env stack | copilot-application, copilot-environment + app tags |
| Workload stack | copilot-application, copilot-environment, copilot-service + app tags |
| ECR repo (StackSet) | copilot-service=<workload> |
ACM certs from dns-cert-validator, cert-replicator |
copilot-application, copilot-environment |
ACM certs from wkld-cert-validator |
+ copilot-service |
| SSM app / env / workload params | app: copilot-application; env: + copilot-environment; workload: copilot-application, copilot-service |
copilot secret init SecureStrings |
copilot-application, copilot-environment |
CloudFormation propagates stack tags to the resources that support tagging. The env-controller's
IAM policy, the EFS file-system policy and the desired-count-delegation lookup all depend on
these tags.
6.3 SSM metadata (internal/pkg/config/store.go, app.go, env.go, workload.go)¶
Schema version "1.0". Every parameter is type String holding JSON.
| Path | Value (JSON fields) |
|---|---|
/copilot/applications/<app> |
name, account, permissionsBoundary, domain, domainHostedZoneID, version, tags |
/copilot/applications/<app>/environments/<env> |
app, name, region, accountID, registryURL, executionRoleARN, managerRoleARN (+ the deprecated customConfig, telemetry) |
/copilot/applications/<app>/components/<workload> |
app, name, type (e.g. Load Balanced Web Service, Backend Service) |
Secrets from copilot secret init (internal/pkg/cli/secret_init.go) are SecureStrings at
/copilot/<app>/<env>/secrets/<name>. They are created outside CloudFormation, so ecsodus
treats them as external-reference.
6.4 Other fixed names¶
-
IAM roles:
<app>-adminrole,<app>-executionrole,<app>-DNSDelegationRole,<app>-<env>-CFNExecutionRole,<app>-<env>-EnvManagerRole. -
ECR repositories:
<app>/<svc>. - Log groups:
/copilot/<app>-<env>-<svc>. - Hosted zones:
- app:
<app>.<domain> - env:
<env>.<app>.<domain> - internal:
<env>.<app>.internal
- app:
- Service discovery namespace:
<env>.<app>.local(legacy<app>.local). - StackSet export:
<app>-ArtifactKey.