Complete guide to deploying SSR Vite apps on AWS with automation

Vue + Vite + SSR + AWS + What we learned?
Apr 11 2022 · 7 min read

Background

We recently switched to SPA for canopas.com, which meant we had to implement SSR for SEO and loading time optimizations.

The process was not straightforward. Earlier we used Amazon S3 + Cloudflare for deployment but that does not work with SSR apps.

Today, we will see how to deploy the SSR Vite app on AWS and automate the deployment process. Though this article refers to only the Vite app, it works for almost all SSR apps. We will use Github actions but you can use any CI.

If you want to have a sneak peek, here’s a preview of our company website. It’s also open-source and the full source of the website is available on Github.

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

First of all, what is SSR?

Server-side rendering(SSR) is a method in which whenever we run a website page, the browser will send the request to the server, the server will process it, generate a fully rendered HTML page, and send it back to the browser.

We can create it in any frontend technology like VueJS, React, etc…

For more information visit Server-side rendering.

Let’s get started

I’m assuming that you have created a server-side rendered app using Vuejs and Vite. Otherwise, you can refer to Vite SSR docs.

I have divided the deployment and automation process into 3 steps.

  • Build an app.
  • Create a Docker Image and push it on ECR.
  • Create a cloudformation stack to automate deployment

Let’s get started!


1. Build an app

Run npm run build command, which will run vite build internally and create a dist folder containing all client and server-side code.

2. Create a Docker Image and push it on ECR.

Create a Docker file in the route directory and add the following code

FROM node:17 AS ui-build
WORKDIR /app
COPY vue-frontend/ ./
RUN npm install && npm run build

FROM node:17 AS server-build
WORKDIR /root/
COPY --from=ui-build /app/dist ./dist
COPY --from=ui-build /app/node_modules ./node_modules
COPY --from=ui-build /app/server.js /app/package*.json ./

EXPOSE 3000

CMD ["npm", "run", "serve"]
  • We are using version 17 of the node. Install dependencies using npm install and build the app using npm run build.
  • In the server-build stage, we will copy dist, server.js, node_modules, and package.json from the ui-build stage to the server-build.
  • Exposed port 3000 and run an app using npm run serve .

It’s time to push the docker image on ECR.

  • Create an ECR repository on the AWS console in the preferred region. After that, you will have the ECR image ARN .
  • Create deploy-ecr-image.sh file in the root directory and add the following scripts.

We will use $GITHUB-SHA to separate images per commit.

IMAGE_TAG="$GITHUB_SHA"

Get the login password of ECR and login into it to push the image.

aws ecr get-login-password --region < AWS-REGION > | docker login --username AWS --password-stdin < YOUR-ECR-URI >

Build a docker image from the Dockerfile.

docker build -t < DOCKER-IMAGE-NAME >:$IMAGE_TAG .

Create a target image to push on ECR.

docker tag < DOCKER-IMAGE-NAME >:$IMAGE_TAG $IMAGE_ARN:$IMAGE_TAG

Push image on ECR

docker push $IMAGE_ARN:$IMAGE_TAG

Here is the full code of deploy-ecr-image.sh.

#! /bin/bash

set -e

IMAGE_TAG="$GITHUB_SHA"
IMAGE_ARN=$1

aws ecr get-login-password --region < AWS-REGION > | docker login --username AWS --password-stdin < YOUR-ECR-URI >

docker build -t < DOCKER-IMAGE-NAME >:$IMAGE_TAG .

docker tag < DOCKER-IMAGE-NAME >:$IMAGE_TAG $IMAGE_ARN:$IMAGE_TAG

docker push $IMAGE_ARN:$IMAGE_TAG

Let’s automate these tasks in Github workflows.

Create IAM ROLE that will have permissions for our cloudformation stack resources.

Create deploy.yml in .github/workflows and add the following code.

  • In the deploy stage, do the basic setup to use node 17.
- name: Checkout        
  uses: actions/[email protected]
- uses: actions/setup-node@v1        
  with:          
    node-version: "17"
- name: Configure AWS credentials        
  uses: aws-actions/configure-aws-credentials@v1        
  with:          
    role-to-assume: < IAM-ROLE-ARN >         
    aws-region: < AWS-REGION >
  • In Build frontend and push on ECR step, we have run deploy-ecr-image.sh the file and passed ECR-IMAGE-ARN as a parameter. This will push the docker image on ECR.
- name: Build frontend and push on ECR        
  run: |          
   cd vue-frontend          
   sh ./deploy-ecr-image.sh < ECR-IMAGE-ARN >

Here is deploy.yml file,

name: DeploySSRApp
on:
  push:
      
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/[email protected]

      - uses: actions/setup-node@v1
        with:
          node-version: "17"

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: < IAM-ROLE-ARN >
          aws-region: < AWS-REGION >

      - name: Build frontend and push on ECR
        run: |
          cd vue-frontend
          sh ./deploy-ecr-image.sh < ECR-IMAGE-ARN >

In the next step, we will run that docker image using ECS.

3. Create a cloudformation stack to automate deployment

Create template.yml file in infrastructure directory for creating a stack. We will add ECS, ELB and security groups resources in the template.

3.1. Create a security group for the Load Balancer

This will add inbound rules for the application load balancer.

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  ApplicationLBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: SG for the Fargate ALB traffic
      GroupName: < SECURITY-GROUP-NAME >
      SecurityGroupIngress:
        - CidrIpv6: ::/0
          FromPort: 80
          ToPort: 80
          IpProtocol: TCP
          Description: "Inbound rule for http IPv6 traffic"
        - CidrIp: 0.0.0.0/0
          FromPort: 80
          ToPort: 80
          IpProtocol: TCP
          Description: "Inbound rule for http IPv4 traffic"
        - CidrIpv6: ::/0
          FromPort: 443
          ToPort: 443
          IpProtocol: TCP
          Description: "Inbound rule for https IPv6 traffic"
        - CidrIp: 0.0.0.0/0
          FromPort: 443
          ToPort: 443
          IpProtocol: TCP
          Description: "Inbound rule for https IPv4 traffic"

3.2. Create a security group for routing traffic from the load balancer to the ECS service

This will add inbound rules for ECS service to allow access to the load balancer.

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  ApplicationLBToECSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: SG for traffic between ALB and ECS service container
      GroupName: < SECURITY-GROUP-NAME >
      SecurityGroupIngress:
        - IpProtocol: -1
          SourceSecurityGroupId: !GetAtt ApplicationLBSecurityGroup.GroupId
          Description: "Inbound rule for all ECS service container traffic"

3.3. Create a target group

Target Group will route Load balancer traffic to a specific target, which is our ECS service.

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  TargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      Name: < TARGET-GROUP-NAME >
      VpcId: < VPC-ID >
      Protocol: HTTP
      Port: 3000
      HealthCheckPath: /actuator/
      TargetType: ip

3.4. Create HTTP listeners for load balancers

We will create HTTP and HTTPS listeners for load balancers. HTTP listener will route traffic to HTTPS.

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  # http (port:80) listener redirects to https
  HTTPListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - 
          Order: 1
          RedirectConfig: 
            Protocol: "HTTPS"
            Port: "443"
            StatusCode: "HTTP_301" #redirect from http to https
          Type: "redirect"
      LoadBalancerArn: !Ref ApplicationLoadBalancer  #load balancer reference
      Port: 80
      Protocol: HTTP
  
  # https (port:443) listener with ssl certificate
  HTTPSListener: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer #load balancer reference
      Port: 443
      Protocol: "HTTPS"
      SslPolicy: "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
      Certificates: 
        - CertificateArn: < CERTIFICATE-ARN >  #created using AWS cerificate manager
      DefaultActions: 
        - 
          Order: 1
          TargetGroupArn: !Ref TargetGroup #target group reference
          Type: "forward"

3.5. Create Application Load Balancer

Application Load Balancer with HTTP and HTTPS listeners will create DNS and we can point that DNS to our domain.

The flow of running the app is like this,

Domain(https://example.com) — ELB DNS — Target Group — AWS Service

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:          
   ApplicationLoadBalancer:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: < APPLICATION_LOAD-BALANCER_NAME >
      Subnets:   #can add multiple subnet ids
        - <SUBNET-ID-1>
        - <SUBNET-ID-2>
      Type: application
      SecurityGroups:
        - !GetAtt ApplicationLBSecurityGroup.GroupId #security group id

3.6. Create an ECS cluster

A cluster is used to run Task definition using services.


AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: < CLUSTER-NAME >

3.7. Create Task definition

Task definition refers ECR Image that we have pushed in the second step.

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      RequiresCompatibilities:
        - "FARGATE"
      #role for executing task definition
      ExecutionRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole 
      Cpu: 256  #reserved .25vCPU
      Memory: 512  #reserved .5GB memory
      NetworkMode: "awsvpc"
      ContainerDefinitions:
        - Name: "ssr-app"
          Image: < ECR-IMAGE-ARN >
          MemoryReservation: 256
          Memory: 512
          PortMappings:
            - ContainerPort: 3000  #mapping port to 3000
              Protocol: tcp

3.8. Create ECS service

Service resides in Cluster uses Task definition and run SSR App

AWSTemplateFormatVersion: 2010-09-09
Description: An ECS, ECR, ALB and cloudfront stack
    
Resources:       
   ECSService:
     Type: AWS::ECS::Service
     DependsOn:
      - HTTPListener
      - HTTPSListener
     Properties:
       LaunchType: FARGATE  #launch type is farget
       Cluster:
         Ref: "ECSCluster"  #ecs cluster reference
       DesiredCount: 1
       TaskDefinition:
         Ref: "TaskDefinition"  #task definition reference
       DeploymentConfiguration:
         MaximumPercent: 100
         MinimumHealthyPercent: 0
       NetworkConfiguration:
            AwsvpcConfiguration:
                AssignPublicIp: ENABLED
                SecurityGroups:
                    - !GetAtt ApplicationLBToECSSecurityGroup.GroupId #security group id
                Subnets: [<SUBNET-ID-1>, <SUBNET-ID-2>] #can add multiple subnet ids
       LoadBalancers:
            - TargetGroupArn:
                Ref: TargetGroup #target group reference
              ContainerPort: 3000
              ContainerName: < ECR-CONTAINER-NAME>

Now we have all the resources of the cloudformation stack in template.yml, We have to execute this template from the deploy.yml file.

Add the following step in deploy.yml

- name: Deploy cloudformation stack
        id: SSR-stack
        uses: aws-actions/aws-cloudformation-github-deploy@v1
        with:
          name: SSR-stack
          template: infrastructure/template.yml
          capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM
          timeout-in-minutes: "10"
          no-fail-on-empty-changeset: "1"

Yes, that’s it!

Now whenever we push code on Github, the deploy.yml will run and deploy your app on AWS.

Conclusion 

That’s it for today. Hope you have an idea of how SSR app deployment works. This is not the only way to deploy it though. AWS offers so many tools that can handle this type of deployment. However, at least you have a starting point if you need to deploy SSR apps.

We’re Grateful to have you with us on this journey!

Suggestions and feedback are more than welcome! 

Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.

Similar articles


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.

canopas-logo
We build products that customers can't help but love!
Get in touch
background-image

Get started today

Let's build the next
big thing!

Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.

Get Free Consultation
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.