From f082c2c5eaa253cadf654d4db100af50ab8e62b3 Mon Sep 17 00:00:00 2001 From: Justin Rowles Date: Tue, 1 Sep 2026 09:03:22 +0100 Subject: [PATCH 1/2] Prep for PROD ECS --- dotcom-rendering/cdk/bin/cdk.ts | 88 +- .../__snapshots__/renderingStack.test.ts.snap | 1637 +++++++++++++++++ .../cdk/lib/renderingStack.test.ts | 17 +- 3 files changed, 1702 insertions(+), 40 deletions(-) diff --git a/dotcom-rendering/cdk/bin/cdk.ts b/dotcom-rendering/cdk/bin/cdk.ts index ed16e84ea4e..8d46b02dae1 100644 --- a/dotcom-rendering/cdk/bin/cdk.ts +++ b/dotcom-rendering/cdk/bin/cdk.ts @@ -97,52 +97,66 @@ new RenderingCDKStack(cdkApp, 'FaciaRendering-PROD', { }); /** Tag pages */ -export const TagPageRenderingPropsCODE: RenderingCDKStackProps = { - guApp: 'tag-page-rendering', - stage: 'CODE', - domainName: 'tag-page-rendering.code.dev-guardianapis.com', - scaling: { minimumInstances: 1, maximumInstances: 3 }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), - imageIdentifier: process.env.IMAGE_DIGEST ?? 'DEV', -}; +export function getTagPageRenderingPropsCODE( + imageIdentifier: string | undefined, +): RenderingCDKStackProps { + return { + guApp: 'tag-page-rendering', + stage: 'CODE', + domainName: 'tag-page-rendering.code.dev-guardianapis.com', + scaling: { minimumInstances: 1, maximumInstances: 3 }, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), + imageIdentifier: imageIdentifier ?? 'DEV', + }; +} new RenderingCDKStack( cdkApp, 'TagPageRendering-CODE', - TagPageRenderingPropsCODE, + getTagPageRenderingPropsCODE(process.env.IMAGE_DIGEST), ); -new RenderingCDKStack(cdkApp, 'TagPageRendering-PROD', { - guApp: 'tag-page-rendering', - stage: 'PROD', - domainName: 'tag-page-rendering.guardianapis.com', - scaling: { - minimumInstances: 9, - maximumInstances: 90, - policies: { - step: { - cpu: cpuScalingSteps, - latency: { - scalingStepsOut: [ - // When latency is lower than 0.4s no scaling up - { lower: 0, upper: 0.4, change: 0 }, - // When latency is higher than 0.4s we scale up by 50% - { lower: 0.4, change: 50 }, - // When latency is higher than 0.5s we scale up by 80% - { lower: 0.5, change: 80 }, - ], - scalingStepsIn: [ - // When latency is higher than 0.35s no scaling down - { lower: 0.35, change: 0 }, - // When latency is lower than 0.35s we scale down by 1 - { upper: 0.35, lower: 0, change: -1 }, - ], +export function getTagPageRenderingPropsPROD( + imageIdentifier: string | undefined = undefined, +): RenderingCDKStackProps { + return { + guApp: 'tag-page-rendering', + stage: 'PROD', + domainName: 'tag-page-rendering.guardianapis.com', + scaling: { + minimumInstances: 9, + maximumInstances: 90, + policies: { + step: { + cpu: cpuScalingSteps, + latency: { + scalingStepsOut: [ + // When latency is lower than 0.4s no scaling up + { lower: 0, upper: 0.4, change: 0 }, + // When latency is higher than 0.4s we scale up by 50% + { lower: 0.4, change: 50 }, + // When latency is higher than 0.5s we scale up by 80% + { lower: 0.5, change: 80 }, + ], + scalingStepsIn: [ + // When latency is higher than 0.35s no scaling down + { lower: 0.35, change: 0 }, + // When latency is lower than 0.35s we scale down by 1 + { upper: 0.35, lower: 0, change: -1 }, + ], + }, }, }, }, - }, - instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), -}); + instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), + imageIdentifier, + }; +} +new RenderingCDKStack( + cdkApp, + 'TagPageRendering-PROD', + getTagPageRenderingPropsPROD(process.env.IMAGE_DIGEST), +); /** Interactive */ new RenderingCDKStack(cdkApp, 'InteractiveRendering-CODE', { diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index edc06116159..7438704e0c5 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -3673,3 +3673,1640 @@ systemctl start tag-page-rendering", }, } `; + +exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD (does not use ECS) 1`] = ` +{ + "Metadata": { + "gu:cdk:constructs": [ + "GuDistributionBucketParameter", + "GuAllowPolicy", + "GuAllowPolicy", + "GuAllowPolicy", + "GuVpcParameter", + "GuSubnetListParameter", + "GuSubnetListParameter", + "GuLoadBalancedAppExperimental", + "GuInstanceRole", + "GuSsmSshPolicy", + "GuDescribeEC2Policy", + "GuLoggingStreamNameParameter", + "GuLogShippingPolicy", + "GuGetDistributablePolicy", + "GuParameterStoreReadPolicy", + "GuAmiParameter", + "GuHttpsEgressSecurityGroup", + "GuAutoScalingGroup", + "GuApplicationTargetGroup", + "GuCertificate", + "GuApplicationLoadBalancer", + "GuAccessLoggingBucketParameter", + "GuHttpsApplicationListener", + "GuSecurityGroup", + "GuAlb5xxPercentageAlarm", + "GuUnhealthyInstancesAlarm", + "GuCname", + ], + "gu:cdk:version": "TEST", + }, + "Outputs": { + "LoadBalancerTagpagerenderingDnsName": { + "Description": "DNS entry for LoadBalancerTagpagerendering", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "DNSName", + ], + }, + }, + }, + "Parameters": { + "AMITagpagerendering": { + "Description": "Amazon Machine Image ID for the app tag-page-rendering. Use this in conjunction with AMIgo to keep AMIs up to date.", + "Type": "AWS::EC2::Image::Id", + }, + "AccessLoggingBucket": { + "Default": "/account/services/access-logging/bucket", + "Description": "S3 bucket to store your access logs", + "Type": "AWS::SSM::Parameter::Value", + }, + "DistributionBucketName": { + "Default": "/account/services/artifact.bucket", + "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", + "Type": "AWS::SSM::Parameter::Value", + }, + "LoggingStreamName": { + "Default": "/account/services/logging.stream.name", + "Description": "SSM parameter containing the Name (not ARN) on the kinesis stream", + "Type": "AWS::SSM::Parameter::Value", + }, + "VpcId": { + "Default": "/account/vpc/primary/id", + "Description": "Virtual Private Cloud to run EC2 instances within. Should NOT be the account default VPC.", + "Type": "AWS::SSM::Parameter::Value", + }, + "tagpagerenderingPrivateSubnets": { + "Default": "/account/vpc/primary/subnets/private", + "Description": "A list of private subnets", + "Type": "AWS::SSM::Parameter::Value>", + }, + "tagpagerenderingPublicSubnets": { + "Default": "/account/vpc/primary/subnets/public", + "Description": "A list of public subnets", + "Type": "AWS::SSM::Parameter::Value>", + }, + }, + "Resources": { + "AllowPolicyCloudwatchLogsA783E5B4": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudwatch:*", + "logs:*", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyCloudwatchLogsA783E5B4", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AllowPolicyDescribeDecryptKmsE91286F3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:kms:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":FrontendConfigKey", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyDescribeDecryptKmsE91286F3", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AllowPolicyGetSsmParamsByPathB54B2DE8": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:GetParametersByPath", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/frontend/*", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/dotcom/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyGetSsmParamsByPathB54B2DE8", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AutoScalingGroupTagpagerenderingASG7F7E0748": { + "Properties": { + "HealthCheckGracePeriod": 120, + "HealthCheckType": "ELB", + "LaunchTemplate": { + "LaunchTemplateId": { + "Ref": "frontendPRODtagpagerenderingA0B91C45", + }, + "Version": { + "Fn::GetAtt": [ + "frontendPRODtagpagerenderingA0B91C45", + "LatestVersionNumber", + ], + }, + }, + "MaxSize": "90", + "MetricsCollection": [ + { + "Granularity": "1Minute", + }, + ], + "MinSize": "9", + "Tags": [ + { + "Key": "App", + "PropagateAtLaunch": true, + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "PropagateAtLaunch": true, + "Value": "TEST", + }, + { + "Key": "gu:repo", + "PropagateAtLaunch": true, + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "LogKinesisStreamName", + "PropagateAtLaunch": true, + "Value": { + "Ref": "LoggingStreamName", + }, + }, + { + "Key": "Stack", + "PropagateAtLaunch": true, + "Value": "frontend", + }, + { + "Key": "Stage", + "PropagateAtLaunch": true, + "Value": "PROD", + }, + { + "Key": "SystemdUnit", + "PropagateAtLaunch": true, + "Value": "tag-page-rendering.service", + }, + ], + "TargetGroupARNs": [ + { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + ], + "VPCZoneIdentifier": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + }, + "Type": "AWS::AutoScaling::AutoScalingGroup", + }, + "CPUScaleUpPolicyUpperAlarm7E15DD91": { + "Properties": { + "AlarmActions": [ + { + "Ref": "CPUScaleUpPolicyUpperPolicyDEE5DA77", + }, + { + "Ref": "CPUScalingAlertsTopic9D0083F7", + }, + ], + "AlarmDescription": "Upper threshold scaling alarm", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "AutoScalingGroupName", + "Value": { + "Ref": "AutoScalingGroupTagpagerenderingASG7F7E0748", + }, + }, + ], + "EvaluationPeriods": 2, + "ExtendedStatistic": "p90", + "MetricName": "CPUUtilization", + "Namespace": "AWS/EC2", + "Period": 30, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Threshold": 60, + "Unit": "Percent", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CPUScaleUpPolicyUpperPolicyDEE5DA77": { + "Properties": { + "AdjustmentType": "PercentChangeInCapacity", + "AutoScalingGroupName": { + "Ref": "AutoScalingGroupTagpagerenderingASG7F7E0748", + }, + "MetricAggregationType": "Average", + "PolicyType": "StepScaling", + "StepAdjustments": [ + { + "MetricIntervalLowerBound": 0, + "MetricIntervalUpperBound": 20, + "ScalingAdjustment": 50, + }, + { + "MetricIntervalLowerBound": 20, + "ScalingAdjustment": 80, + }, + ], + }, + "Type": "AWS::AutoScaling::ScalingPolicy", + }, + "CPUScalingAlertsSubscriptionEmail170D6D82": { + "Properties": { + "Endpoint": "dotcom.platform@theguardian.com", + "Protocol": "email", + "TopicArn": { + "Ref": "CPUScalingAlertsTopic9D0083F7", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + "CPUScalingAlertsTopic9D0083F7": { + "Properties": { + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + "Type": "AWS::SNS::Topic", + }, + "CertificateTagpagerendering5182FE63": { + "DeletionPolicy": "Retain", + "Properties": { + "DomainName": "tag-page-rendering.guardianapis.com", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-PROD/CertificateTagpagerendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "ValidationMethod": "DNS", + }, + "Type": "AWS::CertificateManager::Certificate", + "UpdateReplacePolicy": "Retain", + }, + "DescribeEC2PolicyFF5F9295": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "autoscaling:DescribeAutoScalingInstances", + "autoscaling:DescribeAutoScalingGroups", + "ec2:DescribeTags", + "ec2:DescribeInstances", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "describe-ec2-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GetDistributablePolicyTagpagerendering346128E3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "DistributionBucketName", + }, + "/frontend/PROD/tag-page-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GetDistributablePolicyTagpagerendering346128E3", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE": { + "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": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingfromTagPageRenderingPRODInternalIngressSecurityGroupTagpagerendering1CF134899000DECEA4B2": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingfromTagPageRenderingPRODLoadBalancerTagpagerenderingSecurityGroup55D2582D90005A0A2694": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuLogShippingPolicy981BFE5A": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:Describe*", + "kinesis:Put*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:kinesis:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":stream/", + { + "Ref": "LoggingStreamName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GuLogShippingPolicy981BFE5A", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "High5xxPercentageAlarmTagpagerendering76ED1785": { + "Properties": { + "ActionsEnabled": true, + "AlarmActions": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:sns:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":Frontend-PROD-CriticalAlerts", + ], + ], + }, + ], + "AlarmDescription": "tag-page-rendering exceeded 0.5% error rate", + "AlarmName": "High 5XX error percentage from tag-page-rendering in PROD", + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "Metrics": [ + { + "Expression": "100*(m1+m2)/m3", + "Id": "expr_1", + "Label": "% of 5XX responses served for tag-page-rendering (load balancer and instances combined)", + "ReturnData": true, + }, + { + "Id": "m1", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerFullName", + ], + }, + }, + ], + "MetricName": "HTTPCode_ELB_5XX_Count", + "Namespace": "AWS/ApplicationELB", + }, + "Period": 60, + "Stat": "Sum", + }, + "ReturnData": false, + }, + { + "Id": "m2", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerFullName", + ], + }, + }, + ], + "MetricName": "HTTPCode_Target_5XX_Count", + "Namespace": "AWS/ApplicationELB", + }, + "Period": 60, + "Stat": "Sum", + }, + "ReturnData": false, + }, + { + "Id": "m3", + "MetricStat": { + "Metric": { + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerFullName", + ], + }, + }, + ], + "MetricName": "RequestCount", + "Namespace": "AWS/ApplicationELB", + }, + "Period": 60, + "Stat": "Sum", + }, + "ReturnData": false, + }, + ], + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Threshold": 0.5, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "InstanceRoleTagpagerendering171BC2F9": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Path": "/", + "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": "PROD", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InternalIngressSecurityGroupTagpagerendering38FD16DB": { + "Properties": { + "GroupDescription": "Allow restricted ingress from CIDR ranges", + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.0/8", + "Description": "Allow access on port 443 from 10.0.0.0/8", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + ], + "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": "PROD", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "InternalIngressSecurityGroupTagpagerenderingtoTagPageRenderingPRODGuHttpsEgressSecurityGroupTagpagerendering55A86AD190001055A384": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "LatencyScaleDownPolicyLowerAlarm3AAA79CF": { + "Properties": { + "AlarmActions": [ + { + "Ref": "LatencyScaleDownPolicyLowerPolicy5B8C5A01", + }, + ], + "AlarmDescription": "Lower threshold scaling alarm", + "ComparisonOperator": "LessThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerFullName", + ], + }, + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "TargetGroupTagpagerendering42E428EE", + "TargetGroupFullName", + ], + }, + }, + ], + "EvaluationPeriods": 10, + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 30, + "Statistic": "Average", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Threshold": 0.35, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "LatencyScaleDownPolicyLowerPolicy5B8C5A01": { + "Properties": { + "AdjustmentType": "ChangeInCapacity", + "AutoScalingGroupName": { + "Ref": "AutoScalingGroupTagpagerenderingASG7F7E0748", + }, + "MetricAggregationType": "Average", + "PolicyType": "StepScaling", + "StepAdjustments": [ + { + "MetricIntervalUpperBound": 0, + "ScalingAdjustment": -1, + }, + ], + }, + "Type": "AWS::AutoScaling::ScalingPolicy", + }, + "LatencyScaleUpPolicyUpperAlarmB2CCE62D": { + "Properties": { + "AlarmActions": [ + { + "Ref": "LatencyScaleUpPolicyUpperPolicy4A22B34C", + }, + { + "Ref": "LatencyScalingAlertsTopic1A322157", + }, + ], + "AlarmDescription": "Upper threshold scaling alarm", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "LoadBalancerFullName", + ], + }, + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "TargetGroupTagpagerendering42E428EE", + "TargetGroupFullName", + ], + }, + }, + ], + "EvaluationPeriods": 2, + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 30, + "Statistic": "Average", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Threshold": 0.4, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "LatencyScaleUpPolicyUpperPolicy4A22B34C": { + "Properties": { + "AdjustmentType": "PercentChangeInCapacity", + "AutoScalingGroupName": { + "Ref": "AutoScalingGroupTagpagerenderingASG7F7E0748", + }, + "MetricAggregationType": "Average", + "PolicyType": "StepScaling", + "StepAdjustments": [ + { + "MetricIntervalLowerBound": 0, + "MetricIntervalUpperBound": 0.09999999999999998, + "ScalingAdjustment": 50, + }, + { + "MetricIntervalLowerBound": 0.09999999999999998, + "ScalingAdjustment": 80, + }, + ], + }, + "Type": "AWS::AutoScaling::ScalingPolicy", + }, + "LatencyScalingAlertsSubscriptionEmailF1826DAB": { + "Properties": { + "Endpoint": "dotcom.platform@theguardian.com", + "Protocol": "email", + "TopicArn": { + "Ref": "LatencyScalingAlertsTopic1A322157", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + "LatencyScalingAlertsTopic1A322157": { + "Properties": { + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + "Type": "AWS::SNS::Topic", + }, + "ListenerTagpagerendering92E57078": { + "Properties": { + "Certificates": [ + { + "CertificateArn": { + "Ref": "CertificateTagpagerendering5182FE63", + }, + }, + ], + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": { + "Ref": "LoadBalancerTagpagerenderingB0B7AC4E", + }, + "Port": 443, + "Protocol": "HTTPS", + "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-2021-06", + "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": "PROD", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "LoadBalancerDNS": { + "Properties": { + "Name": "tag-page-rendering.guardianapis.com", + "RecordType": "CNAME", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "DNSName", + ], + }, + ], + "Stage": "PROD", + "TTL": 3600, + }, + "Type": "Guardian::DNS::RecordSet", + }, + "LoadBalancerTagpagerenderingB0B7AC4E": { + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "true", + }, + { + "Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled", + "Value": "true", + }, + { + "Key": "routing.http.drop_invalid_header_fields.enabled", + "Value": "true", + }, + { + "Key": "access_logs.s3.enabled", + "Value": "true", + }, + { + "Key": "access_logs.s3.bucket", + "Value": { + "Ref": "AccessLoggingBucket", + }, + }, + { + "Key": "access_logs.s3.prefix", + "Value": "application-load-balancer/PROD/frontend/tag-page-rendering", + }, + { + "Key": "idle_timeout.timeout_seconds", + "Value": "4", + }, + ], + "Scheme": "internal", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + "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": "PROD", + }, + ], + "Type": "application", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "LoadBalancerTagpagerenderingSecurityGroup4D481A16": { + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB TagPageRenderingPRODLoadBalancerTagpagerenderingF63F513E", + "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": "PROD", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingPRODGuHttpsEgressSecurityGroupTagpagerendering55A86AD190007FB7F7E9": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "ParameterStoreReadTagpagerendering4CA7B6E3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ssm:GetParametersByPath", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/PROD/frontend/tag-page-rendering", + ], + ], + }, + }, + { + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/PROD/frontend/tag-page-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "parameter-store-read-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "RenderingBaseURLParamAE11E777": { + "Properties": { + "Description": "The rendering base URL for frontend to call the tag-page-rendering app in the PROD environment", + "Name": "/frontend/prod/tag-page-rendering.baseURL", + "Tags": { + "Stack": "frontend", + "Stage": "PROD", + "gu:cdk:version": "TEST", + "gu:repo": "guardian/dotcom-rendering", + }, + "Type": "String", + "Value": "https://tag-page-rendering.guardianapis.com", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmSshPolicy4CFC977E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply", + "ssm:UpdateInstanceInformation", + "ssm:ListInstanceAssociations", + "ssm:DescribeInstanceProperties", + "ssm:DescribeDocumentParameters", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "ssm-ssh-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "TargetGroupTagpagerendering42E428EE": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "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": "PROD", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "instance", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "UnhealthyInstancesAlarmTagpagerenderingED314507": { + "Properties": { + "ActionsEnabled": true, + "AlarmActions": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:sns:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":Frontend-PROD-CriticalAlerts", + ], + ], + }, + ], + "AlarmDescription": "tag-page-rendering's instances have failed healthchecks several times over the last 1 hour. + This typically results in the AutoScaling Group cycling instances and can lead to problems with deployment, + scaling or handling traffic spikes. + + Check tag-page-rendering's application logs or ssh onto an unhealthy instance in order to debug these problems.", + "AlarmName": "Unhealthy instances for tag-page-rendering in PROD", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "DatapointsToAlarm": 30, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "ListenerTagpagerendering92E57078", + }, + ], + }, + ], + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "ListenerTagpagerendering92E57078", + }, + ], + }, + ], + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "ListenerTagpagerendering92E57078", + }, + ], + }, + ], + }, + ], + ], + }, + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "TargetGroupTagpagerendering42E428EE", + "TargetGroupFullName", + ], + }, + }, + ], + "EvaluationPeriods": 60, + "MetricName": "UnHealthyHostCount", + "Namespace": "AWS/ApplicationELB", + "Period": 60, + "Statistic": "Maximum", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Threshold": 1, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "frontendPRODtagpagerenderingA0B91C45": { + "DependsOn": [ + "InstanceRoleTagpagerendering171BC2F9", + ], + "Properties": { + "LaunchTemplateData": { + "IamInstanceProfile": { + "Arn": { + "Fn::GetAtt": [ + "frontendPRODtagpagerenderingProfile0369CEFE", + "Arn", + ], + }, + }, + "ImageId": { + "Ref": "AMITagpagerendering", + }, + "InstanceType": "c8g.medium", + "MetadataOptions": { + "HttpTokens": "required", + "InstanceMetadataTags": "enabled", + }, + "Monitoring": { + "Enabled": true, + }, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + ], + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-PROD/frontend-PROD-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + { + "ResourceType": "volume", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-PROD/frontend-PROD-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash +set -ev +groupadd frontend +useradd -r -m -s /usr/bin/nologin -g frontend dotcom-rendering +cd /home/dotcom-rendering +aws --region eu-west-1 s3 cp s3://", + { + "Ref": "DistributionBucketName", + }, + "/frontend/PROD/tag-page-rendering/tag-page-rendering.tar.gz ./ +tar -zxf tag-page-rendering.tar.gz tag-page-rendering +chown -R dotcom-rendering:frontend tag-page-rendering +cd tag-page-rendering +mkdir /var/log/dotcom-rendering +chown -R dotcom-rendering:frontend /var/log/dotcom-rendering +cat > /etc/systemd/system/tag-page-rendering.service << EOF +[Unit] +Description=tag-page-rendering +After=network.target +[Service] +WorkingDirectory=/home/dotcom-rendering/tag-page-rendering +Type=simple +User=dotcom-rendering +Group=frontend +StandardError=journal +StandardOutput=journal +Environment=TERM=xterm-256color +Environment=NODE_ENV=production +Environment=GU_STAGE=PROD +Environment=GU_APP=tag-page-rendering +Environment=GU_STACK=frontend +ExecStart=make prod +Restart=on-failure +[Install] +WantedBy=multi-user.target +EOF +systemctl enable tag-page-rendering +systemctl start tag-page-rendering", + ], + ], + }, + }, + }, + "TagSpecifications": [ + { + "ResourceType": "launch-template", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-PROD/frontend-PROD-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + ], + }, + "Type": "AWS::EC2::LaunchTemplate", + }, + "frontendPRODtagpagerenderingProfile0369CEFE": { + "Properties": { + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::InstanceProfile", + }, + }, +} +`; diff --git a/dotcom-rendering/cdk/lib/renderingStack.test.ts b/dotcom-rendering/cdk/lib/renderingStack.test.ts index c55a7f4a421..c42a045c65f 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.test.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.test.ts @@ -1,7 +1,10 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; -import { TagPageRenderingPropsCODE } from '../bin/cdk'; +import { + getTagPageRenderingPropsCODE, + getTagPageRenderingPropsPROD, +} from '../bin/cdk'; import { RenderingCDKStack } from './renderingStack'; /** @@ -53,12 +56,20 @@ describe('The RenderingCDKStack', () => { expect(template.toJSON()).toMatchSnapshot(); }); + it('matches the snapshot for Tag Page Rendering PROD (does not use ECS)', () => { + const app = new App(); + const stack = new RenderingCDKStack(app, 'TagPageRendering-PROD', { + ...getTagPageRenderingPropsPROD(), // Could have an image identifier in future + }); + const template = Template.fromStack(stack); + expect(template.toJSON()).toMatchSnapshot(); + }); + it('matches the snapshot for Tag Page Rendering CODE (uses ECS)', () => { const app = new App(); const stack = new RenderingCDKStack(app, 'TagPageRendering-CODE', { - ...TagPageRenderingPropsCODE, - imageIdentifier: 'sha256:12345', + ...getTagPageRenderingPropsCODE('sha256:12345'), // Usually taken from process.env }); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); From b06c2f157b59e0bbba37e103a37c37bac5736a00 Mon Sep 17 00:00:00 2001 From: Justin Rowles Date: Thu, 3 Sep 2026 16:55:46 +0100 Subject: [PATCH 2/2] Fix process.env.IMAGE_DIGEST at package.json test script level --- .github/workflows/jest.yml | 2 +- dotcom-rendering/cdk/bin/cdk.ts | 87 +- .../__snapshots__/renderingStack.test.ts.snap | 1014 ++++++++++++++++- .../cdk/lib/renderingStack.test.ts | 20 +- dotcom-rendering/package.json | 6 +- 5 files changed, 1027 insertions(+), 102 deletions(-) diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index d5eff497b3b..27d0b1aa640 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -11,5 +11,5 @@ jobs: uses: ./.github/actions/setup-node-env - name: Run Jest - run: CI=true pnpm test + run: CI=true IMAGE_DIGEST=sha256:12345 pnpm test working-directory: dotcom-rendering diff --git a/dotcom-rendering/cdk/bin/cdk.ts b/dotcom-rendering/cdk/bin/cdk.ts index 8d46b02dae1..7330cd689f1 100644 --- a/dotcom-rendering/cdk/bin/cdk.ts +++ b/dotcom-rendering/cdk/bin/cdk.ts @@ -97,65 +97,58 @@ new RenderingCDKStack(cdkApp, 'FaciaRendering-PROD', { }); /** Tag pages */ -export function getTagPageRenderingPropsCODE( - imageIdentifier: string | undefined, -): RenderingCDKStackProps { - return { - guApp: 'tag-page-rendering', - stage: 'CODE', - domainName: 'tag-page-rendering.code.dev-guardianapis.com', - scaling: { minimumInstances: 1, maximumInstances: 3 }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), - imageIdentifier: imageIdentifier ?? 'DEV', - }; -} +export const tagPageRenderingPropsCODE: RenderingCDKStackProps = { + guApp: 'tag-page-rendering', + stage: 'CODE', + domainName: 'tag-page-rendering.code.dev-guardianapis.com', + scaling: { minimumInstances: 1, maximumInstances: 3 }, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), + imageIdentifier: process.env.IMAGE_DIGEST ?? 'DEV', +}; new RenderingCDKStack( cdkApp, 'TagPageRendering-CODE', - getTagPageRenderingPropsCODE(process.env.IMAGE_DIGEST), + tagPageRenderingPropsCODE, ); -export function getTagPageRenderingPropsPROD( - imageIdentifier: string | undefined = undefined, -): RenderingCDKStackProps { - return { - guApp: 'tag-page-rendering', - stage: 'PROD', - domainName: 'tag-page-rendering.guardianapis.com', - scaling: { - minimumInstances: 9, - maximumInstances: 90, - policies: { - step: { - cpu: cpuScalingSteps, - latency: { - scalingStepsOut: [ - // When latency is lower than 0.4s no scaling up - { lower: 0, upper: 0.4, change: 0 }, - // When latency is higher than 0.4s we scale up by 50% - { lower: 0.4, change: 50 }, - // When latency is higher than 0.5s we scale up by 80% - { lower: 0.5, change: 80 }, - ], - scalingStepsIn: [ - // When latency is higher than 0.35s no scaling down - { lower: 0.35, change: 0 }, - // When latency is lower than 0.35s we scale down by 1 - { upper: 0.35, lower: 0, change: -1 }, - ], - }, +export const tagPageRenderingPropsPROD: RenderingCDKStackProps = { + guApp: 'tag-page-rendering', + stage: 'PROD', + domainName: 'tag-page-rendering.guardianapis.com', + scaling: { + minimumInstances: 9, + maximumInstances: 90, + policies: { + step: { + cpu: cpuScalingSteps, + latency: { + scalingStepsOut: [ + // When latency is lower than 0.4s no scaling up + { lower: 0, upper: 0.4, change: 0 }, + // When latency is higher than 0.4s we scale up by 50% + { lower: 0.4, change: 50 }, + // When latency is higher than 0.5s we scale up by 80% + { lower: 0.5, change: 80 }, + ], + scalingStepsIn: [ + // When latency is higher than 0.35s no scaling down + { lower: 0.35, change: 0 }, + // When latency is lower than 0.35s we scale down by 1 + { upper: 0.35, lower: 0, change: -1 }, + ], }, }, }, - instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), - imageIdentifier, - }; -} + }, + instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), + imageIdentifier: process.env.IMAGE_DIGEST!, +}; + new RenderingCDKStack( cdkApp, 'TagPageRendering-PROD', - getTagPageRenderingPropsPROD(process.env.IMAGE_DIGEST), + tagPageRenderingPropsPROD, ); /** Interactive */ diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index 7438704e0c5..7cdbe74bee7 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -3697,6 +3697,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD "GuHttpsEgressSecurityGroup", "GuAutoScalingGroup", "GuApplicationTargetGroup", + "GuRiffRaffDeploymentIdParameterExperimental", + "GuHttpsEgressSecurityGroup", + "GuApplicationTargetGroup", "GuCertificate", "GuApplicationLoadBalancer", "GuAccessLoggingBucketParameter", @@ -3729,6 +3732,10 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD "Description": "S3 bucket to store your access logs", "Type": "AWS::SSM::Parameter::Value", }, + "DeployToolsAccountIdParameter": { + "Default": "/organisation/accounts/deployTools", + "Type": "AWS::SSM::Parameter::Value", + }, "DistributionBucketName": { "Default": "/account/services/artifact.bucket", "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", @@ -3739,6 +3746,10 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD "Description": "SSM parameter containing the Name (not ARN) on the kinesis stream", "Type": "AWS::SSM::Parameter::Value", }, + "RiffRaffDeploymentId": { + "Description": "Used by Riff-Raff to inject the deployment ID.", + "Type": "String", + }, "VpcId": { "Default": "/account/vpc/primary/id", "Description": "Virtual Private Cloud to run EC2 instances within. Should NOT be the account default VPC.", @@ -3776,6 +3787,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD { "Ref": "InstanceRoleTagpagerendering171BC2F9", }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, ], }, "Type": "AWS::IAM::Policy", @@ -3811,6 +3825,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD { "Ref": "InstanceRoleTagpagerendering171BC2F9", }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, ], }, "Type": "AWS::IAM::Policy", @@ -3860,6 +3877,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD { "Ref": "InstanceRoleTagpagerendering171BC2F9", }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, ], }, "Type": "AWS::IAM::Policy", @@ -4100,50 +4120,74 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD }, "Type": "AWS::IAM::Policy", }, - "GetDistributablePolicyTagpagerendering346128E3": { + "EcsService81FC6EF6": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057", + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "ListenerTagpagerenderingDeterministicRouteToEc2Rule1C5C95A4", + "ListenerTagpagerenderingDeterministicRouteToEcsRuleAB05C756", + "ListenerTagpagerendering92E57078", + ], "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "s3:GetObject", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - "Ref": "DistributionBucketName", - }, - "/frontend/PROD/tag-page-rendering/*", - ], - ], - }, - }, - ], - "Version": "2012-10-17", + "Cluster": { + "Ref": "tagpagerenderingEcsClusterE7696595", }, - "PolicyName": "GetDistributablePolicyTagpagerendering346128E3", - "Roles": [ - { - "Ref": "InstanceRoleTagpagerendering171BC2F9", + "DeploymentConfiguration": { + "Alarms": { + "AlarmNames": [], + "Enable": false, + "Rollback": false, }, - ], - }, - "Type": "AWS::IAM::Policy", - }, - "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE": { - "Properties": { - "GroupDescription": "Allow all outbound HTTPS traffic", - "SecurityGroupEgress": [ + "DeploymentCircuitBreaker": { + "Enable": true, + "Rollback": true, + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 100, + }, + "DeploymentController": { + "Type": "ECS", + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound HTTPS traffic", - "FromPort": 443, - "IpProtocol": "tcp", - "ToPort": 443, + "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", @@ -4166,11 +4210,652 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD "Value": "PROD", }, ], - "VpcId": { - "Ref": "VpcId", + "TaskDefinition": { + "Ref": "EcsTaskDefinition63157ED3", + }, + }, + "Type": "AWS::ECS::Service", + }, + "EcsServiceTaskCountTarget02FCCE22": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057", + "EcsTaskDefinitionTaskRoleB7B6D8DD", + ], + "Properties": { + "MaxCapacity": 2, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "tagpagerenderingEcsClusterE7696595", + }, + "/", + { + "Fn::GetAtt": [ + "EcsService81FC6EF6", + "Name", + ], + }, + ], + ], + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::", + { + "Ref": "AWS::AccountId", + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService", + ], + ], + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs", + }, + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + }, + "EcsTaskDefinition63157ED3": { + "Properties": { + "ContainerDefinitions": [ + { + "DependsOn": [ + { + "Condition": "START", + "ContainerName": "aws-otel-collector", + }, + ], + "DockerLabels": { + "RiffRaffDeploymentId": { + "Ref": "RiffRaffDeploymentId", + }, + }, + "Environment": [ + { + "Name": "STACK", + "Value": "frontend", + }, + { + "Name": "STAGE", + "Value": "PROD", + }, + { + "Name": "APP", + "Value": "tag-page-rendering", + }, + { + "Name": "TASK_NAME", + "Value": "tag-page-rendering", + }, + { + "Name": "GU_REPO", + "Value": "guardian/dotcom-rendering", + }, + { + "Name": "NODE_ENV", + "Value": "production", + }, + { + "Name": "GU_STAGE", + "Value": "PROD", + }, + { + "Name": "GU_APP", + "Value": "tag-page-rendering", + }, + { + "Name": "GU_STACK", + "Value": "frontend", + }, + { + "Name": "OTEL_EXPORTER_OTLP_ENDPOINT", + "Value": "http://localhost:4318", + }, + { + "Name": "OTEL_SERVICE_NAME", + "Value": "tag-page-rendering", + }, + ], + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "DeployToolsAccountIdParameter", + }, + ".dkr.ecr.eu-west-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/guardian/dotcom-rendering@sha256:12345", + ], + ], + }, + "LogConfiguration": { + "LogDriver": "awsfirelens", + "Options": { + "Name": "kinesis_streams", + "region": "eu-west-1", + "retry_limit": "2", + "stream": { + "Ref": "LoggingStreamName", + }, + }, + }, + "Name": "tag-page-rendering", + "PortMappings": [ + { + "ContainerPort": 9000, + "Protocol": "tcp", + }, + ], + "ReadonlyRootFilesystem": true, + "VersionConsistency": "disabled", + }, + { + "Environment": [ + { + "Name": "STACK", + "Value": "frontend", + }, + { + "Name": "STAGE", + "Value": "PROD", + }, + { + "Name": "APP", + "Value": "tag-page-rendering", + }, + { + "Name": "TASK_NAME", + "Value": "tag-page-rendering", + }, + { + "Name": "GU_REPO", + "Value": "guardian/dotcom-rendering", + }, + ], + "Essential": true, + "FirelensConfiguration": { + "Type": "fluentbit", + }, + "Image": "ghcr.io/guardian/devx-logs@sha256:cf91724a5166f1c143e07958820aa2122afb61c164b68555d15cb92abb5acda0", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "EcsTaskDefinitionLogShippingLogGroup0E405BD0", + }, + "awslogs-region": "eu-west-1", + "awslogs-stream-prefix": "frontend/PROD/tag-page-rendering/devx-logs-sidecar", + }, + }, + "MountPoints": [ + { + "ContainerPath": "/init", + "ReadOnly": false, + "SourceVolume": "logging-volume", + }, + ], + "Name": "LogShipping", + "ReadonlyRootFilesystem": true, + "VersionConsistency": "disabled", + }, + { + "Command": [ + "--config=/etc/ecs/ecs-default-config.yaml", + ], + "Cpu": 256, + "Essential": false, + "HealthCheck": { + "Command": [ + "CMD", + "/healthcheck", + ], + "Interval": 5, + "Retries": 2, + "Timeout": 3, + }, + "Image": "public.ecr.aws/aws-observability/aws-otel-collector@sha256:90b3180c21acb9497110480371a413ed91f2836077f8a8fb4507b019d3c481c0", + "LogConfiguration": { + "LogDriver": "awsfirelens", + "Options": { + "Name": "kinesis_streams", + "region": "eu-west-1", + "retry_limit": "2", + "stream": { + "Ref": "LoggingStreamName", + }, + }, + }, + "Memory": 512, + "Name": "aws-otel-collector", + "ReadonlyRootFilesystem": true, + }, + ], + "Cpu": "1024", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "EcsTaskDefinitionExecutionRoleBE450C73", + "Arn", + ], + }, + "Family": "TagPageRenderingPRODEcsTaskDefinition12224835", + "Memory": "2048", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE", + ], + "RuntimePlatform": { + "CpuArchitecture": "ARM64", + "OperatingSystemFamily": "LINUX", + }, + "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": "PROD", + }, + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "Arn", + ], + }, + "Volumes": [ + { + "Name": "logging-volume", + }, + ], + }, + "Type": "AWS::ECS::TaskDefinition", + }, + "EcsTaskDefinitionExecutionRoleBE450C73": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "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": "PROD", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "EcsTaskDefinitionExecutionRoleDefaultPolicy1611A942": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ecr:eu-west-1:", + { + "Ref": "DeployToolsAccountIdParameter", + }, + ":repository/guardian/dotcom-rendering", + ], + ], + }, + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*", + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ], + "Effect": "Allow", + "Resource": "arn:aws:ecr:eu-west-1:694911143906:repository/aws-guardduty-agent-fargate", + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsTaskDefinitionLogShippingLogGroup0E405BD0", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "EcsTaskDefinitionExecutionRoleDefaultPolicy1611A942", + "Roles": [ + { + "Ref": "EcsTaskDefinitionExecutionRoleBE450C73", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "EcsTaskDefinitionLogShippingLogGroup0E405BD0": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 1, + "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": "PROD", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "EcsTaskDefinitionTaskRoleB7B6D8DD": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "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": "PROD", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutLogEvents", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:DescribeLogGroups", + "logs:PutRetentionPolicy", + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets", + "xray:GetSamplingStatisticSummaries", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057", + "Roles": [ + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GetDistributablePolicyTagpagerendering346128E3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "DistributionBucketName", + }, + "/frontend/PROD/tag-page-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GetDistributablePolicyTagpagerendering346128E3", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE": { + "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": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36": { + "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": "tag-page-rendering-ecs", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsfromTagPageRenderingPRODInternalIngressSecurityGroupTagpagerendering1CF134899000E9252A81": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsfromTagPageRenderingPRODLoadBalancerTagpagerenderingSecurityGroup55D2582D900081AA330A": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], }, + "ToPort": 9000, }, - "Type": "AWS::EC2::SecurityGroup", + "Type": "AWS::EC2::SecurityGroupIngress", }, "GuHttpsEgressSecurityGroupTagpagerenderingfromTagPageRenderingPRODInternalIngressSecurityGroupTagpagerendering1CF134899000DECEA4B2": { "Properties": { @@ -4248,6 +4933,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD { "Ref": "InstanceRoleTagpagerendering171BC2F9", }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, ], }, "Type": "AWS::IAM::Policy", @@ -4474,6 +5162,27 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD }, "Type": "AWS::EC2::SecurityGroupEgress", }, + "InternalIngressSecurityGroupTagpagerenderingtoTagPageRenderingPRODGuHttpsEgressSecurityGroupTagpagerenderingecs7BD0DC4E9000C0C61B8C": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, "LatencyScaleDownPolicyLowerAlarm3AAA79CF": { "Properties": { "AlarmActions": [ @@ -4672,8 +5381,21 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD ], "DefaultActions": [ { - "TargetGroupArn": { - "Ref": "TargetGroupTagpagerendering42E428EE", + "ForwardConfig": { + "TargetGroups": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + "Weight": 1, + }, + { + "TargetGroupArn": { + "Ref": "TagpagerenderingEcsTargetGroupTagpagerendering0976D2E5", + }, + "Weight": 0, + }, + ], }, "Type": "forward", }, @@ -4709,6 +5431,106 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD }, "Type": "AWS::ElasticLoadBalancingV2::Listener", }, + "ListenerTagpagerenderingDeterministicRouteToEc2Rule1C5C95A4": { + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + "Type": "forward", + }, + ], + "Conditions": [ + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "X-Gu-Target-Group", + "Values": [ + "ec2", + ], + }, + }, + ], + "ListenerArn": { + "Ref": "ListenerTagpagerendering92E57078", + }, + "Priority": 10, + "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": "PROD", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + }, + "ListenerTagpagerenderingDeterministicRouteToEcsRuleAB05C756": { + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "TagpagerenderingEcsTargetGroupTagpagerendering0976D2E5", + }, + "Type": "forward", + }, + ], + "Conditions": [ + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "X-Gu-Target-Group", + "Values": [ + "ecs", + ], + }, + }, + ], + "ListenerArn": { + "Ref": "ListenerTagpagerendering92E57078", + }, + "Priority": 11, + "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": "PROD", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + }, "LoadBalancerDNS": { "Properties": { "Name": "tag-page-rendering.guardianapis.com", @@ -4856,6 +5678,27 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD }, "Type": "AWS::EC2::SecurityGroupEgress", }, + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingPRODGuHttpsEgressSecurityGroupTagpagerenderingecs7BD0DC4E9000E1B35F77": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, "ParameterStoreReadTagpagerendering4CA7B6E3": { "Properties": { "PolicyDocument": { @@ -4903,6 +5746,9 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD { "Ref": "InstanceRoleTagpagerendering171BC2F9", }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, ], }, "Type": "AWS::IAM::Policy", @@ -4958,6 +5804,55 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering PROD }, "Type": "AWS::IAM::Policy", }, + "TagpagerenderingEcsTargetGroupTagpagerendering0976D2E5": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "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": "PROD", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "ip", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, "TargetGroupTagpagerendering42E428EE": { "Properties": { "HealthCheckIntervalSeconds": 10, @@ -5307,6 +6202,39 @@ systemctl start tag-page-rendering", }, "Type": "AWS::IAM::InstanceProfile", }, + "tagpagerenderingEcsClusterE7696595": { + "Properties": { + "ClusterSettings": [ + { + "Name": "containerInsights", + "Value": "enhanced", + }, + ], + "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": "PROD", + }, + ], + }, + "Type": "AWS::ECS::Cluster", + }, }, } `; diff --git a/dotcom-rendering/cdk/lib/renderingStack.test.ts b/dotcom-rendering/cdk/lib/renderingStack.test.ts index c42a045c65f..7e74eb2881b 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.test.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.test.ts @@ -2,8 +2,8 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; import { - getTagPageRenderingPropsCODE, - getTagPageRenderingPropsPROD, + tagPageRenderingPropsCODE, + tagPageRenderingPropsPROD, } from '../bin/cdk'; import { RenderingCDKStack } from './renderingStack'; @@ -58,9 +58,11 @@ describe('The RenderingCDKStack', () => { it('matches the snapshot for Tag Page Rendering PROD (does not use ECS)', () => { const app = new App(); - const stack = new RenderingCDKStack(app, 'TagPageRendering-PROD', { - ...getTagPageRenderingPropsPROD(), // Could have an image identifier in future - }); + const stack = new RenderingCDKStack( + app, + 'TagPageRendering-PROD', + tagPageRenderingPropsPROD, + ); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); }); @@ -68,9 +70,11 @@ describe('The RenderingCDKStack', () => { it('matches the snapshot for Tag Page Rendering CODE (uses ECS)', () => { const app = new App(); - const stack = new RenderingCDKStack(app, 'TagPageRendering-CODE', { - ...getTagPageRenderingPropsCODE('sha256:12345'), // Usually taken from process.env - }); + const stack = new RenderingCDKStack( + app, + 'TagPageRendering-CODE', + tagPageRenderingPropsCODE, + ); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); }); diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 4914dcc4322..c0eab8f398d 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -9,9 +9,9 @@ "lint:check": "eslint --quiet --cache .", "lint:stats": "pnpm lint --format node_modules/eslint-stats/byError.js", "tsc": "tsc", - "test": "jest --maxWorkers=50%", - "test:watch": "jest --watch --maxWorkers=25%", - "test:ci": "jest --runInBand", + "test": "IMAGE_DIGEST=sha256:12345 jest --maxWorkers=50%", + "test:watch": "IMAGE_DIGEST=sha256:12345 jest --watch --maxWorkers=25%", + "test:ci": "IMAGE_DIGEST=sha256:12345 jest --runInBand", "playwright:open": "playwright test --ui", "playwright:run": "playwright test", "unused-exports": "pnpm ts-unused-exports ./tsconfig.json --ignoreFiles='(/(fixtures|__mocks__)/|.+\\.(stories|mocks))' --exitWithCount",