From 23904be2f2606b4ef9e42ac353705ca15db8f3b9 Mon Sep 17 00:00:00 2001 From: Connor OMalley Date: Wed, 19 Aug 2026 09:55:40 +0100 Subject: [PATCH 1/3] Rubbish version of http mirroring --- .../cdk/lib/HttpTrafficMirroring.ts | 233 ++++++++++++++++++ dotcom-rendering/cdk/lib/renderingStack.ts | 31 +++ 2 files changed, 264 insertions(+) create mode 100644 dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts diff --git a/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts new file mode 100644 index 00000000000..7a2f0c9ea90 --- /dev/null +++ b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts @@ -0,0 +1,233 @@ +import { + Duration, + aws_ec2 as ec2, + aws_events as events, + aws_events_targets as targets, + aws_iam as iam, + aws_lambda as lambda, +} from 'aws-cdk-lib'; +import type { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; +import { Instance, ISubnet, IVpc } from 'aws-cdk-lib/aws-ec2'; +import type { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { Construct } from 'constructs'; + +export interface HttpTrafficMirroringProps { + readonly vpc: IVpc; + readonly privateSubnets: ISubnet[]; + readonly availabilityZone?: string; + readonly trafficSource: AutoScalingGroup; + readonly trafficTarget: ApplicationLoadBalancer; +} + +export class HttpTrafficMirroring extends Construct { + constructor( + scope: Construct, + id: string, + props: HttpTrafficMirroringProps, + ) { + super(scope, id); + + // if (props.trafficTarget.vpc === undefined) { + // throw new Error("VPC not defined in mirroring target application load balancer."); + // } + // const vpc = props.trafficTarget.vpc; + + this.node.addDependency(props.trafficSource); + this.node.addDependency(props.trafficTarget); + + const handlerInstance = this.createEc2Handler( + props.vpc, + props.privateSubnets, + props.trafficTarget, + props.availabilityZone, + ); + + // Ensure the ASG instances can send VXLAN (UDP 4789) to the handler + handlerInstance.connections.allowFrom( + props.trafficSource, + ec2.Port.udp(4789), + 'Allow VXLAN mirrored traffic from ASG instances', + ); + + const mirrorTarget: ec2.CfnTrafficMirrorTarget = + new ec2.CfnTrafficMirrorTarget(this, 'Target', { + networkInterfaceId: this.getENIId(handlerInstance), + }); + + const mirrorFilter: ec2.CfnTrafficMirrorFilter = + new ec2.CfnTrafficMirrorFilter(this, 'Filter', { + description: `Traffic mirror filter created by ${id}`, + }); + + new ec2.CfnTrafficMirrorFilterRule(this, 'AllowAllInbound', { + trafficMirrorFilterId: mirrorFilter.attrId, + ruleAction: 'accept', + ruleNumber: 100, + trafficDirection: 'ingress', + destinationCidrBlock: '0.0.0.0/0', // TODO: Narrow the CIDR block scopes. + sourceCidrBlock: '0.0.0.0/0', + }); + + // Lambda function to attach Mirror Session on ASG instance launch + const attacherLambda = new lambda.Function( + this, + 'SessionAttacherLambda', + { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + timeout: Duration.seconds(30), + code: lambda.Code.fromInline(` + const { EC2Client, DescribeInstancesCommand, CreateTrafficMirrorSessionCommand } = require('@aws-sdk/client-ec2'); + const ec2 = new EC2Client(); + + exports.handler = async (event) => { + const instanceId = event.detail.EC2InstanceId; + const targetId = process.env.TARGET_ID; + const filterId = process.env.FILTER_ID; + + console.log(\`Processing launch event for instance: \${instanceId}\`); + + // Fetch instance details to get primary ENI ID + const describeRes = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [instanceId] })); + const instance = describeRes.Reservations?.[0]?.Instances?.[0]; + const primaryEniId = instance?.NetworkInterfaces?.[0]?.NetworkInterfaceId; + + if (!primaryEniId) { + throw new Error(\`Unable to find primary ENI for instance: \${instanceId}\`); + } + + // Attach Traffic Mirror Session (ASG instance ENI -> EC2 Worker Target ENI) + const sessionRes = await ec2.send(new CreateTrafficMirrorSessionCommand({ + NetworkInterfaceId: primaryEniId, + TrafficMirrorTargetId: targetId, + TrafficMirrorFilterId: filterId, + SessionNumber: 1, + Description: \`Auto-attached traffic mirror for instance \${instanceId}\`, + })); + + console.log(\`Successfully created session: \${sessionRes.TrafficMirrorSession.TrafficMirrorSessionId}\`); + }; + `), + environment: { + TARGET_ID: mirrorTarget.attrId, + FILTER_ID: mirrorFilter.attrId, + }, + }, + ); + + // Grant Lambda EC2 access permissions + attacherLambda.addToRolePolicy( + new iam.PolicyStatement({ + actions: [ + 'ec2:DescribeInstances', + 'ec2:CreateTrafficMirrorSession', + ], + resources: ['*'], + }), + ); + + // 5. EventBridge Rule to trigger Lambda on ASG Instance Launch + const launchRule = new events.Rule(this, 'AsgInstanceLaunchRule', { + eventPattern: { + source: ['aws.autoscaling'], + detailType: ['EC2 Instance Launch Successful'], + detail: { + AutoScalingGroupName: [ + props.trafficSource.autoScalingGroupName, + ], + }, + }, + }); + + launchRule.addTarget(new targets.LambdaFunction(attacherLambda)); + } + + /** + * Spawns an EC2 instance that strips VXLAN headers and replays HTTP payloads to the Target ALB. + */ + private createEc2Handler( + vpc: ec2.IVpc, + subnets: ISubnet[], + targetAlb: ApplicationLoadBalancer, + availabilityZone?: string, + ): ec2.Instance { + const worker = new ec2.Instance(this, 'VXLANHandler', { + vpc: vpc, + vpcSubnets: { subnets: subnets }, + // availabilityZone: availabilityZone, + instanceType: ec2.InstanceType.of( + ec2.InstanceClass.T3, + ec2.InstanceSize.MEDIUM, + ), + machineImage: ec2.MachineImage.latestAmazonLinux2023(), + }); + + // Write a UserData script to unwrap VXLAN on port 4789 and forward HTTP requests to the ALB + worker.userData.addCommands( + 'yum update -y', + 'yum install -y python3-pip gcc python3-devel', + 'pip3 install scapy requests', + + // Minimal Python daemon to capture UDP 4789 (VXLAN), strip frame headers, and fire non-blocking HTTP requests + "cat << 'EOF' > /opt/vxlan_unwrapper.py", + 'import socket', + 'import threading', + 'import requests', + 'from scapy.all import Ether, IP, TCP, Raw', + '', + `TARGET_ALB_URL = "http://${targetAlb.loadBalancerDnsName}"`, + '', + 'def forward_request(method, path, headers, payload):', + ' try:', + ' # Send HTTP request to ALB, fire-and-forget (timeout=0.1 / ignore response)', + ' requests.request(method, TARGET_ALB_URL + path, headers=headers, data=payload, timeout=0.1)', + ' except Exception:', + ' pass', + '', + 'def listen_vxlan():', + ' sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)', + ' sock.bind(("0.0.0.0", 4789))', + ' while True:', + ' data, _ = sock.recvfrom(65535)', + ' # Skip UDP header (8 bytes) & VXLAN header (8 bytes)', + ' inner_packet = data[16:]', + ' try:', + ' pkt = Ether(inner_packet)', + ' if pkt.haslayer(Raw) and pkt.haslayer(TCP):', + ' payload = pkt[Raw].load.decode("utf-8", errors="ignore")', + ' if payload.startswith(("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS")):', + ' lines = payload.split("\\r\\n")', + ' parts = lines[0].split(" ")', + ' if len(parts) >= 2:', + ' method, path = parts[0], parts[1]', + ' threading.Thread(target=forward_request, args=(method, path, {}, None)).start()', + ' except Exception:', + ' pass', + '', + 'if __name__ == "__main__":', + ' listen_vxlan()', + 'EOF', + + // Run as a system service + "cat << 'EOF' > /etc/systemd/system/vxlan-worker.service", + '[Unit]', + 'Description=VXLAN Unwrapper and HTTP Forwarder', + 'After=network.target', + '[Service]', + 'ExecStart=/usr/bin/python3 /opt/vxlan_unwrapper.py', + 'Restart=always', + '[Install]', + 'WantedBy=multi-user.target', + 'EOF', + + 'systemctl daemon-reload', + 'systemctl enable --now vxlan-worker', + ); + + return worker; + } + + getENIId(instance: Instance): string { + return ''; + } +} diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index 05b116e337b..45822e9bdd1 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -3,11 +3,13 @@ import { AccessScope } from '@guardian/cdk/lib/constants'; import type { NoMonitoring } from '@guardian/cdk/lib/constructs/cloudwatch'; import type { GuStackProps } from '@guardian/cdk/lib/constructs/core'; import { + AppIdentity, GuStack as CDKStack, GuDistributionBucketParameter, } from '@guardian/cdk/lib/constructs/core'; import { GuCname } from '@guardian/cdk/lib/constructs/dns/dns-records'; import { GuAllowPolicy } from '@guardian/cdk/lib/constructs/iam'; +import { GuVpc, SubnetType } from '@guardian/cdk/lib/constructs/ec2'; import { GuLoadBalancedAppExperimental } from '@guardian/cdk/lib/experimental/patterns/gu-load-balanced-app'; import type { GuAsgCapacity } from '@guardian/cdk/lib/types'; import { aws_cloudwatch, type App as CDKApp, Duration } from 'aws-cdk-lib'; @@ -22,6 +24,8 @@ import { ClusterSettings } from 'aws-cdk-lib/aws-ecs/mixins'; import { Subscription, SubscriptionProtocol, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { getUserData } from './userData'; +import { HttpTrafficMirroring } from './HttpTrafficMirroring'; +import { log } from 'console'; export interface RenderingCDKStackProps extends Omit { guApp: `${'article' | 'facia' | 'interactive' | 'tag-page'}-rendering`; @@ -221,6 +225,16 @@ export class RenderingCDKStack extends CDKStack { } satisfies Alarms) : ({ noMonitoring: true } satisfies NoMonitoring); + // Same as defaults in GuLoadBalancedAppExperimental, but we need reference to configure traffic mirroring + const vpc = GuVpc.fromIdParameter( + this, + AppIdentity.addAppToStringEnd({ app: guApp }, 'VPC'), + ); + const privateSubnets = GuVpc.subnetsFromParameter(this, { + type: SubnetType.PRIVATE, + app: guApp, + }); + const app = new GuLoadBalancedAppExperimental(this, { app: guApp, access: { @@ -272,6 +286,9 @@ export class RenderingCDKStack extends CDKStack { }), }, + vpc, + privateSubnets, + // Provision ECS resources only when `imageIdentifier` has been provided ...(imageIdentifier == null ? {} @@ -333,6 +350,20 @@ export class RenderingCDKStack extends CDKStack { }, ], }); + + log(vpc); + log(privateSubnets); + const availabilityZones = this.availabilityZones; + log(availabilityZones); + if (!!app.autoScalingGroup) { + new HttpTrafficMirroring(this, 'Ec2ToEcsTrafficMirror', { + vpc: vpc, + privateSubnets: privateSubnets, + availabilityZone: availabilityZones[0], + trafficSource: app.autoScalingGroup, + trafficTarget: app.loadBalancer, + }); + } } } From b4e6120c82356a6a0a6a01c05d45797854b505d4 Mon Sep 17 00:00:00 2001 From: Connor OMalley Date: Mon, 24 Aug 2026 10:32:51 +0100 Subject: [PATCH 2/3] Slightly less rubbish version of http traffic mirroring Lint fix --- .../cdk/lib/HttpTrafficMirroring.ts | 313 ++++--- .../__snapshots__/renderingStack.test.ts.snap | 821 +++++++++++++++++- dotcom-rendering/cdk/lib/renderingStack.ts | 15 +- 3 files changed, 956 insertions(+), 193 deletions(-) diff --git a/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts index 7a2f0c9ea90..d1c754f3dd9 100644 --- a/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts +++ b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts @@ -1,19 +1,38 @@ -import { - Duration, - aws_ec2 as ec2, - aws_events as events, - aws_events_targets as targets, - aws_iam as iam, - aws_lambda as lambda, -} from 'aws-cdk-lib'; +import type { GuStack } from '@guardian/cdk/lib/constructs/core'; +import { GuHttpsEgressSecurityGroup } from '@guardian/cdk/lib/constructs/ec2/security-groups/base'; +import { Duration, type aws_ec2 as ec2 } from 'aws-cdk-lib'; import type { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; -import { Instance, ISubnet, IVpc } from 'aws-cdk-lib/aws-ec2'; -import type { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import type { ISubnet, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { + CfnTrafficMirrorFilter, + CfnTrafficMirrorFilterRule, + CfnTrafficMirrorTarget, +} from 'aws-cdk-lib/aws-ec2'; +import { + Cluster, + ContainerImage, + CpuArchitecture, + Protocol as ECSProtocol, + FargateService, + FargateTaskDefinition, + OperatingSystemFamily, + PropagatedTagSource, +} from 'aws-cdk-lib/aws-ecs'; +import { + type ApplicationLoadBalancer, + Protocol as ELBProtocol, + NetworkLoadBalancer, +} from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import * as events from 'aws-cdk-lib/aws-events'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; export interface HttpTrafficMirroringProps { readonly vpc: IVpc; readonly privateSubnets: ISubnet[]; + readonly app: GuStack; readonly availabilityZone?: string; readonly trafficSource: AutoScalingGroup; readonly trafficTarget: ApplicationLoadBalancer; @@ -35,31 +54,27 @@ export class HttpTrafficMirroring extends Construct { this.node.addDependency(props.trafficSource); this.node.addDependency(props.trafficTarget); - const handlerInstance = this.createEc2Handler( + const handlerNlb = this.createHandler( props.vpc, props.privateSubnets, + props.app, props.trafficTarget, - props.availabilityZone, ); - // Ensure the ASG instances can send VXLAN (UDP 4789) to the handler - handlerInstance.connections.allowFrom( - props.trafficSource, - ec2.Port.udp(4789), - 'Allow VXLAN mirrored traffic from ASG instances', + const mirrorTarget: CfnTrafficMirrorTarget = new CfnTrafficMirrorTarget( + this, + 'Target', + { + networkLoadBalancerArn: handlerNlb.loadBalancerArn, + }, ); - const mirrorTarget: ec2.CfnTrafficMirrorTarget = - new ec2.CfnTrafficMirrorTarget(this, 'Target', { - networkInterfaceId: this.getENIId(handlerInstance), - }); - const mirrorFilter: ec2.CfnTrafficMirrorFilter = - new ec2.CfnTrafficMirrorFilter(this, 'Filter', { + new CfnTrafficMirrorFilter(this, 'Filter', { description: `Traffic mirror filter created by ${id}`, }); - new ec2.CfnTrafficMirrorFilterRule(this, 'AllowAllInbound', { + new CfnTrafficMirrorFilterRule(this, 'AllowAllInbound', { trafficMirrorFilterId: mirrorFilter.attrId, ruleAction: 'accept', ruleNumber: 100, @@ -69,45 +84,46 @@ export class HttpTrafficMirroring extends Construct { }); // Lambda function to attach Mirror Session on ASG instance launch + // TODO: Will this always attach and run before the very first asg instances are created? const attacherLambda = new lambda.Function( this, 'SessionAttacherLambda', { - runtime: lambda.Runtime.NODEJS_20_X, + runtime: lambda.Runtime.NODEJS_24_X, handler: 'index.handler', timeout: Duration.seconds(30), code: lambda.Code.fromInline(` - const { EC2Client, DescribeInstancesCommand, CreateTrafficMirrorSessionCommand } = require('@aws-sdk/client-ec2'); - const ec2 = new EC2Client(); - - exports.handler = async (event) => { - const instanceId = event.detail.EC2InstanceId; - const targetId = process.env.TARGET_ID; - const filterId = process.env.FILTER_ID; - - console.log(\`Processing launch event for instance: \${instanceId}\`); - - // Fetch instance details to get primary ENI ID - const describeRes = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [instanceId] })); - const instance = describeRes.Reservations?.[0]?.Instances?.[0]; - const primaryEniId = instance?.NetworkInterfaces?.[0]?.NetworkInterfaceId; - - if (!primaryEniId) { - throw new Error(\`Unable to find primary ENI for instance: \${instanceId}\`); - } - - // Attach Traffic Mirror Session (ASG instance ENI -> EC2 Worker Target ENI) - const sessionRes = await ec2.send(new CreateTrafficMirrorSessionCommand({ - NetworkInterfaceId: primaryEniId, - TrafficMirrorTargetId: targetId, - TrafficMirrorFilterId: filterId, - SessionNumber: 1, - Description: \`Auto-attached traffic mirror for instance \${instanceId}\`, - })); - - console.log(\`Successfully created session: \${sessionRes.TrafficMirrorSession.TrafficMirrorSessionId}\`); - }; - `), + const { EC2Client, DescribeInstancesCommand, CreateTrafficMirrorSessionCommand } = require('@aws-sdk/client-ec2'); + const ec2 = new EC2Client(); + + exports.handler = async (event) => { + const instanceId = event.detail.EC2InstanceId; + const targetId = process.env.TARGET_ID; + const filterId = process.env.FILTER_ID; + + console.log(\`Processing launch event for instance: \${instanceId}\`); + + // Fetch instance details to get primary ENI ID + const describeRes = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [instanceId] })); + const instance = describeRes.Reservations?.[0]?.Instances?.[0]; + const primaryEniId = instance?.NetworkInterfaces?.[0]?.NetworkInterfaceId; + + if (!primaryEniId) { + throw new Error(\`Unable to find primary ENI for instance: \${instanceId}\`); + } + + // Attach Traffic Mirror Session (ASG instance ENI -> EC2 Worker Target ENI) + const sessionRes = await ec2.send(new CreateTrafficMirrorSessionCommand({ + NetworkInterfaceId: primaryEniId, + TrafficMirrorTargetId: targetId, + TrafficMirrorFilterId: filterId, + SessionNumber: 1, + Description: \`Auto-attached traffic mirror for instance \${instanceId}\`, + })); + + console.log(\`Successfully created session: \${sessionRes.TrafficMirrorSession.TrafficMirrorSessionId}\`); + }; + `), environment: { TARGET_ID: mirrorTarget.attrId, FILTER_ID: mirrorFilter.attrId, @@ -126,7 +142,7 @@ export class HttpTrafficMirroring extends Construct { }), ); - // 5. EventBridge Rule to trigger Lambda on ASG Instance Launch + // EventBridge Rule to trigger Lambda on ASG Instance Launch const launchRule = new events.Rule(this, 'AsgInstanceLaunchRule', { eventPattern: { source: ['aws.autoscaling'], @@ -139,95 +155,116 @@ export class HttpTrafficMirroring extends Construct { }, }); - launchRule.addTarget(new targets.LambdaFunction(attacherLambda)); + launchRule.addTarget(new LambdaFunction(attacherLambda)); } - /** - * Spawns an EC2 instance that strips VXLAN headers and replays HTTP payloads to the Target ALB. - */ - private createEc2Handler( + private createHandler( vpc: ec2.IVpc, subnets: ISubnet[], - targetAlb: ApplicationLoadBalancer, - availabilityZone?: string, - ): ec2.Instance { - const worker = new ec2.Instance(this, 'VXLANHandler', { - vpc: vpc, - vpcSubnets: { subnets: subnets }, - // availabilityZone: availabilityZone, - instanceType: ec2.InstanceType.of( - ec2.InstanceClass.T3, - ec2.InstanceSize.MEDIUM, - ), - machineImage: ec2.MachineImage.latestAmazonLinux2023(), + stack: GuStack, + target: ApplicationLoadBalancer, + ): NetworkLoadBalancer { + const cluster = Cluster.fromClusterAttributes( + this, + 'MirroringHandlerEcsCluster', + { + clusterName: 'MirroringHandlerEcsCluster', + vpc, + }, + ); + + const taskDefinition = new FargateTaskDefinition( + this, + 'MirroringHandlerEcsTaskDefinition', + { + memoryLimitMiB: 2048, + cpu: 1024, + runtimePlatform: { + cpuArchitecture: CpuArchitecture.ARM64, + operatingSystemFamily: OperatingSystemFamily.LINUX, + }, + }, + ); + + // TCP for health check + // We have to add this first as the network load balancer will send health check traffic to the default container. + // If we don't add this first then we fail to add the ECS service to the target group as there is no tcp endpoint. + // Can not do health check over UDP. + // + // Nginx by default serves a simple welcome page on port 80, which can pass the health check. + taskDefinition.addContainer('MirroringHandlerHealthCheckContainer', { + image: ContainerImage.fromRegistry('nginx'), + portMappings: [ + { containerPort: 80, protocol: ECSProtocol.TCP, hostPort: 80 }, + ], + // TODO: logging: fireLensLogDriver, + readonlyRootFilesystem: true, }); - // Write a UserData script to unwrap VXLAN on port 4789 and forward HTTP requests to the ALB - worker.userData.addCommands( - 'yum update -y', - 'yum install -y python3-pip gcc python3-devel', - 'pip3 install scapy requests', - - // Minimal Python daemon to capture UDP 4789 (VXLAN), strip frame headers, and fire non-blocking HTTP requests - "cat << 'EOF' > /opt/vxlan_unwrapper.py", - 'import socket', - 'import threading', - 'import requests', - 'from scapy.all import Ether, IP, TCP, Raw', - '', - `TARGET_ALB_URL = "http://${targetAlb.loadBalancerDnsName}"`, - '', - 'def forward_request(method, path, headers, payload):', - ' try:', - ' # Send HTTP request to ALB, fire-and-forget (timeout=0.1 / ignore response)', - ' requests.request(method, TARGET_ALB_URL + path, headers=headers, data=payload, timeout=0.1)', - ' except Exception:', - ' pass', - '', - 'def listen_vxlan():', - ' sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)', - ' sock.bind(("0.0.0.0", 4789))', - ' while True:', - ' data, _ = sock.recvfrom(65535)', - ' # Skip UDP header (8 bytes) & VXLAN header (8 bytes)', - ' inner_packet = data[16:]', - ' try:', - ' pkt = Ether(inner_packet)', - ' if pkt.haslayer(Raw) and pkt.haslayer(TCP):', - ' payload = pkt[Raw].load.decode("utf-8", errors="ignore")', - ' if payload.startswith(("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS")):', - ' lines = payload.split("\\r\\n")', - ' parts = lines[0].split(" ")', - ' if len(parts) >= 2:', - ' method, path = parts[0], parts[1]', - ' threading.Thread(target=forward_request, args=(method, path, {}, None)).start()', - ' except Exception:', - ' pass', - '', - 'if __name__ == "__main__":', - ' listen_vxlan()', - 'EOF', - - // Run as a system service - "cat << 'EOF' > /etc/systemd/system/vxlan-worker.service", - '[Unit]', - 'Description=VXLAN Unwrapper and HTTP Forwarder', - 'After=network.target', - '[Service]', - 'ExecStart=/usr/bin/python3 /opt/vxlan_unwrapper.py', - 'Restart=always', - '[Install]', - 'WantedBy=multi-user.target', - 'EOF', - - 'systemctl daemon-reload', - 'systemctl enable --now vxlan-worker', + taskDefinition.addContainer('MirroringHandlerContainer', { + image: ContainerImage.fromRegistry('jauderho/goreplay'), + portMappings: [ + { + containerPort: 4789, + protocol: ECSProtocol.UDP, + hostPort: 4789, + }, + ], + command: [ + '--input-raw', + ':80', + '--input-raw-engine', + 'vxlan', + '--output-http', + `http://${target.loadBalancerDnsName}`, + ], + // TODO: logging: fireLensLogDriver, + readonlyRootFilesystem: true, + }); + + const fargateService = new FargateService( + this, + 'MirroringHandlerFargateService', + { + cluster, + taskDefinition, + vpcSubnets: { subnets }, + // Important for service deployments; with the AWS defaults the service can be scaled down when deploying + minHealthyPercent: 100, + // Also important for service deployments; with the AWS defaults we don't get a fast failure when deploying a 'bad' build + circuitBreaker: { enable: true, rollback: true }, + propagateTags: PropagatedTagSource.SERVICE, + // By default, AWS will create a new security group which allows all outbound traffic + // We don't want this so explicitly allow outbound HTTPS only + // This is what we do for the current GuEc2App pattern: + // https://github.com/guardian/cdk/blob/3b5688637024642055ed0bf576f668e56e40830d/src/constructs/autoscaling/asg.ts#L143-L145 + securityGroups: [ + GuHttpsEgressSecurityGroup.forVpc(stack, { + app: `${stack.app}-ecs`, + vpc, + }), + ], + }, ); - return worker; - } + const nlb = new NetworkLoadBalancer(this, 'MirroringHandlerNLB', { + vpc, + internetFacing: false, // Don't think this is needed given the subnets, but i want to be safe + vpcSubnets: { subnets }, + }); + + const listener = nlb.addListener('MirroringHandlerListener', { + port: 4789, + protocol: ELBProtocol.UDP, + }); + + const targetGroup = listener.addTargets('ECSHandlers', { + port: 4789, + protocol: ELBProtocol.UDP, + }); + + targetGroup.addTarget(fargateService); - getENIId(instance: Instance): string { - return ''; + return nlb; } } diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index 3ca44abe4b8..a52083cc69e 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -5,11 +5,11 @@ exports[`The RenderingCDKStack matches the snapshot 1`] = ` "Metadata": { "gu:cdk:constructs": [ "GuDistributionBucketParameter", + "GuVpcParameter", + "GuSubnetListParameter", "GuAllowPolicy", "GuAllowPolicy", "GuAllowPolicy", - "GuVpcParameter", - "GuSubnetListParameter", "GuSubnetListParameter", "GuLoadBalancedAppExperimental", "GuInstanceRole", @@ -1620,11 +1620,11 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "Metadata": { "gu:cdk:constructs": [ "GuDistributionBucketParameter", + "GuVpcParameter", + "GuSubnetListParameter", "GuAllowPolicy", "GuAllowPolicy", "GuAllowPolicy", - "GuVpcParameter", - "GuSubnetListParameter", "GuSubnetListParameter", "GuLoadBalancedAppExperimental", "GuInstanceRole", @@ -1646,6 +1646,7 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "GuAccessLoggingBucketParameter", "GuHttpsApplicationListener", "GuSecurityGroup", + "GuHttpsEgressSecurityGroup", "GuCname", ], "gu:cdk:version": "TEST", @@ -1956,17 +1957,312 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, "Type": "AWS::IAM::Policy", }, - "EcsService81FC6EF6": { + "Ec2ToEcsTrafficMirrorAllowAllInbound46A802A0": { "DependsOn": [ - "EcsTaskDefinitionTaskRoleB7B6D8DD", - "ListenerTagpagerenderingDeterministicRouteToEc2Rule1C5C95A4", - "ListenerTagpagerenderingDeterministicRouteToEcsRuleAB05C756", - "ListenerTagpagerendering92E57078", + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", ], "Properties": { - "Cluster": { - "Ref": "tagpagerenderingEcsClusterE7696595", + "DestinationCidrBlock": "0.0.0.0/0", + "RuleAction": "accept", + "RuleNumber": 100, + "SourceCidrBlock": "0.0.0.0/0", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TrafficDirection": "ingress", + "TrafficMirrorFilterId": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorFilter8854C552", + "Id", + ], + }, + }, + "Type": "AWS::EC2::TrafficMirrorFilterRule", + }, + "Ec2ToEcsTrafficMirrorAsgInstanceLaunchRule32C9EB6F": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "EventPattern": { + "detail": { + "AutoScalingGroupName": [ + { + "Ref": "AutoScalingGroupTagpagerenderingASG7F7E0748", + }, + ], + }, + "detail-type": [ + "EC2 Instance Launch Successful", + ], + "source": [ + "aws.autoscaling", + ], + }, + "State": "ENABLED", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaCA9F6846", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "Ec2ToEcsTrafficMirrorAsgInstanceLaunchRuleAllowEventRuleTagPageRenderingCODEEc2ToEcsTrafficMirrorSessionAttacherLambda0B880B1929CEF922": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaCA9F6846", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorAsgInstanceLaunchRule32C9EB6F", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "Ec2ToEcsTrafficMirrorFilter8854C552": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "Description": "Traffic mirror filter created by Ec2ToEcsTrafficMirror", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::EC2::TrafficMirrorFilter", + }, + "Ec2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinitionCCF3FF96": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "nginx", + "Name": "MirroringHandlerHealthCheckContainer", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 80, + "Protocol": "tcp", + }, + ], + "ReadonlyRootFilesystem": true, + }, + { + "Command": [ + "--input-raw", + ":80", + "--input-raw-engine", + "vxlan", + "--output-http", + { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "DNSName", + ], + }, + ], + ], + }, + ], + "Essential": true, + "Image": "jauderho/goreplay", + "Name": "MirroringHandlerContainer", + "PortMappings": [ + { + "ContainerPort": 4789, + "HostPort": 4789, + "Protocol": "udp", + }, + ], + "ReadonlyRootFilesystem": true, + }, + ], + "Cpu": "1024", + "Family": "TagPageRenderingCODEEc2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinition7E6AD87A", + "Memory": "2048", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE", + ], + "RuntimePlatform": { + "CpuArchitecture": "ARM64", + "OperatingSystemFamily": "LINUX", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinitionTaskRoleD1815E9F", + "Arn", + ], + }, + }, + "Type": "AWS::ECS::TaskDefinition", + }, + "Ec2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinitionTaskRoleD1815E9F": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "Ec2ToEcsTrafficMirrorMirroringHandlerFargateService6F2CB37C": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "Ec2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinitionTaskRoleD1815E9F", + "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerECSHandlersGroupCB72232A", + "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerDC91CFB0", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "Cluster": "MirroringHandlerEcsCluster", "DeploymentConfiguration": { "Alarms": { "AlarmNames": [], @@ -1988,31 +2284,20 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "LaunchType": "FARGATE", "LoadBalancers": [ { - "ContainerName": "tag-page-rendering", - "ContainerPort": 9000, + "ContainerName": "MirroringHandlerHealthCheckContainer", + "ContainerPort": 80, "TargetGroupArn": { - "Ref": "TagpagerenderingEcsTargetGroupTagpagerendering0976D2E5", + "Ref": "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerECSHandlersGroupCB72232A", }, }, ], - "Monitoring": { - "MetricConfigurations": [ - { - "MetricNames": [ - "CPUUtilization", - "MemoryUtilization", - ], - "ResolutionSeconds": 20, - }, - ], - }, "NetworkConfiguration": { "AwsvpcConfiguration": { "AssignPublicIp": "DISABLED", "SecurityGroups": [ { "Fn::GetAtt": [ - "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GuHttpsEgressSecurityGroupUndefinedecs0AE96607", "GroupId", ], }, @@ -2024,10 +2309,6 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, "PropagateTags": "SERVICE", "Tags": [ - { - "Key": "App", - "Value": "tag-page-rendering", - }, { "Key": "gu:cdk:version", "Value": "TEST", @@ -2046,29 +2327,437 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, ], "TaskDefinition": { - "Ref": "EcsTaskDefinition63157ED3", + "Ref": "Ec2ToEcsTrafficMirrorMirroringHandlerEcsTaskDefinitionCCF3FF96", }, }, "Type": "AWS::ECS::Service", }, - "EcsServiceTaskCountTarget02FCCE22": { + "Ec2ToEcsTrafficMirrorMirroringHandlerNLB41FDD9B0": { "DependsOn": [ - "EcsTaskDefinitionTaskRoleB7B6D8DD", + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", ], "Properties": { - "MaxCapacity": 2, - "MinCapacity": 1, - "ResourceId": { - "Fn::Join": [ - "", - [ - "service/", - { - "Ref": "tagpagerenderingEcsClusterE7696595", - }, - "/", - { - "Fn::GetAtt": [ + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false", + }, + ], + "Scheme": "internal", + "Subnets": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Type": "network", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerDC91CFB0": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerECSHandlersGroupCB72232A", + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": { + "Ref": "Ec2ToEcsTrafficMirrorMirroringHandlerNLB41FDD9B0", + }, + "Port": 4789, + "Protocol": "UDP", + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "Ec2ToEcsTrafficMirrorMirroringHandlerNLBMirroringHandlerListenerECSHandlersGroupCB72232A": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "Port": 4789, + "Protocol": "UDP", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaCA9F6846": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleDefaultPolicyA9AD3ECB", + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleBCEFCFAB", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "Code": { + "ZipFile": " + const { EC2Client, DescribeInstancesCommand, CreateTrafficMirrorSessionCommand } = require('@aws-sdk/client-ec2'); + const ec2 = new EC2Client(); + + exports.handler = async (event) => { + const instanceId = event.detail.EC2InstanceId; + const targetId = process.env.TARGET_ID; + const filterId = process.env.FILTER_ID; + + console.log(\`Processing launch event for instance: \${instanceId}\`); + + // Fetch instance details to get primary ENI ID + const describeRes = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [instanceId] })); + const instance = describeRes.Reservations?.[0]?.Instances?.[0]; + const primaryEniId = instance?.NetworkInterfaces?.[0]?.NetworkInterfaceId; + + if (!primaryEniId) { + throw new Error(\`Unable to find primary ENI for instance: \${instanceId}\`); + } + + // Attach Traffic Mirror Session (ASG instance ENI -> EC2 Worker Target ENI) + const sessionRes = await ec2.send(new CreateTrafficMirrorSessionCommand({ + NetworkInterfaceId: primaryEniId, + TrafficMirrorTargetId: targetId, + TrafficMirrorFilterId: filterId, + SessionNumber: 1, + Description: \`Auto-attached traffic mirror for instance \${instanceId}\`, + })); + + console.log(\`Successfully created session: \${sessionRes.TrafficMirrorSession.TrafficMirrorSessionId}\`); + }; + ", + }, + "Environment": { + "Variables": { + "FILTER_ID": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorFilter8854C552", + "Id", + ], + }, + "TARGET_ID": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorTarget64F0FC7C", + "Id", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleBCEFCFAB", + "Arn", + ], + }, + "Runtime": "nodejs24.x", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleBCEFCFAB": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleDefaultPolicyA9AD3ECB": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:CreateTrafficMirrorSession", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleDefaultPolicyA9AD3ECB", + "Roles": [ + { + "Ref": "Ec2ToEcsTrafficMirrorSessionAttacherLambdaServiceRoleBCEFCFAB", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "Ec2ToEcsTrafficMirrorTarget64F0FC7C": { + "DependsOn": [ + "AutoScalingGroupTagpagerenderingASG7F7E0748", + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F", + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F", + ], + "Properties": { + "NetworkLoadBalancerArn": { + "Ref": "Ec2ToEcsTrafficMirrorMirroringHandlerNLB41FDD9B0", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::EC2::TrafficMirrorTarget", + }, + "EcsService81FC6EF6": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "ListenerTagpagerenderingDeterministicRouteToEc2Rule1C5C95A4", + "ListenerTagpagerenderingDeterministicRouteToEcsRuleAB05C756", + "ListenerTagpagerendering92E57078", + ], + "Properties": { + "Cluster": { + "Ref": "tagpagerenderingEcsClusterE7696595", + }, + "DeploymentConfiguration": { + "Alarms": { + "AlarmNames": [], + "Enable": false, + "Rollback": false, + }, + "DeploymentCircuitBreaker": { + "Enable": true, + "Rollback": true, + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 100, + }, + "DeploymentController": { + "Type": "ECS", + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "tag-page-rendering", + "ContainerPort": 9000, + "TargetGroupArn": { + "Ref": "TagpagerenderingEcsTargetGroupTagpagerendering0976D2E5", + }, + }, + ], + "Monitoring": { + "MetricConfigurations": [ + { + "MetricNames": [ + "CPUUtilization", + "MemoryUtilization", + ], + "ResolutionSeconds": 20, + }, + ], + }, + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + }, + }, + "PropagateTags": "SERVICE", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TaskDefinition": { + "Ref": "EcsTaskDefinition63157ED3", + }, + }, + "Type": "AWS::ECS::Service", + }, + "EcsServiceTaskCountTarget02FCCE22": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + ], + "Properties": { + "MaxCapacity": 2, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "tagpagerenderingEcsClusterE7696595", + }, + "/", + { + "Fn::GetAtt": [ "EcsService81FC6EF6", "Name", ], @@ -2655,6 +3344,46 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, "Type": "AWS::EC2::SecurityGroupIngress", }, + "GuHttpsEgressSecurityGroupUndefinedecs0AE96607": { + "Properties": { + "GroupDescription": "Allow all outbound HTTPS traffic", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound HTTPS traffic", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "undefined-ecs", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, "GuLogShippingPolicy981BFE5A": { "Properties": { "PolicyDocument": { diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index 45822e9bdd1..d8f079b3d54 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -8,8 +8,8 @@ import { GuDistributionBucketParameter, } from '@guardian/cdk/lib/constructs/core'; import { GuCname } from '@guardian/cdk/lib/constructs/dns/dns-records'; -import { GuAllowPolicy } from '@guardian/cdk/lib/constructs/iam'; import { GuVpc, SubnetType } from '@guardian/cdk/lib/constructs/ec2'; +import { GuAllowPolicy } from '@guardian/cdk/lib/constructs/iam'; import { GuLoadBalancedAppExperimental } from '@guardian/cdk/lib/experimental/patterns/gu-load-balanced-app'; import type { GuAsgCapacity } from '@guardian/cdk/lib/types'; import { aws_cloudwatch, type App as CDKApp, Duration } from 'aws-cdk-lib'; @@ -23,9 +23,8 @@ import type { CfnService } from 'aws-cdk-lib/aws-ecs'; import { ClusterSettings } from 'aws-cdk-lib/aws-ecs/mixins'; import { Subscription, SubscriptionProtocol, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; -import { getUserData } from './userData'; import { HttpTrafficMirroring } from './HttpTrafficMirroring'; -import { log } from 'console'; +import { getUserData } from './userData'; export interface RenderingCDKStackProps extends Omit { guApp: `${'article' | 'facia' | 'interactive' | 'tag-page'}-rendering`; @@ -351,14 +350,12 @@ export class RenderingCDKStack extends CDKStack { ], }); - log(vpc); - log(privateSubnets); const availabilityZones = this.availabilityZones; - log(availabilityZones); - if (!!app.autoScalingGroup) { + if (app.autoScalingGroup) { new HttpTrafficMirroring(this, 'Ec2ToEcsTrafficMirror', { - vpc: vpc, - privateSubnets: privateSubnets, + vpc, + privateSubnets, + app: this, availabilityZone: availabilityZones[0], trafficSource: app.autoScalingGroup, trafficTarget: app.loadBalancer, From 941927b3e71fd2725af5017f719b4ead42fe5bee Mon Sep 17 00:00:00 2001 From: Connor OMalley Date: Mon, 24 Aug 2026 13:43:14 +0100 Subject: [PATCH 3/3] Variable name change to try to fix CI --- dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts index d1c754f3dd9..d29462ab1f9 100644 --- a/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts +++ b/dotcom-rendering/cdk/lib/HttpTrafficMirroring.ts @@ -164,7 +164,7 @@ export class HttpTrafficMirroring extends Construct { stack: GuStack, target: ApplicationLoadBalancer, ): NetworkLoadBalancer { - const cluster = Cluster.fromClusterAttributes( + const ecsCluster = Cluster.fromClusterAttributes( this, 'MirroringHandlerEcsCluster', { @@ -226,7 +226,7 @@ export class HttpTrafficMirroring extends Construct { this, 'MirroringHandlerFargateService', { - cluster, + cluster: ecsCluster, taskDefinition, vpcSubnets: { subnets }, // Important for service deployments; with the AWS defaults the service can be scaled down when deploying