Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions infrastructure/aws/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions infrastructure/aws/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
12 changes: 12 additions & 0 deletions infrastructure/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Shape Docs AWS Infrastructure

Infrastructure as code for deploying Shape Docs to AWS.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `npx cdk deploy` deploy this stack to your default AWS account/region
* `npx cdk diff` compare deployed stack with current state
* `npx cdk synth` emits the synthesized CloudFormation template
30 changes: 30 additions & 0 deletions infrastructure/aws/bin/infrastructure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import DocsDeployment from '../lib/docs-deployment';

const app = new cdk.App();

/** ACCOUNTS */

const nonProdAccount: cdk.Environment = {
account: "841481405304",
region: "eu-central-1",
};

const prodAccount: cdk.Environment = {
account: "721428964064",
region: "eu-central-1",
};

/** DEPLOYMENTS */

new DocsDeployment(app, 'Staging', {
env: nonProdAccount,
publicCertificateArn: 'arn:aws:acm:eu-central-1:841481405304:certificate/6d513b25-bbca-49ec-9de0-377e303c313f',
})

new DocsDeployment(app, 'Prod', {
env: prodAccount,
publicCertificateArn: 'arn:aws:acm:eu-central-1:841481405304:certificate/6d513b25-bbca-49ec-9de0-377e303c313f', // TODO: Replace with prod cert
})
7 changes: 7 additions & 0 deletions infrastructure/aws/cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"availability-zones:account=841481405304:region=eu-central-1": [
"eu-central-1a",
"eu-central-1b",
"eu-central-1c"
]
}
64 changes: 64 additions & 0 deletions infrastructure/aws/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"app": "npx ts-node --prefer-ts-exts bin/infrastructure.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
}
}
110 changes: 110 additions & 0 deletions infrastructure/aws/lib/app-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as cdk from 'aws-cdk-lib';
import * as sm from 'aws-cdk-lib/aws-secretsmanager';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { EcrImage, FargateService, Secret } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { Construct } from 'constructs';
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';

interface AppStackProps extends cdk.StackProps {
vpc: Vpc;
image: EcrImage;
redisHostname: string,
postgresHostname: string,
postgresUser: string,
postgresDb: string,
postgresPassword: sm.ISecret,
publicCertificateArn: string,
}

export class AppStack extends cdk.Stack {
readonly service: FargateService;
readonly loadBalancer: cdk.aws_elasticloadbalancingv2.ApplicationLoadBalancer;

constructor(scope: Construct, id: string, props: AppStackProps) {
super(scope, id, props);

// list of all env vars to be stored in Secrets Manager
const envVars = [
// GitHub
"GITHUB_APP_ID",
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"GITHUB_ORGANIZATION_NAME",
"GITHUB_PRIVATE_KEY_BASE_64",
"GITHUB_WEBHOK_REPOSITORY_ALLOWLIST",
"GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST",
"GITHUB_WEBHOOK_SECRET",
// Auth0
"AUTH0_BASE_URL",
"AUTH0_CLIENT_ID",
"AUTH0_CLIENT_SECRET",
"AUTH0_ISSUER_BASE_URL",
"AUTH0_MANAGEMENT_CLIENT_ID",
"AUTH0_MANAGEMENT_CLIENT_SECRET",
"AUTH0_MANAGEMENT_DOMAIN",
"AUTH0_SECRET",
// SMTP for sending emails
"SMTP_HOST",
"SMTP_USER",
"SMTP_PASS",
// Other
"SHAPE_DOCS_BASE_URL", // TODO: Could be part of config along with certificate issuing
]

// create the env vars as secrets in Secrets Manager
// Note: secrets are created with an initial value which should be replaced via the AWS SecretsManager Console
// https://eu-central-1.console.aws.amazon.com/secretsmanager/listsecrets?region=eu-central-1
const secrets = envVars.reduce((acc, curr) => {
acc[curr] = new sm.Secret(this, `${id}Secret${curr}`, {
secretName: `${id}/${curr}`,
});
return acc;
}, {} as { [key: string]: sm.Secret });

// must be created & validated in the AWS Console
// https://eu-central-1.console.aws.amazon.com/acm/home?region=eu-central-1
const certificate = Certificate.fromCertificateArn(this, `${id}Certificate`, props.publicCertificateArn)

const app = new ApplicationLoadBalancedFargateService(this, "AppService", {
vpc: props.vpc,
assignPublicIp: false, // run in private network
desiredCount: 1,
cpu: 256,
memoryLimitMiB: 512,
publicLoadBalancer: true,
taskImageOptions: {
image: props.image,
environment: {
REDIS_URL: props.redisHostname,
POSTGRESQL_HOST: props.postgresHostname,
POSTGRESQL_USER: props.postgresUser,
POSTGRESQL_DB: props.postgresDb,
NEXT_PUBLIC_SHAPE_DOCS_TITLE: 'Shape Docs',
},
secrets: {
...envVars.reduce((acc, curr) => { // get each env var from Secrets Manager
acc[curr] = Secret.fromSecretsManager(secrets[curr]);
return acc;
}, {} as { [key: string]: Secret }),
POSTGRESQL_PASSWORD: Secret.fromSecretsManager(props.postgresPassword),
},
containerPort: 3000,
},
circuitBreaker: {
rollback: true,
},
healthCheckGracePeriod: cdk.Duration.seconds(60),
certificate: certificate,
});

app.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '15');

app.targetGroup.configureHealthCheck({
path: "/api/health",
});

this.service = app.service;
this.loadBalancer = app.loadBalancer;
}
}
52 changes: 52 additions & 0 deletions infrastructure/aws/lib/docs-deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Construct } from 'constructs'
import { InfrastructureStack } from './infrastructure-stack';
import { Environment } from 'aws-cdk-lib';
import { PostgresStack } from './postgres-stack';
import { RedisStack } from './redis-stack';
import { AppStack } from './app-stack';
import { ContainerImage } from 'aws-cdk-lib/aws-ecs';

interface DocsDeploymentProps {
env: Environment,
publicCertificateArn: string,
}

export default class DocsDeployment extends Construct {
readonly infrastructure: InfrastructureStack
readonly postgres: PostgresStack
readonly redis: RedisStack
readonly app: AppStack

constructor(scope: Construct, id: string, props: DocsDeploymentProps) {
super(scope, id)

this.infrastructure = new InfrastructureStack(scope, `${id}Infrastructure`, {
env: props.env,
});

this.postgres = new PostgresStack(scope, `${id}Postgres`, {
env: props.env,
vpc: this.infrastructure.vpc,
});

this.redis = new RedisStack(scope, `${id}Redis`, {
env: props.env,
vpc: this.infrastructure.vpc,
});

this.app = new AppStack(scope, `${id}App`, {
env: props.env,
vpc: this.infrastructure.vpc,
image: ContainerImage.fromEcrRepository(this.infrastructure.dockerRepository, 'latest'),
postgresHostname: this.postgres.dbInstance.instanceEndpoint.hostname,
postgresUser: this.postgres.username,
postgresDb: this.postgres.database,
postgresPassword: this.postgres.password,
redisHostname: this.redis.cluster.attrRedisEndpointAddress,
publicCertificateArn: props.publicCertificateArn,
});

this.app.service.connections.allowToDefaultPort(this.redis);
this.app.service.connections.allowToDefaultPort(this.postgres.dbInstance);
}
}
59 changes: 59 additions & 0 deletions infrastructure/aws/lib/infrastructure-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as cdk from 'aws-cdk-lib';
import { IpAddresses, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import { Effect, Policy, PolicyStatement, User } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class InfrastructureStack extends cdk.Stack {
readonly vpc: Vpc;
readonly dockerRepository: Repository;

constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

this.vpc = new Vpc(this, 'VPC', {
ipAddresses: IpAddresses.cidr("10.0.0.0/16"),
maxAzs: 2,
});

this.dockerRepository = new Repository(this, 'Repository', {
repositoryName: 'shapedocs',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

const deploymentPolicy = new Policy(this, 'DeploymentPolicy', {
policyName: 'DeploymentPolicy',
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
// ECR
"ecr:GetAuthorizationToken",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
// ECS
"ecs:DescribeServices",
"ecs:UpdateService",
"ecs:RegisterTaskDefinition",
"ecs:DeregisterTaskDefinition",
"ecs:DescribeTaskDefinition",
"iam:PassRole"
],
resources: [
"*"
],
}),
],
});

const deploymentUser = new User(this, 'GitHubActionsUser');

deploymentPolicy.attachToUser(deploymentUser);

deploymentUser.attachInlinePolicy(deploymentPolicy);
}
}
Loading