Skip to content

Instantly share code, notes, and snippets.

@ADelRosarioH
Created September 6, 2024 17:09
Show Gist options
  • Save ADelRosarioH/b1a8861e2a1fdbb7812e79b8b3300301 to your computer and use it in GitHub Desktop.
Save ADelRosarioH/b1a8861e2a1fdbb7812e79b8b3300301 to your computer and use it in GitHub Desktop.
Typesense Provisioning using AWS CDK, ECS, EFS, ALB wth CloudFront Proxy
import { StackContext } from "sst/constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as efs from "aws-cdk-lib/aws-efs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import { RemovalPolicy, Duration } from "aws-cdk-lib";
import { nanoid } from "nanoid";
export function SearchStack({ stack }: StackContext) {
// Create VPC
const vpc = new ec2.Vpc(stack, "Vpc", {
maxAzs: 2,
});
// Create ECS Cluster
const cluster = new ecs.Cluster(stack, "EcsCluster", {
vpc,
});
// Create security group for EFS to allow NFS (port 2049) traffic
const efsSecurityGroup = new ec2.SecurityGroup(stack, "EfsSecurityGroup", {
vpc,
allowAllOutbound: true,
});
efsSecurityGroup.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(2049), "Allow NFS traffic");
// Create EFS file system for Typesense data with security group
const fileSystem = new efs.FileSystem(stack, "EfsFileSystem", {
vpc,
lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
removalPolicy: RemovalPolicy.DESTROY,
securityGroup: efsSecurityGroup, // Associate the security group with the EFS Mount Targets
enableAutomaticBackups: true,
});
// Create EFS Access Point
const accessPoint = new efs.AccessPoint(stack, "EfsAccessPoint", {
fileSystem,
path: "/data",
posixUser: {
uid: "1000",
gid: "1000",
},
createAcl: {
ownerGid: "1000",
ownerUid: "1000",
permissions: "755",
},
});
// Security group for ECS service (Typesense)
const serviceSecurityGroup = new ec2.SecurityGroup(stack, "ServiceSecurityGroup", {
vpc,
allowAllOutbound: true,
});
serviceSecurityGroup.addIngressRule(efsSecurityGroup, ec2.Port.tcp(2049), "Allow NFS traffic to EFS");
// Security group for ALB
const albSecurityGroup = new ec2.SecurityGroup(stack, "AlbSecurityGroup", {
vpc,
allowAllOutbound: true,
});
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "Allow HTTP traffic");
// IAM Role for ECS Task to access EFS
const taskRole = new iam.Role(stack, "TaskRole", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
});
taskRole.addToPolicy(
new iam.PolicyStatement({
actions: ["elasticfilesystem:ClientMount", "elasticfilesystem:ClientWrite"],
resources: [fileSystem.fileSystemArn],
})
);
// Create Fargate Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(stack, "TaskDef", {
memoryLimitMiB: 2048,
cpu: 1024,
taskRole,
});
// Add Typesense container to the task definition
const typesenseApiKey = nanoid();
const container = taskDefinition.addContainer("TypesenseContainer", {
image: ecs.ContainerImage.fromRegistry("typesense/typesense:27.0"),
logging: ecs.LogDriver.awsLogs({ streamPrefix: "typesense" }),
environment: {
TYPESENSE_API_KEY: typesenseApiKey,
TYPESENSE_DATA_DIR: "/data",
TYPESENSE_ENABLE_CORS: "true",
TYPESENSE_NUM_THREADS: "4",
},
});
// Add EFS volume to task definition, including the Access Point
taskDefinition.addVolume({
name: "efs-volume",
efsVolumeConfiguration: {
fileSystemId: fileSystem.fileSystemId,
transitEncryption: "ENABLED",
authorizationConfig: {
accessPointId: accessPoint.accessPointId,
iam: "ENABLED",
},
},
});
// Mount EFS volume to Typesense container
container.addMountPoints({
containerPath: "/data",
sourceVolume: "efs-volume",
readOnly: false,
});
// Add port mapping for Typesense container (port 8108)
container.addPortMappings({
containerPort: 8108,
protocol: ecs.Protocol.TCP,
});
// Create Fargate Service
const service = new ecs.FargateService(stack, "FargateService", {
cluster,
taskDefinition,
assignPublicIp: true,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
securityGroups: [serviceSecurityGroup],
});
// Create Internet-facing ALB
const alb = new elbv2.ApplicationLoadBalancer(stack, "Alb", {
vpc,
internetFacing: true,
securityGroup: albSecurityGroup,
});
// Add HTTP listener to ALB
const listener = alb.addListener("HttpListener", {
port: 80,
open: true,
});
// Forward traffic from ALB to ECS service (port 8108)
listener.addTargets("EcsTarget", {
port: 8108,
protocol: elbv2.ApplicationProtocol.HTTP, // Specify the protocol
targets: [service],
healthCheck: {
path: "/health",
interval: Duration.seconds(30),
timeout: Duration.seconds(5),
},
});
// Create CloudFront Cache Policy for forwarding all headers
const cachePolicy = new cloudfront.CachePolicy(stack, "CachePolicy", {
headerBehavior: cloudfront.CacheHeaderBehavior.allowList("X-TYPESENSE-API-KEY"), // Forward all headers
});
// Create CloudFront Distribution (without custom domain)
const distribution = new cloudfront.Distribution(stack, "CloudFrontDistribution", {
defaultBehavior: {
origin: new origins.LoadBalancerV2Origin(alb, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, // Use HTTP between CloudFront and ALB
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, // Enforce HTTPS for viewers
cachePolicy, // Attach cache policy that forwards all headers
},
});
// Output the CloudFront URL and ALB DNS name
stack.addOutputs({
CloudFrontURL: distribution.distributionDomainName, // CloudFront URL
AlbDns: alb.loadBalancerDnsName, // ALB DNS (can be used for direct access, HTTP only)
// Typesense API Key
TypesenseApiKey: typesenseApiKey,
});
return {
distribution,
alb,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment