diff --git a/cdk/infra.ts b/cdk/infra.ts index 3c8d498..2de9244 100644 --- a/cdk/infra.ts +++ b/cdk/infra.ts @@ -2,9 +2,9 @@ import { GuEc2App } from '@guardian/cdk'; import { AccessScope } from '@guardian/cdk/lib/constants'; import type { GuStackProps } from '@guardian/cdk/lib/constructs/core'; import { - GuAnghammaradTopicParameter, - GuDistributionBucketParameter, - GuStack, + GuAnghammaradTopicParameter, + GuDistributionBucketParameter, + GuStack, } from '@guardian/cdk/lib/constructs/core'; import { GuCname } from '@guardian/cdk/lib/constructs/dns/'; import { GuVpc } from '@guardian/cdk/lib/constructs/ec2'; @@ -13,25 +13,25 @@ import { GuardianAwsAccounts } from '@guardian/private-infrastructure-config'; import type { App } from 'aws-cdk-lib'; import { Duration, SecretValue } from 'aws-cdk-lib'; import { - InstanceClass, - InstanceSize, - InstanceType, - SecurityGroup, + InstanceClass, + InstanceSize, + InstanceType, + SecurityGroup, } from 'aws-cdk-lib/aws-ec2'; import { - ListenerAction, - ListenerCondition, + ListenerAction, + ListenerCondition, } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { - AccountPrincipal, - ArnPrincipal, - PolicyStatement, + AccountPrincipal, + ArnPrincipal, + PolicyStatement, } from 'aws-cdk-lib/aws-iam'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; interface InfraProps extends GuStackProps { - app: string; - domainName: string; + app: string; + domainName: string; } // It is surprisingly tricky in AWS to setup a static site, with a custom domain @@ -52,27 +52,27 @@ interface InfraProps extends GuStackProps { // // etc. export class Infra extends GuStack { - constructor(scope: App, id: string, props: InfraProps) { - super(scope, id, props); - - const app = props.app; - const bucket = new GuS3Bucket(this, 'static', { - websiteIndexDocument: 'index.html', - app, - }); - - this.overrideLogicalId(bucket, { - logicalId: 'staticD8C87B36', - reason: - 'Retaining a stateful resource previously defined as a Bucket, not a GuS3Bucket', - }); - - const keyPrefix = `${this.stack}/${this.stage}/${app}`; - const port = 9000; - const distBucket = - GuDistributionBucketParameter.getInstance(this).valueAsString; - - const userData = `#!/bin/bash -ev + constructor(scope: App, id: string, props: InfraProps) { + super(scope, id, props); + + const app = props.app; + const bucket = new GuS3Bucket(this, 'static', { + websiteIndexDocument: 'index.html', + app, + }); + + this.overrideLogicalId(bucket, { + logicalId: 'staticD8C87B36', + reason: + 'Retaining a stateful resource previously defined as a Bucket, not a GuS3Bucket', + }); + + const keyPrefix = `${this.stack}/${this.stage}/${app}`; + const port = 9000; + const distBucket = + GuDistributionBucketParameter.getInstance(this).valueAsString; + + const userData = `#!/bin/bash -ev cat << EOF > /etc/systemd/system/${app}.service [Unit] Description=Static Site service @@ -91,151 +91,151 @@ chmod +x /${app} systemctl start ${app} `; - const ec2 = new GuEc2App(this, { - app: app, - access: { - scope: AccessScope.PUBLIC, // But note, Google auth required. - }, - instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), - applicationPort: port, - monitoringConfiguration: { - snsTopicName: - GuAnghammaradTopicParameter.getInstance(this).valueAsString, - unhealthyInstancesAlarm: true, - http5xxAlarm: { - tolerated5xxPercentage: 1, - numberOfMinutesAboveThresholdBeforeAlarm: 60, - }, - }, - certificateProps: { domainName: props.domainName }, - scaling: { minimumInstances: 1, maximumInstances: 2 }, - userData: userData, - imageRecipe: 'arm64-bionic-java11-deploy-infrastructure', - applicationLogging: { enabled: true }, - }); - - // Need to give the ALB outbound access on 443 for the IdP endpoints. - const sg = new SecurityGroup(this, 'ldp-access', { - vpc: GuVpc.fromIdParameter(this, 'vpc', {}), - allowAllOutbound: true, - }); - - ec2.loadBalancer.addSecurityGroup(sg); - - bucket.grantRead(ec2.autoScalingGroup); - - // Google Auth stuff... - - const configPrefix = `/${this.stage}/${this.stack}/${app}`; - const clientIdPath = `${configPrefix}/googleClientID`; - - const clientId = StringParameter.fromStringParameterAttributes( - this, - 'clientID', - { - parameterName: clientIdPath, - }, - ).stringValue; - - // Unfortunately, Cloudformation doesn't support directly using secret - // Parameter Store values. But it is possible to use Secrets Manager. - const secretPath = `${configPrefix}/clientSecret`; - const clientSecret = SecretValue.secretsManager(secretPath); - - const authAction = ListenerAction.authenticateOidc({ - next: ListenerAction.forward([ec2.targetGroup]), - clientId: clientId, - clientSecret: clientSecret, - scope: 'openid email', - - // See the `hd` section of - // https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters. - // Note, this is NOT sufficient to ensure access is limited to Guardian - // emails. Users should also validate the token and check the domain in - // their app. - authenticationRequestExtraParams: { hd: 'guardian.co.uk' }, - - authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', - issuer: 'https://accounts.google.com', - tokenEndpoint: 'https://oauth2.googleapis.com/token', - userInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo', - }); - - ec2.listener.addTargetGroups('PRout', { - priority: 1, - conditions: [ListenerCondition.pathPatterns(['**/_prout'])], - targetGroups: [ec2.targetGroup], - }); - ec2.listener.addAction('auth', { action: authAction }); - - new GuCname(this, 'DNS', { - app: app, - domainName: props.domainName, - resourceRecord: ec2.loadBalancer.loadBalancerDnsName, - ttl: Duration.hours(1), - }); - - // Used in the riff-raff.yaml of static sites to determine the bucket to - // upload resources to. - new StringParameter(this, 'static-site-bucket', { - description: 'Bucket for static sites.', - parameterName: `${configPrefix}/bucket`, - stringValue: bucket.bucketName, - }); - - // Used by static site Cloudformations to attach certs. - new StringParameter(this, 'static-site-alb-dns-name', { - description: 'ALB DNS name for static sites.', - parameterName: `${configPrefix}/loadBalancerDnsName`, - stringValue: ec2.loadBalancer.loadBalancerDnsName, - }); - - new StringParameter(this, 'static-site-lisener-arn', { - description: 'Listener ARN for static sites.', - parameterName: `${configPrefix}/listenerArn`, - stringValue: ec2.listener.listenerArn, - }); - - // Grant access to allow Galaxies data-refresher-lambda to write to relevant portion of the bucket - // https://github.com/guardian/galaxies - Object.values({ - PROD: { - prefix: 'galaxies.gutools.co.uk/data/*', - principals: [ - new ArnPrincipal( - `arn:aws:iam::${GuardianAwsAccounts.DeveloperPlayground}:role/galaxies-data-refresher-lambda-role-PROD`, - ), - ], - }, - CODE: { - prefix: 'galaxies.code.dev-gutools.co.uk/data/*', - principals: [ - new AccountPrincipal(GuardianAwsAccounts.DeveloperPlayground), // for local development - new ArnPrincipal( - `arn:aws:iam::${GuardianAwsAccounts.DeveloperPlayground}:role/galaxies-data-refresher-lambda-role-CODE`, - ), - ], - }, - }).forEach(({ principals, prefix }) => { - bucket.addToResourcePolicy( - new PolicyStatement({ - resources: [bucket.arnForObjects(prefix)], - actions: ['s3:PutObject'], - principals: principals, - }), - ); - bucket.addToResourcePolicy( - new PolicyStatement({ - resources: [bucket.bucketArn], - actions: ['s3:ListBucket'], - principals: principals, - conditions: { - StringLike: { - 's3:prefix': [prefix], - }, - }, - }), - ); - }); - } + const ec2 = new GuEc2App(this, { + app: app, + access: { + scope: AccessScope.PUBLIC, // But note, Google auth required. + }, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), + applicationPort: port, + monitoringConfiguration: { + snsTopicName: + GuAnghammaradTopicParameter.getInstance(this).valueAsString, + unhealthyInstancesAlarm: true, + http5xxAlarm: { + tolerated5xxPercentage: 1, + numberOfMinutesAboveThresholdBeforeAlarm: 60, + }, + }, + certificateProps: { domainName: props.domainName }, + scaling: { minimumInstances: 1, maximumInstances: 2 }, + userData: userData, + imageRecipe: 'arm64-bionic-java11-deploy-infrastructure', + applicationLogging: { enabled: true }, + }); + + // Need to give the ALB outbound access on 443 for the IdP endpoints. + const sg = new SecurityGroup(this, 'ldp-access', { + vpc: GuVpc.fromIdParameter(this, 'vpc', {}), + allowAllOutbound: true, + }); + + ec2.loadBalancer.addSecurityGroup(sg); + + bucket.grantRead(ec2.autoScalingGroup); + + // Google Auth stuff... + + const configPrefix = `/${this.stage}/${this.stack}/${app}`; + const clientIdPath = `${configPrefix}/googleClientID`; + + const clientId = StringParameter.fromStringParameterAttributes( + this, + 'clientID', + { + parameterName: clientIdPath, + }, + ).stringValue; + + // Unfortunately, Cloudformation doesn't support directly using secret + // Parameter Store values. But it is possible to use Secrets Manager. + const secretPath = `${configPrefix}/clientSecret`; + const clientSecret = SecretValue.secretsManager(secretPath); + + const authAction = ListenerAction.authenticateOidc({ + next: ListenerAction.forward([ec2.targetGroup]), + clientId: clientId, + clientSecret: clientSecret, + scope: 'openid email', + + // See the `hd` section of + // https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters. + // Note, this is NOT sufficient to ensure access is limited to Guardian + // emails. Users should also validate the token and check the domain in + // their app. + authenticationRequestExtraParams: { hd: 'guardian.co.uk' }, + + authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + issuer: 'https://accounts.google.com', + tokenEndpoint: 'https://oauth2.googleapis.com/token', + userInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo', + }); + + ec2.listener.addTargetGroups('PRout', { + priority: 1, + conditions: [ListenerCondition.pathPatterns(['**/_prout'])], + targetGroups: [ec2.targetGroup], + }); + ec2.listener.addAction('auth', { action: authAction }); + + new GuCname(this, 'DNS', { + app: app, + domainName: props.domainName, + resourceRecord: ec2.loadBalancer.loadBalancerDnsName, + ttl: Duration.hours(1), + }); + + // Used in the riff-raff.yaml of static sites to determine the bucket to + // upload resources to. + new StringParameter(this, 'static-site-bucket', { + description: 'Bucket for static sites.', + parameterName: `${configPrefix}/bucket`, + stringValue: bucket.bucketName, + }); + + // Used by static site Cloudformations to attach certs. + new StringParameter(this, 'static-site-alb-dns-name', { + description: 'ALB DNS name for static sites.', + parameterName: `${configPrefix}/loadBalancerDnsName`, + stringValue: ec2.loadBalancer.loadBalancerDnsName, + }); + + new StringParameter(this, 'static-site-lisener-arn', { + description: 'Listener ARN for static sites.', + parameterName: `${configPrefix}/listenerArn`, + stringValue: ec2.listener.listenerArn, + }); + + // Grant access to allow Galaxies data-refresher-lambda to write to relevant portion of the bucket + // https://github.com/guardian/galaxies + Object.values({ + PROD: { + prefix: 'galaxies.gutools.co.uk/data/*', + principals: [ + new ArnPrincipal( + `arn:aws:iam::${GuardianAwsAccounts.HiringAndOnboarding}:role/galaxies-data-refresher-lambda-role-PROD`, + ), + ], + }, + CODE: { + prefix: 'galaxies.code.dev-gutools.co.uk/data/*', + principals: [ + new AccountPrincipal(GuardianAwsAccounts.HiringAndOnboarding), // for local development + new ArnPrincipal( + `arn:aws:iam::${GuardianAwsAccounts.HiringAndOnboarding}:role/galaxies-data-refresher-lambda-role-CODE`, + ), + ], + }, + }).forEach(({ principals, prefix }) => { + bucket.addToResourcePolicy( + new PolicyStatement({ + resources: [bucket.arnForObjects(prefix)], + actions: ['s3:PutObject'], + principals: principals, + }), + ); + bucket.addToResourcePolicy( + new PolicyStatement({ + resources: [bucket.bucketArn], + actions: ['s3:ListBucket'], + principals: principals, + conditions: { + StringLike: { + 's3:prefix': [prefix], + }, + }, + }), + ); + }); + } } diff --git a/package-lock.json b/package-lock.json index f045c78..7830838 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@guardian/cdk": "48.5.1", "@guardian/eslint-config-typescript": "^1.0.11", "@guardian/prettier": "^2.0.0", - "@guardian/private-infrastructure-config": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#v2.1.3", + "@guardian/private-infrastructure-config": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#8df31f0cb9fcdec2e0417d728c837acf32f642a8", "@types/jest": "^27.4.1", "@types/js-yaml": "^4.0.5", "@types/node": "^17.0.23", @@ -1876,8 +1876,9 @@ } }, "node_modules/@guardian/private-infrastructure-config": { - "version": "2.1.2", - "resolved": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#834dd66e3880defebfc2a0d9a695957dc5e4b89d", + "version": "2.3.0", + "resolved": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#8df31f0cb9fcdec2e0417d728c837acf32f642a8", + "integrity": "sha512-bBjVdT1AiieUodEhkwppPupDwxINg6HTUpxEXhIAaHhn6QF1uFGJnVk0OT4o5cOgCU5yRRH7ZWwdQiilulby+g==", "dev": true }, "node_modules/@humanwhocodes/config-array": { @@ -10462,9 +10463,10 @@ "requires": {} }, "@guardian/private-infrastructure-config": { - "version": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#834dd66e3880defebfc2a0d9a695957dc5e4b89d", + "version": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#8df31f0cb9fcdec2e0417d728c837acf32f642a8", + "integrity": "sha512-bBjVdT1AiieUodEhkwppPupDwxINg6HTUpxEXhIAaHhn6QF1uFGJnVk0OT4o5cOgCU5yRRH7ZWwdQiilulby+g==", "dev": true, - "from": "@guardian/private-infrastructure-config@git+ssh://git@github.com/guardian/private-infrastructure-config.git#v2.1.3" + "from": "@guardian/private-infrastructure-config@git+ssh://git@github.com/guardian/private-infrastructure-config.git#8df31f0cb9fcdec2e0417d728c837acf32f642a8" }, "@humanwhocodes/config-array": { "version": "0.11.7", diff --git a/package.json b/package.json index fcb7552..ae23335 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,13 @@ }, "author": "nicolas.long@theguardian.com", "license": "ISC", - "eslintConfig": { + "eslintConfig": { "extends": "@guardian/eslint-config-typescript" }, "eslintIgnore": [ "packages/common/dist" ], "prettier": "@guardian/prettier", - "dependencies": { "@actions/artifact": "^1.1.0", "@actions/core": "^1.6.0", @@ -35,7 +34,7 @@ "@guardian/cdk": "48.5.1", "@guardian/eslint-config-typescript": "^1.0.11", "@guardian/prettier": "^2.0.0", - "@guardian/private-infrastructure-config": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#v2.1.3", + "@guardian/private-infrastructure-config": "git+ssh://git@github.com/guardian/private-infrastructure-config.git#8df31f0cb9fcdec2e0417d728c837acf32f642a8", "@types/jest": "^27.4.1", "@types/js-yaml": "^4.0.5", "@types/node": "^17.0.23", @@ -49,4 +48,4 @@ "ts-jest": "^27.1.4", "typescript": "^4.6.3" } -} +} \ No newline at end of file