Repository navigation
Move to the internal load balancer behind an API Gateway #421
Description
Summary
Move BuildBot from the testing Application Load Balancer to the internal one, and put an API Gateway, created by the stack, in front of it. Clients call the API Gateway, which forwards only the application's endpoints to the internal ALB listener, which routes to the ECS service as today.
Changes
1. Switch to the internal load balancer
In cloudformation.json, change Deployment.LoadBalancer.ListenerArn from
arn:aws:elasticloadbalancing:eu-west-1:117769150821:listener/app/testing/c383c5dd580a74f5/e82083ed48688c7a
to
arn:aws:elasticloadbalancing:eu-west-1:117769150821:listener/app/internal/fbd77c1a39a60f7f/e01c11e315c3ff6e
The ListenerRule, TargetGroup and the ECS service's LoadBalancers block stay in place, now attached to the internal listener.
2. Add an API Gateway that forwards to the internal load balancer
Add an API Gateway to cloudformation.json whose integration targets the internal ALB listener above, not the ECS tasks directly. Deploy one route for each endpoint in src/BuildBot/Helpers/Endpoints.*.cs:
| Method | Path | Source |
|---|---|---|
| GET | /ping |
Endpoints.Test.cs |
| POST | /github/ping |
Endpoints.GitHub.cs |
| POST | /github/push |
Endpoints.GitHub.cs |
| POST | /github/status |
Endpoints.GitHub.cs |
| POST | /watchtower/deploy |
Endpoints.Watchtower.cs |
| POST | /cloudformation/deploy |
Endpoints.CloudFormation.cs |
No catch-all ($default / {proxy+}) route. Keep any new per-service values in the Deployment mapping rather than parameter Defaults, per #417.
Notes / open questions
- An internal ALB is reachable from API Gateway only through a VPC Link (HTTP API
AWS::ApiGatewayV2::VpcLink, integrationConnectionType: VPC_LINKwith the listener ARN asIntegrationUri). Does a shared VPC Link to the internal ALB already exist that should be reused, or should this stack create one (needs subnets and security groups)? - The listener rule matches on the host header (
LoadBalancerHostName). Should the API Gateway integration override theHostheader to that value (via request parameter mapping), or should the listener rule change to match on path instead? - Custom domain: should the stack create an API Gateway custom domain / API mapping for the public host name, and who updates DNS?
Activity
[-]Use API Gateway instead of the load balancer in CloudFormation[/-][+]Move to the internal load balancer behind an API Gateway[/+]credfeto commentedon Oct 13, 2026
Networking guidance for the VPC Link (partly answers open question 1):
So:
172.31.0.0/16. No ingress rules are needed.alb-internal's security group needs no change, because it already allows 443 from172.31.0.0/16.Still open: whether this stack creates the VPC Link and its security group, or reuses an existing link.
credfeto commentedon Oct 13, 2026
Decision on open question 1: only BuildBot will sit behind API Gateway, so this stack creates its own VPC Link (
AWS::ApiGatewayV2::VpcLink) and its security group (egress TCP 443 to172.31.0.0/16, no ingress), using the service's existing subnets. No shared link is needed.credfeto commentedon Oct 13, 2026
Decision on open question 2: the API Gateway integration sets the
Hostheader to theLoadBalancerHostNamevalue (HTTP API integrationRequestParameters:overwrite:header.Host), so the existing host-header listener rule on the internal ALB stays unchanged.credfeto commentedon Oct 13, 2026
Decision on open question 3: the stack also manages the public DNS. It creates:
AWS::ApiGatewayV2::DomainName, regional endpoint) with an ACM certificate in eu-west-1;AWS::ApiGatewayV2::ApiMapping) from that domain to the API's stage;AWS::Route53::RecordSet, A and AAAA if wanted) pointing the host name at the custom domain's regional domain name / hosted zone ID.Values needed in the
Deploymentmapping (still to be confirmed):LoadBalancerHostName, or a different one?).Caution: if a DNS record for that host name already exists outside this stack (for example, pointing at the
testingALB), CloudFormation will fail to create it. Delete it or import it into the stack as part of the cut-over.credfeto commentedon Oct 15, 2026
Dependency, from the repository owner in live chat:
Recorded: implementation of this issue waits until #420 (ECS secrets moved to
Secrets, closing #419) merges, then branches frommain. Both changes editcloudformation.json, so this order avoids conflicting edits to the task definition.credfeto commentedon Oct 15, 2026
Correction to the previous comment, which recorded the dependency as #420. The repository owner corrected this in live chat:
Corrected: implementation of this issue waits until the PR for #422 (GitHub webhook signature verification) merges, then branches from
main. #422 itself waits for #420, so the order is #420, then #422, then #421.credfeto commentedon Oct 15, 2026
Direction from the repository owner in live chat:
This is reflected in the implementation plan below, together with the mappings rule recorded on #427 ("everything should be in mappings unless its explicitly set as a parameter in the deploy workflow").
credfeto commentedon Oct 15, 2026
Implementation Plan
Files to change
cloudformation.json:ListenerRuleat the internal listener;ApiHostparameter, which the deploy action already passes;Deploymentmapping.CHANGELOG.md: entries viadotnet changelog, including a Deployment Changes entry for the cut-over steps.README.md: in the Configuration section, note that webhooks now arrive through the API Gateway host.Approach
Load balancer.
Deployment.LoadBalancer.ListenerArnbecomes the internal listener (.../app/internal/fbd77c1a39a60f7f/e01c11e315c3ff6e, the same one funfair-ethereum-proxy-server uses).ListenerRule,TargetGroupand the ECS service'sLoadBalancersblock stay as they are.API Gateway. An HTTP API (
AWS::ApiGatewayV2::Api) with:AWS::ApiGatewayV2::VpcLink, using the existingSubnetIDsand a new security group with only egress TCP 443 to172.31.0.0/16;HTTP_PROXYintegration (ConnectionType: VPC_LINK,IntegrationUri= the internal listener ARN), withRequestParametersoverwrite:header.Host=LoadBalancerHostNameandTlsConfig.ServerNameToVerifyset (see Q6);GET /ping,POST /github/ping,POST /github/push,POST /github/status,POST /watchtower/deployandPOST /cloudformation/deploy;$defaultstage with auto-deploy;AWS::ApiGatewayV2::DomainNameforApiHost, plus anApiMappingand a Route 53 A alias record.Mappings rule. The deploy action already passes
ApiHostandLoadBalancerHostName, so those are declared as parameters.ApiPrefixstays undeclared, because the routes stay at root and the GitHub webhook URL does not change. Everything else new goes in theDeploymentmapping: internal listener ARN, hosted zone, certificate ARN, VPC Link egress CIDR and the TLS server name. #427 later renames that mapping toSettings.How the recent and planned work fits through the gateway:
X-Hub-Signature-256,X-GitHub-DeliveryandX-GitHub-Eventreach the middleware as GitHub sent them. Coding Researcher will confirm this, and howX-Forwarded-Foris populated (see b. and c.).Authorization, which a proxy integration with no authorizer passes through. SNS message-signature verification does not depend on transport. The SNS subscription URL moves to the API Gateway custom domain.Authorizationpass through. feat(watchtower): Authenticate Watchtower notifications to the watchtower deploy endpoint #425 must not use a header name API Gateway treats specially, such asX-Api-Key. Basic auth is the safe choice./cloudformation/deploykeeps accepting SNS'stext/plainbody, and the ALB target-group health check (/ping) still hits the target group directly.Test strategy
/github/pushand confirm the app's 401 comes back (not an API Gateway 403/404);/cloudformation/deploywithContent-Type: text/plainreaches the app;/foo) gets API Gateway's 404.Assumptions
a. Payloads over 10 MB (the HTTP API request limit) are rejected by API Gateway with its own 413 before reaching the app. That is acceptable. #426's 25 MB
BUFFER_LIMITstays as it is.b. API Gateway adds the client's source IP to
X-Forwarded-Forand the internal ALB appends to it, so #426's rejection log still shows the real client. The logged remote address remains the ALB's.c.
ApiHostandLoadBalancerHostNamehold the same value per environment (both come fromAWS_LOAD_BALANCER_HOSTNAME_DEV/_PROD).d. Webhooks delivered during the DNS switch may be lost. GitHub does not automatically retry failed deliveries.
Open questions
Q4 to Q9 continue from the issue's original questions 1 to 3.
Q4. Which Route 53 hosted zone (ID or name) holds the dev and prod host names?
Q5. Is there an existing ACM certificate in eu-west-1 covering both host names (for example a wildcard)? Yes means give the ARN or ARNs; no means the stack requests one with DNS validation.
Q6. What name does the internal ALB's HTTPS listener certificate present, for the integration's
ServerNameToVerify? Is it the same host name?Q7. Is listener rule priority
LoadBalancerPriority(20000 in prod) free on the internal listener? If not, which priority should be used?Q8. The existing DNS records for the host names (currently pointing at the
testingALB) would make the Route 53 record creation fail. Should they be deleted by hand just before the deploy (yes), or imported into the stack (no)?Q9. Sequencing: should #421 go before #427 (template format) and rebase over #428 (Discord webhook, in another session) once that lands? Both edit
cloudformation.json.This issue is marked
Blockedunder the plan-first approval gate (Issue Workflow: Plan First): implementation does not start until a human approves this plan and the open questions are answered.credfeto commentedon Oct 15, 2026
Answers from the repository owner in live chat:
Resolved:
funfair.io.arn:aws:acm:eu-west-1:117769150821:certificate/d9405a08-95c9-4924-acde-d86cda5f29e8.ServerNameToVerifyisbuildbot.funfair.io.A revised plan follows.
credfeto commentedon Oct 15, 2026
Implementation Plan
Revision of the plan above, incorporating the Q4 to Q9 answers and folding in #427.
Files to change
cloudformation.json:CHANGELOG.md: entries viadotnet changelog, including Deployment Changes for the DNS import and cut-over.README.md: a deployment note that webhooks now arrive through API Gateway, plus the one-off DNS import steps.Approach
Template format (#427), applying the owner's rule "everything should be in mappings unless its explicitly set as a parameter in the deploy workflow":
.github/actions/cloudformation-deploy/action.ymlpasses and the template uses:ApplicationName,ApplicationContainer,ApplicationVersion,ApiHost,LoadBalancerHostName,LoadBalancerPriorityandAwsContainerRegistry.Settings/Valuesmapping: everything else, replacing theDeploymentmapping. That covers:funfair);SecretsManagerArn, the execution role ARN (ecsTaskExecutionRole),SecurityGroupIDs,SubnetIDsandVpcID;funfair.io.), the certificate ARN, the VPC Link egress CIDR (172.31.0.0/16) and the TLS server name (buildbot.funfair.io).${AwsContainerRegistry}/${Owner}/${ApplicationContainer}:${ApplicationVersion}, as in the sibling templates.Fn::Subwith aFn::FindInMapvariable for the ARN, because short-formFn::Subcannot read a mapping.Secretsinjection (includingGITHUB__WEBHOOKSECRET) and the deployment circuit breaker.Internal load balancer:
ListenerRulemoves to the internal listener.TargetGroupand the service'sLoadBalancersblock stay as they are. The rule priority is 50 (Q7; see Q10 for how to set it).API Gateway:
172.31.0.0/16;HTTP_PROXYintegration, withConnectionType: VPC_LINKandIntegrationUriset to the internal listener ARN, plus:RequestParametersoverwrite:header.Hostset toLoadBalancerHostName;TlsConfig.ServerNameToVerifyset tobuildbot.funfair.io;GET /ping,POST /github/ping,POST /github/push,POST /github/status,POST /watchtower/deployandPOST /cloudformation/deploy;$defaultstage with auto-deploy;DomainNameforApiHostusing the given ACM certificate, plus anApiMapping;funfair.io.withDeletionPolicy: Retain. It is imported into the stack (Q8; see Q11).How the recent and planned work fits through the gateway (unchanged from the previous plan):
Authorization. SNS signature verification does not depend on transport, and the subscription URL moves to the custom domain.X-Api-Key.Test strategy
aws cloudformation get-templateversus the rendered new template. This is run by the owner or with read-only AWS access./github/pushreturns the app's 401;/cloudformation/deploywithtext/plainreaches the app;Assumptions
a. Payloads over 10 MB are rejected by API Gateway with its own 413 before reaching the app, and that is acceptable.
b. API Gateway puts the client source IP in
X-Forwarded-Forand the ALB appends to it. Coding Researcher confirms this during implementation.c.
ApiHostandLoadBalancerHostNamehold the same value per environment.d. Webhooks delivered during the DNS switch may be lost.
e. The certificate
d9405a08-...covers the dev and prod host names.Open questions
Q10. The deploy workflow passes
LoadBalancerPriority(fromAWS_LOAD_BALANCER_PRIORITY_DEV/_PROD), so under the mappings rule it stays a parameter. Should the priority of 50 be set by changing those repository variables (yes), or by hardcoding 50 in theSettingsmapping and dropping the parameter (no)? What priority should dev use?Q11. A Route 53 record import needs a one-off
aws cloudformation create-change-set --change-set-type IMPORT, and the deploy action cannot do that. Will you run that import by hand from steps the PR documents, before the first deploy (yes)? Or should the cut-over avoid importing (no)?This issue stays
Blockedunder the plan-first approval gate (Issue Workflow: Plan First) until a human approves this revised plan.5 remaining items
credfeto commentedon Oct 16, 2026
Coding Researcher
Questions:
HTTP_PROXYintegration over a VPC Link handle the body and headers?TlsConfigrequire against an HTTPS ALB listener?Outcomes:
$request.bodyparameter mappings, which are not used. Payload limit 10 MB; header limit 10,240 bytes.X-Forwarded-For: HTTP APIs turn incomingX-Forwarded-*into a standardForwardedheader.X-Forwarded-Foris reserved and cannot be mapped. The ALB then createsX-Forwarded-Forcontaining only the VPC Link's private IP, so feat(github): verify webhook signatures with the configured secret #426's rejection log would no longer show the real client. A non-reserved header can be set with$context.identity.sourceIp, for exampleoverwrite:header.X-Client-Ip. Sources: known issues, parameter mapping, ALB X-Forwarded headers.Hostoverride: allowed (not reserved).$defaultstage adds no path prefix.TlsConfig: required. Without it, private integrations use plain HTTP.ServerNameToVerifymust match a CN/SAN on the ALB certificate and is sent as SNI. An ACM public certificate is fine. Private-CA and self-signed certificates are not usable. Source: private integrations.AWS::Route53::RecordSetis not in CloudFormation's resource-import support list, and a normal deploy cannot adopt an existing record (it fails with "already exists"). Source: resource import support. Alternatives:UPSERTafter the deploy (atomic, no gap);credfeto commentedon Oct 16, 2026
Answer from the repository owner in live chat, after research showed that a Route 53 record set cannot be imported:
Resolved, superseding Q8/Q11: the existing
buildbot.funfair.iorecord is deleted by hand just before the first deploy of #433, and the stack then creates and owns theAWS::Route53::RecordSet(A alias to the API Gateway custom domain). Expect an NXDOMAIN window of up to about 15 minutes while resolver negative caches expire. The README and the Deployment Changes entry will document the ordering: delete the record, then deploy immediately. No import procedure is needed.credfeto commentedon Oct 16, 2026
Answer from the repository owner in live chat:
Resolved: the API Gateway integration sets
overwrite:header.X-Client-Ip=$context.identity.sourceIp. Because it overwrites, a client-supplied value cannot spoof it. As its own change in #433, the #426 webhook rejection log adds that header (sanitised like the other client values), so rejections behind the gateway still show the real caller.feat(cloudformation): front BuildBot with an API Gateway HTTP API ove…
feat(github): log the API Gateway client IP on rejected webhooks (#421)
docs(readme): cover recovery from a rolled-back first deploy (#421)
docs(changelog): describe API Gateway, internal ALB and template form…
feat(cloudformation): throttle the API Gateway stage and log access (#…
feat(cloudformation): keep ECS application logs for 30 days (#421)
docs(changelog): note 30-day ECS log retention (#421)
docs(changelog): describe the ECS service naming by its effect (#421)
fix(cloudformation): make the task definition depend on its log group (…
docs(changelog): note the log group ordering fix (#421)
docs(coverage): record coverage at 5838d6a (#421)