Link Service Infrastructure Design

Date: 2026-02-19

Overview

Expose the Link service (port 9999) to the internet at link.getorcha.com and link.prod.getorcha.com, following the same infrastructure pattern as ERP and Admin services.

Current State

Architecture

Internet
    │
    ▼
ALB (HTTPS:443)
    │
    ├─ Host: app.* ──────────────► Target Group (8888) ──► EC2
    ├─ Host: admin.* ─► Cognito ─► Target Group (7777) ──► EC2
    └─ Host: link.* ─────────────► Target Group (9999) ──► EC2

DNS Structure

Management Account (getorcha.com zone)
├── CNAME: link.getorcha.com → link.prod.getorcha.com  [NEW]
└── ACM validation CNAME for link.getorcha.com          [NEW]

Prod Account (prod.getorcha.com zone - via CDK)
├── A: link.prod.getorcha.com → ALB                     [NEW]
└── ACM validation CNAME for link.prod.getorcha.com     [NEW]

Changes Required

1. Security Groups (foundation_stack.py)

Add ALB-to-EC2 rules for port 9999:

# Link service traffic ALB -> EC2 (port 9999)
self.alb_sg.add_egress_rule(
    self.ec2_sg,
    ec2.Port.tcp(9999),
    "Link traffic to EC2",
)
self.ec2_sg.add_ingress_rule(
    self.alb_sg,
    ec2.Port.tcp(9999),
    "Link port from ALB",
)

2. Target Group (compute_stack.py)

New target group for Link service:

self.link_target_group = elbv2.ApplicationTargetGroup(
    self,
    "LinkTargetGroup",
    target_group_name="v1-orcha-link-tg",
    vpc=vpc,
    port=9999,
    protocol=elbv2.ApplicationProtocol.HTTP,
    target_type=elbv2.TargetType.INSTANCE,
    health_check=elbv2.HealthCheck(
        path="/health",
        protocol=elbv2.Protocol.HTTP,
        healthy_threshold_count=2,
        unhealthy_threshold_count=3,
        interval=Duration.seconds(30),
        timeout=Duration.seconds(5),
    ),
    deregistration_delay=Duration.seconds(60),
)
self.asg.attach_to_application_target_group(self.link_target_group)

3. SSL Certificate (compute_stack.py)

Import manually-created certificate (cross-account DNS validation):

if env_name == "prod":
    self.link_certificate = acm.Certificate.from_certificate_arn(
        self,
        "LinkCertificate",
        certificate_arn="<ARN_FROM_SCRIPT>",
    )
else:
    self.link_certificate = acm.Certificate(
        self,
        "LinkCertificate",
        domain_name=f"link.{env_name}.getorcha.com",
        validation=acm.CertificateValidation.from_dns(hosted_zone),
    )

4. ALB Listener Rule (compute_stack.py)

Add certificate and host-based routing:

https_listener.add_certificates("LinkCerts", [self.link_certificate])

https_listener.add_action(
    "LinkRouting",
    priority=20,
    conditions=[
        elbv2.ListenerCondition.host_headers([
            "link.prod.getorcha.com",
            "link.getorcha.com",
        ])
    ],
    action=elbv2.ListenerAction.forward([self.link_target_group]),
)

5. DNS A Record (compute_stack.py)

route53.ARecord(
    self,
    "LinkAliasRecord",
    zone=hosted_zone,
    record_name="link",
    target=route53.RecordTarget.from_alias(
        targets.LoadBalancerTarget(self.alb),
    ),
)

6. CloudWatch Alarm (ops_stack.py)

Pass link_target_group from ComputeStack and add alarm:

link_alb_unhealthy_alarm = cloudwatch.Alarm(
    self,
    "LinkAlbUnhealthyAlarm",
    alarm_name="v1-orcha-link-unhealthy",
    alarm_description="Link target group has no healthy targets",
    metric=cloudwatch.Metric(
        namespace="AWS/ApplicationELB",
        metric_name="HealthyHostCount",
        dimensions_map={
            "TargetGroup": link_target_group.target_group_full_name,
            "LoadBalancer": alb.load_balancer_full_name,
        },
        statistic="Minimum",
        period=Duration.seconds(60),
    ),
    threshold=1,
    comparison_operator=cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
    evaluation_periods=2,
    treat_missing_data=cloudwatch.TreatMissingData.BREACHING,
)
link_alb_unhealthy_alarm.add_alarm_action(cw_actions.SnsAction(self.alert_topic))

7. Docker Compose (deploy/docker-compose.yml)

Expose port 9999:

ports:
  - "8888:8888"
  - "7777:7777"
  - "9999:9999"
  - "9878:9878"

Manual Steps

Before CDK Deploy

  1. Create ACM certificate:

    cd infra
    ./scripts/create-cross-account-cert.sh \
        --name link \
        --primary-domain link.prod.getorcha.com \
        --san link.getorcha.com
    
  2. Add short link CNAME in management account:

    AWS_PROFILE=orcha aws route53 change-resource-record-sets \
      --hosted-zone-id Z02414383CQNYTPGX2EIK \
      --change-batch '{
        "Changes": [{
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "link.getorcha.com",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [{"Value": "link.prod.getorcha.com"}]
          }
        }]
      }'
    
  3. Update CDK code with certificate ARN from step 1

After CDK Deploy

  1. Verify DNS resolution: dig link.prod.getorcha.com
  2. Verify health check: curl https://link.getorcha.com/health
  3. Verify OAuth discovery: curl https://link.getorcha.com/.well-known/oauth-authorization-server

Files Modified

File Changes
infra/stacks/foundation_stack.py Security group rules for port 9999
infra/stacks/compute_stack.py Target group, certificate, listener rule, DNS record
infra/stacks/ops_stack.py CloudWatch alarm for Link unhealthy hosts
infra/app.py Pass link_target_group to OpsStack
deploy/docker-compose.yml Expose port 9999

Outputs Added

Output Value
LinkTargetGroupArn Target group ARN
LinkCertificateArn Certificate ARN
LinkUrl https://link.{env}.getorcha.com