in cdk/infra.ts [55:240]
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
[Service]
Environment="BUCKET=${bucket.bucketName}"
Environment="PORT=${port}"
ExecStart=/${app}
[Install]
WantedBy=multi-user.target
EOF
aws s3 cp s3://${distBucket}/${keyPrefix}/static-site-service /${app}
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],
},
},
}),
);
});
}