Setting Up Zero-Downtime Deployment - Front-End Configuration

Server

Language :

Hi, I’m Lovefield.

Continuing from the previous article, Setting Up Zero-Downtime Deployment - Environment Setup, this post will describe the configuration for achieving zero-downtime deployment of the front-end server. For the front-end framework, I used Nuxt.js, and the runtime environment was set up with Bun.js. Since building was a priority, I will first create a build and upload functionality using Git Actions.

Before writing the Git Action, you need to set the following secrets values:

  • ENV: Environment variable values used by the front-end.
  • HOST_DIR: Base directory location to be used on the server.
  • HOST_IP: Server address.
  • HOST_RUN_DIR: Directory location where scripts are executed on the server.
  • HOST_USER: Server username.
  • PEM_VALUE: PEM key value for accessing the server.

docker-compose.yml

YAML

name: Deploy

on:
   push: # Executed when a push is made to the branch.
       branches:
           - main
   workflow_dispatch: # Manually executed from the GitHub Actions page.
       name:
           description: "print name"

jobs:
   buildAndDeploy:
       runs-on: ubuntu-latest
       permissions:
           contents: read
           packages: write
       steps:
           - uses: actions/checkout@v3
           - uses: oven-sh/setup-bun@v2 # Use bun.js

           - name: Module Install
             run: bun install --frozen-lockfile

           - name: Create env
             run: echo "$ENV" >> .env
             env:
                 ENV: ${{ secrets.ENV }}

           - name: Blue
             run: bun run build

           - name: Create ZIP
             run: zip -qq -r ./.output.zip ./.output

           - name: Upload ZIP file
             uses: appleboy/scp-action@v0.1.7
             with:
                 host: ${{ secrets.HOST_IP }}
                 username: ${{ secrets.HOST_USER }}
                 key: ${{ secrets.PEM_VALUE }}
                 port: 22
                 source: "./.output.zip"
                 target: ${{ secrets.HOST_DIR }}/

           - name: Run Deploy Script
             uses: appleboy/ssh-action@v1.1.0
             env:
                 HOST_DIR: ${{ secrets.HOST_DIR }}
                 HOST_RUN_DIR: ${{ secrets.HOST_RUN_DIR }}
             with:
                 host: ${{ secrets.HOST_IP }}
                 username: ${{ secrets.HOST_USER }}
                 key: ${{ secrets.PEM_VALUE }}
                 port: 22
                 envs: HOST_DIR,HOST_RUN_DIR
                 script: |
                     sudo cp -vf $HOST_DIR/.output.zip $HOST_RUN_DIR/front/blue/.output.zip
                     sudo cp -vf $HOST_DIR/.output.zip $HOST_RUN_DIR/front/green/.output.zip
                     cd $HOST_RUN_DIR
                     sudo sh deploy-frontend.sh

After performing the build and upload using GitHub Actions, the configuration executes ‘deploy-frontend.sh’ on the server. This ‘deploy-frontend.sh’ script updates the Blue and Green containers.

deploy-frontend.sh

Bash

#!/bin/bash

# Check if the blue container is running.
EXIST_BLUE=$(sudo docker compose ps -a | grep "frontend-blue")
target="blue";
target_port="8002"
TIMESTAMP=`date +%Y%m`
LOGFILE="./logs/frontend-$TIMESTAMP.log"

func_reload() # Re-load function
{
   echo "[$(date +%Y-%m-%d\ %H\:%M\:%S)] $1 Container down" >> $LOGFILE

   sudo docker compose down frontend-$1 # container down

   rm -rf ./front/$1/.output # Delete existing files

   sudo unzip -o ./front/$1/.output.zip -d ./front/$1 # unzip file

   rm ./front/$1/.output.zip # delete zip file

   echo "[$(date +%Y-%m-%d\ %H\:%M\:%S)] $1 Container up" >> $LOGFILE

   sudo docker compose up frontend-$1 -d # container up

   sleep 1.5

   while true; do # Perform a health check every 2 seconds
       RESPONSE=$(curl -I http://localhost:$2/health-check | grep "200" | awk '{print $2}')

       echo "[$(date +%Y-%m-%d\ %H\:%M\:%S)] $1 Container Health Check" >> $LOGFILE

       if [ "$RESPONSE" -eq "200" ]; then
           break;
       fi
      
       sleep 2
   done
}

touch $LOGFILE

echo "[$(date +%Y-%m-%d\ %H\:%M\:%S)] >>> Start of the front-end deployment script <<<" >> $LOGFILE

# If the Blue container is running
if [ "$EXIST_BLUE" ]; then
   target="green";
   target_port="8001"
else
   target="blue";
   target_port="8002"
fi

func_reload $target $target_port

if [ "$target" -eq "blue" ]; then
   func_reload "green" "8001"
else
   func_reload "blue" "8002"
fi

echo "[$(date +%Y-%m-%d\ %H\:%M\:%S)] >>> End of the front-end deployment script <<<" >> $LOGFILE

After writing the script as described above, it can be executed either through a Git push or by directly creating an action. The process will follow these steps:
Git Action builds the files based on the main branch.
Git Action compresses the built files and transfers them to the server.
Git Action executes the deployment script on the server.
The deployment script unzips the received file and updates the Blue and Green containers respectively.

Once everything is completed, log files will be created in the logs folder, as shown below.

Plain Text

[2024-12-22 23:28:50] >>> Start of the front-end deployment script <<<
[2024-12-22 23:28:50] green Container down
[2024-12-22 23:29:03] green Container up
[2024-12-22 23:29:11] green Container Health Check
[2024-12-22 23:29:11] blue Container down
[2024-12-22 23:29:22] blue Container up
[2024-12-22 23:29:28] blue Container Health Check
[2024-12-22 23:29:28] >>> End of the front-end deployment script <<<

With this, the front-end deployment configuration is complete.

Lovefield

Web Front-End developer

하고싶은게 많고, 나만의 서비스를 만들고 싶은 변태스러운 개발자입니다.