Skip to content

Commit

Permalink
Infrastructure for AWS (#192)
Browse files Browse the repository at this point in the history
* Basic infrastructure setup for AWS

* Remove incorrect comment

* Stick with 2 AZs for now

* Permit app to send email

* Bump CDK version

* Encapsulate deployment in construct

* Allow redis access from app

* Env vars and certificate

* Fix cert

* Configure postgres

* Configure GitHub Actions user's permissions

* Fix deploy user permissions

* Remove unused tests

* Exclude "infrastructure" folder from main tsconfig

It has it's own tsconfig
  • Loading branch information
ulrikandersen authored Jun 27, 2024
1 parent deb9869 commit 93ae53e
Show file tree
Hide file tree
Showing 15 changed files with 4,784 additions and 1 deletion.
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

0 comments on commit 93ae53e

Please sign in to comment.