Copilot custom resources: what they do, and what their Delete handlers destroy¶
Source: the archived AWS Copilot CLI repository (Apache-2.0), commit a0dbe689 ("fix __inner override").
Everything below comes from reading that source. File paths are relative to the repository root. Anything
we could not confirm in the source is marked (UNCONFIRMED). Statements about how CloudFormation or
AWS behaves in general (not Copilot's own code) are labelled as CloudFormation or AWS behaviour.
Copilot ships 14 Node.js files in cf-custom-resources/lib/. Thirteen of them back Custom::*
resources. One, backlog-per-task-calculator.js, is a Lambda that runs on a schedule and is not a
custom resource. make package-custom-resources minifies each file into
internal/pkg/template/templates/custom-resources/. At deploy time
internal/pkg/deploy/upload/customresource/customresource.go zips each one as index.js and uploads it
to the app's artifact bucket. The templates then reference the zip through
{{ index .CustomResources "<FunctionName>" }} (S3Bucket/S3Key). Every custom-resource Lambda
runs on nodejs20.x.
How files map to Lambda functions and Custom:: types¶
customresource.go maps a function logical ID to a source file, and the mapping differs
by stack kind. Two Custom:: type names each map to more than one file, depending on the stack:
Stack kind (function in customresource.go) |
Function logical ID → file |
|---|---|
Env (Env) |
CertificateValidationFunction → dns-cert-validator.js; CustomDomainFunction → custom-domain.js; DNSDelegationFunction → dns-delegation.js; CertificateReplicatorFunction → cert-replicator.js; BucketCleanerFunction → bucket-cleaner.js; UniqueJSONValuesFunction → unique-json-values.js |
Load Balanced Web Service (LBWS) |
DynamicDesiredCountFunction → desired-count-delegation.js; EnvControllerFunction → env-controller.js; RulePriorityFunction → alb-rule-priority-generator.js; NLBCustomDomainFunction → wkld-custom-domain.js; NLBCertValidatorFunction → wkld-cert-validator.js |
Backend Service (Backend) |
DynamicDesiredCountFunction → desired-count-delegation.js; RulePriorityFunction → alb-rule-priority-generator.js; EnvControllerFunction → env-controller.js |
Worker Service (Worker) |
DynamicDesiredCountFunction → desired-count-delegation.js; BacklogPerTaskCalculatorFunction → backlog-per-task-calculator.js; EnvControllerFunction → env-controller.js |
Request-Driven Web Service (RDWS) |
EnvControllerFunction → env-controller.js; CustomDomainFunction → custom-domain-app-runner.js |
Static Site (StaticSite) |
TriggerStateMachineFunction → trigger-state-machine.js; CertificateValidationFunction → wkld-cert-validator.js; CustomDomainFunction → wkld-custom-domain.js |
Scheduled Job (ScheduledJob) |
EnvControllerFunction → env-controller.js |
Ambiguous type names. Custom::CustomDomainFunction is backed by three different files, and
Custom::CertificateValidationFunction by two. A tool must use the stack kind to tell them apart,
not just the type. The resource properties also differ:
- env
CustomDomainActionhasPublicAccessHostedZoneand a JSON-stringAliases - RDWS has
ServiceARN/CustomDomain - static site has
PublicAccessHostedZoneID/ServiceName - the env
CertificateValidationFunctionhasHandler: index.certificateRequestHandler, while static site usesindex.handler
Summary table¶
"Destructive" means the Delete handler removes or changes something that outlives the handle: a certificate, a DNS record, bucket contents, or resources in another stack. ecsodus fate, from PLAN.md §2.2:
-
Every
Custom::*handle and its Lambda, role and log group aremanual-cleanup, and always retain-patched, so no stack delete ever invokes a Delete handler. -
The out-of-band objects a handler created are
import.
| File | Custom:: type (logical IDs) |
Stack | Creates outside CloudFormation | Delete handler | Destructive | Terraform equivalent | v0.1 scope |
|---|---|---|---|---|---|---|---|
alb-rule-priority-generator.js |
Custom::RulePriorityFunction (HTTPRulePriorityAction, HTTPSRulePriorityAction, HTTPRuleWithDomainPriorityAction, HTTPRedirectRulePriorityAction) |
LBWS, Backend | nothing | no-op | no | literal priority on aws_lb_listener_rule |
in |
backlog-per-task-calculator.js |
none (scheduled Lambda BacklogPerTaskCalculatorFunction) |
Worker | CloudWatch EMF metric BacklogPerTask |
none (not a custom resource) | no | aws_lambda_function + aws_cloudwatch_event_rule/_target + aws_lambda_permission, or a metric-math scaling policy |
blocked (Worker) |
bucket-cleaner.js |
Custom::BucketCleanerFunction (ELBAccessLogsBucketCleanerAction) |
env | nothing | deletes every object version and delete marker in the ELB access-logs bucket | yes | none (aws_s3_bucket with force_destroy = false) |
in |
cert-replicator.js |
Custom::CertificateReplicatorFunction (CertificateReplicator) |
env (CDN) | ACM certificate in us-east-1 |
waits until unused, then DeleteCertificate (us-east-1) |
yes | aws_acm_certificate (us-east-1 provider alias) |
blocked (CDN) |
custom-domain.js |
Custom::CustomDomainFunction (CustomDomainAction) |
env | Route 53 A-alias records for each alias, in the env, app or root zone | DELETEs every alias A record | yes | aws_route53_record (A, alias) |
in |
custom-domain-app-runner.js |
Custom::CustomDomainFunction (CustomDomainAction) |
RDWS | App Runner custom-domain association; CNAME for the domain; ACM validation CNAMEs | disassociates the domain, DELETEs the CNAME and the validation CNAMEs | yes | aws_apprunner_custom_domain_association + aws_route53_record (CNAME) |
blocked (RDWS) |
desired-count-delegation.js |
Custom::DynamicDesiredCountFunction (DynamicDesiredCountAction) |
LBWS, Backend, Worker | nothing | no-op | no | aws_ecs_service.desired_count + ignore_changes |
in |
dns-cert-validator.js |
Custom::CertificateValidationFunction (HTTPSCert) |
env | ACM certificate env.app.domain + *.env.app.domain + aliases; validation CNAMEs in the env, app or root zone |
waits until unused, DELETEs validation CNAMEs not used by a newer cert, then DeleteCertificate |
yes | aws_acm_certificate + aws_route53_record (validation) (+ aws_acm_certificate_validation) |
in |
dns-delegation.js |
Custom::DNSDelegationFunction (DelegateDNSAction) |
env | NS record env.app.domain in the app hosted zone (through the app-account DNS role) |
DELETEs the NS delegation record | yes | aws_route53_record (NS) in the app zone (app-account provider) |
in |
env-controller.js |
Custom::EnvControllerFunction (EnvControllerAction) |
every ECS workload, RDWS, job | nothing outside CFN, but it updates the env stack's parameters | removes the workload from every *Workloads parameter and from Aliases, then runs UpdateStack on the env stack |
yes (indirect) | none: env resources become static Terraform resources | in |
trigger-state-machine.js |
Custom::TriggerStateMachine (TriggerStateMachineAction) |
Static Site | S3 objects (through the CopyAssetsStateMachine execution) |
no-op | no | none (CI aws s3 sync) |
blocked (Static Site) |
unique-json-values.js |
Custom::UniqueJSONValuesFunction (UniqueAliasesAction) |
env (CDN) | nothing | no-op | no | literal aliases list on aws_cloudfront_distribution |
blocked (CDN) |
wkld-cert-validator.js |
Custom::NLBCertValidatorFunction (NLBCertValidatorAction); Custom::CertificateValidationFunction (CertificateValidatorAction) |
LBWS (NLB); Static Site | ACM certificate (static site: us-east-1) tagged with app/env/svc; validation CNAMEs | DELETEs this service's unshared validation CNAMEs, waits until unused, DeleteCertificate |
yes | aws_acm_certificate + aws_route53_record (validation) |
blocked (NLB, Static Site) |
wkld-custom-domain.js |
Custom::NLBCustomDomainFunction (NLBCustomDomainAction); Custom::CustomDomainFunction (CustomDomainAction) |
LBWS (NLB); Static Site | A-alias records for the aliases (to the NLB or CloudFront) | DELETEs every alias A record that still points at this target | yes | aws_route53_record (A, alias) |
blocked (NLB, Static Site) |
Nine of the thirteen custom resources have destructive Delete handlers. The env-controller is
the most dangerous: its Delete never touches a resource directly, but the env-stack update it
triggers can delete the shared ALB, NAT gateways, internal ALB and the EFS file system along with
its data. That update can also cascade into HTTPSCert and CustomDomainAction (see below).
How retain works for custom resources (CloudFormation behaviour). With DeletionPolicy: Retain
on a Custom::* resource, CloudFormation does not send the Lambda a Delete request when the
resource leaves the stack. PLAN.md §2.4/§6 treats this as a hypothesis for the e2e run to confirm.
The "neutralizer" fallback exists in case it proves false. (UNCONFIRMED here: not in the
Copilot source.)
alb-rule-priority-generator.js¶
-
Used by.
workloads/partials/cf/alb.ymldefinesRulePriorityFunction(handlerindex.nextAvailableRulePriorityHandler) andRulePriorityFunctionRole. The custom resources are in:http-listener.yml:HTTPRulePriorityActionhttps-listener.yml:HTTPSRulePriorityAction,HTTPRuleWithDomainPriorityActionimported-alb-resources.yml:HTTPSRulePriorityAction,HTTPRedirectRulePriorityAction,HTTPRulePriorityAction
All are
Type: Custom::RulePriorityFunctionwithServiceToken: !GetAtt RulePriorityFunction.Arn. They appear in LBWS and Backend stacks that have an ALB listener. -
Properties.
RulePath(list), andListenerArn, which comes from!GetAtt EnvControllerAction.HTTPListenerArn/HTTPSListenerArn, or the internal variants for Backend, or a literal imported-listener ARN. -
Create/Update. Calls
DescribeRuleson the listener (paginated).-
Root paths (
/): priority counts down from 50000. It is the lowest existing priority ≥ 48000, minus 1, or 50000 if none. -
Other paths: it is the highest existing priority below 48000, plus 1, or 1.
-
Returns
Priority,Priority1,Priority2, …, one per entry inRulePath. TheListenerRules use them via!GetAtt …PriorityAction.Priority{i}. -
Physical ID:
alb-rule-priority-<LogicalResourceId>. - Out of band. Nothing.
- Delete.
case "Delete": break;, a no-op. - Destructive. No.
- Terraform. The output is just a number. Read the live rule's priority and write it as a
literal
priorityon the importedaws_lb_listener_rule.
-
-
ecsodus fate. The handle is
manual-cleanupand retain-patched. There is nothing out-of-band to import.
backlog-per-task-calculator.js¶
-
Used by.
workloads/partials/cf/autoscaling.ymlrenders it only under{{- if .Autoscaling.QueueDelay }}(Worker Service):BacklogPerTaskCalculatorFunction(index.handler)BacklogPerTaskCalculatorRoleBacklogPerTaskCalculatorLogGroupBacklogPerTaskScheduledRule(AWS::Events::Rule,rate(1 minute))PermissionToInvokeBacklogPerTaskCalculatorLambda
It is not a custom resource. It has no
Custom::type and no CloudFormation handler. -
What it does. Each minute it reads the ECS service's
runningCountand each SQS queue'sApproximateNumberOfMessages. It logs an EMF record for the metricBacklogPerTask(dimensionQueueName, namespace fromNAMESPACE), computed asceil(messages / max(runningCount,1)). The step/target-tracking scaling policies (AutoScalingPolicyEventsQueue,AutoScalingPolicy<svc><topic>EventsQueue) scale on that metric. -
Out of band. CloudWatch metric data only.
- Delete. None.
-
Destructive. No. But note that removing it silently breaks queue-based scaling, because the metric stops being published.
-
Terraform. Keep it as
aws_lambda_function+aws_cloudwatch_event_rule+aws_cloudwatch_event_target+aws_lambda_permission, or replace it with a target-tracking policy that uses metric math over SQS and ECS metrics (UNCONFIRMED design option). -
ecsodus fate. Imported (ADR-0014): the function (code ignored after import), the rule and its target, and the permission. It is load-bearing, so it is never
manual-cleanup.
bucket-cleaner.js¶
-
Used by.
environment/partials/elb-access-logs.yml, which is included inenvironment/cf.ymlonly when.PublicHTTPConfig.ELBAccessLogs.ShouldCreateBucketis true:-
ELBAccessLogsBucketCleanerAction:Type: Custom::BucketCleanerFunction,ServiceToken: !GetAtt BucketCleanerFunction.Arn,BucketName: !Ref ELBAccessLogsBucket -
BucketCleanerFunction(Timeout 900) ELBAccessLogsBucketCleanerRole, which is granteds3:ListBucket,s3:ListBucketVersions,s3:DeleteObjectands3:DeleteObjectVersionon the bucket
-
-
Create/Update. No-op. Physical ID:
bucket-cleaner-<LogicalResourceId>. - Out of band. Nothing.
-
Delete.
-
HeadBucket, then loopListObjectVersions→DeleteObjects(Quiet) over allVersionsand allDeleteMarkers, followingNextKeyMarker/NextVersionIdMarkeruntilIsTruncatedis false. -
If any object fails to delete, it throws an
AggregateErrorand the stack reports FAILED. - It returns early only if
HeadBucketthrowsResourceNotFoundException. S3'sHeadBucketnormally reports a missing bucket asNotFound, so a bucket that is already gone probably makes the Delete fail (UNCONFIRMED: SDK error name).
-
-
Destructive. Yes. It permanently empties the versioned access-logs bucket, so CloudFormation can then delete the bucket, which has no
DeletionPolicy. -
Terraform. No equivalent is needed. Import the bucket (
aws_s3_bucket+ versioning, encryption, public-access-block and policy sub-resources) and never setforce_destroy. -
ecsodus fate. The handle is
manual-cleanupand retain-patched.ELBAccessLogsBucketandELBAccessLogsBucketPolicyareimport.
cert-replicator.js¶
-
Used by.
environment/partials/cdn-resources.yml(only when.CDNConfigis set, and only when not.PublicHTTPConfig.ImportedCertARNs):CertificateReplicatorFunction(index.certificateReplicateHandler,Condition: DelegateDNS)CertificateReplicator:Type: Custom::CertificateReplicatorFunction,Condition: DelegateDNS,TargetRegion: "us-east-1",EnvRegion: !Ref AWS::Region,CertificateArn: !Ref HTTPSCert
CloudFrontDistributionusesAcmCertificateArn: !Ref CertificateReplicator. -
Create/Update.
-
Runs
DescribeCertificateon the env cert, thenRequestCertificateinus-east-1with the sameDomainNameand SANs,ValidationMethod: DNS, and tagscopilot-application/copilot-environment. -
Waits up to 570 s for
CertificateValidated. It writes no DNS records of its own, so it relies on the validation CNAMEs thatHTTPSCertalready created (UNCONFIRMED: this is an inference; the code only waits). -
Physical ID = the new certificate ARN. Every Update requests a new certificate, and CloudFormation then sends Delete for the old physical ID.
-
-
Out of band. An ACM certificate in us-east-1.
-
Delete. Runs only if the physical ID starts with
arn:.-
Polls
DescribeCertificateuntilInUseByis empty: up to 10 attempts, 30 s apart. If the cert is still in use, it throws and the stack reports FAILED. -
DeleteCertificatein us-east-1.ResourceNotFoundExceptionis ignored. - Destructive. Yes. It deletes the CloudFront certificate.
- Terraform.
aws_acm_certificatewith aprovider = aws.us_east_1alias. - ecsodus fate. The CDN is
blockedin v0.1. If migrated later: the handle ismanual-cleanupand retain-patched, and the us-east-1 cert isimport.
-
custom-domain.js (env stack)¶
-
Used by.
environment/partials/lambdas.ymldefinesCustomDomainFunction(Condition: ManagedAliases,index.handler).environment/partials/custom-resources.ymldefinesCustomDomainAction:Type: Custom::CustomDomainFunction,Condition: ManagedAliases-
Aliases: !Ref Aliases(the env parameter the env-controller maintains, a JSON map of workload → aliases) -
AppDNSRole: !Ref AppDNSDelegationRole PublicAccessDNS/PublicAccessHostedZone: the public ALB'sDNSNameandCanonicalHostedZoneID, or CloudFront's domain andZ2FDTNDATAQYW2when a CDN is configured
Both are rendered only when not
.PublicHTTPConfig.ImportedCertARNs. -
Create. For every alias in the flattened
AliasesJSON, it classifies the alias by regex:- env zone
^([^.]+.)?env.app.domain: uses the env-account Route 53 client - app zone
app.domain: uses the client that assumesAppDNSDelegationRole - root zone
domain: uses the same app-role client - other: skipped
It looks up the zone with
ListHostedZonesByName(MaxItems 1), without checking that the returned name matches exactly, then UPSERTs an A alias record toPublicAccessDNS/PublicAccessHostedZonewithEvaluateTargetHealth: true. Physical ID =LogicalResourceId. - env zone
-
Update. UPSERTs the current aliases, then DELETEs the aliases that were in
OldResourceProperties.Aliasesbut are no longer present. -
Out of band. One A-alias record per alias, in the env, app or root hosted zone. Records in the app and root zones live in the app (DNS) account.
-
Delete. DELETEs the A-alias record for every current alias, pointing at the current target. A "Tried to delete resource record set … but it was not found" error is ignored.
-
Destructive. Yes. Custom domains stop resolving.
-
Terraform.
aws_route53_record(typeA,alias {}), one per alias, in the right zone and provider (the app-account provider for app and root zones). -
ecsodus fate. The handle is
manual-cleanupand retain-patched. Each alias record isimport. Inventory must find them through Route 53, because no stack owns them.
custom-domain-app-runner.js (RDWS)¶
-
Used by.
workloads/services/rd-web/cf.yml:CustomDomainFunction(index.handler)CustomDomainAction:Type: Custom::CustomDomainFunction,ServiceARN: !GetAtt Service.ServiceArn,CustomDomain,AppDNSRole,AppDNSName
-
Create/Update.
-
AssociateCustomDomain(an "already associated" error is tolerated andDescribeCustomDomainsis used instead). -
In parallel:
- UPSERT a CNAME
CustomDomain → DNSTarget - poll up to 10 times for
pending_certificate_dns_validation, then UPSERT everyCertificateValidationRecordsCNAME
Both go into the hosted zone found by
ListHostedZonesByName(AppDNSName), using the app-role client. - UPSERT a CNAME
-
Physical ID:
/associate-domain-app-runner/<CustomDomain>. - Out of band. The App Runner custom-domain association (with its App Runner-managed certificate), the domain CNAME, and the validation CNAMEs.
-
-
Delete.
DisassociateCustomDomain. If the error is "No custom domain … found", it returns.- DELETE the domain CNAME and every validation CNAME. Not-found errors are ignored.
- Poll (up to 20 attempts, with backoff) until the domain is no longer listed.
delete_failedthrows.
-
Destructive. Yes.
-
Terraform.
aws_apprunner_custom_domain_association+aws_route53_record(CNAME, and the validation CNAMEs). -
ecsodus fate. RDWS is
blockedin v0.1. App Runner is covered in v0.2.
desired-count-delegation.js¶
-
Used by.
workloads/partials/cf/autoscaling.yml(only when.Autoscalingis set):-
DynamicDesiredCountAction:Type: Custom::DynamicDesiredCountFunction, withCluster(import${AppName}-${EnvName}-ClusterId),App,Env,Svc,DefaultDesiredCount: !Ref TaskCount, andUpdateID: {{ randomUUID }}, which forces an Update on every deploy -
DynamicDesiredCountFunction DynamicDesiredCountFunctionRole
service-base-properties.ymlsetsDesiredCount: !GetAtt DynamicDesiredCountAction.DesiredCountwhen autoscaling is on andDesiredCountOnSpotis not. -
-
Create/Update.
-
Tagging API
GetResources(ecs:service)filtered oncopilot-application,copilot-environmentandcopilot-service. If exactly one match, runsDescribeServicesand returns itsdesiredCount; otherwiseDefaultDesiredCount. -
Any error still reports SUCCESS, with
DefaultDesiredCount. - Physical ID:
copilot/apps/<app>/envs/<env>/services/<svc>/autoscaling. - Out of band. Nothing.
- Delete. No-op.
- Destructive. No.
- Terraform.
aws_ecs_service.desired_countwithlifecycle { ignore_changes = [desired_count] }(PLAN §2.4). The live count is authoritative, and the template'sTaskCountis recorded asdisputed.
-
-
ecsodus fate. The handle is
manual-cleanupand retain-patched.
dns-cert-validator.js (env HTTPSCert)¶
-
Used by.
environment/partials/lambdas.ymldefinesCertificateValidationFunction(Condition: DelegateDNS,index.certificateRequestHandler, Timeout 900).environment/partials/custom-resources.ymldefinesHTTPSCert:Type: Custom::CertificateValidationFunction,Condition: DelegateDNSDependsOnDelegateDNSActionandEnvironmentHostedZone- properties
AppName,EnvName,DomainName: !Ref AppDNSName,Aliases: !Ref Aliases,EnvHostedZoneId,Region,RootDNSRole: !Ref AppDNSDelegationRole
HTTPSListenerusesCertificateArn: !Ref HTTPSCert(unless imported certs are used).CertificateReplicatorcopies it. -
Create.
-
RequestCertificate:DomainName = <env>.<app>.<domain>-
SANs = that name,
*.<env>.<app>.<domain>, and every alias from theAliasesJSON that falls in the env, app or root zone -
DNS validation, tagged
copilot-application/copilot-environment- Physical ID = the cert ARN, set immediately.
- Waits for
DomainValidationOptions, then UPSERTs each validation CNAME:
- env-zone names into
EnvHostedZoneId(env account) - app and root names into the zone found by name, through the
RootDNSRoleclient- Waits up to 570 s for
CertificateValidated.
- Waits up to 570 s for
- Update. Does nothing unless the alias set changed. If it changed, it requests a new certificate (new physical ID) and validates it. CloudFormation then sends Delete for the old certificate during cleanup.
-
-
Out of band. The ACM certificate and its validation CNAMEs (env zone; app and root zones in the app account).
-
Delete. Runs only if the physical ID starts with
arn:.-
Polls up to 10 × 30 s until
InUseByis empty and every option has aResourceRecord. If the cert is still in use, it throws. -
deleteHostedZoneRecords:-
It runs
ListCertificatesfor another certificate with the sameDomainName(a "new" cert). -
It DELETEs every validation CNAME of the old cert whose domain is not a SAN of that new cert, de-duplicated by name and value.
-
On a plain stack delete there is usually no newer cert, so all its validation records are deleted.
-
-
DeleteCertificate.ResourceNotFoundExceptionis swallowed.
(The error path in
deleteHostedZoneRecordsreferences an undefinedoptionvariable, so any unexpected Route 53 error surfaces as aReferenceError. Either way the stack reports FAILED.) -
-
Destructive. Yes. It deletes the listener's certificate and the DNS validation records. Deleting a validation record also stops ACM from auto-renewing any cert that shares it.
-
Terraform.
aws_acm_certificate(import),aws_route53_recordfor each validation CNAME (import), and optionallyaws_acm_certificate_validation. That last one is a wait-only resource: it cannot be imported and creates no AWS object (UNCONFIRMED: provider behaviour). -
ecsodus fate. The handle is
manual-cleanupand retain-patched. The certificate and each validation record areimport.
dns-delegation.js (env DelegateDNSAction)¶
-
Used by.
environment/partials/lambdas.ymldefinesDNSDelegationFunction(Condition: DelegateDNS,index.domainDelegationHandler).environment/partials/custom-resources.ymldefinesDelegateDNSAction:Type: Custom::DNSDelegationFunction,Condition: DelegateDNSDomainName: !Sub ${AppName}.${AppDNSName}SubdomainName: !Sub ${EnvironmentName}.${AppName}.${AppDNSName}NameServers: !GetAtt EnvironmentHostedZone.NameServersRootDNSRole: !Ref AppDNSDelegationRole- Create/Update.
-
Assumes
RootDNSRole, which is the app account's<app>-DNSDelegationRole(fromtemplates/app/app.yml,DNSDelegationRole). -
Runs
ListHostedZonesByName(DomainName)and takesHostedZones[0]without checking an exact name match. -
UPSERTs an NS record
SubdomainName(TTL 60) with the env zone's name servers. - Physical ID =
SubdomainName. - Out of band. The NS delegation record for
env.app.domaininside the app hosted zone (AppHostedZonein the app stack, in the app account). CloudFormation never sees this record.
-
Delete.
- Same zone lookup.
ListResourceRecordSets(StartRecordName=SubdomainName, StartRecordType=NS, MaxItems=1). If the first record is exactlySubdomainName.of type NS, it DELETEs it and waits for the change. Otherwise it returns.
-
Destructive. Yes. Once the delegation is removed, every name under
env.app.domain(the service default aliases, andHTTPSCertrenewals) stops resolving publicly. -
Terraform.
aws_route53_record(typeNS) in the app zone, managed with the app-account provider. -
ecsodus fate. The handle is
manual-cleanupand retain-patched. The NS record isimport. Note that it lives in the app/DNS account, not the env account.
env-controller.js (EnvControllerAction)¶
-
Used by.
workloads/partials/cf/env-controller.yml, included by the templates for lb-web, backend, worker, rd-web and scheduled-job:-
EnvControllerAction:Type: Custom::EnvControllerFunction,ServiceToken: !GetAtt EnvControllerFunction.Arn -
EnvControllerFunction(Timeout 900) EnvControllerRole:cloudformation:DescribeStacks/UpdateStackonstack/${AppName}-${EnvName}/*, plusiam:PassRoleon${AppName}-${EnvName}-CFNExecutionRole, both conditioned on the copilot tags
The properties of
EnvControllerAction:Workload: !Ref WorkloadNameEnvStack: !Sub '${AppName}-${EnvName}'Parameters: {{ envControllerParams . }}EnvVersionAliases, only for an LBWS with ALB aliases and no imported ALB
internal/pkg/deploy/cloudformation/cloudformation.goreferences the type (envControllerResourceType) for progress rendering. -
-
Which parameters a workload requests. From
envControllerParametersininternal/pkg/template/workload.go:- LBWS without an imported ALB:
ALBWorkloads(if the ALB is enabled) andAliases - Backend with an ALB and no imported ALB:
InternalALBWorkloads - RDWS that is private without an existing endpoint:
AppRunnerPrivateWorkloads - any workload with
network.vpc.placement: private(PrivateSubnets):NATWorkloads - any workload with a Copilot-managed EFS volume (
Storage.ManagedVolumeInfo != nil):EFSWorkloads
- LBWS without an imported ALB:
-
Create/Update.
controlEnv:DescribeStacksthe env stack.-
For every parameter ending in
Workloads: add this workload to the comma list if it was requested, and remove it if it was not. -
Rewrite the
AliasesJSON if this workload's alias list changed. An empty list removes the key, and an empty map becomes"". -
If nothing changed, return the env Outputs (minus
EnabledFeaturesandLastForceDeployID, to stay under 4 KB) without updating. -
Otherwise run
UpdateStackwithUsePreviousTemplate: true, the same capabilities, andRoleARN= the env'sCFNExecutionRoleARNoutput. If the stack is alreadyUPDATE_IN_PROGRESS, wait and retry. -
Wait for
StackUpdateComplete(up to 870 s) and return the new env Outputs. Workload templates consume these through!GetAtt EnvControllerAction.<Output>, for exampleHTTPListenerArn,PublicLoadBalancerDNSName,EnvironmentSecurityGroupandInternalWorkloadsHostedZone. -
There is a 14.5-minute deadline. Physical ID:
envcontoller/<EnvStack>/<Workload>(sic). - Out of band. Nothing outside CloudFormation. It mutates another stack: every shared env
feature (the ALB, internal ALB, NAT gateways, EFS, App Runner VPC endpoint) exists only while at
least one workload's name is in the matching parameter (see
copilot-stacks.md, env conditions).
-
Delete.
controlEnv(EnvStack, Workload, []), with noParametersargument:- the workload is removed from every
*Workloadsparameter it appears in - its key is removed from
Aliases - then the env stack runs
UpdateStackwithUsePreviousTemplate: true
- the workload is removed from every
-
What that env update deletes, when this was the last workload using a feature:
-
ALBWorkloads→ "" makesCreateALBfalse. Deleted:PublicLoadBalancer,HTTPListener,HTTPSListener,DefaultHTTPTargetGroup, both public-LB security groups and their ingress rules,ELBAccessLogsBucketPolicy,UniqueAliasesAction/UniqueJSONValuesFunction/role, andCloudFrontDistribution.ManagedAliasesalso becomes false, which deletesCustomDomainActionand fires the destructivecustom-domain.jsDelete handler that removes the A records. -
InternalALBWorkloads→ "": deleted:InternalLoadBalancer, the internal listeners,DefaultInternalHTTPTargetGroup,InternalLoadBalancerSecurityGroupand its ingress rules, andInternalWorkloadsHostedZone. -
NATWorkloads→ "": deleted: everyNatGateway{n},NatGateway{n}Attachment(EIP),PrivateRouteTable{n},PrivateRoute{n}andPrivateRouteTable{n}Association. -
EFSWorkloads→ "": deleted:FileSystem(the data too),MountTarget{n},EFSSecurityGroupandEFSSecurityGroupIngressFromEnvironment. -
AppRunnerPrivateWorkloads→ "": deleted:AppRunnerVpcEndpointand its security group and ingress rule. -
An
Aliaseschange (the workload had aliases) updatesHTTPSCert, which requests a new cert. CloudFormation then sends Delete for the old cert (thedns-cert-validator.jsDelete).CertificateReplicatorupdates too.
-
-
Destructive. Yes, indirectly. This is PLAN §1's "env-controller" landmine.
-
Terraform. None. After migration these env resources are plain, unconditional Terraform resources. The outputs the workload consumed become references to them.
-
ecsodus fate. The handle is
manual-cleanupand must be retain-patched in every workload stack before any workload stack is deleted.Note (CloudFormation behaviour): the env-controller uses
UsePreviousTemplate: true, so an env-controller update after the env stack is retain-patched keeps the patch. And a resource whose condition turns false during an update is removed from the stack under itsDeletionPolicy, so withRetainit is orphaned rather than deleted. RetainingEnvControllerActionitself is still what stops the parameter change from happening at all.
trigger-state-machine.js¶
-
Used by.
workloads/services/static-site/cf.yml:TriggerStateMachineFunctionTriggerStateMachineAction:Type: Custom::TriggerStateMachine(note: noFunctionsuffix), withStateMachineARN: !GetAtt CopyAssetsStateMachine.ArnandAssetMappingFilePath
-
Create/Update.
StartSyncExecutionon the state machine, which copies the site assets intoBucket. It fails if the status is notSUCCEEDED. Deadline 14 min. Physical ID =LogicalResourceId. -
Out of band. S3 objects in the static-site bucket, written by the state machine.
- Delete. No-op (the comment says "this isn't a 'real' resource").
- Destructive. No.
- Terraform. None. Asset upload becomes a CI step.
- ecsodus fate. Static Site is
blockedin v0.1.
unique-json-values.js¶
-
Used by.
environment/partials/cdn-resources.yml(CDN, and imported cert or DelegateDNS):UniqueJSONValuesFunctionRole,Condition: CreateALBUniqueJSONValuesFunction,Condition: CreateALBUniqueAliasesAction:Type: Custom::UniqueJSONValuesFunction,Condition: CreateALB,Aliases: !Ref Aliases,FilterFor: !Ref ALBWorkloads, and optionalAdditionalStrings(the static CDN alias)
-
Create/Update. Parses the
AliasesJSON and keeps the keys listed inFilterFor. ReturnsUniqueValues: the sorted, de-duplicated union of their aliases plusAdditionalStrings.CloudFrontDistributionuses it asAliases: !GetAtt UniqueAliasesAction.UniqueValues. -
Out of band. Nothing.
- Delete. No-op.
- Destructive. No.
- Terraform. A literal
aliases = [...]onaws_cloudfront_distribution. - ecsodus fate. The CDN is
blockedin v0.1.
wkld-cert-validator.js¶
-
Used by.
-
workloads/partials/cf/nlb.yml(LBWS with an NLB, when.NLB.CertificateRequired):NLBCertValidatorAction:Type: Custom::NLBCertValidatorFunction,Condition: HasAssociatedDomain,LoadBalancerDNS,Aliases; plusNLBCertValidatorFunction/Role -
workloads/services/static-site/cf.yml:CertificateValidatorAction:Type: Custom::CertificateValidationFunction,IsCloudFrontCertificate: true; plusCertificateValidationFunction/CertificateValidatorRole
-
-
Create/Update. Update does nothing unless the alias set changed; otherwise it behaves like Create.
-
Validate the aliases: an existing A record that is not this LB's alias throws "already in use".
-
RequestCertificate:-
DomainName=<svc>-nlb.<env>.<app>.<domain>, or<svc>.<env>.<app>.<domain>for CloudFront, where the ACM client is pinned tous-east-1 -
SANs = the aliases
- tagged
copilot-application,copilot-environment,copilot-service - IdempotencyToken = md5(
/<svc>/<sorted aliases>)- UPSERT the validation CNAMEs (env zone in the env account; app and root zones through
RootDNSRole).
- UPSERT the validation CNAMEs (env zone in the env account; app and root zones through
-
-
Wait for validation.
- Physical ID = the cert ARN.
- Out of band. The ACM certificate and its validation CNAMEs.
- Delete. Runs only if the physical ID starts with
arn:. -
unusedValidationOptions:-
find this service's certificates through the tagging API (app/env/svc tags,
acm:certificate) -
drop validation records shared with the service's other certs
- for each remaining one, keep it if the alias's A record points at a different LB. Unrecognised domains are kept, and for CloudFront every existing record counts as "mine".
-
-
devalidate: DELETE the remaining validation CNAMEs. "Not found" is ignored. deleteCertificate: poll up to 12 × 30 s forInUseByto be empty (still in use → throw), thenDeleteCertificate.
-
-
Destructive. Yes.
-
Terraform.
aws_acm_certificate(us-east-1 provider for CloudFront) +aws_route53_recordfor each validation CNAME. -
ecsodus fate. NLB and Static Site are
blockedin v0.1.
wkld-custom-domain.js¶
-
Used by.
-
workloads/partials/cf/nlb.yml(NLB with.NLB.Aliases):NLBCustomDomainAction:Type: Custom::NLBCustomDomainFunction,Condition: HasAssociatedDomain,PublicAccessDNS/PublicAccessHostedZoneID= the NLB; plusNLBCustomDomainFunction/Role -
workloads/services/static-site/cf.yml(alias without an imported cert):CustomDomainAction:Type: Custom::CustomDomainFunction, pointing at CloudFront (Z2FDTNDATAQYW2)
-
-
Create. Validates the aliases (same "already in use" check as above), then UPSERTs an A alias record for each alias in the env, app or root zone. Physical ID =
LogicalResourceId. -
Update. Returns early if the aliases, target DNS and hosted zone are all unchanged. Otherwise it validates, UPSERTs the new records, and DELETEs aliases that are no longer present (using the old target).
-
Out of band. The A-alias records.
-
Delete. DELETEs the A record for every alias, using the current target. Errors are ignored in two cases:
- "not found"
- "values provided do not match", meaning the record now points elsewhere and is left alone
- Destructive. Yes.
- Terraform.
aws_route53_record(A, alias). - ecsodus fate. NLB and Static Site are
blockedin v0.1.