platform.engineering/awsofficialv0.1.17stableresource

AWS resource plugin (CloudControl-based)

installation
$formae plugin install aws
changelog as of v0.1.18-dev.1 · structured per keep a changelog
v0.1.17stableinstalled default
Added
  • AWS::CodeBuild::ImageBuild gains a versionUri field and matching res.versionUri resolvable: the full <repo>:<versionTag> reference, declared by the author and placed as a create-once immutable pin on the manifest the build pushes. Because the value is declared rather than produced by the build, a consumer (say, a task definition's container image) that references res.versionUri is planned in the same apply that rebuilds the image, so an image roll converges in one apply; deploying through res.imageRef (the digest, which exists only after the build) takes two. The digest resolvables are unchanged and remain available.

  • Resources formae created in order to reach this account are no longer offered for import. The connect role and the account-global OIDC provider its trust policy names carry an ownership marker, and discovery now excludes anything carrying it, so a reconcile can no longer take away formae's own access. EFS file systems and access points are matched through their per-type tag properties, which a generic tag filter never sees. Sharing the formae name prefix is not enough on its own: unrelated roles stay visible.

  • Discovery for four more resource types: AWS::IAM::ManagedPolicy, AWS::KMS::Alias, AWS::EC2::PrefixList, and AWS::EC2::Route (scoped per route table). AWS-managed inventory is kept out of discovery: AWS-managed policies (arn:aws:iam::aws:policy/*) and reserved aliases (alias/aws/*) are skipped at list time before the per-resource read, AWS-owned prefix lists are excluded by owner, and the implicit local route every route table carries is excluded by gateway.

  • Every resource type that keeps discoverable = false now carries an inline comment recording why: no listable inventory, no CloudControl list support, a defect worth recording, or AWS-managed flooding without a filterable signal.

  • Polymorphic auth on the target config: DefaultChainAuth (the existing default credential provider chain — env vars, shared config, IMDS/IRSA — optionally pinned to a shared-config profile) or OidcAuth (federated identity: an OIDC identity token from the paired oidc-credential broker, exchanged for credentials by assuming a role you name in the target account). auth and the legacy flat profile field are mutually exclusive; setting both is rejected at eval.

    OidcAuth requires a formae agent with oidc-credential broker support (minFormaeVersion = "0.89.0" in formae-plugin.pkl). Paired with an older agent, or an agent with no broker paired, credential resolution fails closed with an explicit error rather than ever falling back to ambient credentials.

    Known limitation: the STS exchange uses a default-configured client. The AssumeRoleWithWebIdentity call that turns the identity token into credentials is made with a region-only STS client, so AWS_USE_FIPS_ENDPOINT, AWS_ENDPOINT_URL_STS and a custom CA bundle are not honoured on that one exchange. Proxy settings are honoured, because they come from the HTTP transport rather than from SDK configuration. Every other AWS call the plugin makes uses the fully configured client and is unaffected.

    One-time bookkeeping, not drift. Existing targets carry no auth block, and adding the field to the schema is a change formae records against every target's stored metadata regardless of whether the target's declared configuration actually changed. Expect a single, resource-inert target-metadata update on the first reconcile after upgrading to this version — no cloud resource is read, created, updated or destroyed by it. If a stack shows exactly one such update per target immediately after the upgrade, this is why; it is not drift and does not recur.

  • AWS::RDS::Database and AWS::RDS::DatabaseRole support. A PostgreSQL database inside an Aurora cluster, and its owning login role, are now first-class declared resources. CloudControl models a cluster and its instances but nothing inside the engine, so both are driven directly over the RDS Data API — the plugin needs no network path to the database, only IAM-authenticated HTTPS against the cluster ARN and a Secrets Manager secret in the standard RDS JSON format. Aurora PostgreSQL only; a cluster running another engine, or one without the Data API enabled, is rejected with an explicit error rather than a failed statement.

    Modelled on the database: clusterArn, adminSecretArn, databaseName and owner. On the role: clusterArn, adminSecretArn, roleName, password and canLogin. Both expose resolvables so a database, its owning role and the secret holding its credentials are wired through the resource graph rather than by naming convention. The admin secret is deliberately mutable — an update proves the replacement credential can reach the cluster before anything starts depending on it — while the cluster and the object's name are create-only.

    The password never reaches the SQL text. A PASSWORD clause cannot take a bind parameter, so the plugin composes the PostgreSQL SCRAM-SHA-256 verifier itself and sends that; the plaintext appears in neither CloudTrail's rds-data events nor any statement logging. The password field is write-only and opaque, so it is hashed at rest and never compared against the salted verifier the engine stores. Rotation is therefore driven from the change being applied: the password is written when that change touches it, and also when it carries no usable signal — a missed rotation is a correctness bug, while writing the same password again converges. Each write stores a freshly salted verifier, so a redundant one is convergent rather than a no-op. A password the plugin cannot read is treated the same way. Once formae holds an opaque value only as a hash it stops sending the value itself, and what arrives in its place is deliberately not the type the schema declares; an update that is not writing the password tolerates that rather than failing, so the login attribute and the admin credential stay changeable, while one that is writing it fails outright rather than storing something no client could authenticate against. Passwords must be printable ASCII (U+0020 to U+007E), the range PostgreSQL's SASLprep normalization passes through unchanged; Secrets Manager's generated passwords already satisfy this.

    Destroying either resource is destructive and has no snapshot or retain semantics. Destroying a database runs DROP DATABASE, escalating to WITH (FORCE) — which terminates open sessions — only when the engine reports the database in use. Destroying a role runs DROP ROLE, which PostgreSQL refuses while the role owns any object, including objects in databases formae does not manage; that refusal is surfaced as an actionable error naming the role. Creating a database also grants the admin membership of the owning role where PostgreSQL requires it, and that grant is never revoked: it confers nothing the admin could not re-grant itself, and a revoke could not be made atomic.

    Neither type is discoverable — enumerating objects inside a cluster needs admin credentials discovery cannot supply — so listing fails with an explicit unsupported error rather than an empty page. The principal running the plugin needs rds-data:ExecuteStatement on the cluster, secretsmanager:GetSecretValue on the admin secret (plus kms:Decrypt on its key for a customer-managed key), and rds:DescribeDBClusters for the create-time preflight.

    A cluster that is not serving yet is waited out, not failed. An Aurora cluster reports itself available, with the Data API enabled, for several minutes before it can answer a statement — until it has a running DB instance every call is rejected. Creating a database or a role probes the cluster first, and a cluster that cannot answer yet defers the create: the operation reports itself as still running and finishes from a later status check, once the cluster serves. Declaring a cluster, its instance and a database in one forma therefore works, instead of exhausting the retry budget minutes short of the cluster being ready. The wait is bounded — a create still waiting after fifteen minutes fails, naming the cluster — and no statement that changes anything is sent until the cluster answers. A wait interrupted by the plugin restarting fails and asks for the apply to be run again, rather than reporting a create that never ran. Other conditions that clear on their own (an endpoint still coming up, a resuming or unavailable database, a service fault or timeout) are reported to the agent as recoverable on every operation, so it retries them instead of failing the resource. Faults that will not clear — access denied, an unusable secret, a rejected statement — stay terminal, keep their diagnosis, and never start a wait.

  • Discovery for seven more resource types whose CloudControl list support was verified against the type registry and a live account: AWS::IAM::User, AWS::IAM::VirtualMFADevice, AWS::ECS::CapacityProvider, AWS::RDS::CustomDBEngineVersion, AWS::S3::AccessGrantsInstance, AWS::Lambda::Permission (scoped per function), and AWS::ElasticLoadBalancingV2::ListenerRule (scoped per listener). Live resources of these types now appear in inventory and can be brought under management.

  • Discovery for AWS::ApiGateway::Resource, AWS::ApiGateway::Method, and AWS::CloudFront::Distribution, and extract for AWS::CloudFront::Distribution. Live API Gateway resources and methods and CloudFront distributions now appear in inventory and can be brought under management. Method has no CloudControl list handler, so the plugin enumerates methods via the API Gateway control plane, scoped per REST API so methods on the implicit root resource are discovered too.

  • 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.

  • AWS::Lambda::Version now exposes a res.functionArn resolvable carrying its qualified (versioned) function ARN, so a CloudFront distribution's lambdaFunctionAssociations entry can reference a published version by reference instead of a hand-edited literal ARN pin. Lambda@Edge rejects $LATEST, so the association has always needed a versioned ARN; previously that ARN had to be copied in by hand. Note the caveat this doesn't remove: every field on Version is create-only, so a function code change publishes a replacement version rather than updating the existing one. The safe order is publish the new version, re-point the distribution at it, wait for CloudFront to propagate the change, and only then delete the old version, and a replicated Lambda@Edge version can stay undeletable for a while after the association is removed. Exposing the resolvable removes the hand-edit; it does not change that ordering.

  • AWS::ECS::TaskDefinition: ContainerDefinition.image now accepts a resolvable (String|formae.Resolvable) instead of a bare String, so a container image can be wired through the resource graph: an AWS::CodeBuild::ImageBuild digest, an AWS::ECR::Repository URI, or any other resolvable image reference. It was the only reference-shaped field on the resource family still typed as a plain string, which forced the image to be pinned by hand and re-pinned on every rebuild.

  • AWS::ServiceDiscovery::PrivateDnsNamespace and AWS::ServiceDiscovery::Service support (AWS Cloud Map). A workload can now be registered for service discovery from a forma: declare a private DNS namespace, declare a service in it, and reference that service's res.arn from an ECS service's serviceRegistries[].registryArn. ECS then registers each task's address as the task starts and deregisters it as the task stops, so <service name>.<namespace name> resolves to the live task addresses inside the VPC. The ECS field already accepted a reference; until now there was no resource to point it at.

    The namespace exposes res.id, res.arn, res.name and res.hostedZoneId resolvables. The hosted zone id matters because Cloud Map creates and owns a Route53 private hosted zone for the namespace, and nothing else gives you a handle on it.

    CloudControl reports the namespace type as non-provisionable, so the plugin drives it directly through the Cloud Map API. Its create, update and delete are asynchronous operations that the plugin polls to completion. A delete that AWS rejects because the namespace still holds services is retried under a single timeout rather than failed outright, since ECS deregisters task instances asynchronously after an ECS service is deleted and a stack destroy can legitimately reach the namespace while that is still settling.

    Two limitations to know about before adopting this. The namespace's vpc is create-only and write-only, and the namespace is not extractable: Cloud Map returns a namespace's VPC from no API and offers no way to change it, so an extract could never populate the field and a namespace created outside formae cannot be brought under management by extracting it. Discovery still lists namespaces; only extraction is unavailable. Second, a namespace name must be unique within a VPC, and a colliding create is accepted and then fails asynchronously with a hosted-zone conflict rather than being rejected up front.

    On the service, dnsConfig.dnsRecords is replaced as a whole on update, which is what the AWS API itself does with that list. Health checking is Cloud Map's own healthCheckCustomConfig for services in a private namespace; healthCheckConfig is only valid in a public namespace and AWS rejects it otherwise.

    Deliberately not modelled in this version: HttpNamespace, PublicDnsNamespace, and Instance. Instances are registered and deregistered by ECS itself, and the other two namespace types are non-provisionable in the same way as the private one, so each would need its own provisioner rather than coming for free with a schema.

  • AWS::CodeBuild::ImageBuild gains additionalTags: immutable pins placed on the manifest a build pushes, alongside imageTag. imageTag moves to the new digest on every in-place rebuild, so a consumer that pinned the previous digest lost the image it pinned — the rebuild left that manifest untagged, and an untagged predecessor is exactly what the resource prunes. A pin gives the predecessor a name of its own, so it is not untagged, so it survives: a rollback target that outlives the next build.

    Pins are create-once. A pin declared for the first time is placed on the manifest that build produced, but only if the tag does not already exist — a newly declared pin whose tag is taken fails the apply, naming the tag and the digest it currently resolves to, so a reused release name cannot yield a green apply whose rollback pin names an unrelated image. A pin carried over from the previous apply is left exactly where it is. The plugin never moves and never deletes a pin. Placement re-registers the identical manifest bytes and media type, so the pin resolves to the same digest by construction, and adding one does not force a rebuild — the build that produced the image is not re-run to give it a second name.

    Two consequences to declare against. Removing a pin from the listing is a no-op in the registry, so a placed pin is removable only out of band or by a lifecycle policy. And a repository that accumulates pins cannot be emptied by tearing the ImageBuild down: teardown removes only imageTag, while every pin is by design a manifest meant to outlive the build that produced it. Its lifecycle becomes yours to manage — a repository lifecycle policy that ages pins out, or an out-of-band delete. A formae-managed AWS::ECR::Repository that still holds images cannot be destroyed, and emptyOnDelete does not currently change that: CloudControl's delete carries no resource model, so the repository's delete handler never sees the property.

  • Secrets Manager secrets adopt formae's first-class secret types. A secret's value can now be referenced with secret.res.secretValue, and secret.res.secretValue.json("path") reaches into a JSON payload. The existing res.secretString, res.arn, res.id and res.name accessors keep working unchanged.

  • Every secret-bearing property can now be bound to a formae generator, so the credential is drawn (and rotated) by the agent instead of being pinned in the forma file. Twelve fields accept a generator output, written pw.gen.value: AWS::EC2::VerifiedAccessTrustProvider OidcOptions.clientSecret, AWS::EC2::VPNConnection VpnTunnelOptionsSpecifications.preSharedKey, AWS::ElasticLoadBalancingV2::Listener and ...::ListenerRule AuthenticateOidcConfig.clientSecret, AWS::IAM::ServerCertificate privateKey, AWS::IAM::User LoginProfile.password, AWS::Lambda::Permission eventSourceToken, AWS::RDS::DatabaseRole password, AWS::RDS::DBCluster masterUserPassword, AWS::RDS::DBInstance masterUserPassword and tdeCredentialPassword, and AWS::SecretsManager::Secret secretString. Every one of these fields was already opaque and stays opaque, so a generated value is still hashed at rest. Literal strings, formae.Value, formae.SecretValue and (where the field already accepted one) formae.Resolvable keep working unchanged, and the two fields that carry a pattern constraint on the literal (ServerCertificate.privateKey, Permission.eventSourceToken) still reject a string that does not match.

Changed
  • AWS::RDS::DBInstance no longer models certificateDetails as a schema property. The field is read-only on AWS's side and its contents move on their own: RDS rotates the server certificate automatically around its half-life, so treating the details as a declarable property with a provider default made an unchanged forma reject reconciles after every rotation. The values are still stored with the resource and still referenceable through the certificateDetailsCAIdentifier and certificateDetailsValidTill resolvables. A forma that declared certificateDetails now fails at eval; delete the block, the field never accepted a user value.

  • Every hasProviderDefault schema annotation now carries a recorded disposition in schema/provider-default-dispositions.json, enforced by a unit test: new annotations fail CI until classified, and rows for removed fields fail as stale. Fields confirmed as provider-default-once are marked keep with a pin; fields a co-actor legitimately writes (such as AWS::RDS::DBInstance.engineVersion, which RDS moves under auto minor version upgrades) are marked co-owned while their handling is designed.

  • AWS::S3::Object is no longer discoverable. A bucket's object count is unbounded, and objects are data rather than infrastructure, so a discovery scan enumerated every key in every bucket in the account — slow enough to stall the scan on a real account, and it filled the inventory with rows nobody had declared. Objects formae manages are created, read, updated and destroyed exactly as before; they are simply no longer found by discovery, so an existing object now has to be declared to be brought under management.

  • AWS::CodeBuild::ImageBuild prunes the predecessor manifest after an in-place rebuild — behaviour present since the resource landed and not previously written down. A rebuild moves imageTag to the new digest and leaves the prior manifest untagged; the prune deletes it, so a co-managed repository stays empty enough to tear down. It only ever deletes a manifest that carries no tag at all, which is what lets the new additionalTags retain a predecessor without changing the prune.

  • 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:

    • add projectName, resolved from the declared project's res.name;
    • remove serviceRoleArn, computeType, buildEnvironmentImage and timeoutMinutes; their equivalents are now Project properties (serviceRole, environment.computeType, environment.image, timeoutInMinutes);
    • declare the 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.
Deprecated
  • The flat profile field on the target config is deprecated in favor of auth = new DefaultChainAuth { profile = ... }. It continues to work unchanged — a target set this way still authenticates via the default credential chain pinned to that profile — and logs one deprecation warning per plugin process rather than on every call. There is no removal in this release; flat profile will be removed at a future major version, posted ahead of time.

    Migrating is a normal target update. Both profile and auth are declared mutable, so rewriting a target from the flat profile to an auth block changes the target's configuration in place and touches no cloud resource.

    This holds only on an agent at or above the release that carries these hints. An older agent does not see them, classifies the dropped top-level Profile key as an immutable change, and plans a target replace, which destroys and recreates every resource on that target. Upgrade the agent first, then migrate.

Fixed
  • Updating a writeOnly property no longer fails. Cloud Control accepts only an add operation for a property it marks writeOnlyProperties, since such a property is absent from every read and there is nothing there to replace, and it rejected the replace that formae plans. Cloud Control names the offending paths when it rejects one, so the plugin now rewrites exactly those operations and resends. This covers every resource type without the plugin having to know which properties are writeOnly ahead of time: AWS::Lambda::Function has ten such properties (Code/ZipFile among them, so an inline-code function could not be changed at all) and AWS::RDS::DBInstance sixteen. It replaces the hardcoded special case that did the same for AWS::SecretsManager::Secret SecretString alone.

  • AWS::RDS::Database and AWS::RDS::DatabaseRole no longer fail when the cluster's admin credential is momentarily refused. RDS rotates an RDS-managed master-user secret on its own schedule, starting within a minute of the cluster being created, and while a rotation settles the Data API rejects some statements authenticated with that secret while sibling statements carrying the same secret succeed a second either side. That authentication failure was classified as an invalid request, which the agent does not retry, so a create landed on a coin flip. It is now reported as not-yet-stabilized: the readiness probe parks the create and polls, and a rotation that lands mid-statement is retried.

  • AWS::CloudFront::Distribution Origin.originCustomHeaders[].headerValue is now opaque. A custom origin header is the documented way to stop callers reaching an origin directly: CloudFront sends the header and the origin rejects requests without it, which makes the value a shared credential, but it was typed as a plain String and so was stored in cleartext. headerName is deliberately left non-opaque, because it is the identity half of the pair and is used to match a header across a diff.

  • AWS::ApiGateway::ApiKey value is now opaque, so an author-supplied API key is hashed at rest instead of being stored in cleartext. The field carries the credential clients present in the x-api-key header, but it was typed as a plain String, and a field is marked opaque from its type: without a formae.SecretValue in the union the agent had nothing telling it to hash the value, so a declared key persisted in the clear. It now names formae.ValueSource like every other credential field here, so it also accepts a generator binding. It still accepts a literal string, and AWS still generates the key when none is declared, so a forma that does not set value is unaffected.

  • AWS::Lambda::Version discovery now surfaces published versions. The list post-filter compared the parent function's name against the ARN form CloudControl echoes back, dropping every listed version, so version discovery silently found nothing.

  • AWS::Lambda::Permission discovery now finds permissions attached to a published version or an alias, not only those on the bare function. Lambda keeps a separate resource policy per qualifier and the CloudControl list only returns the unqualified one, so a permission granted against a version's or alias's ARN (the shape API Gateway and Lambda@Edge wiring uses) was invisible to discovery. The plugin now walks every policy scope through the Lambda control plane.

  • An AWS::CertificateManager::Certificate with subjectAlternativeNames no longer plans a destructive replace on every reconcile. ACM injects the certificate's primary domainName into the subject alternative names it returns from DescribeCertificate, whatever was requested, and subjectAlternativeNames is createOnly — so a certificate declaring only the additional names differed from the read-back on an immutable property and every reconcile planned a destroy-and-recreate of an issued certificate, cascading to the DNS validation records wired to its resolvables. The read-back now reports the names other than the primary domain, so an unchanged certificate reconciles as a no-op, while a genuine change to the list still replaces the certificate. Declared names are unchanged on the way out: whatever the forma lists is still sent to ACM verbatim.

    The canonical way to declare subjectAlternativeNames is therefore the additional names only, excluding the primary domainName. If your forma lists the primary domain in subjectAlternativeNames — the workaround for this bug — it will plan a replacement of the issued certificate once you pick up this release. Remove the primary domain from the list. That edit is declaration-only: ACM stores the same certificate either way, so the declaration converges against the existing certificate with no cloud change. Simulate before applying to confirm the plan is empty.

  • Resources are no longer reported as failed while AWS is still rate-limiting a request that would have succeeded. When CloudControl throttled a call, the plugin absorbed the retries inside a single request to the agent, which could take around three minutes and left the agent with no sign of progress; the agent's check for an unresponsive plugin fired after 40 seconds and failed the resource, cascading to everything that depended on it. Applying many resources at once made this most likely, and subnet creation was the usual casualty. The plugin now bounds how long any one request may spend retrying and reports the work as still in progress instead, so the agent sees continuous progress while the retries continue. Requires formae 0.89.0 or later for the matching change on the agent side.

  • A Secrets Manager resource policy is now replaced as a whole document when it changes. Individual statements were compared as a set, so a patch-mode update that edited a statement added the new one without removing the old, leaving the live policy holding both. This was previously masked by a separate defect that recreated the policy on every update; with that fixed in formae 0.89.0, policy updates apply in place and need this hint to be correct.

v0.1.16stable
Changed
  • Bump examples to the latest formae 0.88.0 schema.
v0.1.15stable
Added
  • 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.
Changed
  • Genuine secret-value fields are now typed 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.
  • Requires formae 0.88.0 or newer; minFormaeVersion is bumped to 0.88.0 accordingly.
Fixed
  • 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.
  • S3 object discovery no longer skips buckets that live in another region. 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.
  • Network Firewall 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.
v0.1.14stable
Changed
  • Requires formae 0.87.1 or newer. This release gates IAM role inline-policy enrichment on the caller's prior model, carried by ReadRequest.PriorProperties, which the agent populates from formae 0.87.1. minFormaeVersion is bumped to 0.87.1 accordingly.
Fixed
  • An IAM 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.
v0.1.13stable
Added
  • AWS EventBridge support: 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.
  • DynamoDB 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.
  • ApiGateway 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.
  • An 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.
  • A CloudFront 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.
Changed
  • Requires formae 0.87.0 or newer. This release uses two capabilities added in formae 0.87.0: embedding resolvables inside text fields (used by CloudFront 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.
  • Network Firewall enum fields (such as a rule group's rule order and a firewall policy's stateful default actions) are now constrained to their valid values at pkl eval time, so an invalid value is caught when the forma is evaluated rather than rejected deep in the apply by AWS.
Fixed
  • An IAM 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.
  • A 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.
  • An ApiGateway 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.
v0.1.12stable
Added
  • AWS Network Firewall support: 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.
Changed
  • Requires formae 0.86.2 or newer. Updating a Network Firewall 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.
  • Applies are more resilient to AWS CloudControl API throttling. When many resources are created or updated at once, status reads that AWS throttled could previously exhaust their retry budget and fail an otherwise-healthy apply; the budget is widened so they ride out throttling bursts.
  • Plugin log lines now appear at their true severity in the agent log and carry per-operation attributes (namespace, operation, resource type, label). Previously, benign 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.
Fixed
  • Route 53 records whose value is a hostname no longer show a phantom update on every reconcile, and no longer risk blocking the stack. AWS canonicalises domain-name record values with a trailing dot, so a 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.
  • ACM certificate DNS-validation records now reconcile cleanly. The validation 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.
  • A secret's value now writes through. Changing 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.
v0.1.11stable
Added
  • Seven new resource types bring fully-managed CloudFront stacks to formae, distributions, their policies, edge functions, and the ACM certificates they depend on, all wired together through the resource graph. The set is 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.
  • When a CloudControl operation fails with an error code formae doesn't already handle, the plugin now logs the full progress event: operation, error code, status message, resource type, request token, and identifier. Previously these failures flowed back with no record of the underlying AWS error code, making resource-type-specific timeout and stabilization failures (such as the occasional ECS Service delete that fails once and succeeds on retry) hard to diagnose. The line is emitted once per genuine failure, so it doesn't add noise to the many in-progress polls during long-running operations.
Changed
  • Requires formae 0.86.0 or newer. This release marks 60 provider-immutable fields across 13 services (AppRunner, EC2, ECR, ECS, EKS, Elastic Beanstalk, ELBv2, KMS, Lambda, RDS, Route 53, S3, SageMaker) as 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.
Fixed
  • Security group rules are now destroyed after the workloads that sit behind the security group, instead of in the first destroy wave. Previously 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.
  • An EFS file system's mount targets are now torn down ahead of the file system itself, so destroying a stack that mounts EFS into ECS tasks no longer strands resources or fails on dependency-still-attached errors. This is driven by typed edge annotations (runtimeDependency) that pull the mount targets into the destroy ordering ahead of their file system.
v0.1.10
Added
  • 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.
v0.1.9
Fixed
  • ELBv2 target groups no longer produce phantom drift that blocks 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.
v0.1.8
Fixed
  • ECS 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.
  • Inverts the destroy edge between an ECS Service and its target groups via 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.
  • Transient 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.
  • Discovery of 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 ListConfigurationSetsGetConfigurationSetEventDestinations and emits properly-formed composite identifiers.
v0.1.7
Added
  • Amazon SES support for outbound transactional email. 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.
Fixed
  • 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.
  • Container-level changes on 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.
v0.1.6
Fixed
  • Updating an 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.
  • ELBv2 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.
v0.1.5
Added
  • Complete EKS resource coverage. 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.
  • ECS 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.
  • ALB Listener URL resolvable, Listeners now expose a computed 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.
  • ~225 reference-bearing properties (IDs, ARNs, names) across 77 schema files now accept formae.Resolvable, and all resources with Resolvable classes now have hidden res wired up.
Changed
  • The 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.
Fixed
  • ECS 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.
  • TaskDefinition was missing its resolvable wiring, 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.
  • VPCGatewayAttachment now exposes resolvable references (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 now exposes 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.
  • Patch-mode updates to resources with optional nested objects (for example, Lambda 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.
  • Reapplying an unchanged forma containing an ECS Service no longer spuriously replaces the service. AWS fills in default port-mapping values on 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.
  • ECS Service and TaskSet no longer produce spurious replacements when the Cluster field is referenced via ARN. CloudControl normalises the Cluster to a short name on Read, which flipped the createOnly field and triggered a full replace. ARN-vs-short-name differences are now normalised before comparison.
  • TaskSet updates no longer hang indefinitely. CloudControl's update handler for TaskSet Scale never returns; a custom ECS SDK update provisioner is used instead.
v0.1.4
Fixed
  • Route53 RecordSets with the same name but different types (e.g., SOA and NS for the same domain) were discovered with identical labels, causing duplicate entries that churned on every sync cycle. Labels now include the record type, producing unique entries like example.com.-SOA and example.com.-NS.
  • Resources with composite CloudControl identifiers (e.g., ECS Services, Lambda EventInvokeConfigs) could show up as duplicates in inventory, one from the initial create and another from discovery. This happened because AWS CloudControl returns full ARNs during create but short names during list. Identifiers are now normalized so the resource created by apply and the resource found by discovery are correctly recognized as the same thing.
  • Discovery of subnet route table associations could cause apply commands to get permanently stuck. AWS returns VPC-level (main) route table associations in discovery results that cannot be read, which triggered a cascade of internal failures. These associations are now filtered out during discovery.
  • Updates to resources with provider-default nested objects (e.g. Lambda EventInvokeConfig destination config) could fail with CloudControl validation errors. Empty nested objects left behind after stripping unused fields were not being removed, causing required-field violations.
v0.1.3
Added
  • Conformance test coverage for 88 resource types across EC2, ECS, EKS, ELBv2, Elastic Beanstalk, Lambda, API Gateway, RDS, Route53, S3, SQS, IAM, KMS, EFS, ECR, CloudWatch Logs, Secrets Manager, and DynamoDB, validating the full create, read, update, delete, sync, and discovery lifecycle.
  • Cross-resource references: TransitGateway, FlowLog, ResourcePolicy, ConfigurationTemplate, NetworkAclEntry, and TargetGroupTuple fields now support resolvable references, enabling correct dependency ordering during apply.
Fixed
  • Several resource types had broken operations through AWS CloudControl that are now fixed:
    • S3 BucketPolicy: reads were broken, now works correctly
    • S3 StorageLensGroup: updates were missing resource properties in the response
    • SQS QueuePolicy: was not provisionable through CloudControl, now works via direct SQS API
    • IAM Policy: was not provisionable through CloudControl, now works via direct IAM API
    • IAM AccessKey: was not provisionable through CloudControl, now works via direct IAM API
    • IAM InstanceProfile: suffered from a 60-second propagation delay through CloudControl, now works via direct IAM API
    • EC2 NetworkAclEntry: was not supported through CloudControl, now works via direct EC2 API
    • Elastic Beanstalk ConfigurationTemplate: updates through CloudControl injected CloudFormation references, now works via direct EB API
v0.1.2
Added
  • Phase 1 conformance tests covering 32 standalone resources, validating the full CRUD and discovery lifecycle.
  • Expanded Route53 HealthCheck schema with fully typed HealthCheckConfig and AlarmIdentifier sub-resources, enabling richer health check definitions in Pkl.
  • Added ResourceLifecycleConfig fields to ElasticBeanstalk environments and configuration templates.
  • Added an AppRunner example demonstrating a simple web service deployment.
Changed
  • Renamed apprunner/service.pkl to apprunner/apprunnerservice.pkl for consistency with the naming convention used across other resource schemas.
Fixed
  • Extract now correctly filters ListResults, preventing unrelated resources from appearing in extracted Pkl output.
v0.1.1
Added
  • Added support for AppRunner resources (AWS::AppRunner::Service), enabling management of AppRunner web services through formae.
Fixed
  • Added missing 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.
v0.1.0
Added
  • Initial release of the AWS plugin as a standalone package built on the formae Plugin SDK.
type
resource
category
cloud
license
FSL-1.1-ALv2
originator
platform.engineering
namespace
AWS
latest
v0.1.17 · stable
platforms
repo
github.com/platform-engineering-labs/formae-plugin-aws
versions (50)
select a version to jump to its notes
v0.1.18-dev.1dev
2026-09-21
v0.1.18-dev.0dev
2026-09-10
v0.1.17stable
2026-09-07
v0.1.17-dev.14dev
2026-09-05
v0.1.17-dev.13dev
2026-09-05
v0.1.17-dev.12dev
2026-09-03
v0.1.17-dev.11dev
2026-09-02
v0.1.17-dev.10dev
2026-09-01
v0.1.17-dev.9dev
2026-09-01
v0.1.17-dev.8dev
2026-08-23
v0.1.17-dev.7dev
2026-08-22
v0.1.17-dev.6dev
2026-08-14
v0.1.17-dev.5dev
2026-08-11
v0.1.17-dev.4dev
2026-08-09
v0.1.17-dev.3dev
2026-08-09
v0.1.17-dev.2dev
2026-08-07
v0.1.17-dev.1dev
2026-08-02
v0.1.17-dev.0dev
2026-07-30
v0.1.16stable
2026-07-27
v0.1.15stable
2026-07-25
v0.1.15-dev.0dev
2026-07-25
v0.1.14stable
2026-07-17
v0.1.13stable
2026-07-01
v0.1.13-dev.7dev
2026-06-30
v0.1.13-dev.6dev
2026-06-29
v0.1.13-dev.5dev
2026-06-28
v0.1.13-dev.4dev
2026-06-28
v0.1.13-dev.3dev
2026-06-28
v0.1.13-dev.2dev
2026-06-28
v0.1.13-dev.1dev
2026-06-27
v0.1.13-dev.0dev
2026-06-26
v0.1.12stable
2026-06-24
v0.1.12-dev.4dev
2026-06-16
v0.1.12-dev.3dev
2026-06-16
v0.1.12-dev.2dev
2026-06-15
v0.1.12-dev.1dev
2026-06-15
v0.1.12-dev.0dev
2026-06-14
v0.1.11stable
2026-06-06
v0.1.11-dev.12dev
2026-06-05
v0.1.11-dev.11dev
2026-06-05
v0.1.11-dev.10dev
2026-06-05
v0.1.11-dev.9dev
2026-06-05
v0.1.11-dev.8dev
2026-06-04
v0.1.11-dev.7dev
2026-06-04
v0.1.11-dev.6dev
2026-06-03
v0.1.11-dev.5dev
2026-06-03
v0.1.11-dev.4dev
2026-06-03
v0.1.11-dev.3dev
2026-05-30
v0.1.11-dev.2dev
2026-05-30
v0.1.11-dev.1dev
2026-05-29