Skip to content

Commit 499df8c

Browse files
committed
fix(operator): use Cloud Map SRV + service-ARN integration (fixes live 503/BadRequest)
Live E2E against a real account revealed the documented Option-1 wiring does not work: an HTTP API VPC_LINK integration rejects a raw 'http://<dns>:<port>' URI with BadRequestException: For VpcLink VPC_LINK, integration uri should be a valid ELB listener ARN or a valid Cloud Map service ARN. Correct wiring (matches CDK's HttpServiceDiscoveryIntegration): - Cloud Map service uses an SRV record (not A) so the container port is captured; ECS registers the task IP + port into it. - ECS service registry sets containerName/containerPort and the task def exposes the container port, so ECS writes the SRV record. - API Gateway integration URI is the Cloud Map *service ARN* with connectionType=VPC_LINK and integrationMethod=ANY; the port is resolved from SRV. Verified end-to-end (POST through API Gateway → VPC Link → Cloud Map SRV → Fargate task returned HTTP 200 with the request echoed at /prod/webhook/telegram), then torn down cleanly. build + clippy -D warnings + cargo test (11 passed).
1 parent 67d80a8 commit 499df8c

3 files changed

Lines changed: 41 additions & 41 deletions

File tree

operator/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ spec:
241241

242242
On `apply` this reconciles (idempotently, reused by name):
243243

244-
1. **Cloud Map** private DNS namespace + a per-service A record
244+
1. **Cloud Map** private DNS namespace + a per-service **SRV** record (carries the container port; a plain A record does not work as a VPC-Link integration target)
245245
2. **ECS service registry** wiring (attached at service creation)
246246
3. **VPC Link** (shared `oab-vpc-link`), waits until `AVAILABLE`
247247
4. **API Gateway HTTP API** (`oab-webhook-<ns>-<name>`, one per bot) + `HTTP_PROXY` integration over the VPC Link

operator/src/apply.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,25 @@ async fn apply_ecs(
182182
.collect();
183183

184184
// 4. Register task definition
185-
let container = ContainerDefinition::builder()
185+
let mut container = ContainerDefinition::builder()
186186
.name("openab")
187187
.image(&m.spec.image)
188188
.essential(true)
189189
.set_environment(Some(env_vars))
190-
.set_secrets(if secrets.is_empty() { None } else { Some(secrets) })
191-
.build();
190+
.set_secrets(if secrets.is_empty() { None } else { Some(secrets) });
191+
192+
// Ingress needs the container port exposed so ECS can register an SRV record
193+
// (Cloud Map + API Gateway learn the target port from it).
194+
if let Some(ingress) = &m.spec.ingress {
195+
container = container.port_mappings(
196+
aws_sdk_ecs::types::PortMapping::builder()
197+
.container_port(ingress.container_port as i32)
198+
.protocol(aws_sdk_ecs::types::TransportProtocol::Tcp)
199+
.build(),
200+
);
201+
}
202+
203+
let container = container.build();
192204

193205
let task_def = ecs
194206
.register_task_definition()
@@ -294,11 +306,16 @@ async fn apply_ecs(
294306
.network_configuration(network_config);
295307

296308
if let Some(cm) = &cloud_map {
297-
create_req = create_req.service_registries(
298-
aws_sdk_ecs::types::ServiceRegistry::builder()
299-
.registry_arn(&cm.registry_arn)
300-
.build(),
301-
);
309+
let mut registry = aws_sdk_ecs::types::ServiceRegistry::builder()
310+
.registry_arn(&cm.registry_arn);
311+
// SRV records require the container name + port so ECS registers the
312+
// task's port alongside its IP.
313+
if let Some(ingress) = &m.spec.ingress {
314+
registry = registry
315+
.container_name("openab")
316+
.container_port(ingress.container_port as i32);
317+
}
318+
create_req = create_req.service_registries(registry.build());
302319
}
303320

304321
create_req
@@ -329,7 +346,7 @@ async fn apply_ecs(
329346
ingress,
330347
&ecs_rt.networking.subnets,
331348
&ecs_rt.networking.security_groups,
332-
&cm.dns_name,
349+
&cm.registry_arn,
333350
)
334351
.await?;
335352
println!(" 🔗 Webhook URL(s) for {}:", m.metadata.name);

operator/src/ingress.rs

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@ fn api_name(namespace: &str, name: &str) -> String {
2929

3030
/// Result of Cloud Map reconciliation, consumed when creating the ECS service.
3131
pub struct CloudMapResult {
32-
/// Cloud Map service ARN — used as the ECS service registry ARN.
32+
/// Cloud Map service ARN — used both as the ECS service registry ARN and as
33+
/// the API Gateway integration URI.
3334
pub registry_arn: String,
34-
/// Private DNS name the task registers under, e.g. `oab-prod-mybot.oab`.
35-
pub dns_name: String,
3635
}
3736

3837
/// Step 1: ensure the Cloud Map private DNS namespace and service exist.
@@ -63,10 +62,7 @@ pub async fn ensure_cloud_map(
6362
eprintln!(" ✓ Created Cloud Map service: {dns_name}");
6463
}
6564

66-
Ok(CloudMapResult {
67-
registry_arn,
68-
dns_name,
69-
})
65+
Ok(CloudMapResult { registry_arn })
7066
}
7167

7268
/// Step 2: ensure VPC Link, HTTP API, integration, routes, stage, and the
@@ -78,7 +74,7 @@ pub async fn ensure_gateway(
7874
ingress: &Ingress,
7975
subnets: &[String],
8076
security_groups: &[String],
81-
dns_name: &str,
77+
cloud_map_service_arn: &str,
8278
) -> Result<Vec<String>> {
8379
let api = aws_sdk_apigatewayv2::Client::new(config);
8480
let ec2 = aws_sdk_ec2::Client::new(config);
@@ -93,9 +89,10 @@ pub async fn ensure_gateway(
9389
// ── HTTP API (one per bot — avoids cross-bot path collisions) ──────────
9490
let (api_id, api_endpoint) = ensure_api(&api, &api_name).await?;
9591

96-
// ── Integration: VPC Link → Cloud Map DNS name on the container port ────
97-
let integration_uri = integration_uri(dns_name, ingress.container_port);
98-
let integration_id = ensure_integration(&api, &api_id, &vpc_link_id, &integration_uri).await?;
92+
// ── Integration: VPC Link → Cloud Map service (URI is the service ARN;
93+
// the port is resolved from the service's SRV record) ───────────────
94+
let integration_id =
95+
ensure_integration(&api, &api_id, &vpc_link_id, cloud_map_service_arn).await?;
9996

10097
// ── One route per webhook path, all → the same integration ─────────────
10198
for path in &ingress.paths {
@@ -108,11 +105,6 @@ pub async fn ensure_gateway(
108105
Ok(webhook_urls(&api_endpoint, &ingress.paths))
109106
}
110107

111-
/// Integration URI for the VPC Link → Cloud Map target on the container port.
112-
fn integration_uri(dns_name: &str, port: u16) -> String {
113-
format!("http://{dns_name}:{port}")
114-
}
115-
116108
/// API Gateway route key for a webhook path (POST only).
117109
fn route_key(path: &str) -> String {
118110
format!("POST {path}")
@@ -258,9 +250,12 @@ async fn ensure_service(
258250
}
259251
}
260252

261-
// Create with a single A record (TTL 60). SRV does NOT work for VPC Link.
253+
// Create with an SRV record. For an HTTP API private integration whose URI
254+
// is the Cloud Map service ARN, API Gateway learns the target port from the
255+
// SRV record — a plain A record carries no port and does NOT work. ECS
256+
// registers the task's IP + container port into this SRV record.
262257
let dns_record = DnsRecord::builder()
263-
.r#type(RecordType::A)
258+
.r#type(RecordType::Srv)
264259
.ttl(60)
265260
.build()
266261
.context("failed to build DNS record")?;
@@ -532,7 +527,7 @@ async fn ensure_integration(
532527
.create_integration()
533528
.api_id(api_id)
534529
.integration_type(IntegrationType::HttpProxy)
535-
.integration_method("POST")
530+
.integration_method("ANY")
536531
.integration_uri(integration_uri)
537532
.connection_type(ConnectionType::VpcLink)
538533
.connection_id(vpc_link_id)
@@ -617,18 +612,6 @@ async fn ensure_stage(api: &aws_sdk_apigatewayv2::Client, api_id: &str) -> Resul
617612
mod tests {
618613
use super::*;
619614

620-
#[test]
621-
fn integration_uri_uses_dns_and_port() {
622-
assert_eq!(
623-
integration_uri("oab-prod-mybot.oab", 8080),
624-
"http://oab-prod-mybot.oab:8080"
625-
);
626-
assert_eq!(
627-
integration_uri("svc.ns", 3000),
628-
"http://svc.ns:3000"
629-
);
630-
}
631-
632615
#[test]
633616
fn route_key_is_post_prefixed() {
634617
assert_eq!(route_key("/webhook/telegram"), "POST /webhook/telegram");

0 commit comments

Comments
 (0)