Deploying Redis and an ECS Service with websocket on AWS with Terraform

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    Deploying Redis and an ECS Service with websocket on AWS with Terraform

    Amazon Web Services (AWS) provides a robust infrastructure for deploying scalable applications. In this tutorial, we will walk through setting up an Amazon ElastiCache Redis cluster and an ECS (Fargate) service using Terraform.


    This setup includes:
    • A Redis cluster for caching.
    • An ECS service running an API container.
    • Security groups to control network access.
    • CloudWatch logging for monitoring.


    Prerequisites


    Before you begin, ensure you have the following installed:
    • Terraform (latest version)
    • AWS CLI (configured with credentials)
    • A VPC with available subnets
    • An ECR repository for your application image


    1. Configuring the Redis Cluster


    First, create an ElastiCache parameter group to customize Redis settings:






    resource "aws_elasticache_parameter_group" "custom_redis_parameter_group_dev" {
    name = "custom-redis-parameter-group-dev"
    family = "redis7"

    parameter {
    name = "maxmemory-policy"
    value = "allkeys-lru"
    }

    parameter {
    name = "timeout"
    value = "3600"
    }
    }







    Now, define a subnet group for Redis:






    resource "aws_elasticache_subnet_group" "redis_public_subnet_group_dev" {
    name = "redis-public-subnet-group-dev"
    subnet_ids = [var.subnet_id_a, var.subnet_id_b]

    tags = {
    Name = "redis-public-subnet-group-dev"
    }
    }







    Next, create the Redis cluster:






    resource "aws_elasticache_cluster" "redis_cluster_dev" {
    cluster_id = "redis-cluster-dev"
    engine = "redis"
    node_type = "cache.t2.micro"
    num_cache_nodes = 1
    parameter_group_name = aws_elasticache_parameter_group.custom_redis_param eter_group_dev.name
    port = 6379
    security_group_ids = [aws_security_group.redis_dev_sg.id]
    subnet_group_name = aws_elasticache_subnet_group.redis_public_subnet_g roup_dev.name
    }







    2. Configuring the ECS Service


    Define the ECS Task Definition:






    resource "aws_ecs_task_definition" "game_api_task" {
    family = "game-api-task-dev"
    network_mode = "awsvpc"
    requires_compatibilities = ["FARGATE"]
    cpu = "256"
    memory = "512"
    execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
    task_role_arn = aws_iam_role.ecs_task_role.arn

    container_definitions = jsonencode([
    {
    name = "game_api_container_dev"
    image = "${aws_ecr_repository.game_api_ecr_dev.repository_ url}:latest"
    cpu = 256
    memory = 512
    essential = true

    portMappings = [
    {
    containerPort = 3000
    hostPort = 3000
    protocol = "tcp"
    }
    ]

    logConfiguration = {
    logDriver = "awslogs"
    options = {
    awslogs-group = aws_cloudwatch_log_group.game_api_dev_log_group.na me
    awslogs-region = "us-east-1"
    awslogs-stream-prefix = "ecs"
    }
    }
    }
    ])
    }







    Create a CloudWatch log group for logs:






    resource "aws_cloudwatch_log_group" "game_api_dev_log_group" {
    name = "/ecs/game-api-task-dev"
    retention_in_days = 7
    }







    3. Configuring Security Groups


    Define the security group for the API Load Balancer:






    resource "aws_security_group" "api_load_balancer_dev_sg" {
    name = "api-sg-dev"
    description = "Allow HTTP traffic"
    vpc_id = var.vpc_id

    ingress {
    description = "Allow HTTP traffic"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }

    ingress {
    description = "Allow HTTPS traffic"
    from_port = 443
    to_port = 443
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }

    ingress {
    description = "Allow API traffic"
    from_port = 3000
    to_port = 3000
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }

    egress {
    description = "Allow all outbound traffic"
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    }
    }







    Define the security group for Redis:






    resource "aws_security_group" "redis_dev_sg" {
    name = "redis-sg-dev"
    description = "Security group for Redis cluster"
    vpc_id = var.vpc_id

    ingress {
    description = "Allow Redis access from ECS"
    from_port = 6379
    to_port = 6379
    protocol = "tcp"
    }







    Implementing a WebSocket Gateway in NestJS


    In real-time applications, WebSockets enable bidirectional communication between clients and servers. NestJS provides a built-in WebSocket module to facilitate this. Below, we define a WebSocket gateway using the @nestjs/websockets package.


    Setting Up the WebSocket Gateway


    The MatchGateway class listens for specific messages and manages client connections.






    import {
    SubscribeMessage,
    WebSocketGateway,
    WebSocketServer,
    MessageBody,
    ConnectedSocket
    } from "@nestjs/websockets";
    import { Server, Socket } from "socket.io";
    import { PlayerConnectionDto } from "./dto/player.dto";
    import { SocketMessagesEnum } from "@/domain/enum/socket-messages";
    import { AddPlayerToRoundUseCase } from "@/use-cases/player/add-player-to-round";

    @WebSocketGateway({
    cors: {
    origin: "*",
    methods: ["GET", "POST"]
    },
    transports: ["websocket"]
    })
    export class MatchGateway {
    @WebSocketServer() server: Server;

    constructor(
    private readonly addPlayerToRoundUseCase: AddPlayerToRoundUseCase
    ) {}

    handleConnection(client: Socket): void {
    console.log("Client connected:", client.id);
    }

    @SubscribeMessage(SocketMessagesEnum.PLAYER_CONNEC TION)
    async handlePlayerOn(
    @MessageBody() player: PlayerConnectionDto,
    @ConnectedSocket() client: Socket
    ): Promise {
    try {
    const playerData = { id: player.id };
    client.emit(SocketMessagesEnum.PLAYER_CONNECTION, playerData);
    } catch (error) {
    console.error("Error in WebSocket handler:", error);
    client.emit(SocketMessagesEnum.ERROR, {
    message: `Error processing player connection: ${error.message}`
    });
    }
    }

    @SubscribeMessage(SocketMessagesEnum.CHART_DATA)
    async handleChartData(): Promise {
    try {
    // Handle real-time chart data
    } catch (error) {
    console.error("Error in chart data handler:", error);
    }
    }
    }








    Setting Up the WebSocket Adapter


    To ensure the WebSocket server runs smoothly within the NestJS application, an adapter is needed. This can be added in the bootstrap function:






    import { IoAdapter } from "@nestjs/platform-socket.io";
    import { NestFactory } from "@nestjs/core";
    import { AppModule } from "./modules/app.module";

    async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.useWebSocketAdapter(new IoAdapter(app));
    app.enableCors({ origin: "*", methods: ["GET", "POST"] });

    const port = process.env.PORT || 3000;
    await app.listen(port);
    console.log(`🚀 Server running on port ${port}`);
    }

    bootstrap();








    Automating Deployment with GitHub Actions


    To automate deployments, create a GitHub Actions workflow (.github/workflows/deploy.yml) with the following:






    name: Deploy API to ECS

    on:
    push:
    branches:
    - dev

    jobs:
    deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout the repository
    uses: actions/checkout@v3

    - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v3
    with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: "us-east-1"

    - name: Login to Amazon ECR
    uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push the Docker image to Amazon ECR
    env:
    IMAGE_TAG: ${{ github.sha }}
    run: |
    docker build -t $ECR_REPOSITORY:$IMAGE_TAG .
    docker push $ECR_REPOSITORY:$IMAGE_TAG

    - name: Deploy to ECS
    run: |
    aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force-new-deployment









    More...
Working...