Deploying Fider on AWS ECS: A Step-by-Step Guide to Deploy a Feedback Platform

Mohamed Wasim - Nov 5 - - Dev Community

This guide provides detailed instructions for deploying Fider on AWS ECS with the tasks running inside the private subnet and the application load balancer(ALB) as front-facing.

“Simply put, things always had to be in a production-ready state: if you wrote it, you darn well had to be there to get it running!”

AWS services used

  • Elastic container registry(ECR)
  • Elastic container service(ECS Fargate launch type)
  • Application Load Balancer(ALB)
  • Elastic File System(EFS)
  • Amazon Certificate Manager (ACM)

What is Fider ?

Image description

Fider is an intuitive, open-source feedback platform that empowers teams to listen to their users, prioritize improvements, and foster a transparent dialogue with their community. Built with simplicity and user-friendliness, Fider allows organizations to create a centralized platform where users can submit ideas, upvote others' suggestions, and discuss features or improvements.

Fider's Tech Stack

It is a web application primarily built with the Go programming language for backend logic, while the frontend is built with TypeScript and React. It only uses PostgreSQL as the database, ensuring robust data handling and scalability for large amounts of feedback. The entire application can be easily containerized with Docker, making it highly portable and suitable for deployment on cloud platforms like AWS ECS, Kubernetes, or even self-hosted servers.

Deployment Architecture

Image description

Pre-flight checks:

Before starting, ensure that you have an AWS account with access to the free tier or a valid payment method on file.

Install AWS CLI:

Step 1:

You need the awscli package installed for pushing the images built locally to Elastic Container Registry(ECR).

Step 2:

If you use ubuntu distro, Check the snap version by using the following command.

  snap version
Enter fullscreen mode Exit fullscreen mode

Run the following snap install command for the AWS CLI.

  sudo snap install aws-cli --classic
Enter fullscreen mode Exit fullscreen mode

After the installation is complete, verify the installation by using the command,

  aws --version
Enter fullscreen mode Exit fullscreen mode

Image description

With Snap package you always get the latest version of AWS CLI as snap packages automatically refresh.

For command line installer check out the documentation: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html

Step 4: Configure AWS CLI

Use this command to configure the AWS CLI.

  aws configure
Enter fullscreen mode Exit fullscreen mode

Once configured, verify your AWS CLI credentials using the following command.

  aws configure list 
Enter fullscreen mode Exit fullscreen mode

Step 5: Pull official docker images from DockerHub

Pull the official docker image for Fider from the DockerHub registry at: https://hub.docker.com/r/getfider/fider/tags

Use the docker pull command to pull the image from the DockerHub.

  docker pull getfider/fider:main
Enter fullscreen mode Exit fullscreen mode

Image description

Now pull the official docker image of Postgres from the DockerHub registry at: https://hub.docker.com/_/postgres/tags

Use the docker pull command to pull postgres image.

  docker pull postgres:13.16
Enter fullscreen mode Exit fullscreen mode

Image description

Check if the docker images are available in your instance.

  docker images 
Enter fullscreen mode Exit fullscreen mode

Image description

Push the Docker images to ECR

Step 1: Visit the ECR console

Create a public or private repository based on your use case by clicking on the create repository prompt in the top right corner.

Image description

Name the repo getfider/fider and repeat the same process for postgres.

Image description

Check if the repositories are created.

Image description

Step 2: View the push commands

View the push commands by clicking on the respective repository.

Image description

Use the AWS CLI to retrieve an authentication token and authenticate your Docker client to your registry.

Image description

tag your pulled image so you can push the image to this repository.

Image description

Run the following command to push this image to your newly created AWS repository.

Image description

Repeat the same steps for tagging and pushing the postgres image to the repository and view the pushed images in the ECR.

Image description

Image description

Create a VPC

Step 1: Create a standalone VPC for deploying Fider application

Login to the management console then navigate to the VPC service console and click on Create VPC.

Image description

Create VPC only for now, you can create public and private subnets manually and can attach these subnets to particular route tables.

Image description

A route table will also get created with route to local traffic.

Image description

Step 2: Create an Internet gateway

Create an IGW and attach it to the VPC you created.

Image description

Image description

The IGW is now attached to the VPC.

Associate the IGW with the route table by clicking on edit routes.

Image description

By clicking on Add route attach the the IGW to the route table.

Image description

The route table now has a route to the IGW, which means the subnets associated with this route table can have Internet access.

Image description

Step 3: Create Public Subnets

Create at least two public subnets, so that the Application Load Balancer can reside in it.

Navigate to the VPC Console, under the subnets section create two public subnets.

Decide the subnet CIDR range and provision it accordingly.

Image description

Associate the public subnets to the public route table where the IGW is also attached.

Image description

Step 4: Create Private Subnets

Create at least two private subnets, so that your ECS tasks and EFS can reside in it.

Navigate to the VPC Console, under the subnets section create at least two private subnets.

Image description

Under the actions dropdown choose Edit subnet settings and uncheck the Enable auto-assign public IPv4 address.

Image description

Step 5: Create a Private route table

Create a private route table in the VPC console under the route tables section to route traffic locally.

Image description

Image description

Associate the private subnets to the private route table, like how you did with the public route table, and follow the same steps.

Step 6: Create NAT gateway

Create a NAT gateway to allow tasks in the private subnet to connect to the internet.

Navigate to NAT gateway section under the VPC console and create a NAT gateway.

Image description

Associate it with a public subnet and also associate an Elastic IP address to it.

Set the connectivity type to public.

Attach the NAT gateway to the private route table that has three private subnets associated with it.

Image description

The private route table now has a route to the NAT gateway.

Image description

Your VPC lineage should now look like this.

Image description

Create an EFS volume

Step 1:

Navigate to the EFS console and click on create file system prompt.

Image description

Name it pg_data_volume and choose the VPC you created.

Image description

Step 2: Create an access point

Navigate into the file system and create an access point.

Image description

Image description

Set the default path to the root directory for now and click on Create access point.

Image description

Step 3: Change the security group configuration for EFS

Under the network tab click on manage to change the settings.

Image description

Change the default security group setting to EFS-sg settings as EFS listens on port 2049, so create security group for that.

Image description

The EFS volume can now be attached to the ECS task via task definition.

Why use ECS instead of EC2

There are three major reasons to use ECS instead of EC2,

1. Cost Efficiency:

ECS supports Fargate, a serverless compute engine that lets you run containers without managing the underlying EC2 instances. Fargate charges based on the resources each container consumes, which can be more cost-effective than maintaining idle EC2 resources, especially for applications with variable traffic.

2. Reduced Operational Overhead:

With ECS, AWS takes on much of the responsibility for managing the infrastructure. This reduces the need for manual OS patching, Docker updates, or monitoring of EC2 health. ECS manages the lifecycle of your containers, so tasks like restarting containers, updating configurations, and redeploying are automated.

3. Security and Networking Configuration

ECS allows fine-grained control over network and security configurations for each task (container), such as running tasks in a private subnet for backend services while exposing the application to the internet through a load balancer in a public subnet. ECS integrates with AWS IAM, allowing you to apply specific IAM roles to Fider containers, improving security without requiring elevated permissions for the entire EC2 instance.

Create an ECS cluster

Step 1:

Navigate to the ECS console and create an ECS cluster with Fargate launch type.

Click on Create cluster.

Image description

Image description

This will create an ECS cluster via Cloudformation in the backend.

Image description

Create Task Definition

Step 1:

Navigate to the task definition section under the ECS console and create a task definition named fider-app-task.

Image description

The task definition can be created via both the console and JSON.

Attach the EFS volume provisioned within the task definition.

Image description

Set the configuration type to configure at task definition creation.

Refer to the following JSON task definition for creating the task definition via JSON.

  {
    "taskDefinitionArn": "arn:aws:ecs:us-east-2:545009831165:task-definition/fider-app-task:7",
    "containerDefinitions": [
        {
            "name": "Fider-app",
            "image": "545009831165.dkr.ecr.us-east-2.amazonaws.com/getfider/fider:main",
            "cpu": 2048,
            "memory": 6144,
            "memoryReservation": 5120,
            "portMappings": [
                {
                    "name": "fider-app-3000-tcp",
                    "containerPort": 3000,
                    "hostPort": 3000,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [
                {
                    "name": "EMAIL_SMTP_PORT",
                    "value": "587"
                },
                {
                    "name": "DATABASE_URL",
                    "value": "postgres://<username>:<password>@localhost:5432/<database>?sslmode=disable"
                },
                {
                    "name": "EMAIL_SMTP_PASSWORD",
                    "value": "Your-SMTP-Password"
                },
                {
                    "name": "JWT_SECRET",
                    "value": "Generate a secure secret, for example using https://jwtsecret.com"
                },
                {
                    "name": "EMAIL_SMTP_ENABLE_STARTTLS",
                    "value": "'true'"
                },
                {
                    "name": "EMAIL_SMTP_USERNAME",
                    "value": "Use-SMTP-Username-here"
                },
                {
                    "name": "BASE_URL",
                    "value": "Provide your URL"
                },
                {
                    "name": "EMAIL_SMTP_HOST",
                    "value": "Provide your SMTP host"
                },
                {
                    "name": "EMAIL_NOREPLY",
                    "value": "Provide your noreply mail"
                }
            ],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "dependsOn": [
                {
                    "containerName": "postgres-db",
                    "condition": "HEALTHY"
                }
            ],
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/fider-app-task",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "us-east-2",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "systemControls": []
        },
        {
            "name": "postgres-db",
            "image": "545009831165.dkr.ecr.us-east-2.amazonaws.com/postgres:13.16",
            "cpu": 2048,
            "memory": 6144,
            "memoryReservation": 5120,
            "portMappings": [
                {
                    "name": "postgres-db-5432-port",
                    "containerPort": 5432,
                    "hostPort": 5432,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [
                {
                    "name": "POSTGRES_USER",
                    "value": "fider"
                },
                {
                    "name": "POSTGRES_PASSWORD",
                    "value": "s0m3g00dp4ssw0rd"
                },
                {
                    "name": "POSTGRES_DB",
                    "value": "fider"
                },
                {
                    "name": "PGDATA",
                    "value": "/var/lib/postgresql/data"
                }
            ],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "healthCheck": {
                "command": [
                    "CMD-SHELL",
                    "pg_isready -U fider"
                ],
                "interval": 10,
                "timeout": 5,
                "retries": 5
            },
            "systemControls": []
        }
    ],
    "family": "fider-app-task",
    "executionRoleArn": "arn:aws:iam::545009831165:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "revision": 7,
    "volumes": [
        {
            "name": "pg_data_volume",
            "efsVolumeConfiguration": {
                "fileSystemId": "fs-08874b6ca635e5349",
                "rootDirectory": "/",
                "transitEncryption": "ENABLED",
                "transitEncryptionPort": 2049,
                "authorizationConfig": {
                    "accessPointId": "fsap-0bc6e6c2dd68da7b8",
                    "iam": "DISABLED"
                }
            }
        }
    ],
    "status": "ACTIVE",
    "requiresAttributes": [
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.ecr-auth"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.28"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
        },
        {
            "name": "ecs.capability.container-health-check"
        },
        {
            "name": "ecs.capability.execution-role-ecr-pull"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        },
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.24"
        },
        {
            "name": "ecs.capability.efsAuth"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "ecs.capability.efs"
        },
        {
            "name": "ecs.capability.container-ordering"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.25"
        }
    ],
    "placementConstraints": [],
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "4096",
    "memory": "24576",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "registeredAt": "2024-10-27T11:10:15.921Z",
    "registeredBy": "arn:aws:sts::your-account-id:assumed-role/AWSReservedSSO_AdministratorAccess_06c094dca8d9665c/your-account-name",
    "enableFaultInjection": false,
    "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Configuring mail server for Fider

Fider requires a mail server so administrators can set up SMTP (Simple Mail Transfer Protocol) details in Fider's environment settings. This usually involves specifying the mail server’s hostname, port, and authentication credentials (if needed) or via API, ensuring Fider can send emails reliably and securely.

The application requires email verification as part of the signup process to prevent spam and ensure only valid users access the platform.

Step 1: Setup a Mailgun account

Sign up with Sinch Mailgun and create an account at: https://signup.mailgun.com/new/signup?

If you don't want to add the payment information at the time of sign up, you can ignore that and proceed without providing it.

Image description

Step 2: Navigate to Domains paths under send

Once the sign up process is completed, Log in with your account and navigate to the

Domain path under the sending navigation panel.

Image description

You can either verify your domain and add the DNS records that Mailgun provides you with, to your DNS registrar or you can go with Mailgun's sandbox account to test the email functionality of the application.

Step 3: Use the SMTP credentials

There are two options, You can either use Mailgun's API or its SMTP credentials to plug into your app's settings.

Click on the sandbox account domain, it'll take you to the overview section.

Image description

I'll be using the SMTP credentials in my app's settings (environment variables).

Image description

Sandbox domains are restricted to authorized email recipients only, so verify your email address beforehand.

Image description

Use these SMTP credentials inside the task definition of the Elastic container service.

Create an ECS service

Step 1:

Navigate to the ECS cluster you created and click on it, once inside the cluster click on service.

Image description

Step 2: Configuration parameters

In the compute configuration choose the launch type and choose Fargate as a launch type in the dropdown.

Image description

Choose service as the application type in the deployment configuration and Replica as the service type.

Image description

In the networking configuration choose the VPC you created and choose the same subnets where your EFS too resides, else you'll run into EFS utils error while initializing the task.

Image description

Turn off the public IP address as the tasks will use NAT gateway.

Configure the Application load balancer and place it inside the public subnet as front-facing for your application.

Image description

Create target-group on the fly in the ECS console itself.

Image description

Click on create in the end and navigate to Cloudformation to view the status.

Image description

Step 3: Use ACM for SSL certificates

The service is up and running now.

Image description

Navigate to the ACM console and click on request a certificate.

Image description

Request a public certificate and click on Next.

Image description

Provide your domain name and choose the DNS validation method.

Image description

Click on request and ACM provides you with the DNS records, verify the records by mapping them to your DNS registrar settings.

Image description

Your SSL certificate is ready to be used with the application load balancer.

Image description

Step 4: Configure HTTPS for ALB

Navigate to the ALB console and setup a HTTPS listener by clicking on Add listener.

Image description

Choose ACM as a certificate source in Secure listener settings.

Image description

Edit the HTTP listener rule and redirect the HTTP traffic to the HTTPS listener.

Image description

In routing actions select the Redirect to URL option and click on save changes.

Image description

Step 5: Map the ALB endpoint to your subdomain

Image description

Navigate to your DNS registrar's DNS settings and map it to your subdomain.

Image description

Try passing it as CNAME record if your registrar does not allow A record mapping for ALB.

Step 6:

Now enter the URL you passed in the ECS task definition in the web browser.

Image description

Viola! There you have it. Fider's up and about with the tasks running securely inside a private subnet of their own and the ALB routing requests to them.

. . . . . . . . . . . .