AWS resource plugin (CloudControl-based)
S3 BucketEncryption: ServerSideEncryptionRule now models
BlockedEncryptionTypes (an EncryptionType listing of NONE/SSE-C), so
buckets that block SSE-C round-trip through extract and reconcile instead of
having the setting stripped on bring-under-management.
AWS::CodeBuild::Project support. A CodeBuild build project is now a
first-class declared resource. CloudControl reports the type as
non-provisionable, so the plugin drives it directly through the CodeBuild API.
Modelled: name, description, serviceRole, source (type, inline
buildSpec, location), artifacts (type, location, name, packaging),
environment (type, computeType, image, privilegedMode,
imagePullCredentialsType, environmentVariables), cache (type,
location, modes), logsConfig (CloudWatch Logs and S3 destinations),
timeoutInMinutes, queuedTimeoutInMinutes, concurrentBuildLimit, tags,
and the assigned arn. A project exposes res.name and res.arn
resolvables, so its service role, its log group and the image build that runs
on it are wired through the resource graph instead of by naming convention.
Note that CodeBuild's update call leaves an unspecified field untouched, so
every modelled field is sent on every update: removing a field from the forma
clears it rather than leaving the previous value in place.
Deliberately not modelled in this version: vpcConfig, secondarySources /
secondaryArtifacts, fileSystemLocations, buildBatchConfig, badge,
triggers (webhooks), and visibility. A project created outside formae is
likely to use at least one of them, and adopting it would silently drop that
configuration on the first update — so the resource is not discoverable
in this version. Discovery can be enabled once the full property surface is
modelled.
Breaking. AWS::CodeBuild::ImageBuild is now a pure build-and-push
action: it creates no IAM role, no CodeBuild project and no log group.
It runs one build on a project you declare and name, and its only effect in
the account is the pushed image. Previously (0.1.15) it idempotently created
and updated an internal IAM service role and CodeBuild project, leaving
resources in the account that no forma described and that no audit of the
forma would predict.
To migrate a forma:
projectName, resolved from the declared project's res.name;serviceRoleArn, computeType, buildEnvironmentImage and
timeoutMinutes; their equivalents are now Project properties
(serviceRole, environment.computeType, environment.image,
timeoutInMinutes);AWS::CodeBuild::Project, the AWS::IAM::Role it runs as, and
the AWS::Logs::LogGroup it logs to. The project must use a privileged
LINUX_CONTAINER environment with source.type = "NO_SOURCE" and
artifacts.type = "NO_ARTIFACTS"; a project that does not is rejected with
a message naming the offending value before any build starts. The project's
own build spec is a placeholder — the image build supplies the spec it runs
per build as an override, and never reads the project's.AWS::CloudTrail::Trail support. You can now author and discover CloudTrail trails declaratively. Management and S3 data-event selectors are modelled, including advanced event selectors with resources.ARN StartsWith; selector lists apply as atomic (wholesale-replace) updates to match put-event-selectors, and booleans AWS echoes on read are treated as provider defaults to avoid perpetual drift. Discovery labels a trail by its TrailName (trails carry no Name tag), and AWS::S3::BucketPolicy gains a resolvable so a trail can order after its delivery bucket's policy.AWS::CodeBuild::ImageBuild support. You can now build a container image from a supplied Dockerfile via AWS CodeBuild, push it to ECR, and pin a downstream consumer (an ECS task definition, a Kubernetes pod) to exactly the image produced, making an image build a declarative step inside formae instead of an out-of-band docker build/push. The resource idempotently ensures an internal IAM service role and CodeBuild project (or adopts a supplied serviceRoleArn), runs the build, and exposes the pushed image's immutable digest as resolvables, imageRef assembled as repo@sha256:… from the exported digest, alongside imageDigest and imageUri, so a consumer pins to the digest rather than a mutable tag. AWS::ECR::Repository gains a repositoryUri resolvable to feed the build's target repository.formae.SecretValue so their values are hashed at rest end-to-end (previously stored in cleartext on the read/actual-state path). Covers AWS::SecretsManager::Secret secretString; AWS::RDS::DBInstance masterUserPassword/tdeCredentialPassword; AWS::RDS::DBCluster masterUserPassword; AWS::EC2::VerifiedAccessTrustProvider, AWS::ElasticLoadBalancingV2::Listener and AWS::ElasticLoadBalancingV2::ListenerRule clientSecret; AWS::EC2::VPNConnection preSharedKey; AWS::IAM::ServerCertificate privateKey; AWS::IAM::User login-profile password; and AWS::Lambda::Permission eventSourceToken. Requires a formae agent on the matching release.minFormaeVersion is bumped to 0.88.0 accordingly.AWS::EKS::Cluster no longer plans a destructive replace on every reconcile. The parent accessConfig field was annotated createOnly, writeOnly, and hasProviderDefault at once: writeOnly stripped accessConfig from the read-back actual state, so an unchanged declared accessConfig appeared only in the desired state and emitted a spurious add op, and because the field was also createOnly that phantom op flagged the cluster for a roughly fifteen-minute destroy-and-recreate that takes every workload on it down. The annotations now match the CloudControl contract: accessConfig keeps only hasProviderDefault (AWS auto-populates authenticationMode, a mutable in-place field), and the write-once bootstrapClusterCreatorAdminPermissions child is createOnly alongside its existing writeOnly. An unchanged cluster now reconciles as a no-op and an auth-mode change applies in place instead of replacing the cluster.ListObjectsV2 must be addressed to a bucket's home region; a bucket in a different region than the configured client answered with a 301 PermanentRedirect, so discovery logged a list error and skipped that bucket's objects. The list path now reads the home region from the redirect's x-amz-bucket-region header and retries the request against that region.LoggingConfiguration discovery no longer errors every cycle. CloudControl does not support the LIST action for AWS::NetworkFirewall::LoggingConfiguration (it returns UnsupportedActionException), so background discovery hit a 400 on this type each cycle. A logging configuration is a per-firewall singleton, so the plugin now registers a custom List, scoped to one firewall via the FirewallArn list parameter and reading that firewall's logging configuration, with the Firewall declared as the discovery parent.ReadRequest.PriorProperties, which the agent populates from formae 0.87.1. minFormaeVersion is bumped to 0.87.1 accordingly.Role whose inline policies are managed as standalone AWS::IAM::RolePolicy resources no longer has its updates rejected or its sibling-managed policies wiped. The role's read enriched Properties.Policies with the role's inline policies whenever the role had any, regardless of how the caller modelled them; for a caller managing them as standalone resources the stored row has no Policies key, so the enriched read registered as drift, rejecting every pending update on the role and risking a reconcile that wiped the sibling-managed policies (since Role.policies is atomic). Enrichment is now gated on the caller's prior model: policies are embedded only when the prior model is unknown (a create, a status read-back, or discovery) or already declares Policies; a known prior that omits Policies suppresses the embed. This keeps the no-phantom-drift behaviour for roles that declare inline policies (0.1.13) while unbreaking callers that manage them as standalone resources.AWS::Events::EventBus, AWS::Events::Archive, and AWS::Events::Rule. You can now manage custom event buses, their archives, and their rules, including the full rule target tree, declaratively. A rule wires to its bus and its targets through the resource graph, and each bus exposes its ARN as a resolvable (eventBus.res.arn), so a producer's events:PutEvents permission can reference the bus directly. Rules are modelled as children of their event bus so that rules on a custom bus are discovered correctly.Table now exposes resolvables, table.res.arn, table.res.streamArn, and table.res.tableName. A table's stream ARN can be wired straight into a Lambda::EventSourceMapping's eventSourceArn, so a stream-triggered Lambda no longer needs a hand-constructed stream ARN, previously there was no way to reference it at all.RestApi now exposes an execute-api ARN resolvable, api.res.executeApiArn. A Lambda::Permission that lets API Gateway invoke a function can source its sourceArn from the API itself (api.res.executeApiArn) instead of a hand-built, account-scoped wildcard ARN. The plugin derives the ARN, which CloudControl doesn't return, and fills in the partition and account.AWS::IAM::ServerCertificate now exposes resolvables, serverCertificate.res.arn and serverCertificate.res.serverCertificateName. Other resources (for example an HTTPS load balancer listener) can now reference an uploaded server certificate through the resource graph instead of a hand-written ARN.AWS::S3::Object support. You can now manage S3 objects declaratively, including shipping a local file as the object's body: the CLI reads the file relative to the apply working directory and uploads it. Object tags round-trip on create and update.AWS::S3::Object can now fetch its body from a URL. Instead of inline content, the object's source can be a structured remote source, the agent downloads the body over HTTPS at apply time, so the bytes never pass through the CLI. The fetch can send request headers (to pull an authenticated artifact; the header value is write-only and is not stored in cleartext) and can extract a named file from a downloaded zip archive. A typical use is delivering a Lambda deployment package from a versioned build artifact: point the object at a release URL templated by version and resolve the function's Code from the object's version. Publishing a new build then redeploys the function, while re-applying the same version is a no-op, no phantom redeploys. The fetch is restricted to HTTPS and refuses to reach loopback, private-network, or instance-metadata addresses. A full walkthrough, handler, release, deploy, and the redeploy loop, is in the lambda-http-source example.Function's functionCode can now embed references to other resources. Using formae 0.87.0's formae.embed, a function's JavaScript body can splice in another resource's generated value (for example a Key Value Store's Id) at apply time, instead of applying the store, copying the Id by hand, and re-applying the function.AWS::EC2::VPNGatewayRoutePropagation support. You can now manage VPN gateway route propagation declaratively, having a virtual private gateway automatically propagate its learned routes into a route table, and removing that propagation again. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the EC2 API.AWS::EC2::NetworkInterfacePermission support. You can now grant (and revoke) another AWS account permission to attach or associate one of your network interfaces. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the EC2 API. A network interface can also now be referenced by other resources through the resource graph (someInterface.res.id).AWS::Route53::RecordSetGroup support. You can now manage a group of Route 53 records that are created, updated, and deleted together in a single atomic change, useful when a set of records must always change as a unit. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now applies the whole group through one Route 53 change batch. Scope is simple records (name, type, TTL, values, and alias targets); weighted, latency, geolocation, and other routing-policy records are rejected with a clear error rather than silently dropped.AWS::IAM::UserToGroupAddition support. You can now manage an IAM group membership, adding a user to a group and removing it again, declaratively. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the IAM API. Each resource models one user-in-group membership, so to add several users to a group you declare one UserToGroupAddition per user.Function.functionCode, above), and the new field hint for values a provider drops unless they are re-sent on every update. Secret- and configuration-class fields that need that treatment (an OIDC client secret, several ECS service-configuration fields, EC2 Launch Template write-only fields, and an IAM user's initial console password) are now annotated so they keep applying on update under 0.87.0's revised write-only behaviour. minFormaeVersion is bumped to 0.87.0 accordingly.pkl eval time, so an invalid value is caught when the forma is evaluated rather than rejected deep in the apply by AWS.Role with inline policies no longer shows a phantom update on every reconcile. AWS stores a role's inline policies separately and CloudControl's read doesn't return them, so formae re-proposed adding them on every reconcile even though they were already present. A role's inline policies are now read back, so a role that hasn't changed reconciles as a no-op. Note: manage a role's inline policies through policies or as standalone AWS::IAM::RolePolicy resources, not both on the same role.Lambda::EventSourceMapping no longer plans a destructive replace on every reconcile. Its FunctionName was incorrectly treated as immutable; because AWS reads the field back as the function's full ARN while a forma typically declares the short name, every reconcile saw a "change" to an immutable field and planned a destroy-and-recreate of the mapping. FunctionName is now correctly mutable (AWS updates it in place), so the mapping reconciles without a replace. Reference the target function by its ARN (someFunction.res.arn) for a clean no-op.Method with a Lambda-proxy integration no longer shows a phantom Integration update on every reconcile. The function reference is written into the integration as an invocation URI, but the read didn't translate it back, so the stored integration never matched what the forma declared. The read now restores the function reference from the invocation URI, and the same translation is applied when the integration is updated, so re-pointing a method at a different Lambda applies as an in-place update instead of failing.AWS::NetworkFirewall::Firewall, AWS::NetworkFirewall::FirewallPolicy, AWS::NetworkFirewall::RuleGroup, and AWS::NetworkFirewall::LoggingConfiguration. You can now manage Network Firewall egress controls declaratively, including FQDN-allowlisting outbound traffic from private subnets. Rule groups, policies, and the firewall wire together through the resource graph (policy and rule-group ARNs, the firewall ARN, VPC and subnet IDs, and the log group are all Resolvables). The firewall exposes its per-AZ endpoints as a resolvable map, firewall.res.endpointIds.at("<az>"), so a route table can send 0.0.0.0/0 through the firewall endpoint in its own availability zone; existing EC2 routes accept a VPC-endpoint target unchanged. The firewall withholds create/update success until those per-AZ endpoints have propagated, so routes that depend on them don't resolve against an endpoint that isn't ready yet.FirewallPolicy's default-action lists relies on the whole-list replace behaviour added in formae 0.86.2; on earlier agents the update sends the old and new actions together and AWS rejects them as mutually exclusive. minFormaeVersion is bumped to 0.86.2 accordingly.INFO/WARN lines, such as recoverable CloudControl throttling retries, surfaced as ERROR and carried none of those attributes, making the agent log noisier and harder to filter by resource.CNAME, DNAME, NS, PTR, MX, or SRV record (or an ALIAS target) declared without the trailing dot read back dotted and produced a perpetual no-op diff. Because record-set updates are applied as delete-then-create, that mismatch could also fail with InvalidChangeBatch and block the apply. formae now normalises the trailing dot on read for these record types, so a dot-less declaration reconciles as a no-op. TXT and SPF values (quoted character strings) and A/AAAA (IP addresses) are left untouched.CNAME's value is sourced from the certificate via cert.res.validationRecords, and AWS returns it with a trailing dot while the record set read it back without one, leaving a phantom update on every reconcile. The certificate-sourced value is now normalised the same way, so the validation record settles as a no-op.secret.res.arn on AWS::SecretsManager::Secret now resolves to the secret's ARN. It previously failed to resolve at all, so any resource referencing a secret's ARN that way errored out before any AWS call was made. secret.res.arn, secret.res.id, and secret.res.ref all now resolve to the ARN.secretString on AWS::SecretsManager::Secret previously had no effect, the value never reached AWS, so the secret didn't rotate. It's now applied, and opaque.setOnce is honoured so an unrelated edit won't re-write a set-once value. Requires formae 0.86.2, this release's floor.AWS::SES::EmailIdentity updates now apply reliably. CloudControl's asynchronous update handler for this type can fail with GeneralServiceException ("The security token included in the request is invalid"), failing an otherwise-valid update. The plugin now applies EmailIdentity updates directly through the SES v2 API, MAIL FROM, DKIM signing, feedback forwarding, configuration set, and tags, instead of routing through CloudControl, mirroring how its Read is already handled.AWS::EKS::Cluster no longer shows a phantom update on every reconcile. Newer Kubernetes versions (1.32 and later) return a control-plane egress mode that AWS populates itself, which formae previously treated as unexpected drift. It is now recognised as a provider-managed default, so a cluster that hasn't changed reconciles as a no-op.AWS::CertificateManager::Certificate, AWS::CloudFront::Function, AWS::CloudFront::KeyValueStore, AWS::CloudFront::CachePolicy, AWS::CloudFront::OriginRequestPolicy, AWS::CloudFront::ResponseHeadersPolicy, and AWS::CloudFront::OriginAccessControl. ACM certs ship with a full custom provisioner that talks to the ACM API directly (the resource type is non-provisionable through CloudControl); cert.res.validationRecords exposes the DNS validation CNAMEs ACM publishes, so a Route53::RecordSet (or any other DNS-publisher resource) can wire them through the resource graph instead of being filled in by hand. CloudFront Distributions can now reference all of the above through Resolvable links, cache policy ID, origin-request policy ID, response-headers policy ID, origin access control ID, function ARN, lambda ARN, and ACM cert ARN, and formae orders creates and destroys correctly based on those references.AWS::ECS::Service now exposes an endpoints resolvable (service.res.endpoints.at("containerName:containerPort")) so downstream resources can wire their config URL through the service itself instead of through the listener. Because the endpoint resolves only once the service is operationally stable (the deployment-stability gating added in 0.1.10), anything consuming it waits until tasks are actually serving traffic. This closes the fresh-apply race where a listener URL resolved seconds before the tasks behind it were healthy, leaving consumers pointed at an endpoint returning 503s. Alongside this, AWS::ElasticLoadBalancingV2::ListenerRule now exposes its target group ARN so consumers of rule-routed services get correct ordering instead of racing the rule, and a load balancer name longer than the AWS 32-character limit is now rejected at pkl eval time rather than deep in apply. Listener-rule path/host routing, weighted target groups, and NLB endpoints are deferred follow-ups.createOnly, to line up with the new planning behaviour in formae 0.86.0. Under 0.86.0, fields that aren't explicitly immutable are treated as mutable and updated in place rather than triggering a replace. Any field AWS actually rejects on update needs to be marked immutable, or the apply fails at the provider. With this release every such field is annotated correctly, so the 0.86.0 in-place-update behaviour lands without surprise provider rejections. minFormaeVersion is bumped to 0.86.0 accordingly.AWS::EC2::SecurityGroupIngress and AWS::EC2::SecurityGroupEgress had no incoming edges in the destroy graph, so they were torn down first, severing ALB-to-task health checks, task-to-EFS NFS, and task-to-internet connectivity, which then cascade-failed the rest of the teardown. The destroy order is now workloads, then the security group rules, then the security group itself.AWS::Lambda::EventInvokeConfig no longer fails intermittently on create. CloudControl injects an empty DestinationConfig (with empty OnFailure/OnSuccess sub-objects) into every read of this resource, even when you never set one. formae's required-field validation then walked into the injected empty object and reported a missing Destination, surfacing as a flaky apply failure. The empty sub-objects are now stripped on read; genuine user-set destinations are non-empty and pass through untouched.runtimeDependency) that pull the mount targets into the destroy ordering ahead of their file system.AWS::ECS::Service now reports success only once the deployment is operationally stable: the rollout has completed, the running task count matches the desired count, and at least one healthy target exists behind each attached target group. Previously the service reported success as soon as CloudControl acknowledged the request, so any downstream resource reachable through the load balancer (for example a Grafana target driving its config through the listener URL) frequently hit 503s before the tasks were serving traffic. Non-standard service shapes (CODE_DEPLOY and EXTERNAL deployment controllers, the DAEMON scheduling strategy, classic-ELB attachments without a target group ARN, and desiredCount = 0) fall through to safe defaults rather than waiting on target health.formae apply. The target group's Targets field is populated at runtime by ECS Services (and anything else calling the register-targets API), and LoadBalancerArns is populated when a listener attaches the target group to a load balancer; neither is meaningfully user-settable. Tracking them in formae state meant the periodic synchronizer rewrote the resource on every ECS task placement and every listener attach, after which reconcile rejected the next apply with the stacks-have-been-modified error even though the forma hadn't changed, forcing operators into force mode or extract-and-absorb on every reconcile. Both fields are now dropped from the schema and stripped from the AWS read response before they reach formae state.Service creation no longer fails with target group <arn> does not have an associated load balancer when the Service and its Listener are scheduled together in the same apply. The plugin now treats that specific CloudControl error (InvalidRequest + matching message text, on Create operations only) as a transient in-progress state; the PluginOperator's existing status-poll loop absorbs the race until the Listener finishes wiring the target group to the load balancer. No PKL change needed, direct tg.res.targetGroupArn references keep working and don't have to be rewritten through listener.res.targetGroupArn to avoid the race.attachesTo field hints on Service.LoadBalancer.targetGroupArn and Service.VpcLatticeConfiguration.targetGroupArn. AWS rejects target-group deletion while a Service is still attached; without the annotation, the Service tore down in parallel with the listener chain, and any plugin-target driving CRUD through the Service's listener URL (Grafana, Loki, Tempo, and similar) wedged mid-tear-down with no URL backing it. With the annotation, the Service is destroyed before its target group. Requires formae 0.85.0 or newer to take effect: 0.84.0 agents silently ignore the annotation (the plugin still installs and every other fix applies, but the destroy-edge inversion no-ops). The plugin's minFormaeVersion stays at 0.84.0 deliberately, sibling plugins built against the same SDK family work fine on 0.84.0 and forcing an agent upgrade for one annotation isn't worth the disruption.CloudControl errors are no longer silently terminal. Synchronous errors from CCAPI on Create, Update, and Delete were previously surfaced to the agent as bare Go errors, which the agent classified as UnforeseenError, a non-recoverable code that bypasses the retry pipeline entirely. Even errors that AWS itself flags as recoverable (Throttling, NotStabilized, ResourceConflict, …) ended up as terminal failures. The plugin now translates these into the typed OperationErrorCode formae's PluginOperator understands, so the agent retries recoverable conditions instead of failing the whole apply.AWS::RDS::DBSubnetGroup no longer fails non-deterministically when its subnets are created in the same forma. AWS RDS rejected freshly-created EC2 subnets with InvalidRequestException: Some input subnets ... are invalid until RDS's internal subnet cache caught up, a classic cross-service eventual-consistency window between EC2 and RDS. The plugin recognises this specific class of InvalidRequest as a recoverable race, so the agent retries through the propagation gap and the subnet group lands on the first apply.AWS::SES::ConfigurationSetEventDestination updates now succeed. Previously, any update to an event destination (toggling enabled, changing matchingEventTypes, switching destination targets) failed within milliseconds with no AWS API round-trip. CCAPI rejects this resource's composite <csName>|<edName> identifier on Update with ValidationException: not valid for identifier [/properties/Id], the same limitation that affected Read in 0.1.7. Updates now route through sesv2.UpdateConfigurationSetEventDestination directly.AWS::SES::ConfigurationSetEventDestination deletes now succeed. Same CCAPI composite-identifier limitation as Update. The most user-visible symptom was that formae destroy failed on any stack containing an event destination, and replace flows (when the parent ConfigurationSet.name changes) failed at the destroy step. Deletes now go through sesv2.DeleteConfigurationSetEventDestination, with a missing-destination error treated as a successful no-op so retried destroys are idempotent.AWS::SES::ConfigurationSetEventDestination now correctly surfaces existing event destinations as unmanaged resources. CCAPI's ListResources returns bare EventDestinationNames ("bounces") instead of the composite <csName>|<edName> the resource's Read path requires, so every discovered destination failed its per-resource Read and never made it into the inventory. The plugin's List now walks ListConfigurationSets → GetConfigurationSetEventDestinations and emits properly-formed composite identifiers.AWS::SES::EmailIdentity covers verified sending domains or addresses (with bundled MAIL FROM, feedback, and Easy DKIM attributes). AWS::SES::ConfigurationSet and AWS::SES::ConfigurationSetEventDestination route bounce/complaint/delivery events to SNS, Kinesis Firehose, EventBridge, or CloudWatch, exactly one of the four destination types is enforced at PKL evaluation time, so bad shapes fail at pkl eval rather than at apply time. AWS::SES::EmailIdentityVerification is a polling gate downstream consumers depend on for send-readiness; it sits between the identity (and the DNS records that verify it) and any resource that needs to send mail, breaking the apply-time deadlock where verification needs DNS, DNS depends on the identity, and the identity can't wait on either.EmailIdentity.res.requiredDnsRecords is a typed listing resolvable that exposes the DNS records SES expects, 3 DKIM CNAMEs, plus an MX and SPF TXT for MAIL FROM when configured. A forma drives Route53 (or any DNS plugin) directly off id.res.requiredDnsRecords.at(N).name and .values, with no manual token extraction. See examples/ses-basic/main.pkl in the plugin repo for the full pattern. Terraform, Pulumi, and Crossplane all force users to extract verification_token/dkim_tokens[] strings and hand-author the records; this is the first IaC tool to wire them automatically.AWS::EC2::PlacementGroup's spreadLevel is now marked hasProviderDefault. AWS auto-populates SpreadLevel after a partition-strategy create even when not specified, which previously caused replace flows (where the user switches strategy from spread to partition) to fail with Property SpreadLevel is not expected and not a provider default. The field stays createOnly.AWS::ECS::TaskDefinition are no longer silently dropped from the diff. 19 user-canonical sub-fields on ContainerDefinition, including environment, portMappings, mountPoints, secrets, command, entryPoint, dependsOn, extraHosts, and dockerLabels, were previously annotated hasProviderDefault, which symmetrically stripped them from both the desired and actual sides before comparison. Any real change to those fields produced an empty plan on formae apply, forcing operators into out-of-band aws ecs register-task-definition workarounds (which then showed up as drift on the next reconcile). The annotation is now scoped to the genuinely cloud-defaulted scalars (cpu, essential, versionConsistency).AWS::Lambda::LayerVersion's compatibleArchitectures, compatibleRuntimes, and description are now marked hasProviderDefault. AWS Read returns empty values for these when the user omits them in PKL, and because every LayerVersion field is createOnly, the resulting phantom drift would schedule a destroy + create on every reapply.AWS::Lambda::EventInvokeConfig no longer fails with Model validation failed: required key [Destination] not found. CloudControl's update handler for this resource type re-validates the full server-side state on every patch, even fields the patch doesn't touch, and AWS materialises empty DestinationConfig.OnFailure / OnSuccess sub-objects into the response on Read whether or not the caller ever set them, which then fail the schema's "if present, Destination is required" rule. Updates that only changed MaximumRetryAttempts or MaximumEventAgeInSeconds would still get rejected on the server side. Updates now route through the Lambda UpdateFunctionEventInvokeConfig API directly, which has no such cross-field re-validation.Listener.defaultActions[*].forwardConfig and ECS Cluster.clusterSettings are now marked hasProviderDefault. ELBv2 derives ForwardConfig (target groups + stickiness defaults) on Read from the action's TargetGroupArn when the user specifies a simple forward target, and ECS populates clusterSettings with default entries like containerInsights: disabled when none are configured. Without the annotations, every reapply emitted a no-op patch on these fields that surfaced as a spurious update.Addon, AccessEntry, FargateProfile, PodIdentityAssociation, and IdentityProviderConfig are now first-class resources alongside Cluster and Nodegroup, enabling end-to-end EKS cluster management (including discovery of cluster-child resources) from a single forma.ExpressGatewayService support. Express Gateway services can now be managed through formae. Uses the native ECS SDK because the CloudControl handler for this type is broken server-side.url property that combines the parent ALB's DNS name with the Listener's protocol and port. Use listener.res.url to wire load balancer endpoints into target configs without manual URL construction.formae.Resolvable, and all resources with Resolvable classes now have hidden res wired up.profile field is now mutable, changing it updates the target in place without recreating resources. The region field remains immutable; changing it triggers a full target replace as before. See Per-field config mutability for details.Service and TaskSet discovery no longer spam InvalidRequestException: Missing Or Invalid ResourceModel property errors on every cycle. Service is now discovered as a child of Cluster (its list handler requires a Cluster filter), and TaskSet is enumerated via the ECS SDK's DescribeServices because its CloudControl list handler demands an Id and is effectively a Read.taskDef.res.taskDefinitionArn now works as expected, enabling ECS Services to reference task definitions via resolvables.EFSVolumeConfiguration.filesystemId now accepts resolvable references, so ECS tasks can reference EFS filesystems created in the same forma.igwAttach.res.internetGatewayId). Routes should reference the gateway ID through the attachment, not the gateway directly, to ensure correct destroy ordering. Without this, destroying a stack with Routes and an IGW attachment could hang for hours because formae tried to detach the IGW before deleting the routes that use it.listener.res.targetGroupArn, resolving to the target-group ARN attached via the listener's first default action. ECS Services (and any other consumer that needs the target group already wired to the load balancer) should reference this instead of tg.res.targetGroupArn. Without it, AWS rejects ECS service creation with "target group does not have an associated load balancer" when formae schedules the service before the listener attaches the TG. Same pattern as igwAttach.res.internetGatewayId for routes/IGW.EventInvokeConfig.DestinationConfig) could fail with CloudControl errors like required key [Destination] not found when the nested object was empty. The plugin now strips empty sub-objects from replace operations the same way it already did for add operations, so these updates succeed.awsvpc tasks that the user didn't set, which previously made the planner think the task definition had changed. Those fields are now recognised as provider-populated and ignored during comparison. Requires formae 0.84.0.EFSVolumeConfiguration.rootDirectory no longer causes phantom replacements. CloudControl returns RootDirectory as "/" even when it was never set, which made the planner see a change on every reapply.createOnly field and triggered a full replace. ARN-vs-short-name differences are now normalised before comparison.example.com.-SOA and example.com.-NS.HealthCheckConfig and AlarmIdentifier sub-resources, enabling richer health check definitions in Pkl.ResourceLifecycleConfig fields to ElasticBeanstalk environments and configuration templates.apprunner/service.pkl to apprunner/apprunnerservice.pkl for consistency with the naming convention used across other resource schemas.ListResults, preventing unrelated resources from appearing in extracted Pkl output.AWS::AppRunner::Service), enabling management of AppRunner web services through formae.af-south-1 availability zone pattern. The Region typealias already included af-south-1, but the AvailabilityZone constraint was missing it, causing Pkl evaluation failures for resources in that region.