AWS deployment provider for Packkit-generated projects — contract-driven and language-agnostic.
Packkit generates a project and describes how to deploy it in a provider-neutral
deployment contract. This package turns that contract into OpenTofu/Terraform +
a GitHub-OIDC deploy pipeline — so a static site generated by create-packkit (JS),
create-packkit-py, or create-packkit-go deploys to AWS the same way. A provider is
provider × contract, never provider × language.
any generator ──► GeneratedProject.deploymentContract ──► @packkit/provider-aws ──► infra/*.tf + deploy.yml
Unlike an API-driven provider (e.g. @packkit/provider-netlify), AWS deployment is
infrastructure-as-code: prepare() emits the Terraform and the pipeline; the pipeline
(or a human running tofu apply) does the provisioning. This package never holds AWS
credentials — CI authenticates with GitHub OIDC and assumes a repo-scoped IAM role.
npm install @packkit/provider-aws@packkit/core is a peer dependency (^0.4.0).
import { prepare, plan, supports } from '@packkit/provider-aws';
// `project` is any generator's GeneratedProject (only its deploymentContract is read).
const project = {
deploymentContract: { type: 'static', buildCommand: 'npm run build', outputDirectory: 'dist' },
};
// 1. Check support (pure).
if (!supports(project.deploymentContract).supported) throw new Error('unsupported');
// 2. Plan (pure, deterministic) — files + an auditable resource list, no AWS calls.
const p = plan({
project,
options: { name: 'my-site', repository: { owner: 'PackkitLabs', name: 'my-site' } },
});
// 3. Prepare the files (pure) — the host writes them into the repo.
const { files } = prepare({
project,
options: { name: 'my-site', repository: { owner: 'PackkitLabs', name: 'my-site' } },
});
// files: infra/*.tf, infra/bootstrap/*.tf, infra/README.md, .github/workflows/deploy.yml, …Three archetypes, one per deployment-contract type — all cost-conscious by construction:
static→ a private, versioned, AES256 S3 bucket behind CloudFront (OAC, HTTPS, SPA fallbacks, cheapest edge tier).service→ AWS App Runner (managed HTTPS + autoscaling, health check on the contract's path, image from ECR) — no VPC, no NAT gateway, no load balancer.worker→ ECS Fargate (one task, egress-only) on a minimal VPC with public subnets and no NAT gateway, logs with explicit 30-day retention.
Plus, for every archetype:
infra/bootstrap/— run once by a human: the Terraform state bucket, the GitHub OIDC provider, and a repo-scoped IAM deploy role (permissions scoped to the archetype). Solves both chicken-and-eggs..github/workflows/deploy.yml—fmt → lint → validate → planon PRs,applyon merge via OIDC (container archetypes build+push the image to ECR first).- S3 backend with native state locking (
use_lockfile) — no DynamoDB table.
Every generated project is tofu fmt-clean and tofu validate-clean; CI proves it
across all three archetypes.
supports/prepare/planare pure — no filesystem, no network. Same inputs → identical output.prepare()returns files as data; the host writes them (ideally via@packkit/core/node's writer).- No ambient credentials. There is no runtime
applythat touches AWS; the emitted pipeline deploys via OIDC. (A futureapply()bound to an injectedtofurunner can be added the same wayprovider-netlifyinjects its client.) - Contract-driven. Support and planning read the deployment contract, never raw config, frameworks, or the language.
0.1.0 — static (S3 + CloudFront), service (App Runner), and worker (ECS
Fargate). The same language-neutral contract that generalized across generators maps
cleanly onto these AWS primitives, so a Node, Python, or Go service deploys identically.
fullstack and a runtime apply() (bound to an injected tofu runner) are the roadmap;
unsupported contract types are reported, not guessed.
MIT © DanMat