diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9695104c347..8c396266a26 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,6 +51,10 @@ jobs: - frontend-cfn rendering: - rendering + front-web-cfn: + - front-web-cfn + front-web: + - front-web frontend-static: - frontend-static diff --git a/dotcom-rendering/cdk/bin/cdk.ts b/dotcom-rendering/cdk/bin/cdk.ts index c5b03d052b3..f009c38a29c 100644 --- a/dotcom-rendering/cdk/bin/cdk.ts +++ b/dotcom-rendering/cdk/bin/cdk.ts @@ -5,13 +5,13 @@ import { DotcomRendering } from '../lib/dotcom-rendering'; const app = new App(); const sharedProps = { - app: 'rendering', stack: 'frontend', region: 'eu-west-1', }; new DotcomRendering(app, 'DotcomRendering-PROD', { ...sharedProps, + app: 'rendering', stage: 'PROD', minCapacity: 30, maxCapacity: 120, @@ -20,8 +20,28 @@ new DotcomRendering(app, 'DotcomRendering-PROD', { new DotcomRendering(app, 'DotcomRendering-CODE', { ...sharedProps, + app: 'rendering', stage: 'CODE', minCapacity: 1, maxCapacity: 4, instanceType: 't4g.micro', }); + +new DotcomRendering(app, 'DotcomRendering-front-web-CODE', { + ...sharedProps, + app: 'front-web', + stage: 'CODE', + minCapacity: 1, + maxCapacity: 4, + instanceType: 't4g.micro', +}); + +new DotcomRendering(app, 'DotcomRendering-front-web-PROD', { + ...sharedProps, + app: 'front-web', + stage: 'PROD', + // TODO: up this once we have code working + minCapacity: 1, + maxCapacity: 4, + instanceType: 't4g.micro', +}); diff --git a/dotcom-rendering/cdk/lib/__snapshots__/dotcom-rendering.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/dotcom-rendering.test.ts.snap index 70e275fdda2..579bcbfdadb 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/dotcom-rendering.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/dotcom-rendering.test.ts.snap @@ -1081,6 +1081,25 @@ sudo NODE_ENV=$NODE_ENV GU_STAGE=$GU_STAGE -u dotcom-rendering -g frontend make }, "Type": "AWS::IAM::InstanceProfile", }, + "loadBalancerDnsName0B1DEBAD": { + "Properties": { + "Name": "/frontend/TEST/rendering.loadBalancerDnsName", + "Tags": { + "Stack": "frontend", + "Stage": "TEST", + "gu:cdk:version": "TEST", + "gu:repo": "guardian/dotcom-rendering", + }, + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "InternalLoadBalancer", + "DNSName", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, }, } `; diff --git a/dotcom-rendering/cdk/lib/dotcom-rendering.ts b/dotcom-rendering/cdk/lib/dotcom-rendering.ts index 13b12ba9188..d599475e4b2 100644 --- a/dotcom-rendering/cdk/lib/dotcom-rendering.ts +++ b/dotcom-rendering/cdk/lib/dotcom-rendering.ts @@ -23,6 +23,7 @@ import { import { CfnAlarm } from 'aws-cdk-lib/aws-cloudwatch'; import { InstanceType, Peer } from 'aws-cdk-lib/aws-ec2'; import { LoadBalancingProtocol } from 'aws-cdk-lib/aws-elasticloadbalancing'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import type { DCRAlarmConfig, DCRProps } from './types'; import { getUserData } from './userData'; @@ -88,7 +89,11 @@ export class DotcomRendering extends GuStack { /** * TODO - migrate this ELB (classic load balancer) to an ALB (application load balancer) * @see https://github.com/guardian/cdk/blob/512536bd590b26d9fcac5d39329e8217103d7859/src/constructs/loadbalancing/elb.ts#L24-L46 + * + * GOTCHA: The load balancer name appends `-ELB` when the `app = "rendering"` for backwards compatibility + * We removed this to avoid the `LoadBalancerName.length > 32`. This will be fixable once we migrate to ALBs. */ + const loadBalancerName = app === 'rendering' ? `${stack}-${stage}-${app}-ELB` : `${stack}-${stage}-${app}`; const loadBalancer = new GuClassicLoadBalancer( this, 'InternalLoadBalancer', @@ -129,7 +134,7 @@ export class DotcomRendering extends GuStack { ], subnetSelection: { subnets: publicSubnets }, propertiesToOverride: { - LoadBalancerName: `${stack}-${stage}-${app}-ELB`, + LoadBalancerName: loadBalancerName, // Note: this does not prevent the GuClassicLoadBalancer // from creating a default security group, though it does // override which one is used/associated with the load balancer @@ -145,6 +150,13 @@ export class DotcomRendering extends GuStack { new CfnOutput(this, 'LoadBalancerUrl', { value: loadBalancer.loadBalancerDnsName, }); + + new StringParameter(this, 'loadBalancerDnsName', { + // Annoyingly this doesn't follow the same pattern as the other SSM parameters + parameterName: `/${stack}/${stage}/${app}.loadBalancerDnsName`, + stringValue: loadBalancer.loadBalancerDnsName, + }); + // ------------------------------------ // ------------------------------------ diff --git a/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs b/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs index 958c5ef61a2..8747cd80279 100755 --- a/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs +++ b/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs @@ -8,89 +8,120 @@ import { log, warn } from '../env/log.js'; const dirname = url.fileURLToPath(new URL('.', import.meta.url)); const target = path.resolve(dirname, '../..', 'target'); -// This task generates the riff-raff bundle. It creates the following -// directory layout under target/ -// -// target -// ├── build.json -// ├── riff-raff.yaml -// ├── frontend-cfn -// │ ├── DotcomRendering-CODE.template.json -// │ ├── DotcomRendering-PROD.template.json -// ├── frontend-static -// │ ├── assets -// │ │ └── ** -// │ │ └── * -// │ └── static -// │ ├── frontend -// │ │ └── ** -// │ │ └── * -// │ └── etc -// └── rendering -// └── dist -// └── rendering.zip - -const copyCfn = () => { - log(' - copying cloudformation config'); - return cpy( +/** This task generates the riff-raff bundle. It creates the following + * directory layout under target/ + * target + * ├── build.json + * ├── riff-raff.yaml + * ├── ${copyFrontendStatic()} + * ├── ${copyApp('rendering')} + * └── ${copyApp('renderi-front')} + */ + +/** + * This method creates a bundle needed to run an app including: + * - CloudFormation files + * - .zip artefact comprised of the JS app + * + * It generates a folder like this: + * ├── ${appName}-cfn + * │ ├── DotcomRendering-${appName}-CODE.template.json + * │ └── DotcomRendering-${appName}-PROD.template.json + * └── ${appName} + * └── dist + * └── ${appName}.zip + * + * Except for the instance where appName === 'rendering' due to backwards compatibility + * + * @param appName {string} + **/ +const copyApp = (appName) => { + // GOTCHA: This is a little hack to be backwards compatible with the naming for when this was a single stack app + const cfnTemplateName = appName === 'rendering' ? '' : `-${appName}`; + const cfnFolder = + appName === 'rendering' ? 'frontend-cfn' : `${appName}-cfn`; + + log(` - copying app: ${appName}`); + + log(` - ${appName}: copying cloudformation config`); + const cfnJob = cpy( [ - 'cdk.out/DotcomRendering-CODE.template.json', - 'cdk.out/DotcomRendering-PROD.template.json', + `cdk.out/DotcomRendering${cfnTemplateName}-CODE.template.json`, + `cdk.out/DotcomRendering${cfnTemplateName}-PROD.template.json`, ], - path.resolve(target, 'frontend-cfn'), + path.resolve(target, cfnFolder), ); + + log(` - ${appName}: copying makefile`); + const makefileJob = cpy(['makefile'], path.resolve(target, appName)); + + log(` - ${appName}: copying server dist`); + const serverDistJob = cpy( + path.resolve(dirname, '../../dist/**'), + path.resolve(target, appName, 'dist'), + { + nodir: true, + }, + ); + + log(`' - ${appName}: copying scripts`); + const scriptsJob = cpy( + path.resolve(dirname, '../../scripts/**'), + path.resolve(target, appName, 'scripts'), + { + nodir: true, + }, + ); + + return [cfnJob, makefileJob, serverDistJob, scriptsJob]; }; -const copyStatic = () => { +/** + * This method copies the static files over the frontend-static folder, which is then deployed to S3. + * + * It generates a folder like this: + * ├── frontend-static + * ├── assets + * │ └── ** + * │ └── * + * └── static + * ├── frontend + * │ └── ** + * │ └── * + * └── etc + */ +const copyFrontendStatic = () => { log(' - copying static'); - return cpy( + const staticJob = cpy( path.resolve(dirname, '../../src/static/**'), path.resolve(target, 'frontend-static', 'static', 'frontend'), { nodir: true, }, ); -}; -const copyDist = () => { - log(' - copying dist'); const source = path.resolve(dirname, '../../dist'); const dest = path.resolve(target, 'frontend-static', 'assets'); - return Promise.all([ - cpy(path.resolve(source, '**/*.!(html|json)'), dest, { - nodir: true, - }), - cpy(path.resolve(source, 'stats'), path.resolve(dest, 'stats'), { - nodir: true, - }), - ]); -}; -const copyScripts = () => { - log(' - copying scripts'); - return cpy( - path.resolve(dirname, '../../scripts/**'), - path.resolve(target, 'rendering', 'scripts'), + log(' - copying dist => assets'); + const distToAssetsJob = cpy( + path.resolve(source, '**/*.!(html|json)'), + dest, { nodir: true, }, ); -}; -const copyDistServer = () => { - log(' - copying server dist'); - return cpy( - path.resolve(dirname, '../../dist/**'), - path.resolve(target, 'rendering', 'dist'), + log(' - copying stats => assets'); + const statsToAssetsJob = cpy( + path.resolve(source, 'stats'), + path.resolve(dest, 'stats'), { nodir: true, }, ); -}; -const copyMakefile = () => { - log(' - copying makefile'); - return cpy(['makefile'], path.resolve(target, 'rendering')); + return [staticJob, distToAssetsJob, statsToAssetsJob]; }; const copyRiffRaff = () => { @@ -99,15 +130,11 @@ const copyRiffRaff = () => { }; Promise.all([ - copyCfn(), - copyMakefile(), - copyStatic(), - copyDist(), - copyDistServer(), - copyScripts(), + ...copyApp('rendering'), + ...copyApp('front-web'), + ...copyFrontendStatic(), copyRiffRaff(), -]) - .catch((err) => { - warn(err.stack); - process.exit(1); - }); +]).catch((err) => { + warn(err.stack); + process.exit(1); +}); diff --git a/dotcom-rendering/scripts/deploy/riff-raff.yaml b/dotcom-rendering/scripts/deploy/riff-raff.yaml index 98073dea797..861dee284fa 100755 --- a/dotcom-rendering/scripts/deploy/riff-raff.yaml +++ b/dotcom-rendering/scripts/deploy/riff-raff.yaml @@ -3,9 +3,18 @@ regions: [eu-west-1] allowedStages: - CODE - PROD +templates: + cloudformation: + type: cloud-formation + parameters: + amiEncrypted: true + amiTags: + # Keep the Node version in sync with `.nvmrc` + Recipe: dotcom-rendering-ARM-jammy-node-18.17.0 + AmigoStage: PROD deployments: frontend-cfn: - type: cloud-formation + template: cloudformation parameters: templateStagePaths: CODE: DotcomRendering-CODE.template.json @@ -13,11 +22,6 @@ deployments: cloudFormationStackByTags: false cloudFormationStackName: rendering amiParameter: AMIRendering - amiEncrypted: true - amiTags: - # Keep the Node version in sync with `.nvmrc` - Recipe: dotcom-rendering-ARM-jammy-node-18.17.0 - AmigoStage: PROD rendering: type: autoscaling parameters: @@ -25,6 +29,22 @@ deployments: dependencies: - frontend-static - frontend-cfn + front-web-cfn: + template: cloudformation + parameters: + templateStagePaths: + CODE: DotcomRendering-front-web-CODE.template.json + PROD: DotcomRendering-front-web-PROD.template.json + cloudFormationStackByTags: false + cloudFormationStackName: front-web + amiParameter: AMIFrontweb + front-web: + type: autoscaling + parameters: + bucketSsmKey: /account/services/dotcom-artifact.bucket + dependencies: + - frontend-static + - frontend-cfn frontend-static: type: aws-s3 parameters: