@cfxlabsinc/b2b-services
    Preparing search index...

    Module @cfxlabsinc/ecr

    @cfxlabsinc/ecr

    Pulumi-managed ECR repositories and pull-through cache for CFX container apps.

    Per-app repositories — one ECR repository per container app (utila-cosigner, admin-dashboard, temporal-worker, internal-dashboard) per env, with:

    • scanOnPush enabled
    • A lifecycle policy that expires untagged images after 1 day and keeps the last 10 tagged images

    The single source of truth for which repos exist is pulumi/repos.ts. Adding a new container app means appending a name there and running pulumi up against each env stack (or letting the ecr-pulumi-cd-{dev,prod} job in deploy-env.yml do it on merge to main).

    ECR Public pull-through cache — an aws.ecr.PullThroughCacheRule per env with ecrRepositoryPrefix: "ecr-public" proxies public.ecr.aws (AWS's public Docker Hub mirror). Our Dockerfiles accept a BASE_IMAGE_PREFIX build arg; CI sets it to <account>.dkr.ecr.us-west-2.amazonaws.com/ecr-public/docker/library/ so e.g. node:24-bookworm-slim resolves through the in-VPC ECR cache instead of crossing NAT to Docker Hub. First pull populates the cache lazily; subsequent builds hit ECR directly. Local nx image-build leaves the prefix empty and pulls from Docker Hub directly.

    Why ECR Public and not Docker Hub directly: AWS rejects unauthenticated Docker Hub pull-through with UnsupportedUpstreamRegistryException. ECR Public is anonymous-friendly, AWS-managed, in-region, and mirrors every Docker official library image at public.ecr.aws/docker/library/<image> — which is what we use for node and alpine. No Secrets Manager hop, no credentials to rotate.

    Repos and cache rules live in the same workload account that builds and consumes them (206248878611 for dev, 372806568664 for prod). No cross-account pull policy is needed — ECS tasks and Lambda functions read images from the same account they run in.

    Until this package existed, ECR repos were created and re-asserted on every CI build by the .github/actions/create-ecr-repo composite action via raw aws ecr {create-repository,set-repository-policy,put-lifecycle-policy} calls. That left repo policy + lifecycle drift impossible to detect, and made changes land asynchronously (only on the next CI run that happened to invoke the action). Owning it in Pulumi makes the policy diff-able and reviewable.

    Two stacks — one per env:

    Stack Account Backend
    dev 206248878611 s3://cfx-pulumi-iac-backend-dev?region=us-west-2
    prod 372806568664 s3://cfx-pulumi-iac-backend-prod?region=us-west-2

    Each stack provisions its repos in its own workload account via the cfx-role-devops OIDC role already trusted by the container CI/CD workflows.

    No manual bootstrap. cd-pulumi.yml invokes pulumi up --upsert, so the first CD run per env creates the stack from Pulumi.<stack>.yaml. The Repository resources have the import: option set, so the first apply adopts the existing repos that .github/actions/create-ecr-repo/ previously created imperatively — it doesn't try to re-create them.

    Wired into deploy-env.yml as ecr-pulumi-cd-{dev,prod} jobs, gated by nx affected on packages/ecr/**. The imperative create-ecr-repo is still in .github/actions/container-build-push/action.yml as a belt-and-suspenders safety net (no-op once the repo exists) — drop it in a follow-up PR after both env applies are confirmed green at least once.

    Lifecycle policies adopt via standard pulumi create on first apply (there's no prior pulumi state of them to import; the imperative action set them via aws ecr put-lifecycle-policy, which pulumi can overwrite cleanly).

    Before this rework, a single cicd stack lived in account 764105176280 and emitted a cross-account pull policy granting 206248878611 and 372806568664 read access. The CICD account is being decommissioned — repos and their consumers now share an account per env, so the cross-account dance is gone.

    If the old cicd stack was ever applied, the cfx-<app> repos in 764105176280 are orphaned by this change. Decide whether to delete them or leave them as the source for one final migration push, then clean up the stack:

    cd packages/ecr/pulumi
    pulumi login s3://cfx-pulumi-iac-backend-cicd?region=us-west-2 # if it ever existed
    pulumi stack rm cicd