Link Service Infrastructure Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Expose the Link service (port 9999) to the internet at link.getorcha.com.

Architecture: Add ALB routing, target group, and DNS for Link service following the same pattern as ERP (port 8888) and Admin (port 7777). Link handles OAuth internally, so no ALB Cognito action needed.

Tech Stack: AWS CDK (Python), Route53, ACM, ALB, CloudWatch


Task 0: Create ACM Certificate (Manual)

Purpose: Create the SSL certificate before CDK changes (cross-account DNS validation requires manual steps).

Step 1: Run the certificate creation script

cd /home/volrath/code/orcha/orcha/infra
./scripts/create-cross-account-cert.sh \
    --name link \
    --primary-domain link.prod.getorcha.com \
    --san link.getorcha.com

Expected: Script creates certificate, adds validation CNAMEs to both zones, waits for ISSUED status, outputs ARN.

Step 2: Save the certificate ARN

Note the ARN from script output (format: arn:aws:acm:eu-central-1:700558745280:certificate/<uuid>).

Step 3: 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"}]
      }
    }]
  }'

Expected: ChangeInfo with PENDING status.


Task 1: Add Security Group Rules

Files:

Step 1: Add Link port security group rules

After the Admin port rules (around line 209), add:

        # 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",
        )

Step 2: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 3: Commit

git add infra/stacks/foundation_stack.py
git commit -m "infra: Add security group rules for Link service (port 9999)"

Files:

Step 1: Add class attribute for link_target_group

At line ~98, add to class attributes:

    link_target_group: elbv2.IApplicationTargetGroup

Step 2: Add Link target group after Admin target group

After the Admin target group and ASG attachment (around line 616), add:

        # =====================================================================
        # Link Target Group
        # =====================================================================

        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),
        )

        # Register ASG with link target group
        self.asg.attach_to_application_target_group(self.link_target_group)

Step 3: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 4: Commit

git add infra/stacks/compute_stack.py
git commit -m "infra: Add Link target group (port 9999)"

Files:

Step 1: Add class attribute for link_certificate

At line ~98, add to class attributes:

    link_certificate: acm.ICertificate

Step 2: Add Link certificate after Admin certificate

After the Admin certificate block (around line 193), add:

        # =====================================================================
        # Link ACM Certificate
        # =====================================================================
        #
        # Same pattern as ERP/Admin - manually created for cross-account
        # DNS validation. Covers link.prod.getorcha.com + link.getorcha.com
        #
        if env_name == "prod":
            self.link_certificate = acm.Certificate.from_certificate_arn(
                self,
                "LinkCertificate",
                certificate_arn="<REPLACE_WITH_ARN_FROM_TASK_0>",
            )
        else:
            self.link_certificate = acm.Certificate(
                self,
                "LinkCertificate",
                domain_name=f"link.{env_name}.getorcha.com",
                validation=acm.CertificateValidation.from_dns(hosted_zone),
            )

Step 3: Replace placeholder with actual ARN

Replace <REPLACE_WITH_ARN_FROM_TASK_0> with the ARN from Task 0.

Step 4: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 5: Commit

git add infra/stacks/compute_stack.py
git commit -m "infra: Add Link SSL certificate"

Files:

Step 1: Add Link certificate to HTTPS listener

After https_listener.add_certificates("AdminCerts", ...) (around line 632), add:

        # Add link certificate for SNI
        https_listener.add_certificates("LinkCerts", [self.link_certificate])

Step 2: Add Link routing rule

After the Admin routing action (around line 653), add:

        # Link routing (handles OAuth internally, no ALB auth)
        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]),
        )

Step 3: Add Link DNS A record

After the Admin A record (around line 686), add:

        # Link A Record
        route53.ARecord(
            self,
            "LinkAliasRecord",
            zone=hosted_zone,
            record_name="link",  # Creates link.{env}.getorcha.com
            target=route53.RecordTarget.from_alias(
                targets.LoadBalancerTarget(self.alb),
            ),
        )

Step 4: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 5: Commit

git add infra/stacks/compute_stack.py
git commit -m "infra: Add Link listener rule and DNS record"

Files:

Step 1: Add Link outputs

After the Admin outputs (around line 760), add:

        CfnOutput(
            self,
            "LinkTargetGroupArn",
            value=self.link_target_group.target_group_arn,
            description="Link Target Group ARN",
            export_name=f"{env_name}-link-tg-arn",
        )

        CfnOutput(
            self,
            "LinkCertificateArn",
            value=self.link_certificate.certificate_arn,
            description="Link ACM Certificate ARN",
            export_name=f"{env_name}-link-cert-arn",
        )

        CfnOutput(
            self,
            "LinkUrl",
            value=f"https://link.{env_name}.getorcha.com",
            description="Link service URL",
        )

Step 2: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 3: Commit

git add infra/stacks/compute_stack.py
git commit -m "infra: Add Link CDK outputs"

Files:

Step 1: Add link_target_group parameter to OpsStack

In ops_stack.py, add to __init__ parameters (around line 74):

        link_target_group: elbv2.IApplicationTargetGroup,

Step 2: Add Link unhealthy alarm

After the Admin unhealthy alarm (around line 193), add:

        # 1c. Link ALB Unhealthy Hosts
        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))

Step 3: Pass link_target_group from app.py

In app.py, add to the OpsStack instantiation (after line 129):

        link_target_group=compute.link_target_group,

Step 4: Verify CDK synth succeeds

cd /home/volrath/code/orcha/orcha/infra
cdk synth --context env_name=prod -q

Expected: No errors.

Step 5: Commit

git add infra/stacks/ops_stack.py infra/app.py
git commit -m "infra: Add Link CloudWatch unhealthy hosts alarm"

Task 7: Expose Port 9999 in Docker Compose

Files:

Step 1: Add port 9999 mapping

Change the ports section from:

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

To:

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

Step 2: Commit

git add deploy/docker-compose.yml
git commit -m "deploy: Expose Link service port 9999"

Task 8: CDK Diff and Deploy

Step 1: Run CDK diff

cd /home/volrath/code/orcha/orcha/infra
cdk diff --context env_name=prod

Expected: Shows changes to FoundationStack (security groups), ComputeStack (target group, listener, DNS), OpsStack (alarm).

Step 2: Deploy (if diff looks correct)

cdk deploy --all --context env_name=prod

Expected: All stacks deploy successfully.


Task 9: Verify Deployment

Step 1: Verify DNS resolution

dig link.prod.getorcha.com +short
dig link.getorcha.com +short

Expected: Both return the ALB DNS name (or CNAME chain to it).

Step 2: Verify health check

curl -I https://link.getorcha.com/health

Expected: HTTP 200.

Step 3: Verify OAuth discovery

curl https://link.getorcha.com/.well-known/oauth-authorization-server

Expected: JSON with OAuth server metadata.

Step 4: Commit any final changes and tag

git log --oneline -10  # Review commits