The backend for Foundry (FastAPI app titled FOUNDRYFORGER) — a visual infrastructure builder.
A user drags AWS services onto a ReactFlow canvas in the frontend and draws arrows between them. This backend turns that canvas JSON into a CloudFormation template (via troposphere), deploys it to AWS, streams live deployment progress back over WebSocket, and then wires up a GitHub → CodeBuild → CodeDeploy pipeline so the user's app code auto-deploys onto the EC2 instance that was just created.
Built for ACM Projects (Oct–Dec 2025).
- What it actually does
- Architecture
- Repository map
- The canvas contract
- The deployment pipeline, step by step
- The CI/CD pipeline, step by step
- Resource naming and IAM
- Demo mode
- API reference
- Database schema
- Environment variables
- Running it locally
- Branch guide — where the working code lives
- Known issues and refactor candidates
Four AWS services are supported as canvas nodes: EC2, S3, RDS, and DynamoDB.
The interesting part is the edges. An edge from an S3 node to an EC2 node doesn't just draw a line — it causes the backend to:
- generate an IAM role + instance profile scoped to exactly that bucket,
- attach the profile to that EC2 instance,
- and (intended, see §14) inject
S3_BUCKET_NAMEinto the instance's environment.
So "draw an arrow from the bucket to the server" compiles down to least-privilege IAM plus service discovery.
Same story for DynamoDB (IAM role + DYNAMODB_TABLE_NAME) and RDS (no IAM — it injects
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD / DB_ENGINE instead, since RDS auth is credential-based).
On top of that there is a second, largely independent subsystem: once a stack is up, the user can point a
GitHub repo at their build. The backend registers a webhook, and every push triggers a full
zip → S3 → CodeBuild → CodeDeploy → running-FastAPI-app cycle on the EC2 instance the canvas created.
┌────────────────────────────────────────────────────────────────────────────┐
│ Frontend (ReactFlow canvas — separate repo) │
└───────────────┬─────────────────────────────────┬──────────────────────────┘
│ POST /canvas/deploy │ WS /canvas/deploy/track/{stack}
│ (canvas JSON + buildId) │ WS /github/ws/{build_id}
▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ app.py — FastAPI, CORS wide open │
│ routers/canvas.py → /canvas/* and /builds/* │
│ routers/github_webhook.py→ /github/* │
└───────────────┬────────────────────────────────────┬───────────────────────┘
│ │
┌──────────▼────────────┐ ┌──────────▼─────────────┐
│ CFCreators/ │ │ CICD/ │
│ "canvas → CF → AWS" │ │ "git push → EC2" │
│ │ │ │
│ CFCreator.py │ │ addYamlZip.py │
│ orchestrator │ │ inject buildspec/ │
│ template_composer.py │ │ appspec into repo zip│
│ canvas → troposphere│ │ deploymentScripts.py │
│ singleServiceCreator/│ │ inject start/stop/ │
│ EC2/S3/RDS/Dynamo/ │ │ install .sh │
│ IAM builders │ │ upload_s3.py │
│ key_pair_manager.py │ │ trigger_codebuild.py │
│ SSH keypairs │ │ code_Deploy.py │
│ aws_deployer.py │ │ add_webhook.py │
│ boto3 CFN calls │ └──────────┬─────────────┘
│ deploymentModal/ │ │
│ poll CFN events →WS │ │
└──────────┬────────────┘ │
│ │
┌──────────▼────────────┐ ┌──────────────┐ ┌──▼──────────────────────┐
│ costs/ logs/ │ │ database.py │ │ AWS: CodeBuild, │
│ CloudWatch + pricing │ │ Postgres/RDS │ │ CodeDeploy, S3 │
└───────────────────────┘ └──────────────┘ └─────────────────────────┘
Two DB access styles coexist: database.py uses psycopg2 (sync, with a get_db_connection
context manager), while many endpoints in routers/canvas.py open asyncpg connections inline.
See §14.
| File | Role |
|---|---|
app.py |
FastAPI app. Mounts canvas.router (/canvas), canvas.builds (/builds), github_router (/github). CORS is allow_origins=["*"]. |
| File | Role |
|---|---|
canvas.py (1051 lines) |
The main API. Deploy / update / delete stacks, change sets, build CRUD, GitHub repo listing, invites, project settings, costs, logs, S3 file upload, and the deployment-tracking WebSocket. This is the file most in need of splitting. |
github_webhook.py |
Webhook registration + the push handler that runs the whole CI/CD chain. Also hosts a per-build_id WebSocket used to stream CodeBuild/CodeDeploy status strings. |
| File | Role |
|---|---|
CFCreator.py |
Public façade. createGeneration() (canvas → template, saves a copy to allJSONs/createdCFs/), deployToAWS() (5-step pipeline), getStackStatus(), deleteStack(). |
template_composer.py |
The core translation logic. Parses edges into a dependency map, creates non-EC2 resources first (phase 2), then creates EC2s with the IAM roles / env vars their edges imply (phase 3). |
aws_deployer.py |
CloudFormationDeployer — thin boto3 wrapper. Auto-discovers the default VPC/subnet/SG, creates a DB subnet group when RDS is present, create_stack, change-set create/execute/delete, status. |
key_pair_manager.py |
Creates one EC2 SSH key pair per EC2 node before the stack goes up; returns private key material to the caller (it's only retrievable once). Also bulk-deletes key pairs on stack teardown. |
singleServiceCreator/EC2_creation.py |
Builds AWS::EC2::Instance. AMI friendly-name → SSM parameter resolution, block devices, tags, instance profile, key name, UserData. |
singleServiceCreator/S3_creation.py |
Builds AWS::S3::Bucket. Encryption, public-access block, and BucketOwnerEnforced are hardcoded on. |
singleServiceCreator/RDS_creation.py |
Builds AWS::RDS::DBInstance. db.t4g.micro, 20 GB gp3, encrypted, single-AZ, private, 7-day backups — all hardcoded. |
singleServiceCreator/DynamoDB_creation.py |
Builds AWS::DynamoDB::Table. PAY_PER_REQUEST, SSE on, PITR on — hardcoded. |
singleServiceCreator/IAM_creation.py |
Three role factories: S3-only, DynamoDB-only, and multi-service. Only the multi-service one is actually called. |
deploymentModal/event_tracker.py |
Polls describe_stack_events, dedupes by EventId, maintains a per-resource status cache and progress percentage. |
deploymentModal/websocket_handler.py |
DeploymentWebSocketManager — fan-out to multiple clients per stack, one 3-second polling task per stack, cancels the task when the last client leaves. Exposed as the module-global deployment_ws_manager. |
deploymentModal/deployment_formatter.py |
Pure formatting functions → the JSON message shapes the frontend consumes. |
allJSONs/createdCFs/ |
Every generated template, saved as CF_<build_id>.json. Gitignored on paper, ~190 files committed in practice. |
allJSONs/templateConfigurationExamples/ |
Self-documenting per-service canvas JSON examples (field types, validation rules, choices). Good reference for the frontend contract. |
NAMING_CONVENTION.md, IAM_ROLE_ARCHITECTURE.md, SSH_KEY_PAIR_GUIDE.md |
Design docs written alongside the code. |
| File | Role |
|---|---|
addYamlZip.py |
Holds the buildspec.yml and appspec.yml templates and injects them into the downloaded repo zip. |
deploymentScripts.py |
Holds start.sh / stop.sh / install.sh (venv + pip install -r requirements.txt + nohup uvicorn main:app --port 8000) and injects them under scripts/. |
upload_s3.py |
Uploads the rewritten zip to the CodeBuild source bucket. |
trigger_codebuild.py |
start_build with S3 source override, then polls batch_get_builds every 3 s, emitting status over the build's WebSocket. |
code_Deploy.py |
Creates/updates the CodeDeploy application and deployment group (targeted by the BuildId EC2 tag), starts the deployment, polls to completion, emits status. |
add_webhook.py |
POST /repos/{owner}/{repo}/hooks with the user's OAuth token. |
test.py, testCodeDeploy.py |
Standalone scratch scripts from development. |
| File | Role |
|---|---|
database.py |
psycopg2 helpers: save_build (8-digit random ID with collision retry), get_build, update_build_canvas_and_template, get_builds_by_owner, log_activity, get_access_token_for_owner. |
create_schema.py |
One-shot DDL for all tables. |
costs/s3.py |
Finds the stack's buckets by CFN tag, reads BucketSizeBytes from CloudWatch, multiplies by $0.023/GB. |
costs/ec2.py |
Finds instances by BuildId tag, computes hours since launch × a hardcoded per-type rate table. |
costs/dynamo.py |
Fully commented out. |
logs/logs.py |
Last hour of CPUUtilization per instance, as time series points. |
settings/get_user.py |
asyncpg query listing user emails. |
github_webhook_test/ |
An earlier standalone FastAPI app used to prototype the webhook flow. Superseded by routers/github_webhook.py. |
test_*.py, check_tables.py, verify_build_table.py, demo_stack_update.py |
Root-level manual/integration scripts (not pytest suites — most are print-driven walkthroughs run by hand). |
The canvas is standard ReactFlow JSON. Only nodes[].id, nodes[].type, nodes[].data, and
edges[].source/edges[].target are read; position is ignored by the backend.
Edge semantics. source is the resource being consumed, target is the EC2 doing the consuming.
Edges whose target isn't an EC2 node are silently ignored (template_composer.py:92).
imageId friendly names resolve to AWS-maintained SSM parameters that always point at the latest AMI
({{resolve:ssm:/aws/service/...}}): Amazon Linux, Ubuntu, Windows. macOS is deliberately unsupported
(needs dedicated hosts). Anything starting with ami- passes through untouched.
POST /canvas/deploy → CFCreator.deployToAWS():
- Key pairs —
create_key_pairs_for_deployment()walks the canvas for EC2 nodes and callsec2:CreateKeyPairfor each, naming them{build_id}-{node_id[:6]}-{instance_name}-key. The PEM private key comes back in the API response and is never stored server-side — the frontend must hand it to the user immediately. - Template generation —
createGeneration()→template_composer.make_stack_template():- Adds
VpcId/SubnetId/SecurityGroupIdparameters (plusDBSubnetGroupNameif any RDS node exists). - Phase 1: walk edges, build
ec2_dependencies = {ec2_id: {s3: [...], dynamodb: [...], rds: [...]}}. - Phase 2: create S3 / RDS / DynamoDB resources, recording each in
resource_refskeyed by node id. - Phase 3: for each EC2, look up its dependencies, create the IAM role + instance profile (or use the demo profile), assemble the environment-variable dict, and add the instance.
- The JSON is also written to
CFCreators/allJSONs/createdCFs/CF_<build_id>.json.
- Adds
- Deployer init —
CloudFormationDeployer(region)creates cloudformation/ec2/rds boto3 clients. - VPC discovery — finds the default VPC, its first subnet, and its
defaultsecurity group. If RDS is present, it also gets-or-createsfoundry-db-subnet-group-{vpc_id}spanning two AZs. - Deploy —
create_stackwithCapabilities=['CAPABILITY_NAMED_IAM'](needed because roles get explicit names) andOnFailure='ROLLBACK'. Stack name isfoundry-stack-{build_id}.
Then back in the router: the canvas + template are persisted to the build row, and an activity_log entry
is written. A DB failure here is caught and logged but does not fail the response — the stack is already
up, so the deploy is reported as successful.
Live tracking. In parallel the frontend opens WS /canvas/deploy/track/{stack_name}.
DeploymentWebSocketManager starts one polling task per stack (3 s interval), diffs describe_stack_events
against seen event IDs, and broadcasts resource_update messages with a progress percentage, finishing with a
stack_complete message carrying stack outputs and a human-readable duration.
Updates (POST /canvas/deploy/update) go through change sets, not blind updates: generate the new
template, create_change_set, return the formatted list of adds/modifies/removes (including whether a change
forces resource replacement) for the user to review, then either auto_execute or wait for
POST /canvas/deploy/execute-changeset / DELETE /canvas/deploy/changeset. The "no changes" case is detected
by inspecting the failed waiter's StatusReason and returned cleanly rather than as an error.
Deletes (POST /canvas/deploy/delete) call delete_stack then sweep key pairs matching the stack name.
POST /github/add_webhookwith{owner, repo, build_id}. The user's GitHub OAuth token is looked up from theaccounttable, apushwebhook is registered against the ngrok URL, and(owner, repo) → build_idis recorded in the in-process dictbuild_id_store.- GitHub sends
pushtoPOST /github/webhook. The HMAC-SHA256 signature is verified againstGITHUB_WEBHOOK_SECRET(only when both the secret and the header are present). - The repo is downloaded as a zip from the GitHub zipball API.
buildspec.yml,appspec.yml, andscripts/{start,stop,install}.share injected into the zip — each injector rewrites the archive into a temp file and moves it over the original. The user's repo doesn't need to contain any AWS config.- Zip →
s3://foundry-codebuild-zip/{owner}/{repo}-{ref}.zip. trigger_codebuild()starts thefoundryCICDCodeBuild project with an S3 source override and polls until terminal, streaming each status over the build's WebSocket.- On
SUCCEEDED,codeDeploy()creates/updates a CodeDeploy application and a deployment group whose EC2 tag filter isBuildId = {build_id}— this is the join between the two subsystems: the tag thatEC2_creation.pystamps on every instance is what CodeDeploy uses to find the target. - The instance's public IP is read back via
describe_instancesfiltered on the same tag, andhttp://{ip}:8000is written tobuild.endpoint.
Everything is named {build_id}-{...}-{user_supplied_name} so resources from one build are greppable and
never collide with another build's.
| Resource | Pattern | Sanitizer |
|---|---|---|
| Stack | foundry-stack-{build_id} |
— |
EC2 Name tag |
{build_id}-{name} |
sanitize_ec2_name (alnum, _, -) |
| S3 bucket | {build_id}-{bucketName}, ≤63 chars |
sanitize_bucket_name_part (lowercase, alnum, -) |
| RDS identifier | {build_id}-{dbName} |
sanitize_rds_identifier (lowercase, must start with a letter) |
| DynamoDB table | {build_id}-{tableName} |
sanitize_dynamodb_name (alnum, _, -, .) |
| IAM role | {build_id}-{node_id[:6]}-ec2-multi-service-role |
sanitize_iam_name |
| Key pair | {build_id}-{node_id[:6]}-{name}-key |
inline replace |
CloudFormation logical IDs are separate: they're {Type}{node_id with -, :, _ stripped}, since logical IDs
must be alphanumeric.
Every resource also carries the tags Name, OriginalName (what the user actually typed), ManagedBy, and
BuildId. The BuildId tag is load-bearing — CodeDeploy targeting, cost lookup, and log lookup all key off it.
IAM roles are per-EC2 and per-resource-ARN, not shared: an EC2 wired to one bucket gets a policy naming that
bucket's ARN and {ARN}/*, and nothing else.
Live IAM role creation costs 30–90 seconds of AWS eventual-consistency waiting, which was too slow for a
2-minute demo. Setting DEMO_MODE=true makes two substitutions:
template_composer.pyskips creating IAM resources entirely and attaches the pre-existing instance profile named byDEMO_IAM_INSTANCE_PROFILE_NAME(defaultfoundry-demo-ec2-profile). This is whyEC2_creation.pyaccepts an instance profile that is either a string (demo) or a troposphere object (production).resolve_image_id()returns a hardcoded pre-baked Ubuntu AMI (ami-002382dae3b809f72onhola) that already has the CodeDeploy agent and Python deps installed, instead of the stock SSM-resolved Ubuntu image.
USE_DEFAULT_BUILD_ID=true is a companion flag that forces every resource to use the literal string default
as its build ID prefix, so demo resource names are predictable.
The setup script for this lives on the liveDemo branch, not hola — see §13.
| Method | Path | Notes |
|---|---|---|
GET |
/canvas/health |
Liveness. |
POST |
/canvas/deploy |
Body: {buildId, canvas, owner_id?, region?}. Returns stack id/name/status, outputs, and keyPairs including PEM private keys. |
GET |
/canvas/deploy/status/{stack_name} |
?region= optional. |
POST |
/canvas/deploy/update |
Body: {build_id, stack_name, canvas, auto_execute?}. Creates a change set. |
POST |
/canvas/deploy/execute-changeset |
Query params, not a body: stack_name, change_set_name, build_id. |
DELETE |
/canvas/deploy/changeset |
Query params: stack_name, change_set_name. |
POST |
/canvas/deploy/delete |
Body: {stack_name, build_id?, cleanup_key_pairs?}. |
WS |
/canvas/deploy/track/{stack_name} |
Streams initial_state / resource_update / stack_complete / error. |
| Method | Path | Notes |
|---|---|---|
GET |
/builds/new?id={owner_id} |
Creates an empty build row, returns {build_id}. Must be called before /canvas/deploy. |
GET |
/builds/?id={owner_id} |
All builds for an owner. |
GET |
/builds/invitations?id={user_id} |
|
POST |
/builds/invitations/accept · /builds/invitations/decline |
Body {id}. |
| Method | Path | Notes |
|---|---|---|
GET |
/canvas/ |
Lists the caller's GitHub repos. Requires Authorization: Bearer <github-token>. |
GET |
/canvas/users |
All users. |
GET/POST |
/canvas/settings |
Read/write project name + description. |
POST |
/canvas/invite |
Body {invite_id[], build_id, owner_id, project_name, description}. |
POST |
/canvas/deployments |
Marks build.status = true. |
GET |
/canvas/endpoint/?build_id= |
The deployed app's URL. |
GET |
/canvas/costs?build_id= |
{s3, ec2} estimates. |
GET |
/canvas/logs?build_id= |
CPU time series per instance. |
POST |
/canvas/s3/upload |
Multipart: file, bucket_name, node_id. |
| Method | Path | Notes |
|---|---|---|
POST |
/github/add_webhook |
Body {owner, repo, build_id}. |
POST |
/github/webhook |
GitHub's endpoint. HMAC-verified. |
WS |
/github/ws/{build_id} |
Streams raw CodeBuild/CodeDeploy status strings. |
PostgreSQL on RDS. create_schema.py creates:
| Table | Purpose |
|---|---|
users |
id, name, username, email. |
account |
GitHub OAuth linkage. Read at runtime via github_login → github_access_token. |
build |
id (8-digit random, not the SERIAL the DDL declares), owner_id, canvas JSONB, cf_template JSONB, created_at. |
build_access |
Per-build role grants. |
activity_log |
build_id, user_id, change — free-text audit trail. |
deployments |
Stack tracking table. Declared but not written to by any current code path. |
create_schema.py has drifted from reality. Columns the running code reads or writes that the DDL doesn't
declare: build.project_name, build.description, build.status, build.endpoint; the whole invites table;
and account.github_login / account.github_access_token (the DDL declares user_id + token instead).
Treat the live database as the source of truth and regard create_schema.py as stale.
Read from .env (gitignored; database.py loads it with override=True).
| Variable | Purpose |
|---|---|
DATABASE_URL |
asyncpg DSN, used by the inline-async endpoints. |
RDS_HOST / RDS_PORT / RDS_DATABASE / RDS_USER / RDS_PASSWORD |
psycopg2 config in database.py. SSL is forced (sslmode=require). |
GITHUB_WEBHOOK_SECRET |
HMAC secret for webhook signature verification. |
DEMO_MODE |
true → skip IAM creation, use pre-baked AMI. |
DEMO_IAM_ROLE_NAME / DEMO_IAM_INSTANCE_PROFILE_NAME |
Names of the pre-created demo role/profile. |
USE_DEFAULT_BUILD_ID |
true → force every resource prefix to the literal default. |
AWS credentials come from the standard boto3 chain (~/.aws/credentials, env vars, or instance role) —
they are not read from .env.
Prerequisites: Python 3.12, AWS credentials with CloudFormation/EC2/S3/RDS/DynamoDB/IAM/CodeBuild/CodeDeploy permissions, and network access to the Postgres instance.
pip install -r requirements.txtuvicorn app:app --reload --port 8000Interactive docs at http://localhost:8000/docs. Sanity-check the DB with:
python database.pyFor the GitHub webhook you need a public URL. The handler currently has an ngrok URL hardcoded at
routers/github_webhook.py:104 — update it to your own tunnel:
ngrok http 8000Typical end-to-end flow: GET /builds/new?id=1 → POST /canvas/deploy with the returned buildId →
open WS /canvas/deploy/track/foundry-stack-{buildId} → POST /github/add_webhook → git push.
The repo has 12 remote branches. Development happened by branching per person/feature and merging sideways
rather than through main, so main is not the most current code — it was last updated by PR #3
(deployment_modal) and is missing 22 commits of subsequent work.
origin/hola (last commit f55797e, 2025-12-03) is the most complete and most recent branch.
git branch -r --merged origin/hola confirms that enaya, greetings, efrain, ak2, StackUpdates,
deployment_modal, and webhook are all ancestors of it — their work is already in. It has:
- the full CI/CD chain in
routers/github_webhook.py, including the correctedtrigger_codebuildargument order that older branches got wrong, - the
costs/,logs/, and/canvas/s3/uploadfeatures, - all of
liveDemo's demo-mode logic (BRANCH_SYNC_SUMMARY.mddocuments that merge, and diffingtemplate_composer.pybetween the two confirms they're identical), - the newest demo AMI (
ami-002382dae3b809f72, newer thanliveDemo'sami-0650b7c7445670128), - a clean
requirements.txt(liveDemo's was overwritten by a conda export containing brokenfile:///home/conda/...local paths and is missingasyncpg,websockets, andpython-multipart).
Your local working tree is already on hola and is clean apart from a stray .pyc and an untracked
nume-config.yml.
setup_demo_iam_role.py and DEMO_MODE_SETUP.md — the script that creates the demo IAM role and profile
that DEMO_MODE=true expects to already exist. Without them, demo mode fails at deploy time unless the role
was created by hand. These are worth cherry-picking onto hola:
git checkout origin/liveDemo -- setup_demo_iam_role.py DEMO_MODE_SETUP.md| Branch | Last commit | Status |
|---|---|---|
hola |
2025-12-03 | ✅ Most current. Work here. |
hola2 |
2025-12-03 | hola minus 2 commits, plus one demo tweak that drops the build_id prefix from DynamoDB table names (so a demo app could hardcode the table name). Not wanted generally. |
liveDemo |
2025-12-02 | Demo-day snapshot. Only setup_demo_iam_role.py + DEMO_MODE_SETUP.md are worth keeping. |
enaya |
2025-12-02 | Fully contained in hola. |
greetings |
2025-12-01 | Fully contained in hola. |
efrain |
2025-11-19 | The costs/logs work; already merged into hola. |
main |
2025-12-05 | Behind by 22 commits despite the newer timestamp (the timestamp is a merge commit). |
deployment_modal, StackUpdates, ak2, webhook, efrain_2 |
Nov 2025 | Older feature branches, all merged in. |
Recommendation: treat hola as trunk. Cherry-pick the two liveDemo files, then fast-forward main to
hola so the default branch stops lying about the state of the project.
Catalogued from reading the code, ordered by impact. Nothing here has been changed.
-
EC2 environment-variable injection is dead code.
EC2_creation.py:153-173carefully buildscombined_user_datawith all theS3_BUCKET_NAME/DYNAMODB_TABLE_NAME/DB_*exports — and then line 225 unconditionally overwritesprops["UserData"]with a hardcoded CodeDeploy-agent install script.combined_user_datais never read. Deployed instances never receive the env vars the canvas edges imply, even thoughtemplate_composer.pycomputes them correctly and the docs promise them. This is the single highest-value fix in the repo. -
S3 buckets ignore
build_id.template_composer.py:139callsS3_creation.add_s3_bucket(t, node, logical_id=logical_id)without passingbuild_id, so the parameter falls back to its"default"default and every bucket from every build is nameddefault-<name>— which means the second build with the same bucket name fails on S3's global uniqueness. This is also why/canvas/s3/uploadhardcodessecond_name = f"default-{bucket_name}"(routers/canvas.py:1033); fix the composer and that hack goes away. RDS and DynamoDB do passbuild_idcorrectly. -
Key-pair cleanup never matches. Keys are named
{build_id}-...-keybutcleanup_key_pairs_for_stack()matches against the stack namefoundry-stack-{build_id}, sostack_name in key_nameis always false. Deleting a stack reports0 key pairs deletedand leaks every key pair. Match on the build ID, or better, pass the known key names through from the delete request. -
addBuildSpecreturnsNoneon the common path. Itsreturn target_pathsits inside theelsebranch (addYamlZip.py:103), so when the repo already has abuildspec.ymlandoverWrite=True, it returnsNone. ThatNonebecomesbuildspecOverrideintrigger_codebuild, silently changing which buildspec CodeBuild uses. Move the return out of the conditional. -
time.sleep(3)inside anasync def.trigger_codebuild.py:43blocks the whole event loop for the entire duration of a CodeBuild run — every other request, including all live WebSockets, stalls.code_Deploy.pygets this right withawait asyncio.sleep(2); make them consistent. -
build_id_storeis in-process memory. The(owner, repo) → build_idmapping ingithub_webhook.py:20is lost on restart and not shared across workers, so a push after a restart deploys withbuild_id = None. It belongs in the database. -
/canvas/deploy/updatedropsbuild_idduring template generation. It callscreateGeneration(request.canvas)with nobuild_id, so the regenerated template falls back to a timestamp-based ID and every resource gets a new name — change sets will show spurious replacements. -
The webhook handler returns nothing when a build fails.
github_webhook.py:193only handlesbuild_status == "SUCCEEDED"; there is noelse, so a failed CodeBuild falls off the end of the function and FastAPI returns200 null. The frontend can't distinguish a failure from a success. (An earlier version onefraindid return{"message": "Build failed, skipping deploy"}— the branch was lost in a later rewrite.) Thetry/exceptaround the endpoint-recording block has the same shape: it prints and returnsNone.
-
Copy-pasted zip injectors.
addBuildSpec,addAppSpec,addStartScript,addStopScript, andaddInstallScriptare five near-identical ~45-line functions differing only intarget_path. They collapse into oneinject_into_zip(zip_path, target_suffix, content, overwrite=True)— about 200 lines → about 30. (Several still print"Injected start.sh"regardless of what they wrote, betraying the copy-paste.) -
get_access_token_for_owneris defined twice, byte-identical, atdatabase.py:238and:263, and imported twice on consecutive lines ingithub_webhook.py:16-17. -
Duplicated blocks in the webhook handler.
github_webhook.pyextractsowner/repo/build_idtwice (lines 72-97), repeats the ping/refguard twice (lines 138-161), and declares bothbuild_id_storeandsocketstwice each. -
Three IAM role factories, one caller.
create_ec2_s3_roleandcreate_ec2_dynamodb_roleare exported but never invoked —create_ec2_multi_service_rolehandles both cases. Delete the two dead ones (~200 lines), or keep them and delete the branch in the multi-service function. -
Env-var assembly duplicated inside
template_composer.py. The S3/DynamoDB env-var loops appear once in the production branch (lines 226-242) and again in the "still add environment variables even in demo mode" block (lines 252-265), guarded by anif not environment_variablescheck that only works by accident. Hoist it above the demo/production split. -
Four hand-rolled sanitizers.
sanitize_ec2_name,sanitize_bucket_name_part,sanitize_rds_identifier,sanitize_dynamodb_name, andsanitize_iam_nameare the same algorithm with different allowed-character sets. One parameterizedsanitize(name, allowed, lowercase=False, must_start_alpha=False)covers all five. -
The
USE_DEFAULT_BUILD_IDcheck appears in six places (template_composer, all four service creators, two of three IAM factories — andcreate_ec2_s3_roleis missing it, an inconsistency). Resolve it once inmake_stack_templateand pass the result down. -
Two database access patterns.
database.py(psycopg2, pooled through a context manager) versus ~10 endpoints incanvas.pythat callawait asyncpg.connect(...)inline — opening a fresh connection per request and, in most cases, never closing it. Several of these also swallow exceptions with a bareprintand returnNone, which FastAPI serialises asnullwith a 200 status, so the frontend can't tell success from failure. Consolidate on one driver behinddatabase.py. -
Imports scattered mid-function.
canvas.pyre-importsget_build,log_activity,CloudFormationDeployer, andcreateGenerationinside handler bodies even though several are already imported at module top.
- Hardcoded credentials in
create_schema.py:8-14— a real database host, user, and password committed to git. Rotate the password and move the config to.env. (database.py:19also carries the same host as a default.) - CORS is
allow_origins=["*"]withallow_credentials=True(app.py:14-20) — an invalid and unsafe combination. Pin the frontend origin. - No authentication anywhere. Every endpoint takes
owner_idas a plain request field defaulting to1; the# TODO: Replace with actual authcomments say as much. Any caller can read or delete any build. - SSH private keys are returned in the deploy HTTP response and printed to logs on the way through. Acceptable for a demo, not for anything real.
- Hardcoded infrastructure identifiers scattered through the code: the CodeDeploy service role ARN
including the AWS account number (
code_Deploy.py:11), bucket namesfoundry-codebuild-zip/foundry-artifacts-bucket, the project namefoundryCICD, the ngrok URL, andregion='us-east-1'in six modules despite the API accepting aregionparameter. All belong in config. - The
founryCICDtypo (missingd) is baked into artifact names in bothtrigger_codebuild.pyandcode_Deploy.py. They agree, so it works — just note that fixing one without the other breaks deploys. .pycfiles and ~190 generatedcreatedCFs/*.jsonare committed despite being listed in.gitignore(they were added before the ignore rules).git rm -r --cachedthem.- Root directory clutter. 9
test_*.pyscripts,check_tables.py,verify_build_table.py,demo_stack_update.py,tempfile.txt(empty),fastAPIserver-main.zip, and 4 status-report markdown files sit at the top level. Thetest_*files are manual walkthroughs, not a pytest suite — there is no automated test coverage of the template composer, which is the piece most worth testing. github_webhook_test/duplicatesrouters/github_webhook.pyin an obsolete standalone app. Delete it.costs/dynamo.pyis entirely commented out, and its import incanvas.py:30is commented out to match.
If/when we do the refactor, this sequencing keeps things working at every step:
- Fix bugs 1–3 first (they're behavioural and independently verifiable).
- Collapse the zip injectors (9) and sanitizers (14) — pure, mechanical, easy to test.
- Delete duplicates 10, 11, 12, 26, 27.
- Unify the DB layer (16), which also fixes the leaked connections and swallowed errors.
- Extract config (22) and split
canvas.pyintodeploy/builds/project/observabilityrouters. - Then auth (20) and CORS (19), which change the API contract and need frontend coordination.
{ "nodes": [ { "id": "ec2-1", "type": "EC2", // "EC2" | "S3" | "RDS" | "DynamoDB" "data": { "name": "api-server", // required "imageId": "Ubuntu", // friendly name or "ami-xxxx" "instanceType": "t3.micro", // required "keyName": "optional-existing-key", "userData": "#!/bin/bash ...", "storage": { "rootVolumeSizeGiB": 20, // default 20 "rootVolumeType": "gp3", // default gp3 "deleteOnTermination": true } } }, { "id": "s3-1", "type": "S3", "data": { "bucketName": "my-app-storage" } }, { "id": "ddb-1", "type": "DynamoDB", "data": { "tableName": "users", "partitionKey": "id", "partitionKeyType": "S", "sortKey": "", "sortKeyType": "S" } }, { "id": "rds-1", "type": "RDS", "data": { "dbName": "appdb", "engine": "postgres", "masterUsername": "admin", "masterUserPassword": "..." } } ], "edges": [ { "source": "s3-1", "target": "ec2-1" } // direction matters: resource → EC2 ] }