diff --git a/dotcom-rendering/cdk/bin/cdk.ts b/dotcom-rendering/cdk/bin/cdk.ts index 87be989429a..147f222443c 100644 --- a/dotcom-rendering/cdk/bin/cdk.ts +++ b/dotcom-rendering/cdk/bin/cdk.ts @@ -1,11 +1,15 @@ import { App, Duration } from 'aws-cdk-lib'; -import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; +import { + InstanceType as Ec2InstanceType, + InstanceClass, + InstanceSize, +} from 'aws-cdk-lib/aws-ec2'; import type { RenderingCDKStackProps } from '../lib/renderingStack'; import { RenderingCDKStack } from '../lib/renderingStack'; function getImageIdentifier(): string { const imageIdentifier: string | undefined = process.env.IMAGE_DIGEST; - if (!imageIdentifier) { + if (imageIdentifier == null) { throw new Error('Image identifier must be provided'); } return imageIdentifier; @@ -24,50 +28,6 @@ const cpuScalingSteps = { ], }; -/** Article */ -new RenderingCDKStack(cdkApp, 'ArticleRendering-CODE', { - guApp: 'article-rendering', - stage: 'CODE', - domainName: 'article-rendering.code.dev-guardianapis.com', - ec2Props: { - scaling: { minimumInstances: 1, maximumInstances: 3 }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), - }, -}); -new RenderingCDKStack(cdkApp, 'ArticleRendering-PROD', { - guApp: 'article-rendering', - stage: 'PROD', - domainName: 'article-rendering.guardianapis.com', - ec2Props: { - scaling: { - minimumInstances: 42, - maximumInstances: 240, - policies: { - step: { - cpu: cpuScalingSteps, - latency: { - scalingStepsOut: [ - // When latency is lower than 0.2s no scaling up - { lower: 0, upper: 0.2, change: 0 }, - // When latency is higher than 0.2s we scale up by 50% - { lower: 0.2, change: 50 }, - // When latency is higher than 0.3s we scale up by 80% - { lower: 0.3, change: 80 }, - ], - scalingStepsIn: [ - // When latency is higher than 0.12s no scaling down - { lower: 0.12, change: 0 }, - // When latency is lower than 0.12s we scale down by 1 - { upper: 0.12, lower: 0, change: -1 }, - ], - }, - }, - }, - }, - instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), - }, -}); - /** Facia */ new RenderingCDKStack(cdkApp, 'FaciaRendering-CODE', { guApp: 'facia-rendering', @@ -75,7 +35,7 @@ new RenderingCDKStack(cdkApp, 'FaciaRendering-CODE', { domainName: 'facia-rendering.code.dev-guardianapis.com', ec2Props: { scaling: { minimumInstances: 1, maximumInstances: 3 }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), + instanceType: Ec2InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), }, }); new RenderingCDKStack(cdkApp, 'FaciaRendering-PROD', { @@ -108,7 +68,10 @@ new RenderingCDKStack(cdkApp, 'FaciaRendering-PROD', { }, }, }, - instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), + instanceType: Ec2InstanceType.of( + InstanceClass.C8G, + InstanceSize.MEDIUM, + ), }, }); @@ -185,7 +148,7 @@ export const InteractiveRenderingPropsCODE: RenderingCDKStackProps = { domainName: 'interactive-rendering.code.dev-guardianapis.com', ec2Props: { scaling: { minimumInstances: 1, maximumInstances: 3 }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MICRO), + instanceType: Ec2InstanceType.of(InstanceClass.T4G, InstanceSize.MICRO), }, ecsProps: { imageIdentifier: getImageIdentifier(), @@ -247,7 +210,10 @@ export const InteractiveRenderingPropsPROD: RenderingCDKStackProps = { }, }, }, - instanceType: InstanceType.of(InstanceClass.C8G, InstanceSize.MEDIUM), + instanceType: Ec2InstanceType.of( + InstanceClass.C8G, + InstanceSize.MEDIUM, + ), }, }; new RenderingCDKStack( @@ -255,3 +221,82 @@ new RenderingCDKStack( 'InteractiveRendering-PROD', InteractiveRenderingPropsPROD, ); + +/** Article */ +export const ArticleRenderingPropsCODE: RenderingCDKStackProps = { + guApp: 'article-rendering', + stage: 'CODE', + domainName: 'article-rendering.code.dev-guardianapis.com', + ec2Props: { + scaling: { minimumInstances: 1, maximumInstances: 3 }, + instanceType: Ec2InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), + }, + ecsProps: { + imageIdentifier: getImageIdentifier(), + taskCpu: 2048, + taskMemoryLimitMiB: 4096, + scaling: { + minimumTasks: 1, + maximumTasks: 3, + cpuScaling: { + targetValue: 20, + scaleInCooldown: Duration.seconds(60), + scaleOutCooldown: Duration.seconds(60), + }, + }, + // Route all traffic to EC2 while ECS is tested in CODE. + targetGroupWeights: { + ec2: 1, + ecs: 0, + }, + }, +}; + +export const ArticleRenderingPropsPROD: RenderingCDKStackProps = { + guApp: 'article-rendering', + stage: 'PROD', + domainName: 'article-rendering.dev-guardianapis.com', + ec2Props: { + scaling: { + minimumInstances: 42, + maximumInstances: 240, + policies: { + step: { + cpu: cpuScalingSteps, + latency: { + scalingStepsOut: [ + // When latency is lower than 0.2s no scaling up + { lower: 0, upper: 0.2, change: 0 }, + // When latency is higher than 0.2s we scale up by 50% + { lower: 0.2, change: 50 }, + // When latency is higher than 0.3s we scale up by 80% + { lower: 0.3, change: 80 }, + ], + scalingStepsIn: [ + // When latency is higher than 0.12s no scaling down + { lower: 0.12, change: 0 }, + // When latency is lower than 0.12s we scale down by 1 + { upper: 0.12, lower: 0, change: -1 }, + ], + }, + }, + }, + }, + instanceType: Ec2InstanceType.of( + InstanceClass.C8G, + InstanceSize.MEDIUM, + ), + }, +}; + +new RenderingCDKStack( + cdkApp, + 'ArticleRendering-CODE', + ArticleRenderingPropsCODE, +); + +new RenderingCDKStack( + cdkApp, + 'ArticleRendering-PROD', + ArticleRenderingPropsPROD, +); diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index b2bd3abd366..1901192411b 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -1,5 +1,2083 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`The RenderingCDKStack configures ECS for Article Rendering CODE without routing traffic to it 1`] = ` +{ + "Metadata": { + "gu:cdk:constructs": [ + "GuDistributionBucketParameter", + "GuAllowPolicy", + "GuAllowPolicy", + "GuAllowPolicy", + "GuVpcParameter", + "GuSubnetListParameter", + "GuSubnetListParameter", + "GuLoadBalancedAppExperimental", + "GuInstanceRole", + "GuSsmSshPolicy", + "GuDescribeEC2Policy", + "GuLoggingStreamNameParameter", + "GuLogShippingPolicy", + "GuGetDistributablePolicy", + "GuParameterStoreReadPolicy", + "GuAmiParameter", + "GuHttpsEgressSecurityGroup", + "GuAutoScalingGroup", + "GuApplicationTargetGroup", + "GuRiffRaffDeploymentIdParameterExperimental", + "GuHttpsEgressSecurityGroup", + "GuApplicationTargetGroup", + "GuCertificate", + "GuApplicationLoadBalancer", + "GuAccessLoggingBucketParameter", + "GuHttpsApplicationListener", + "GuSecurityGroup", + "GuCname", + ], + "gu:cdk:version": "TEST", + }, + "Outputs": { + "LoadBalancerArticlerenderingDnsName": { + "Description": "DNS entry for LoadBalancerArticlerendering", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerArticlerendering49883BBC", + "DNSName", + ], + }, + }, + }, + "Parameters": { + "AMIArticlerendering": { + "Description": "Amazon Machine Image ID for the app article-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", + }, + "ArtifactAccountIdParameter": { + "Default": "/organisation/accounts/artifacts", + "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", + }, + "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.", + "Type": "AWS::SSM::Parameter::Value", + }, + "articlerenderingPrivateSubnets": { + "Default": "/account/vpc/primary/subnets/private", + "Description": "A list of private subnets", + "Type": "AWS::SSM::Parameter::Value>", + }, + "articlerenderingPublicSubnets": { + "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": "InstanceRoleArticlerendering0D4717E8", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "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": "InstanceRoleArticlerendering0D4717E8", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "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": "InstanceRoleArticlerendering0D4717E8", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "ArticlerenderingEcsTargetGroupArticlerenderingD522DD78": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "ip", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "AutoScalingGroupArticlerenderingASG8488C3F1": { + "Properties": { + "HealthCheckGracePeriod": 120, + "HealthCheckType": "ELB", + "LaunchTemplate": { + "LaunchTemplateId": { + "Ref": "frontendCODEarticlerenderingCAC5605E", + }, + "Version": { + "Fn::GetAtt": [ + "frontendCODEarticlerenderingCAC5605E", + "LatestVersionNumber", + ], + }, + }, + "MaxSize": "3", + "MetricsCollection": [ + { + "Granularity": "1Minute", + }, + ], + "MinSize": "1", + "Tags": [ + { + "Key": "App", + "PropagateAtLaunch": true, + "Value": "article-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": "CODE", + }, + { + "Key": "SystemdUnit", + "PropagateAtLaunch": true, + "Value": "article-rendering.service", + }, + ], + "TargetGroupARNs": [ + { + "Ref": "TargetGroupArticlerendering3605E498", + }, + ], + "VPCZoneIdentifier": { + "Ref": "articlerenderingPrivateSubnets", + }, + }, + "Type": "AWS::AutoScaling::AutoScalingGroup", + }, + "CertificateArticlerendering3297EFD3": { + "DeletionPolicy": "Retain", + "Properties": { + "DomainName": "article-rendering.code.dev-guardianapis.com", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "ArticleRendering-CODE/CertificateArticlerendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "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": "InstanceRoleArticlerendering0D4717E8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "EcsService81FC6EF6": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057", + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "ListenerArticlerenderingDeterministicRouteToEc2Rule2FE1C744", + "ListenerArticlerenderingDeterministicRouteToEcsRule264A8CD2", + "ListenerArticlerendering9419D5F6", + ], + "Properties": { + "Cluster": { + "Ref": "articlerenderingEcsCluster778E37FC", + }, + "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": "article-rendering", + "ContainerPort": 9000, + "TargetGroupArn": { + "Ref": "ArticlerenderingEcsTargetGroupArticlerenderingD522DD78", + }, + }, + ], + "Monitoring": { + "MetricConfigurations": [ + { + "MetricNames": [ + "CPUUtilization", + "MemoryUtilization", + ], + "ResolutionSeconds": 20, + }, + ], + }, + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "articlerenderingPrivateSubnets", + }, + }, + }, + "PropagateTags": "SERVICE", + "Tags": [ + { + "Key": "App", + "Value": "article-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": [ + "EcsTaskDefinitionTaskRoleDefaultPolicy1FD2F057", + "EcsTaskDefinitionTaskRoleB7B6D8DD", + ], + "Properties": { + "MaxCapacity": 3, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "articlerenderingEcsCluster778E37FC", + }, + "/", + { + "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", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "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": "CODE", + }, + { + "Name": "APP", + "Value": "article-rendering", + }, + { + "Name": "TASK_NAME", + "Value": "article-rendering", + }, + { + "Name": "GU_REPO", + "Value": "guardian/dotcom-rendering", + }, + { + "Name": "NODE_ENV", + "Value": "production", + }, + { + "Name": "GU_STAGE", + "Value": "CODE", + }, + { + "Name": "GU_APP", + "Value": "article-rendering", + }, + { + "Name": "GU_STACK", + "Value": "frontend", + }, + { + "Name": "OTEL_EXPORTER_OTLP_ENDPOINT", + "Value": "http://localhost:4318", + }, + { + "Name": "OTEL_SERVICE_NAME", + "Value": "article-rendering", + }, + ], + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "ArtifactAccountIdParameter", + }, + ".dkr.ecr.eu-west-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/guardian/dotcom-rendering/dotcom-rendering@sha256:12345", + ], + ], + }, + "LogConfiguration": { + "LogDriver": "awsfirelens", + "Options": { + "Name": "kinesis_streams", + "region": "eu-west-1", + "retry_limit": "2", + "stream": { + "Ref": "LoggingStreamName", + }, + }, + }, + "Name": "article-rendering", + "PortMappings": [ + { + "ContainerPort": 9000, + "Protocol": "tcp", + }, + ], + "ReadonlyRootFilesystem": true, + "VersionConsistency": "disabled", + }, + { + "Environment": [ + { + "Name": "STACK", + "Value": "frontend", + }, + { + "Name": "STAGE", + "Value": "CODE", + }, + { + "Name": "APP", + "Value": "article-rendering", + }, + { + "Name": "TASK_NAME", + "Value": "article-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/CODE/article-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": "2048", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "EcsTaskDefinitionExecutionRoleBE450C73", + "Arn", + ], + }, + "Family": "ArticleRenderingCODEEcsTaskDefinitionCFB2AAF0", + "Memory": "4096", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE", + ], + "RuntimePlatform": { + "CpuArchitecture": "ARM64", + "OperatingSystemFamily": "LINUX", + }, + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "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": "article-rendering", + }, + { + "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", + }, + "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": "ArtifactAccountIdParameter", + }, + ":repository/guardian/dotcom-rendering/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": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "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": "article-rendering", + }, + { + "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", + }, + "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", + }, + "GetDistributablePolicyArticlerendering7F5C432B": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "DistributionBucketName", + }, + "/frontend/CODE/article-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GetDistributablePolicyArticlerendering7F5C432B", + "Roles": [ + { + "Ref": "InstanceRoleArticlerendering0D4717E8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GuHttpsEgressSecurityGroupArticlerendering4B299451": { + "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": "article-rendering", + }, + { + "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", + }, + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D": { + "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": "article-rendering-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", + }, + "GuHttpsEgressSecurityGroupArticlerenderingecsfromArticleRenderingCODEInternalIngressSecurityGroupArticlerendering9629F60190000B9B2E74": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupArticlerendering3856ADE0", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupArticlerenderingecsfromArticleRenderingCODELoadBalancerArticlerenderingSecurityGroup64C7B7C89000EE0525A0": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerArticlerenderingSecurityGroupB13618F9", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupArticlerenderingfromArticleRenderingCODEInternalIngressSecurityGroupArticlerendering9629F601900045261015": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerendering4B299451", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupArticlerendering3856ADE0", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupArticlerenderingfromArticleRenderingCODELoadBalancerArticlerenderingSecurityGroup64C7B7C890009C4C4659": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerendering4B299451", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerArticlerenderingSecurityGroupB13618F9", + "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": "InstanceRoleArticlerendering0D4717E8", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstanceRoleArticlerendering0D4717E8": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Path": "/", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "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", + }, + "InternalIngressSecurityGroupArticlerendering3856ADE0": { + "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": "article-rendering", + }, + { + "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", + }, + "InternalIngressSecurityGroupArticlerenderingtoArticleRenderingCODEGuHttpsEgressSecurityGroupArticlerenderingEF30A8659000018F8AEF": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerendering4B299451", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupArticlerendering3856ADE0", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "InternalIngressSecurityGroupArticlerenderingtoArticleRenderingCODEGuHttpsEgressSecurityGroupArticlerenderingecs4649E47D9000B8CB9728": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupArticlerendering3856ADE0", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "ListenerArticlerendering9419D5F6": { + "Properties": { + "Certificates": [ + { + "CertificateArn": { + "Ref": "CertificateArticlerendering3297EFD3", + }, + }, + ], + "DefaultActions": [ + { + "ForwardConfig": { + "TargetGroups": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupArticlerendering3605E498", + }, + "Weight": 1, + }, + { + "TargetGroupArn": { + "Ref": "ArticlerenderingEcsTargetGroupArticlerenderingD522DD78", + }, + "Weight": 0, + }, + ], + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": { + "Ref": "LoadBalancerArticlerendering49883BBC", + }, + "Port": 443, + "Protocol": "HTTPS", + "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-2021-06", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "ListenerArticlerenderingDeterministicRouteToEc2Rule2FE1C744": { + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupArticlerendering3605E498", + }, + "Type": "forward", + }, + ], + "Conditions": [ + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "X-Gu-Target-Group", + "Values": [ + "ec2", + ], + }, + }, + ], + "ListenerArn": { + "Ref": "ListenerArticlerendering9419D5F6", + }, + "Priority": 10, + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + }, + "ListenerArticlerenderingDeterministicRouteToEcsRule264A8CD2": { + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "ArticlerenderingEcsTargetGroupArticlerenderingD522DD78", + }, + "Type": "forward", + }, + ], + "Conditions": [ + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "X-Gu-Target-Group", + "Values": [ + "ecs", + ], + }, + }, + ], + "ListenerArn": { + "Ref": "ListenerArticlerendering9419D5F6", + }, + "Priority": 11, + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + }, + "LoadBalancerArticlerendering49883BBC": { + "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/CODE/frontend/article-rendering", + }, + { + "Key": "idle_timeout.timeout_seconds", + "Value": "4", + }, + ], + "Scheme": "internal", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LoadBalancerArticlerenderingSecurityGroupB13618F9", + "GroupId", + ], + }, + { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupArticlerendering3856ADE0", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "articlerenderingPrivateSubnets", + }, + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Type": "application", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "LoadBalancerArticlerenderingSecurityGroupB13618F9": { + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB ArticleRenderingCODELoadBalancerArticlerendering7CA83746", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "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", + }, + "LoadBalancerArticlerenderingSecurityGrouptoArticleRenderingCODEGuHttpsEgressSecurityGroupArticlerenderingEF30A865900081CE56A1": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerendering4B299451", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerArticlerenderingSecurityGroupB13618F9", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "LoadBalancerArticlerenderingSecurityGrouptoArticleRenderingCODEGuHttpsEgressSecurityGroupArticlerenderingecs4649E47D90002EEFECC3": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerenderingecs463B523D", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerArticlerenderingSecurityGroupB13618F9", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "LoadBalancerDNS": { + "Properties": { + "Name": "article-rendering.code.dev-guardianapis.com", + "RecordType": "CNAME", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "LoadBalancerArticlerendering49883BBC", + "DNSName", + ], + }, + ], + "Stage": "CODE", + "TTL": 3600, + }, + "Type": "Guardian::DNS::RecordSet", + }, + "ParameterStoreReadArticlerendering1D29E674": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ssm:GetParametersByPath", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/frontend/article-rendering", + ], + ], + }, + }, + { + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/frontend/article-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "parameter-store-read-policy", + "Roles": [ + { + "Ref": "InstanceRoleArticlerendering0D4717E8", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "RenderingBaseURLParamAE11E777": { + "Properties": { + "Description": "The rendering base URL for frontend to call the article-rendering app in the CODE environment", + "Name": "/frontend/code/article-rendering.baseURL", + "Tags": { + "App": "article-rendering", + "Stack": "frontend", + "Stage": "CODE", + "gu:cdk:version": "TEST", + "gu:repo": "guardian/dotcom-rendering", + }, + "Type": "String", + "Value": "https://article-rendering.code.dev-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": "InstanceRoleArticlerendering0D4717E8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "TargetGroupArticlerendering3605E498": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "instance", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "articlerenderingCpuScaling661251A9": { + "Properties": { + "PolicyName": "ArticleRenderingCODEarticlerenderingCpuScalingC954D21F", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "EcsServiceTaskCountTarget02FCCE22", + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilizationHighResolution", + }, + "ScaleInCooldown": 60, + "ScaleOutCooldown": 60, + "TargetValue": 20, + }, + }, + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + }, + "articlerenderingEcsCluster778E37FC": { + "Properties": { + "ClusterSettings": [ + { + "Name": "containerInsights", + "Value": "enhanced", + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::ECS::Cluster", + }, + "frontendCODEarticlerenderingCAC5605E": { + "DependsOn": [ + "InstanceRoleArticlerendering0D4717E8", + ], + "Properties": { + "LaunchTemplateData": { + "IamInstanceProfile": { + "Arn": { + "Fn::GetAtt": [ + "frontendCODEarticlerenderingProfile9B990A92", + "Arn", + ], + }, + }, + "ImageId": { + "Ref": "AMIArticlerendering", + }, + "InstanceType": "t4g.small", + "MetadataOptions": { + "HttpTokens": "required", + "InstanceMetadataTags": "enabled", + }, + "Monitoring": { + "Enabled": true, + }, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupArticlerendering4B299451", + "GroupId", + ], + }, + ], + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "ArticleRendering-CODE/frontend-CODE-article-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + { + "ResourceType": "volume", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "ArticleRendering-CODE/frontend-CODE-article-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + ], + "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/CODE/article-rendering/article-rendering.tar.gz ./ +tar -zxf article-rendering.tar.gz article-rendering +chown -R dotcom-rendering:frontend article-rendering +cd article-rendering +mkdir /var/log/dotcom-rendering +chown -R dotcom-rendering:frontend /var/log/dotcom-rendering +cat > /etc/systemd/system/article-rendering.service << EOF +[Unit] +Description=article-rendering +After=network.target +[Service] +WorkingDirectory=/home/dotcom-rendering/article-rendering +Type=simple +User=dotcom-rendering +Group=frontend +StandardError=journal +StandardOutput=journal +Environment=TERM=xterm-256color +Environment=NODE_ENV=production +Environment=GU_STAGE=CODE +Environment=GU_APP=article-rendering +Environment=GU_STACK=frontend +ExecStart=make prod +Restart=on-failure +[Install] +WantedBy=multi-user.target +EOF +systemctl enable article-rendering +systemctl start article-rendering", + ], + ], + }, + }, + }, + "TagSpecifications": [ + { + "ResourceType": "launch-template", + "Tags": [ + { + "Key": "App", + "Value": "article-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "ArticleRendering-CODE/frontend-CODE-article-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + ], + }, + "Type": "AWS::EC2::LaunchTemplate", + }, + "frontendCODEarticlerenderingProfile9B990A92": { + "Properties": { + "Roles": [ + { + "Ref": "InstanceRoleArticlerendering0D4717E8", + }, + ], + }, + "Type": "AWS::IAM::InstanceProfile", + }, + }, +} +`; + exports[`The RenderingCDKStack matches the snapshot 1`] = ` { "Metadata": { diff --git a/dotcom-rendering/cdk/lib/renderingStack.test.ts b/dotcom-rendering/cdk/lib/renderingStack.test.ts index b58ef4a1527..6be1a527e30 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.test.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.test.ts @@ -1,7 +1,12 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; -import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; import { + InstanceType as Ec2InstanceType, + InstanceClass, + InstanceSize, +} from 'aws-cdk-lib/aws-ec2'; +import { + ArticleRenderingPropsCODE, InteractiveRenderingPropsCODE, InteractiveRenderingPropsPROD, TagPageRenderingPropsCODE, @@ -50,7 +55,7 @@ describe('The RenderingCDKStack', () => { }, }, }, - instanceType: InstanceType.of( + instanceType: Ec2InstanceType.of( InstanceClass.T4G, InstanceSize.MICRO, ), @@ -60,18 +65,16 @@ describe('The RenderingCDKStack', () => { expect(template.toJSON()).toMatchSnapshot(); }); - // Note: this is handled at the GuCDK library level - it('should not be possible to provision a stack without any compute', () => { - expect(() => { - const app = new App(); - new RenderingCDKStack(app, 'ArticleRendering', { - domainName: 'tag-page-rendering.guardianapis.com', - guApp: 'tag-page-rendering', - stage: 'PROD', - }); - }).toThrow( - "At least one of 'ec2Props' or 'ecsProps' must be specified", + it('configures ECS for Article Rendering CODE without routing traffic to it', () => { + const app = new App(); + const stack = new RenderingCDKStack( + app, + 'ArticleRendering-CODE', + ArticleRenderingPropsCODE, ); + const template = Template.fromStack(stack); + expect(template.toJSON()).toMatchSnapshot(); + //expectEcsWithEc2Traffic(Template.fromStack(stack)); }); it('matches the snapshot for Tag Page Rendering PROD', () => { diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index 09c8e175d28..eab566ca751 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -16,7 +16,7 @@ import type { ScalingInterval } from 'aws-cdk-lib/aws-applicationautoscaling'; import { AdjustmentType, StepScalingPolicy } from 'aws-cdk-lib/aws-autoscaling'; import { Metric, Unit } from 'aws-cdk-lib/aws-cloudwatch'; import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'; -import type { InstanceType } from 'aws-cdk-lib/aws-ec2'; +import type { InstanceType as EC2InstanceType } from 'aws-cdk-lib/aws-ec2'; import { Peer } from 'aws-cdk-lib/aws-ec2'; import { ContainerDependencyCondition, @@ -46,7 +46,7 @@ export interface RenderingCDKStackProps extends Omit { domainName: string; ec2Props?: { - instanceType: InstanceType; + instanceType: EC2InstanceType; scaling: RenderingEc2ScalingProps; };