Skip to content

Instantly share code, notes, and snippets.

@mcalavera81
Forked from bkono/awsecsdeploy-stack.ts
Last active April 14, 2021 10:19
Show Gist options
  • Save mcalavera81/4c807109bc59ce9f3a1180262f243fac to your computer and use it in GitHub Desktop.
Save mcalavera81/4c807109bc59ce9f3a1180262f243fac to your computer and use it in GitHub Desktop.
CDK App: ecs, alb, certs, postgres, elasticache, elasticsearch
import cdk = require('@aws-cdk/cdk');
import ec2 = require('@aws-cdk/aws-ec2');
import elastic = require('@aws-cdk/aws-elasticache');
import rds = require ('@aws-cdk/aws-rds');
import es = require("@aws-cdk/aws-elasticsearch");
import ecs = require('@aws-cdk/aws-ecs');
import s3 = require("@aws-cdk/aws-s3");
import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2');
import route53 = require('@aws-cdk/aws-route53');
import certificateManager = require("@aws-cdk/aws-certificatemanager");
import {Protocol} from "@aws-cdk/aws-ecs";
import {ApplicationProtocol} from "@aws-cdk/aws-elasticloadbalancingv2";
interface mastodonprops extends cdk.StackProps {
vpc: ec2.VpcNetwork,
}
// A stack that holds all the shared resources. In this case this vpc is used by everything
class SharedResources extends cdk.Stack {
vpc: ec2.VpcNetwork;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Network to run everything in
this.vpc = new ec2.VpcNetwork(this, 'MastodonVpc', {
maxAZs: 2,
natGateways: 1
});
}
}
class RedisCluster extends cdk.Stack {
securityGroup: ec2.SecurityGroup;
connections: ec2.Connections;
cluster: elastic.CfnCacheCluster;
constructor(scope: cdk.Construct, id: string, props: mastodonprops) {
super(scope, id, props);
const targetVpc = props.vpc;
// Define a group for telling Elasticache which subnets to put cache nodes in.
const subnetGroup = new elastic.CfnSubnetGroup(this, `${id}-subnet-group`, {
description: `List of subnets used for redis cache ${id}`,
subnetIds: targetVpc.privateSubnets.map(subnet => subnet.subnetId)
});
// The security group that defines network level access to the cluster
this.securityGroup = new ec2.SecurityGroup(this, `${id}-security-group`, {vpc: targetVpc});
this.connections = new ec2.Connections({
securityGroups: [this.securityGroup],
defaultPortRange: new ec2.TcpPort(6379)
});
// The cluster resource itself.
this.cluster = new elastic.CfnCacheCluster(this, `${id}-cluster`, {
cacheNodeType: 'cache.t2.micro',
engine: 'redis',
numCacheNodes: 1,
autoMinorVersionUpgrade: true,
cacheSubnetGroupName: subnetGroup.subnetGroupName,
vpcSecurityGroupIds: [
this.securityGroup.securityGroupId,
]
});
};
}
export class Postgres extends cdk.Stack {
securityGroup: ec2.SecurityGroup;
connections: ec2.Connections;
subnetGroup: rds.CfnDBSubnetGroup;
db: rds.CfnDBInstance;
constructor(scope: cdk.Construct, id: string, props: mastodonprops) {
super(scope, id, props);
const targetVpc = props.vpc;
// The security group that defines network level access to the cluster
this.securityGroup = new ec2.SecurityGroup(this, `${id}-security-group`, {vpc: targetVpc});
this.subnetGroup = new rds.CfnDBSubnetGroup(this, `${id}-subnet-group`, {
subnetIds: targetVpc.privateSubnets.map(subnet => subnet.subnetId),
dbSubnetGroupDescription: "Subnet for the PostgresDB"
});
this.connections = new ec2.Connections({
securityGroups: [this.securityGroup],
defaultPortRange: new ec2.TcpPort(5432)
});
this.db = new rds.CfnDBInstance(this, id, {
engine: rds.DatabaseEngine.Postgres,
engineVersion: "9.6.5",
autoMinorVersionUpgrade: true,
allowMajorVersionUpgrade: false,
multiAz: true,
dbInstanceClass: 'db.t2.micro',
storageType: 'gp2',
allocatedStorage: '10',
dbName: '<REDACTED>',
masterUserPassword: '<REDACTED>',
masterUsername: '<REDACTED>',
vpcSecurityGroups: [this.securityGroup.securityGroupId],
dbSubnetGroupName: this.subnetGroup.dbSubnetGroupName
})
}
}
export class ElasticSearch extends cdk.Stack {
securityGroup: ec2.SecurityGroup;
connections: ec2.Connections;
domain: es.CfnDomain;
constructor(scope: cdk.Construct, id: string, props: mastodonprops) {
super(scope, id, props);
const targetVpc = props.vpc;
// The security group that defines network level access to the cluster
this.securityGroup = new ec2.SecurityGroup(this, `${id}-security-group`, {vpc: targetVpc});
this.connections = new ec2.Connections({
securityGroups: [this.securityGroup],
defaultPortRange: new ec2.TcpPort(9200)
});
this.domain = new es.CfnDomain(this, id, {
vpcOptions: {
subnetIds: [targetVpc.privateSubnets[0].subnetId],
securityGroupIds: [this.securityGroup.securityGroupId]
},
elasticsearchClusterConfig: {
instanceType: 't2.small.elasticsearch',
},
ebsOptions: {
ebsEnabled: true,
volumeSize: 10,
volumeType: 'gp2'
}
})
}
}
export class AwsecsdeployStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
const shared = new SharedResources(this, "SharedStuff");
const search = new ElasticSearch(this, "MastodonSearch", {
vpc: shared.vpc
});
const redis = new RedisCluster(this, "MastodonRedis", {
vpc: shared.vpc
});
const postgres = new Postgres(this, "MastodonDB", {vpc: shared.vpc});
const mastodonBucket = new s3.Bucket(this, 'MastodonBucket', {
publicReadAccess: true
});
const mastodonCluster = new ecs.Cluster(this, 'MastodonCluster', {
vpc: shared.vpc
});
const zone = new route53.HostedZoneProvider(this, {
domainName: 'ithance.com'
}).findAndImport(this, 'IThanceZone');
const environment = {
DB_HOST: postgres.db.dbInstanceEndpointAddress,
DB_PORT: postgres.db.dbInstanceEndpointPort,
LOCAL_DOMAIN: 'mastodon.example.com',
STREAMING_API_BASE_URL: 'mastodon-stream.example.com',
DB_PASS: '<REDACTED>',
DB_USER: '<REDACTED>',
DB_NAME: '<REDACTED>',
REDIS_HOST: redis.cluster.cacheClusterRedisEndpointAddress,
REDIS_PORT: redis.cluster.cacheClusterRedisEndpointPort,
SECRET_KEY_BASE: '<REDACTED>',
OTP_SECRET: '<REDACTED>',
S3_ENABLED: 'true',
S3_BUCKET: mastodonBucket.bucketName,
ES_ENABLED: 'true',
ES_HOST: search.domain.domainEndpoint,
SMTP_SERVER: 'smtp.mailgun.org',
SMTP_PORT: '587',
SMTP_LOGIN: '<REDACTED>',
SMTP_PASSWORD: '<REDACTED>',
VAPID_PRIVATE_KEY: '<REDACTED>',
VAPID_PUBLIC_KEY: '<REDACTED>'
};
const image = ecs.ContainerImage.fromAsset(this, 'MastodonImage', {
directory: '../'
});
const webTaskDefinition = new ecs.FargateTaskDefinition(this, "WebTask");
const web = new ecs.ContainerDefinition(this, "web", {
taskDefinition: webTaskDefinition,
image,
environment,
command: ["bash", "-c", "bundle exec rake db:migrate; bundle exec rails s -p 3000 -b '0.0.0.0'"],
logging: new ecs.AwsLogDriver(this, 'web-logs', {
streamPrefix: 'web'
})
});
web.addPortMappings({
containerPort: 3000,
protocol: Protocol.Tcp
});
const webService = new ecs.FargateService(this, "MastodonWebService", {
taskDefinition: webTaskDefinition,
cluster: mastodonCluster
});
const webLB = new elbv2.ApplicationLoadBalancer(this, 'WebLB', {
vpc: shared.vpc,
internetFacing: true,
});
const WebListener = webLB.addListener('WebListener', {port: 443});
WebListener.addTargets('webService', {
port: 3000,
protocol: ApplicationProtocol.Http,
targets: [webService]
});
const webCert = new certificateManager.DnsValidatedCertificate(this, "MastodonWebCert", {
hostedZone: zone,
domainName: "mastodon.example.com"
});
new route53.AliasRecord(this, "WebRecord", {
zone,
recordName: "mastodon",
target: webLB
});
WebListener.addCertificateArns("WebCert", [webCert.certificateArn]);
const StreamTaskDefinition = new ecs.FargateTaskDefinition(this, "StreamingTask");
const streaming = new ecs.ContainerDefinition(this, "streaming", {
taskDefinition: StreamTaskDefinition,
image,
environment,
command: ["yarn", "start"],
logging: new ecs.AwsLogDriver(this, 'streaming-logs', {
streamPrefix: 'streaming'
})
});
streaming.addPortMappings({
containerPort: 4000,
protocol: Protocol.Tcp
});
const StreamingService = new ecs.FargateService(this, "MastodonStreamingService", {
taskDefinition: StreamTaskDefinition,
cluster: mastodonCluster
});
const StreamLB = new elbv2.ApplicationLoadBalancer(this, 'StreambLB', {
vpc: shared.vpc,
internetFacing: true
});
const StreamListener = StreamLB.addListener('StreamListener', {port: 443});
StreamListener.addTargets('StreamService', {
port: 4000,
protocol: ApplicationProtocol.Http,
targets: [StreamingService]
});
const streamCert = new certificateManager.DnsValidatedCertificate(this, "MastodonSteamCert", {
hostedZone: zone,
domainName: 'mastodon-stream.example.com'
});
new route53.AliasRecord(this, "StreamRecord", {
zone,
recordName: "mastodon-stream",
target: StreamLB
});
StreamListener.addCertificateArns("StreamCert", [streamCert.certificateArn]);
const sideKickTaskDefinition = new ecs.FargateTaskDefinition(this, "SidekickTask");
new ecs.ContainerDefinition(this, "SideKick", {
taskDefinition: sideKickTaskDefinition,
image,
environment,
command: ["bundle", "exec", "sidekiq"],
logging: new ecs.AwsLogDriver(this, 'worker-logs', {
streamPrefix: 'worker'
})
});
const sideKickService = new ecs.FargateService(this, "Worker", {
taskDefinition: sideKickTaskDefinition,
cluster: mastodonCluster
});
[webService, StreamingService, sideKickService].map(service => {
service.connections.allowToDefaultPort(redis);
service.connections.allowToDefaultPort(search);
service.connections.allowToDefaultPort(postgres);
mastodonBucket.grantReadWrite(service.taskDefinition.obtainExecutionRole())
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment