DEV Community

Cover image for Lift and Shift to AWS with IaC
Franco Cerutti
Franco Cerutti

Posted on

Lift and Shift to AWS with IaC

What is Lift and Shift?

Lift and Shift, or re-hosting, is one of the strategies to migrate existing applications into the cloud, where the application is not modified, only the hosting platform is changed.

Check the next link for the 6'R of migrations: AWS Docs

In this article case we're going to see the Open Source project of E-Commerce Medusa Backend Server and an Admin Panel

Medusa Architecture

What is Medusa?

Medusa is an open source project for building blocks of e-commerce, in order to not re-invent the wheel, which provides a extensive API's, Backoffice and the UI using SSR/SSG with Next.JS

How to migrate to AWS?

To migrate to AWS, we have to make some considerations before dropping that artifact in a compute and expose it with a DNS.

Considerations when migrating to AWS

1 - Operations

What things do we need to keep running in order to sustain the platform?

2 - Security

What security assessment or compliances i need to be comply to?

3 - Costs

What is my monthly and annually budget?

4 - Performance

Which is my desired traffic, or user consumption business goal?

5 - Availability

How is my application going to be available in case of a cloud provider failure?

6 - Resillience

Is my application going to be ready to fail, and keep running?

With that being said we can choose the right resources in AWS to make this lift-shift migration strategy

Ready?

Let's migrate.

Steps to migrate to AWS

Install Dependencies

npm install @medusajs/medusa-cli -g

Create the Medusa Backend Server

medusa new my-medusa-store

Dockerize the application

You can use the next Docker, i recommend to make this one with Multi-stage pattern for PROD and keep the platform in case the CI is running on arm64 if not, remove it

FROM  --platform=linux/amd64 public.ecr.aws/docker/library/node:lts

# Create app directory
WORKDIR /usr/src/app

# Bundle app source
COPY . .

RUN npm install

# If you are building your code for production
# RUN npm ci --omit=dev

EXPOSE 80

CMD [ "node", "index.js" ]
Enter fullscreen mode Exit fullscreen mode

Build Docker and Run it Locally

docker build -f ./Dockerfile. -t <myImage> --no-cache

docker run <myImage> -p 80:80

Deploy to AWS

For this architecture we decided to go with AWS ECS Launch Type Fargate, since the business needs a fully managed container service orchestration to handle the deployment, load balancing, where Amazon ECS will launch, monitor, and scale your application across flexible compute options with automatic integrations to other supporting AWS services that your application needs

*How we are going to deploy *

For deployment we're going to use best practice DevOps using Infrastructure as Code ( IaC ) with Pulumi

Run the next code at the root level

pulumi new aws-typescript --dir ./infra

First we need to create Roles, Repository of ECR and Image for our Docker

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

const imageName = 'medusa-ecr-image';
const repositoryName = 'medusa-ecr-repository';

const repository = new awsx.ecr.Repository(repositoryName);

const imageArgs: awsx.ecr.ImageArgs = {
    dockerfile: '../dockerfile',
    path: '../',
    repositoryUrl: repository.url,
};

const dockerImage = new awsx.ecr.Image(imageName, imageArgs);

// Define the Log Group Creator IAM
const logGroupCreatorPolicy = new aws.iam.Policy("logGroupCreatorPolicy", {
    policy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Action: [
                "*"
            ],
            Effect: "Allow",
            Resource: "*"
        }]
    })
});

// Create a new Role
const ecsRole = new aws.iam.Role("ecsRole", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Action: "sts:AssumeRole",
            Principal: {
                Service: "ecs-tasks.amazonaws.com",
            },
            Effect: "Allow",
        }],
    }),
});

// Attach the log creator policy to the role
new aws.iam.RolePolicyAttachment("rolePolicyAttachment", {
    role: ecsRole.name,
    policyArn: logGroupCreatorPolicy.arn,
});

const executionECSRole = {
    roleArn: ecsRole.arn,
};

Enter fullscreen mode Exit fullscreen mode

Now we need to create:

  • ECS Cluster
  • Fargate Service
  • Task Definition
  • Load Balancer

// create ECS cluster

const clusterName = 'medusa-ecs-cluster';
const loadBalancer = 'medusa-ecs-load-balancer';
const ecsService = 'medusa-ecs-service';


const configLoadBalancer: awsx.lb.ApplicationLoadBalancerArgs = {
    defaultTargetGroup: {
        port: 80,
        healthCheck: {
            path: '/health',
        }
    },
    enableWafFailOpen: true,
};

const lb = new awsx.lb.ApplicationLoadBalancer(loadBalancer, configLoadBalancer);


const cluster = new aws.ecs.Cluster(clusterName, {});

const service = new awsx.ecs.FargateService(ecsService, {
    cluster: cluster.arn,
    assignPublicIp: true,
    desiredCount: 2,
    taskDefinitionArgs: {
        executionRole: executionECSRole,
        containers: {
            app: {
                image: dockerImage.imageUri,
                cpu: 512,
                memory: 1024,
                essential: true,
                portMappings: [{
                    containerPort: 80,
                    hostPort: 80,
                    targetGroup: lb.defaultTargetGroup,
                }],
                healthCheck: {
                    command: ["CMD-SHELL", "curl --fail http://localhost/health || exit 1"],
                    interval: 30,
                    timeout: 3,
                    retries: 3
                },
                logConfiguration: {
                    logDriver: "awslogs",
                    options: {
                        "awslogs-create-group": "true",
                        "awslogs-group": "medusa-logs-group",
                        "awslogs-region": "us-east-1",
                        "awslogs-stream-prefix": "medusa-ecs-logs"
                    }
                }
            }
        },
    },

});

export const url = lb.loadBalancer.dnsName;
Enter fullscreen mode Exit fullscreen mode

You can see important things here.

Load balancer config with healthcheck route, the definition of the container app with hardware resources and port mapping, this will allow that the Task Definition of the Cluster exposes a port to the cluster and to the container.

Last the container has a healthcheck inside where checks if the Docker is healthy

Now let's wrap this up!

pulumi up

Resources Graph

Pulumi will expose the DNS for our service, check the /health route and it should be returning OK.

Also check the /app which holds the Admin Panel.

Hope you enjoy it and learn how to Lift and Shift an App to AWS using ECS Fargate.

Top comments (0)