-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #331 from aligent/feature/DO-1342-prerender-fargate
DO-1342: Prerender hosted in fargate
- Loading branch information
Showing
12 changed files
with
1,191 additions
and
423 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
!lib/prerender/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Prerender in Fargate | ||
A construct to host [Prerender](https://github.com/prerender/prerender) in Fargate. | ||
|
||
## Props | ||
`prerenderName`: Name of the Prerender service | ||
`domainName`: Domain name for Prerender | ||
`vpcId`: VPC to host Prerender in | ||
`bucketName`: Optional S3 bucket name | ||
`expirationDays`: Optional days until items expire in bucket (default to 7 days) | ||
`basicAuthList`: List of basic auth credentials to accept | ||
`certificateArn`: Certificate arn to match the domain | ||
`desiredInstanceCount`: Number of Prerender instances to run (default 1) | ||
`maxInstanceCount`: Maximum number of Prerender instances to run (default 2) | ||
`instanceCPU`: CPU to allocate to each instance (default 512) | ||
`instanceMemory`: Amount of memory to allocate to each instance (default 1024) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { PrerenderFargate } from "./lib/prerender-fargate"; | ||
|
||
export { PrerenderFargate }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { Construct } from 'constructs'; | ||
import * as ecs from 'aws-cdk-lib/aws-ecs'; | ||
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; | ||
import * as ec2 from 'aws-cdk-lib/aws-ec2'; | ||
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; | ||
import { HostedZone } from 'aws-cdk-lib/aws-route53'; | ||
import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3' | ||
import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; | ||
import { AccessKey, User } from 'aws-cdk-lib/aws-iam'; | ||
import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; | ||
import * as path from 'path'; | ||
|
||
export interface PrerenderOptions { | ||
prerenderName: string, | ||
domainName: string, | ||
vpcId: string, | ||
bucketName?: string, | ||
expirationDays?: number, | ||
basicAuthList: Array<string[]>, | ||
certificateArn: string, | ||
desiredInstanceCount?: number, | ||
maxInstanceCount?: number, | ||
instanceCPU?: number, | ||
instanceMemory?: number | ||
} | ||
|
||
export class PrerenderFargate extends Construct { | ||
constructor(scope: Construct, id: string, props: PrerenderOptions) { | ||
super(scope, id); | ||
|
||
// Create bucket for prerender storage | ||
const bucket = new Bucket(this, `${props.prerenderName}-bucket`, { | ||
bucketName: props.bucketName, | ||
lifecycleRules: [{ | ||
enabled: true, | ||
expiration: Duration.days(props.expirationDays || 7) // Default to 7 day expiration | ||
}], | ||
removalPolicy: RemovalPolicy.DESTROY, | ||
autoDeleteObjects: true, | ||
blockPublicAccess: BlockPublicAccess.BLOCK_ALL, | ||
}); | ||
|
||
// Configure access to the bucket for the container | ||
const user = new User(this, 'PrerenderAccess'); | ||
bucket.grantReadWrite(user); | ||
|
||
const accessKey = new AccessKey(this, 'PrerenderAccessKey', { | ||
user: user, | ||
serial: 1 | ||
}); | ||
|
||
const vpc = ec2.Vpc.fromLookup(this, "vpc", { vpcId: props.vpcId }); | ||
|
||
const cluster = new ecs.Cluster(this, `${props.prerenderName}-cluster`, { vpc: vpc }); | ||
|
||
const directory = path.join(__dirname, 'prerender'); | ||
const asset = new ecrAssets.DockerImageAsset(this, `${props.prerenderName}-image`, { | ||
directory, | ||
}); | ||
|
||
// Create a load-balanced Fargate service | ||
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService( | ||
this, | ||
`${props.prerenderName}-service`, | ||
{ | ||
cluster, | ||
serviceName: `${props.prerenderName}-service`, | ||
desiredCount: props.desiredInstanceCount || 1, | ||
cpu: props.instanceCPU || 512, // 0.5 vCPU default | ||
memoryLimitMiB: props.instanceMemory || 1024, // 1 GB default to give Chrome enough memory | ||
taskImageOptions: { | ||
image: ecs.ContainerImage.fromDockerImageAsset(asset), | ||
enableLogging: true, | ||
containerPort: 3000, | ||
environment: { | ||
S3_BUCKET_NAME: bucket.bucketName, | ||
AWS_ACCESS_KEY_ID: accessKey.accessKeyId, | ||
AWS_SECRET_ACCESS_KEY: accessKey.secretAccessKey.toString(), | ||
AWS_REGION: Stack.of(this).region, | ||
BASIC_AUTH: props.basicAuthList.toString() | ||
} | ||
}, | ||
healthCheckGracePeriod: Duration.seconds(20), | ||
publicLoadBalancer: true, | ||
assignPublicIp: true, | ||
listenerPort: 443, | ||
redirectHTTP: true, | ||
domainName: props.domainName, | ||
domainZone: new HostedZone(this, 'hosted-zone', { zoneName: props.domainName }), | ||
certificate: Certificate.fromCertificateArn(this, 'cert', props.certificateArn) | ||
} | ||
); | ||
|
||
// Setup AutoScaling policy | ||
const scaling = fargateService.service.autoScaleTaskCount({ | ||
maxCapacity: props.maxInstanceCount || 2, | ||
}); | ||
scaling.scaleOnCpuUtilization(`${props.prerenderName}-scaling`, { | ||
targetUtilizationPercent: 50, | ||
scaleInCooldown: Duration.seconds(60), | ||
scaleOutCooldown: Duration.seconds(60), | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
FROM node:16-alpine | ||
|
||
ENV CHROME_BIN=/usr/bin/chromium-browser | ||
ENV CHROME_PATH=/usr/lib/chromium/ | ||
ENV MEMORY_CACHE=0 | ||
|
||
# install chromium, tini and clear cache | ||
RUN apk add --update-cache chromium tini \ | ||
&& rm -rf /var/cache/apk/* /tmp/* | ||
|
||
USER node | ||
WORKDIR "/home/node" | ||
|
||
COPY ./package.json . | ||
COPY ./server.js . | ||
|
||
# install npm packages | ||
RUN npm install --no-package-lock | ||
|
||
EXPOSE 3000 | ||
|
||
HEALTHCHECK CMD netstat -ltn | grep -c 3000 | ||
|
||
ENTRYPOINT ["tini", "--"] | ||
CMD ["node", "server.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "prerender-alpine", | ||
"version": "6.5.0", | ||
"description": "lightweight prerender container built on alpine linux", | ||
"main": "server.js", | ||
"scripts": { | ||
"start": "node server.js" | ||
}, | ||
"dependencies": { | ||
"prerender": "5.20.0", | ||
"prerender-aws-s3-cache": "1.0.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
'use strict'; | ||
|
||
const prerender = require('prerender'); | ||
const s3Cache = require('prerender-aws-s3-cache'); | ||
|
||
const server = prerender({ | ||
chromeFlags: ['--no-sandbox', '--headless', '--disable-gpu', '--remote-debugging-port=9222', '--hide-scrollbars', '--disable-dev-shm-usage'], | ||
forwardHeaders: true, | ||
chromeLocation: '/usr/bin/chromium-browser' | ||
}); | ||
|
||
server.use(prerender.blacklist()); | ||
server.use(prerender.httpHeaders()); | ||
server.use(prerender.removeScriptTags()); | ||
server.use(s3Cache); | ||
|
||
server.use({ | ||
requestReceived: (req, res, next) => { | ||
let auth = req.headers.x-prerender-authorization; | ||
if (!auth) return res.send(401); | ||
|
||
// malformed | ||
let parts = auth.split(' '); | ||
if ('basic' != parts[0].toLowerCase()) return res.send(401); | ||
if (!parts[1]) return res.send(401); | ||
auth = parts[1]; | ||
|
||
// credentials | ||
auth = new Buffer.from(auth, 'base64').toString(); | ||
auth = auth.match(/^([^:]+):(.+)$/); | ||
if (!auth) return res.send(401); | ||
|
||
// compare credentials in header to list of allowed credentials | ||
let basicAuthAllowList = []; | ||
|
||
const basicAuthEnvList = process.env.BASIC_AUTH.toString().split(','); | ||
|
||
for (const [index, element] of basicAuthEnvList.entries()) { | ||
const authIndex = (index - index % 2) / 2 | ||
if (index % 2 === 0) { | ||
basicAuthAllowList [authIndex] = [element]; | ||
} else { | ||
basicAuthAllowList[authIndex].push(element) | ||
} | ||
} | ||
|
||
let authenticated = false; | ||
for (const basicAuth of basicAuthAllowList) { | ||
authenticated = auth[1] === basicAuth[0] && auth[2] === basicAuth[1] | ||
|
||
if (authenticated) break; | ||
} | ||
if (!authenticated) return res.send(401); | ||
|
||
req.prerender.authentication = { | ||
name: auth[1], | ||
password: auth[2] | ||
}; | ||
|
||
return next(); | ||
} | ||
}); | ||
|
||
server.start(); |
Oops, something went wrong.