Skip to content

Repository files navigation

fakecloud-web

A minimal reproducible repository for 15 bugs in fakecloud. It is a hello-world SPA on CloudFront + S3, deployed with CDK to a local fakecloud rather than real AWS - the smallest stack that hits every one of them. Run the deploy below against an unpatched fakecloud and it fails; against release/v0.44.11 it serves the site. Each bug and the branch that fixes it is listed under The 15 bugs.

app.js              CDK app: S3 bucket, CloudFront distribution, ACM certificate
web/index.html      the SPA (no build step)
stack.test.js       proves the deployed stack and every fix below (npm test)
docker-compose.yml  fakecloud, built from a local source checkout
cdk.json            { "app": "node app.js" }

Prerequisites

  • Docker (fakecloud runs in a container, and runs Lambda in sibling containers)
  • Node 20+ and the AWS CLI
  • A checkout of fakecloud at ../../github/fakecloud, on release/v0.44.11

The compose file builds the branch that checkout has open rather than pulling the published image, because this stack depends on fixes that are not released yet - so the branch you leave it on is the image you get. Point build.context at your own checkout if it lives elsewhere, or switch back to image: ghcr.io/faiscadev/fakecloud:latest once those land.

The 15 bugs

Getting cdk deploy to work end to end turned up 15 bugs in fakecloud. Each is listed below with its one-line cause and the branch that fixes it - a branch off main with a regression test, carrying that fix alone bar the two SSE-KMS ones, which share a branch because they share a root cause. release/v0.44.11 carries all 15, plus feat/cfn-response-url-internal below, and is what the compose file builds. The 15th branches off release/v0.44.11 rather than main, because it builds on two of the others. None are upstream yet.

The first 14 are rebased onto current upstream main and re-checked there. Eight touch no file upstream has changed since, so their code is byte-identical and the bug is untouched. The other six overlap on extras.rs, lib.rs or the docs page, and each was confirmed still broken at the point it fixes: the DescribeChangeSet stub is still there, fetch_template_from_url still reads stored bytes without decrypting, and ResponseURL and the custom_resource_response module do not exist on main at all.

The commit message on each branch has the full explanation and the evidence.

Nothing served the site.

  • CloudFront proxied S3 REST origins to real AWS, because a bucket REST endpoint resolves in real DNS, so the distribution never served the bucket at all. Fix: fix/cloudfront-s3-origin-resolution
  • DefaultRootObject was parsed and never read, so / reached the origin as / and returned the bucket's ListBucketResult XML instead of the SPA shell. Fix: fix/cloudfront-default-root-object
  • A bucket with no BucketName took the CFN logical id as its physical name, but CDK ids are mixed case and bucket names are not, so virtual-hosted 404'd on objects the bucket visibly held. Fix: fix/cfn-s3-bucket-physical-name
  • CustomErrorResponses were dropped in provisioning: CFN types ResponseCode as an integer and the CloudFront API as a string, so every rule failed to deserialize, and ErrorCachingMinTTL was mis-spelled by rename_all. The SPA had no deep-link fallback. Fix: fix/cfn-cloudfront-custom-error-responses

Nothing uploaded the assets. All four broke BucketDeployment, the Lambda custom resource that copies web/ into the bucket.

  • Inline Code.ZipFile is source text in CFN but an archive in the Lambda API; the provisioner stored the raw text where downstream expects a deployment package. Fix: fix/cfn-lambda-inline-zipfile
  • Objects in an SSE-KMS bucket are stored as a KMS envelope, and only S3's own read paths unwrapped it. Every internal reader got the envelope, including the provisioner hydrating Lambda code from S3. Fix: fix/s3-sse-kms-internal-readers
  • The same envelope reached fetch_template_from_url. It is base64 ASCII, so it survived UTF-8 decoding and parsed as a template with no resources: the stack reported CREATE_COMPLETE having provisioned nothing. Fix: fix/s3-sse-kms-internal-readers (same branch, second commit)
  • Function containers got no AWS environment - no region, no credentials - so an SDK client inside a handler failed before it reached an endpoint, and BucketDeployment reported success having copied no files. Fix: fix/lambda-container-aws-endpoint

Custom resources could not report their outcome. Fixing the first of these turned silently-broken deploys into visibly failing ones, which is what exposed the rest.

  • A handler that raises returns 200 with the error in the body. The response was discarded, so a custom resource that did nothing still reached CREATE_COMPLETE. Fix: fix/cfn-custom-resource-failure
  • The event carried no ResponseURL, so cfn-response - which every CDK handler uses - threw before it could signal. Fix: feat/cfn-custom-resource-response-url
  • CDK's handler calls https.request and builds it from the hostname with no port, so Node defaults to 443 whatever URL we send. The endpoint has to be TLS on 443. Fix: feat/cfn-response-url-tls
  • That certificate is self-signed, and handlers were told to skip verification with NODE_TLS_REJECT_UNAUTHORIZED and PYTHONHTTPSVERIFY. The Lambda python3.13 runtime ignores the latter, so Python handlers still failed with CERTIFICATE_VERIFY_FAILED. They now get fakecloud's CA instead. Fix: feat/lambda-ca-bundle
  • CFN stringifies every scalar in ResourceProperties before the handler sees it. fakecloud passed real JSON types, so props.get('Prune', 'true').lower() raised 'bool' object has no attribute 'lower'. Fix: fix/cfn-custom-resource-property-strings
  • None of the above reached cdk deploy. create_custom_resource has two arms, and the changeset path takes the one that queues the invoke and discards its result, so a handler that raised still produced a CREATE_COMPLETE stack. The same gap wrote the terminal status before the queued invokes had run at all, so BucketDeployment reported success with the bucket still empty - cdk deploy returned 4.7s before index.html appeared. Real CloudFormation holds a custom resource CREATE_IN_PROGRESS until it signals. Fix: fix/cfn-changeset-custom-resource-outcome Found 2026-09-10 by stack.test.js, after the other 14 were already fixed: with a Node handler that throws, CreateStack gave CREATE_FAILED and ExecuteChangeSet gave CREATE_COMPLETE. Deploy time went 0.1s -> 10.1s because it now genuinely waits.

cdk deploy and cdk bootstrap hung forever against a stack that already existed. Before creating a change set the CDK CLI deletes any leftover one and polls DescribeChangeSet until it 404s. On a miss fakecloud fabricated CREATE_COMPLETE, so the change set was never gone and the poll never ended. Fix: fix/cfn-describe-change-set-not-found Verified fixed 2026-09-10 against the image the compose file builds: with CDKToolkit and SpaStack already deployed, DescribeChangeSet returns ChangeSetNotFoundException and make up re-bootstraps and re-deploys in 1.5s. The Makefile no longer guards npm run bootstrap behind a describe-stacks check.

One more, not a bug

  • Nothing was wrong with reaching the ResponseURL through the host, but it meant fakecloud had to own the host's 443, which is impossible on a machine where anything else already holds it. Function containers now join fakecloud's own network (FAKECLOUD_LAMBDA_NETWORK) and reach the endpoint on its container port instead, so nothing is published. Fix: feat/cfn-response-url-internal

Hit along the way, not fixed here

  • cargo test --workspace is red on upstream main: fakecloud-e2e --test apigwv2_websocket fails two tests with $connect lambda should have fired. Reproduced on an unmodified checkout, so it is not ours. Everything else passes.
  • conformance-baseline.json records cloudformation at 3426 variants; the probe reports 3434, with or without any of these branches. The baseline is stale.
  • PYTHONHTTPSVERIFY=0 does nothing in the Lambda python3.13 runtime. It looks like it should work, which is why feat/lambda-ca-bundle exists.
  • A port mapping like 8443:443 can never work: the handler dials the host gateway with no port at all, so only the host's own 443 is reachable. That is what feat/cfn-response-url-internal sidesteps rather than fixes.
  • cloudformation_custom_resource_invokes_lambda is red on release/v0.44.11. fix/cfn-custom-resource-property-strings stringifies every scalar but never updated that e2e test, which still asserts ResourceProperties.Count == 42 and now gets "42". Confirmed failing on an otherwise-unmodified checkout of that branch. A one-line test fix, and it belongs on that branch rather than a new one.
  • A custom-resource handler killed by the Lambda timeout reads as success. The killed runtime returns no parseable error body, and fix/cfn-custom-resource-failure deliberately does not invent a failure from one. Real CloudFormation fails the stack when ServiceTimeout expires. The probes in stack.test.js set an explicit Timeout to stay clear of it.

Deploy

docker compose up -d --wait

export AWS_ENDPOINT_URL=http://localhost:4566 \
       AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
       AWS_DEFAULT_REGION=us-east-1

npx cdk bootstrap aws://123456789012/us-east-1
npm run deploy

The stack uploads web/ itself; no manual sync is needed.

npm test (stack.test.js) asserts every fix above against the running emulator - each test names the branch it proves. Measured 2026-09-10:

image built from result
release/v0.44.11 12 pass
release/v0.44.11~1 (the first 14) 7 fail
ghcr.io/faiscadev/fakecloud:latest 11 fail

Without the 15th fix the six asset and serving tests fail alongside the changeset one: the deploy returns before BucketDeployment has copied web/, so the bucket is still empty when the suite reads it. Nothing here waits for the assets - a wait would hide exactly that.

Reaching it

The distribution answers on fakecloud's main port, routed by the Host header - there is no separate port to hit:

curl -H "Host: fakecloud-web.localhost" http://localhost:4566/
curl -H "Host: fakecloud-web.localhost" http://localhost:4566/about   # deep link

For a browser, add the domain to /etc/hosts pointing at 127.0.0.1 and visit http://fakecloud-web.localhost:4566/, so the Host header comes from the URL and client-side routing works normally.

To reach it by the distribution's own domain instead:

DOMAIN=$(curl -s http://localhost:4566/_fakecloud/cloudfront/distributions \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["distributions"][0]["domainName"])')
curl -H "Host: $DOMAIN" http://localhost:4566/

Why 443 never reaches the host

CloudFormation custom resources report success or failure by HTTPS PUT to the ResponseURL on their event, and fakecloud serves that endpoint over TLS on 443. The port is not negotiable for CDK's Node handlers: cfn-response builds the request from the URL's hostname with no port, so Node defaults to 443 whatever the URL says. CDK's Python handler does honour the whole URL (urlopen(Request(responseUrl))), so BucketDeployment alone would be happy on any port; autoDeleteObjects is the one that pins it.

That 443 used to have to be the host's, published from the container, because a function container could only reach fakecloud through the host - and on a machine where anything else already holds 443, it simply could not be given. FAKECLOUD_LAMBDA_NETWORK in the compose file removes that: it attaches the function containers to fakecloud's own network, where they reach its container port 443 directly by DNS. Nothing is published, and the host's 443 is free. Fix: feat/cfn-response-url-internal

cdk bootstrap needs none of this - the CDKToolkit stack has no custom resources.

This stack is local-only

fakecloud-web.localhost cannot be validated or resolved on real AWS, and CloudFront requires a certificate covering every alternate domain name. To deploy for real, swap DOMAIN in app.js for a domain you own and use CertificateValidation.fromDns(zone) with its hosted zone.

Teardown

npm run destroy     # or: docker compose down, which discards all fakecloud state

About

A minimal reproducible repository for bugs in fakecloud. It is a hello-world SPA on CloudFront + S3, deployed with CDK to a local fakecloud rather than real AWS - the smallest stack that hits every one of them.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages