@magic-alm/aws
v0.2.0
Published
AWS adapters for the Magic ALM Matrix toolkit — DynamoDB, API Gateway, CloudFront, SQS, CloudWatch, Terraform / CDK, IAM, Cost Explorer, Secrets Manager, plus Bitbucket / CodePipeline / ECR.
Readme
@magic-alm/aws
AWS adapters for the Magic ALM Matrix toolkit. Implements the provider interfaces from @magic-alm/core against AWS (and adjacent) services.
Adapters
| Subsystem | Adapter |
|---|---|
| DataStore | DynamoDataStore |
| Gateway | AwsApiGateway |
| IaC | TerraformIac, CdkIac |
| Secrets | AwsSecretsManager |
| Queue | AwsSqs |
| Alerting | AwsCloudWatch |
| Governance | AwsGovernance |
| Networking | AwsNetworkIsolation |
| Edge | AwsCloudFront |
| CI/CD | AwsCodePipelineCI |
| Test | AwsCodeBuildTest |
| Artifact | AwsEcrArtifact |
| Repository | BitbucketRepository |
| Observability | AwsCloudWatchObservability |
| Telemetry | AwsCloudWatchTelemetry |
| Identity | AwsIamIdentity |
| Cost | AwsCostExplorer |
Stack Reconciliation
AwsAdapter is the canonical AWS stack-reconciliation boundary. Pass the complete
MAM stack document so environment, region, resources, and typed outputs remain one
versioned contract:
import { AwsAdapter, type AwsResourceGraphDocument } from "@magic-alm/aws";
const stack: AwsResourceGraphDocument = {
environment: "preview",
region: "us-east-1",
resources: {
documents: {
type: "aws:s3Bucket",
props: { bucketName: "embarcadera-preview-documents" },
},
},
outputs: {
S3_BUCKET_NAME: { ref: "documents", attr: "bucketName" },
},
};
const result = await new AwsAdapter({ region: stack.region! }).apply(stack);
console.log(result.outputs.S3_BUCKET_NAME);Output references and attributes are validated before any mutating AWS command is executed. The adapter returns resolved output values without adding them to command logs, preserving the existing command redaction boundary.
Legacy callers that own only a resource map may continue to pass that map directly
to plan() or apply(). New integrations should make the compatibility boundary
explicit with createAwsResourceGraphDocument(resources, metadata). Downstream
repositories should import these MAM contracts instead of redeclaring a local
MamAdapter interface.
The maintained EP compatibility fixture is
examples/embarcadera-stack.yaml. It mirrors
EP's current 55-resource, 10-output development topology and contains only resource
metadata and Secrets Manager references. Its out-of-band GitHub deploy role carries
managementMode: external, which requires the role to exist and prevents MAM from
creating it from an incomplete local trust declaration. EmbarcaderaTopology.test.ts proves that
an EP-style consumer compiles against the exported AwsAdapterOptions,
AwsResourceGraphDocument, and AwsResolvedResourceState contracts and that every
fixture node plans without an unsupported type. Downstream migration is therefore:
- Remove the locally declared
MamAdapter/plan/apply interface. - Import the canonical adapter and document types from
@magic-alm/aws. - Pass the complete stack document rather than extracting only
resources. - Keep live apply disabled until the guarded procedure in
../../docs/embarcadera-live-reconciliation-runbook.mdhas produced reviewed compatibility evidence.
Drift safety contract
plan() reports one of five explicit outcomes for every resource:
| Action | Meaning |
|---|---|
| create | The stable resource identity is absent and create commands are available. |
| noop | Managed state matches, or the existing resource is explicitly preserved with non-observable properties identified as unmanaged in reason. |
| update | Every command is an allow-listed in-place or append-only operation described below. |
| blocked | Replacement, destructive, conflicting, secret-ambiguous, or unsafe-to-observe drift was detected. |
| unsupported | The resource type is outside the adapter contract. |
apply() computes the complete plan and refuses all mutation if any item is
blocked or unsupported. It then rebuilds executable update commands separately
from the sanitized commands returned by plan(). A failed command stops subsequent
resources; already completed AWS commands are not rolled back, and the thrown error
identifies the command that failed. Successful results separate created, updated,
and preserved existing resource names; callers should use AWS audit logs when an
apply fails after earlier mutations succeeded.
The current automatic update allow-list is intentionally small:
| Resource | Safe updates | Blocked or preserved behavior |
|---|---|---|
| aws:s3Bucket | Enable versioning, enable all four public-access blocks, and add SSE-S3 default encryption when no bucket encryption is configured. | Suspending versioning, relaxing public access, and changing an existing encryption algorithm are blocked. Lifecycle and tags are preserved but unmanaged. |
| aws:ecsTaskDefinition | Register a new revision when normalized declared fields differ from the latest complete live revision. A container with imageManagement: preserveLive keeps the exact image from the latest live revision when another managed field changes. | Existing revisions are never changed or deregistered. Incomplete or ambiguous live state, missing externally managed images, and repository mismatches block registration. |
ECS image ownership handoff
By default, imageRef is declaration-owned and participates in task-definition
drift. CI-managed image deployments can instead opt a container into an explicit
handoff:
containerDefinitions:
- name: api
imageRef: ecr-api
imageManagement: preserveLivepreserveLive is a per-container contract. imageRef remains required: it supplies
the bootstrap image when the task family does not exist and identifies the only ECR
repository from which an existing live image may be preserved. For an existing
family, MAM matches declared and live containers by unique name, treats an
image-only difference as noop, and carries the exact latest live tag or digest into
any new revision required by a managed non-image change. The apply path repeats live
classification at the per-item execution boundary immediately before registration,
so it does not reuse a stale image captured during planning or preflight.
A family planned for bootstrap creation is checked again at that same boundary; if
it has appeared, MAM adopts/reclassifies the live revision instead of blindly
registering the bootstrap image.
Missing or duplicate container names, missing live images, absent imageRef, an
unsupported imageManagement value, and live images outside the declared repository
fail closed before registration. Plan reasons identify the affected container without
printing its registry account, URI, tag, or digest. The adapter registers revisions
append-only; it never changes or deregisters an existing revision and does not update
an ECS service.
All other supported types remain non-destructive create-or-preserve resources in this phase. Their stable identity and the topology used by discovery are owned; other declared fields are named as unmanaged in the plan reason and are not silently claimed as reconciled:
| Surface | Preservation rules |
|---|---|
| Networking | VPCs, subnets, route tables, internet gateways, EIPs, NAT gateways, security groups, and associations are never replaced, detached, shrunk, or deleted. A route at the declared destination with a different live target is blocked. |
| RDS | Instances, subnet groups, and parameter groups are adopted by stable name and preserved. Instance sizing, engine, storage, credentials, membership, parameters, and tags are not updated by this phase. |
| Cognito | Pools, clients, groups, and matching domains are preserved. A domain assigned to another pool or an IdP with a different provider type is blocked. Existing secret-backed IdP details are preserved and explicitly unmanaged because AWS cannot safely prove secret equality; a future rotation contract must supply an explicit rotation token. Users and group membership are never touched. |
| KMS | Asymmetric ECC_SECG_P256K1 signing keys are discovered by their required tags.Name value and preserved. Ambiguous or paginated identity discovery blocks reconciliation. Key policy, description, rotation, and other live properties are never changed. |
| Secrets Manager and IAM | Secrets and roles are adopted by name. Secret values, descriptions, policies, trust, attachments, and tags are preserved and unmanaged; no value read is used for drift output. |
| ECR | Repositories are adopted by name. Mutability, scanning, lifecycle, and tags are preserved and unmanaged. |
| ECS services and clusters | Existing services and clusters are preserved. Desired count, service deployment, network/load-balancer attachments, execute-command settings, and cluster settings are not changed. Task-definition revisions follow the separate append-only rule above. |
| ALB | Load balancers, target groups, and matching listeners are preserved. Listener rules, target registration, health checks, networking, and attributes are not changed. |
Discovery permission failures, unexpected command failures, and malformed non-empty
JSON responses fail closed as blocked; they are never interpreted as absence.
Known absent responses and valid empty discovery collections permit create. Plan reasons and commands redact
Secrets Manager placeholders and resolved sensitive values.
There is deliberately no destructive option in AwsAdapterOptions. A future phase
may add a separately reviewed, explicit approval contract for replacement or deletion,
but callers cannot opt into those operations through this implementation.
Network and RDS-supporting resources
The adapter supports the EP network topology with create-or-preserve reconciliation.
Existing matching resources are returned as noop with their unmanaged preservation
boundary in the reason; the adapter does not replace, delete, detach, or re-associate
network resources.
| Resource type | Required properties | Behavior |
|---|---|---|
| aws:internetGateway | vpcRef, tags.Name | Creates and attaches an internet gateway to the referenced VPC. |
| aws:routeTable | vpcRef, tags.Name | Creates a tagged route table in the referenced VPC. |
| aws:route | routeTableRef, destinationCidrBlock, exactly one of gatewayRef or natGatewayRef | Creates a public or private IPv4 route. Adoption requires both the destination and gateway target to match. |
| aws:routeTableAssociation | routeTableRef, subnetRef | Associates a subnet with a route table; an existing matching association is adopted. |
| aws:eip | domain: "vpc", tags.Name | Allocates a tagged VPC Elastic IP. |
| aws:natGateway | allocationRef, subnetRef, tags.Name | Creates a NAT gateway and waits for AWS nat-gateway-available before dependent routes run. Adopted gateways receive the same readiness check. |
| aws:dbParameterGroup | family, description | Creates a named parameter group and applies optional parameters entries. Each entry accepts name, value, and optional applyMethod (pending-reboot by default). |
Every resource reference must name a resource of the expected type and must also be
declared in dependsOn. Validation runs before AWS discovery or mutation calls, so
missing properties, references, dependency edges, and ambiguous route targets fail
closed. RDS instances may set parameterGroupRef to attach an
aws:dbParameterGroup; that reference must likewise appear in dependsOn.
NAT gateways and route operations can affect cost and connectivity. Ordinary tests use an injected command runner and never create live resources. Live compatibility checks belong in an explicitly approved environment.
Cognito identity resources
The adapter also reconciles EP's Cognito identity topology using create-or-preserve
semantics. Existing resources are matched by their stable name within the referenced
user pool. Matching resources are noop; secret-backed IdP details are preserved
and marked unmanaged until a future rotation-token contract exists. Conflicting
domains and provider types are blocked. Reconciliation never updates, replaces,
or deletes pools, clients, domains, identity providers, groups, users, or group
membership.
| Resource type | Stable adoption identity | Supported EP properties |
|---|---|---|
| aws:cognitoUserPool | Pool name | Username and verification attributes, admin-only creation, password policy, account recovery, MFA configuration, and tags. |
| aws:cognitoUserPoolClient | name within userPoolRef | Public/secret client mode, OAuth flows/scopes, identity providers, explicit auth flows, callback/logout URLs, existence-error protection, and token validity. |
| aws:cognitoUserPoolDomain | domainPrefix assigned to userPoolRef | Cognito hosted-UI prefix. A domain assigned to another pool is not adopted. |
| aws:cognitoUserPoolIdentityProvider | providerName and providerType within userPoolRef | Google, Sign in with Apple, and OIDC provider details plus attribute mapping. |
| aws:cognitoUserPoolGroup | name within userPoolRef | Description and optional precedence. Membership is intentionally untouched. |
Every userPoolRef must point to an aws:cognitoUserPool and be declared in
dependsOn. A client that enables a federated provider must depend on a matching
aws:cognitoUserPoolIdentityProvider for the same pool. Validation of references,
provider-specific OAuth fields, and sensitive-value handling completes before AWS
discovery or mutation calls.
Identity-provider details accept the CloudFormation-compatible Secrets Manager form
used by EP, for example
{{resolve:secretsmanager:embarcadera/dev/oauth:SecretString:GOOGLE_CLIENT_SECRET}}.
Sensitive fields such as client_secret and private_key must use this form. The
adapter resolves them only for the create call and redacts resolved values from
planned commands, execution arguments, command output, and errors. User-pool IDs and
client IDs are available through the userPoolId and clientId output attributes.
Dependencies
- Hard dependency:
@magic-alm/core. - Optional peer dependencies:
@aws-sdk/client-dynamodb,@aws-sdk/lib-dynamodb(onlyDynamoDataStoreloads them, and that's lazy).
License
Apache 2.0.
