-
-
Save revmischa/07bf2ce470863967fcc5f3c4b423f8d7 to your computer and use it in GitHub Desktop.
Deploys Mastodon using AWS CDK v2
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
import cdk = require("aws-cdk-lib"); | |
import ec2 = require("aws-cdk-lib/aws-ec2"); | |
import elastic = require("aws-cdk-lib/aws-elasticache"); | |
import rds = require("aws-cdk-lib/aws-rds"); | |
import es = require("aws-cdk-lib/aws-elasticsearch"); | |
import ecs = require("aws-cdk-lib/aws-ecs"); | |
import s3 = require("aws-cdk-lib/aws-s3"); | |
import elbv2 = require("aws-cdk-lib/aws-elasticloadbalancingv2"); | |
import route53 = require("aws-cdk-lib/aws-route53"); | |
import certificateManager = require("aws-cdk-lib/aws-certificatemanager"); | |
import { Protocol } from "aws-cdk-lib/aws-ecs"; | |
import { ApplicationProtocol } from "aws-cdk-lib/aws-elasticloadbalancingv2"; | |
import { Construct } from "constructs"; | |
import { Port } from "aws-cdk-lib/aws-ec2"; | |
import { StorageType } from "aws-cdk-lib/aws-rds"; | |
import { AlbTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2-targets"; | |
import * as route53_targets from "aws-cdk-lib/aws-route53-targets"; | |
interface mastodonprops extends cdk.StackProps { | |
vpc: ec2.Vpc; | |
} | |
// A stack that holds all the shared resources. In this case this vpc is used by everything | |
class SharedResources extends cdk.Stack { | |
vpc: ec2.Vpc; | |
constructor(scope: Construct, id: string, props?: cdk.StackProps) { | |
super(scope, id, props); | |
// Network to run everything in | |
this.vpc = new ec2.Vpc(this, "MastodonVpc", { | |
natGateways: 1, | |
}); | |
} | |
} | |
class RedisCluster extends cdk.Stack { | |
securityGroup: ec2.SecurityGroup; | |
connections: ec2.Connections; | |
cluster: elastic.CfnCacheCluster; | |
constructor(scope: 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], | |
defaultPort: Port.tcp(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.cacheSubnetGroupName, | |
vpcSecurityGroupIds: [this.securityGroup.securityGroupId], | |
}); | |
} | |
} | |
export class Postgres extends cdk.Stack { | |
securityGroup: ec2.SecurityGroup; | |
connections: ec2.Connections; | |
subnetGroup: rds.SubnetGroup; | |
db: rds.DatabaseInstance; | |
constructor(scope: 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.SubnetGroup(this, `${id}-subnet-group`, { | |
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }, | |
description: "Subnet for PostgresDB", | |
vpc: targetVpc, | |
}); | |
this.connections = new ec2.Connections({ | |
securityGroups: [this.securityGroup], | |
defaultPort: Port.tcp(5432), | |
}); | |
this.db = new rds.DatabaseInstance(this, id, { | |
vpc: targetVpc, | |
engine: rds.DatabaseInstanceEngine.postgres({ | |
version: rds.PostgresEngineVersion.VER_14_4, | |
}), | |
autoMinorVersionUpgrade: true, | |
allowMajorVersionUpgrade: false, | |
multiAz: true, | |
instanceType: ec2.InstanceType.of( | |
ec2.InstanceClass.T3, | |
ec2.InstanceSize.MICRO | |
), | |
storageType: StorageType.STANDARD, | |
securityGroups: [this.securityGroup], | |
subnetGroup: this.subnetGroup, | |
}); | |
} | |
} | |
export class ElasticSearch extends cdk.Stack { | |
securityGroup: ec2.SecurityGroup; | |
connections: ec2.Connections; | |
domain: es.CfnDomain; | |
constructor(scope: 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], | |
defaultPort: Port.tcp(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 MastodonStack extends cdk.Stack { | |
constructor(scope: 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 = route53.HostedZone.fromLookup(this, "MastodonZone", { | |
domainName: "mastodon.example.com", | |
}); | |
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.attrRedisEndpointAddress, | |
REDIS_PORT: redis.cluster.attrRedisEndpointPort, | |
SECRET_KEY_BASE: "<REDACTED>", | |
OTP_SECRET: "<REDACTED>", | |
S3_ENABLED: "true", | |
S3_BUCKET: mastodonBucket.bucketName, | |
ES_ENABLED: "true", | |
ES_HOST: search.domain.attrDomainEndpoint, | |
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.fromRegistry("tootsuite/mastodon"); | |
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({ streamPrefix: "mastodon/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.ARecord(this, "WebRecord", { | |
zone, | |
recordName: "mastodon", | |
target: route53.RecordTarget.fromAlias( | |
new route53_targets.LoadBalancerTarget(webLB) | |
), | |
}); | |
WebListener.addCertificates("WebCert", [webCert]); | |
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({ | |
streamPrefix: "mastodon/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.ARecord(this, "StreamRecord", { | |
zone, | |
recordName: "mastodon-stream", | |
target: route53.RecordTarget.fromAlias( | |
new route53_targets.LoadBalancerTarget(StreamLB) | |
), | |
}); | |
StreamListener.addCertificates("StreamCert", [streamCert]); | |
const sideKickTaskDefinition = new ecs.FargateTaskDefinition( | |
this, | |
"SidekickTask" | |
); | |
new ecs.ContainerDefinition(this, "SideKick", { | |
taskDefinition: sideKickTaskDefinition, | |
image, | |
environment, | |
command: ["bundle", "exec", "sidekiq"], | |
logging: new ecs.AwsLogDriver({ | |
streamPrefix: "mastodon/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