반응형
250x250
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

3티어 기반 배포(프론트엔드) 본문

UpLog 릴리즈노트 프로젝트

3티어 기반 배포(프론트엔드)

5_hyun 2023. 8. 22. 22:41

이번 프로젝트에서는 Kakao I Cloud(이하 KIC)에서 배포를 했다. 이유는 KIC를 지원해 줬기 때문이다!

사실 처음 CI/CD를 제대로 해보는 느낌이라 삽질을 많이 했다. 그리고 이전에 배포를 제대로 해 본 적이 없으니 기본적인 3 티어 기반으로 배포를 해봤다. 이 중에서 나는 프론트엔드 부분을 배포했다.

아키텍처 설계도

우선 우리는 이런 식으로 3 티어 기반의 설계를 하고 CI/CD를 했다. 
주요 사용 Tool은 Jenkins, Docker, Dockerhub, GitHub, Nginx 등이며, 깃허브에 main 브랜치에 push가 되면 자동으로 배포가 되게 설계하였다.

key pair

KIC를 이용하기 위해서 가장 먼저 해야 할 일은 key를 발급받아야 한다. 

key를 발급받으면 public key는 서버에 저장이 되고, private key만 다운로드된다. 이때 주의 할 점은 재발급이 안되니 잘 보관해야 한다.

security group

-인바운드 정책

또한 security group에서 인바운드 정책을 설정해 줘야 인스턴스에 접근이 가능하다.

이런 식으로 추가를 해줘야 그 ip에서 접근이 가능하다. 일단 bastion 인스턴스에 접근하기 위해서 22 port를 허용해 줬다.

만약 모든 인바운드를 허용하려면 위 사진처럼 설정하면 되지만 보안에 좋지 않다.

 

-아웃바운드 정책

아웃바운드 정책은 모두 허용해 줬다.

인스턴스 구성

일단 기본적으로 멘토님이 처음에 main, subnet 이렇게 2개로 서브넷을 나눠서 주셨다.

  • main: bastion, LB
  • subnet: WS, WAS, DB

이런 식으로 나눠서 구성을 하였고, main에 있는 bastion과 LB에만 public IP를 부여하였다.

그리고 LB에 Nignx Proxy Manager(이하 NPM)을 설치하여 nignx를 reverse proxy로 사용해 관리했다.

(각 인스턴스 스펙은 4 vcpu, 16 memory로 구성했다.)

서브넷을 나누는 것의 장점은?

서브넷을 나누는 이유는 용도별로 네트워크의 관리적인 측면도 있지만 가장 큰 이유는 보안의 이슈 때문이다. 서브네팅을 통해서 VPC안에서도 외부에서 접근가능한 Public subnet, 외부에서 접근이 불가능한 Private subnet으로 구분할 수가 있게 되는 것이다.

다른 인스턴스에 접근하려면?(bastion)

ssh.cfg 파일을 로컬에 만들고 이를 통해 접속했다. 

 

-ssh.cfg

Host jump
	HostName {bastion publiuc IP}
	User ubuntu #유져 네임
	Port 22
	IdentityFile /Users/kwon-ohhyun/Downloads/{private key 파일 이름} #priavte key 위치

Host 10.0.*
	StrictHostKeyChecking no
	ProxyCommand ssh -F ssh.cfg -W %h:%p jump

이런 식으로 작성하였으며

ssh -F ssh.cfg -i {private key 파일 이름} ubuntu@{접속 할 인스턴스의 사설 IP}

이 명령어로 접속할 수 있는데, 주의할 점은 반드시 private key가 저장된 경로에서 해줘야 한다!


리액트 파일에 설정할 것

Dockerfile

#### Stage 1: Build the react application
FROM node:lts-alpine as build

# Configure the main working directory inside the docker image.
WORKDIR /app

# Copy the package.json as well as the yarn.lock and install the dependencies.
COPY package.json yarn.lock ./
RUN yarn install
RUN yarn set version berry

# Copy the main application
COPY . ./

# Build the application
RUN yarn build

CMD ["yarn", "dev"]

# Stage 2: Create the production image with Nginx
FROM nginx:latest

# Remove default nginx configurations
RUN rm -rf /etc/nginx/conf.d/*

# Copy the build artifacts from the previous stage
COPY --from=build /app/dist /usr/share/nginx/html

# Copy the nginx.conf file
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Expose the default Nginx port
EXPOSE 3070

# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

2개의 stage로 나눠서 Dockerfile을 작성했다.

첫 번째 stage, 처음엔 docker에 image가 올라가게 하는 것이다. node를 lts로 설치하고 yarn install 및 yarn berry로 설정을 해줬다. 그 후에 yarn build와 yarn dev가 실행되도록 했다.

두 번째 stage, nginx와 연결하는 부분이다. nginx를 최신 버전으로 설치하고 실행하는 코드이다. 여기서 고생했던 부분은 "EXPOSE 3070"을 하지 않아서 힘겨웠다. 빌드된 리액트 이미지를 컨테이너에 올리고 실행할 때, 포트포워딩을 3070으로 했기 때문에 저 부분도 3070으로 해줬어야 했다.

nginx.conf

upstream backend{
        least_conn;
        server {첫 번째 백엔드 URL};
        server {두 번째 백엔드 URL};
}

server {
  listen 3070;
  server_name localhost;

  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html;
  }

  location /api {
  rewrite /api/(.*) /$1  break;

    proxy_pass http://backend;
    proxy_redirect     off;
    proxy_set_header Host $host;
  }
}

nginx.conf 파일은 위와 같다.

upstream backend{
        least_conn;
        server {첫 번째 백엔드 URL};
        server {두 번째 백엔드 URL};
}

우선 upstram backend 부분에는 기본값인 라운드 로빈(least_conn) 알고리즘을 사용했다. 그리고 백엔드 url을 다음과 같이 설정해서 2개의 주소에 로드밸런싱 되도록 설정했다.

server {
  listen 3070;
  server_name localhost;

  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html;
  }

  location /api {
  rewrite /api/(.*) /$1  break;

    proxy_pass http://backend;
    proxy_redirect     off;
    proxy_set_header Host $host;
  }
}

이 부분에서도 리액트 이미지 실행 시 포트포워딩을 3070으로 해서 listen을 3070으로 설정했다.

그리고 프록시 서버를 이용해 axios 요청 코드에서 "/api"를 붙이면 자동으로 로드밸런싱 되는 백엔드 url로 요청이 보내지도록 설정했다.

 

  • 트러블 슈팅!
    1. 이 부분에서 가장 애를 먹었던 것은 OS마다 설치하는 라이브러리가 다르다는 것이다.
    나는 처음에 Mac OS에서 먼저 테스트를 했을 때는 esbuild-linux-arm64이 라이브러리를 설치했어야 했다. 하지만 ubuntu vm에서는 저 라이브러리 때문에 실행이 안 됐었다. 이것 때문에 하루 정도 고생했다ㅠㅠ

    2. sharp이 라이브러리는 이미지 리사이징을 도와주는 라이브러리이다. 우선 이 라이브러리는 python 3 버전이 설치되지 않으면 에러가 났었다.

      이런 에러가 나서 ububtu vm에 python 3 버전을 설치하니까 해결이 됐다.

      하지만 sharp를 제거해야 빌드가 성공할 때가 있고 설치해야 성공할 때도 있어서 아직 좀 헷갈린다..!

수동 빌드 및 실행 명령어

docker build -t react .

이 명령어로 리액트를 이미지화시킬 수 있다.

docker run -d -p 3070:3070 react:latest

그 후에 3070으로 포트포워딩해서 실행시키면 된다!


Nginx Proxy manager(NPM)

NPM은 외부와 내부의 통신을 연결시켜 주는 보안 통로 역할을 한다. Reverse proxy, Redirection, 보안인증, SSL인증서등의 보안등을 GUI로 관리할 수 있게끔 하는 툴이다. docker로도 설치할 수 있지만 docker-compose를 이용해서 설치한다.

NPM은 LB 인스턴스에서 구현했다.

docker-compose 및 NPM 설치

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose- $(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

위 명령어로 docker-compose를 설치한다.

chmod +x /usr/local/bin/docker-compose

위 명령어로 docker-compose 폴더에 권한을 준다.

docker-compose --version

이 명령어로 버전 확인이 되면 잘 설치된 것이다.

 

-docker-compose.yml

version: '3' 
services:
  app:
  	image: 'jc21/nginx-proxy-manager:latest' 
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
      - '10001-10199:10001-10199'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

이렇게 내용을 입력하고

docker-compose up -d

이러면 NPM이 설치가 된다.

NPM

Email address: admin@example.com

Password: changeme

맨 처음 아이디와 비밀번호는 다음과 같다. 이제 포트 매핑으로 프록시 관리를 할 수 있다.

 

리액트는 WS 인스턴스에서 3070 포트로 돌아가고 있어서 이제 매핑을 해주면 된다.

Streams에서 10001 포트를 사용하게 했다. 가린 부분은  "{WS의 사설 IP}:3070"으로 해줬다.

이렇게 설정하면 이제 "{LB의 공인 IP}:10001"로 접속하면 실행 중인 리액트 애플리케이션에 접속할 수 있다!

(젠킨스는 10005번 포트를 사용하도록 설정했다!)


젠킨스 설치

jenkins라는 이름의 인스턴스를 새로 만들고 진행했다.

 

docker search Jenkins

우선 젠킨스 이미지를 찾는다.

docker pull jenkins/jenkins:lts

그리고 젠킨스 최신 버전을 다운로드한다.

docker run -d -p 8181:8080 --restart=always --name my_jenkins -u root
jenkins/jenkins:lts

jenkins를 실행시켰다.

 

위 NPM에서 젠킨스를 10005번 포트에서 열리도록 설정해서 "{공인 IP}:10005"로 접속하면 이런 화면이 나온다.

vi /var/jenkins_home/secrets/initialAdminPassword

이 경로로 들어가면 비밀번호가 있고 입력해 주면 된다. 그러면 초기 설정이 실행되는데 다운로드가 아주 느리다..!

이후에 모든 설정이 완료되면 이렇게 관리자 설정을 할 수 있다.

젠킨스 설치 완료!!


젠킨스 파이프라인 구축

위에서 수동으로 Dockerizing을 한 것을 기반으로 파이프 라인을 구축해 보겠다!

plugin 설치

NodeJS를 설치했다.

그리고 Tools에 가서 NodeJS를 LTS인 18 버전으로 설정하고, yarn berry를 이용해서 yarn도 설치하게 했다.

Git  설정

git을 파이프라인 스크립트에서 평소처럼 git이라고 사용하겠다는 설정이다.

Credentials

우분투에 접근하기 위해서 private key, repository 및 webhook을 위해 Github Token, Dockerhub에 올리기 위한 Dockerhub Token 이렇게 3가지를 등록했다.

파이프라인 스크립트

pipeline {
    agent any
    tools {
        nodejs "node18"
        git "git"
    }
      environment {
        DOMAIN = 'http://gak'
        PUBLIC_IP = {pulic IP}
    }
    stages {
        stage('prepare') {
            steps {
                echo 'prepare'
                git branch: "main", credentialsId: "GIT_ACCOUNT", url: 'https://github.com/GAK-coding/UpLog-frontEnd'
            }
        }
        stage('Create .env') {
            steps {
                echo 'Creating .env file'
                script {
                    def envContent = """
                    	{.env 내용}
                        """
                    writeFile file: '.env', text: envContent
                }
            }
        }
        stage('build') {
            steps {
                sh "docker build -t 5hyun/react ."
            }
        }
        stage('Push Docker') {
          agent any
          steps {
            echo 'Push Docker'
            script {
                withDockerRegistry([ credentialsId: "5hyun_dockerhub", url: "" ]) {
                sh "docker push 5hyun/react:latest"
            }
          }
        }
    }
        stage('SERVER WS') {
          agent any
          steps {
            echo 'ssh'
            script {
                withCredentials([sshUserPrivateKey(credentialsId: 'ubuntu', keyFileVariable: 'SSH_PRIVATE_KEY')]) {
                    sh '''
                       ssh -o "StrictHostKeyChecking=no" -i ${SSH_PRIVATE_KEY} ubuntu@{WS ip}  "sudo docker login -u 5hyun -p {dockerhub 비밀번호} && docker stop uplog_react && docker rm uplog_react && docker rmi 5hyun/react:latest && docker pull 5hyun/react:latest &&docker run -d -p 3070:3070 —name uplog_react 5hyun/react:latest"
                    '''
                 
                }
             }
          }
        }
    }
     post {
        success {
            slackSend (
                channel: '#uplog', 
                color: '#00FF00', 
                message: """
                    SUCCESS: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] (${DOMAIN}) 
                    [TEST URL: http://${PUBLIC_IP}:10001]
                    """
            )
        }
        failure {
            slackSend (
                channel: '#uplog', 
                color: '#FF0000', 
                message: "FAIL: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] (${DOMAIN})"
            )
        }
    }
}

최종적인 파이프라인 스크립트는 이렇다!
깃허브에 webhook 설정을 했고, slack과 연동을 해놨다.

 

-초기 설정

    agent any
    tools {
        nodejs "node18"
        git "git"
    }
      environment {
        DOMAIN = 'http://gak'
        PUBLIC_IP = {pulic IP}
    }
    stages {
        stage('prepare') {
            steps {
                echo 'prepare'
                git branch: "main", credentialsId: "GIT_ACCOUNT", url: 'https://github.com/GAK-coding/UpLog-frontEnd'
            }
        }
        stage('Create .env') {
            steps {
                echo 'Creating .env file'
                script {
                    def envContent = """
                    	{.env 내용}
                        """
                    writeFile file: '.env', text: envContent
                }
            }
        }

먼저 nodejs와 git을 정의하고, environment에서 주로 slack 연동에서 사용할 변수들을 정의했다. 
prepare에서는 깃허브에서 프론트엔드 코드를 클론 했다. main 브랜치를 기준으로 클론 받게 설정했다.

Create .env에서는 .env 파일의 내용을 써줬다.

 

-빌드 및 배포

        stage('build') {
            steps {
                sh "docker build -t 5hyun/react ."
            }
        }
        stage('Push Docker') {
          agent any
          steps {
            echo 'Push Docker'
            script {
                withDockerRegistry([ credentialsId: "5hyun_dockerhub", url: "" ]) {
                sh "docker push 5hyun/react:latest"
            }
          }
        }
    }
        stage('SERVER WS') {
          agent any
          steps {
            echo 'ssh'
            script {
                withCredentials([sshUserPrivateKey(credentialsId: 'ubuntu', keyFileVariable: 'SSH_PRIVATE_KEY')]) {
                    sh '''
                       ssh -o "StrictHostKeyChecking=no" -i ${SSH_PRIVATE_KEY} ubuntu@{WS ip}  "sudo docker login -u 5hyun -p {dockerhub 비밀번호} && docker stop uplog_react && docker rm uplog_react && docker rmi 5hyun/react:latest && docker pull 5hyun/react:latest &&docker run -d -p 3070:3070 —name uplog_react 5hyun/react:latest"
                    '''
                 
                }
             }
          }
        }
    }

build 부분에서는 docker에 코드를 build 했다.

Dockerhub

Push Docker 부분에서는 Dockerhub에 빌드된 이미지를 올렸다. 그리고 Dockerhub에는 이렇게 미리 Repository를 만들어놔야 잘 실행된다.

SERVER WS 부분에서는 WS vm으로 가서 기존에 실행되던 컨테이너, 이미지를 삭제하고, Dockerhub에서 새롭게 pull을 받고 3070으로 포트포워딩하여 실행되게 하였다.

 

-slack

     post {
        success {
            slackSend (
                channel: '#uplog', 
                color: '#00FF00', 
                message: """
                    SUCCESS: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] (${DOMAIN}) 
                    [TEST URL: http://${PUBLIC_IP}:10001]
                    """
            )
        }
        failure {
            slackSend (
                channel: '#uplog', 
                color: '#FF0000', 
                message: "FAIL: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] (${DOMAIN})"
            )
        }

마지막으로 slack에도 알림이 오게 해 놨다.

빌드가 잘 성공하면 위 사진처럼 알람이 온다.(url은 혹시 모르니 가렸다!)

Webhook

이렇게 원하는 Repository에 가서 webhook 설정을 했다. 가린 url은 "젠킨스 주소/github-webhook/" 이렇게 하면 된다.

 

우선 GitHub project에 깃허브 url을 등록했다.

 

"GitHub hook trigger for GITScm polling" 또한 체크했다.

이렇게 하면 webhook 또한 잘 된다!


최종 결론

main 브랜치에 push를 하면 이제 자동으로 배포가 된다!

 

잘 보인다!!

 

-후기

처음으로 나름 제대로 동작하는 3 티어 서비스를 구축해 봤다. 물론 중간중간 허점도 많다는 것을 잘 알고 있다. 그래도 처음으로 CI/CD를 해본 것에 대해 나름 만족스러움을 가지고 있다.

첫 시도를 할 때는 막막함을 엄청나게 느끼고 대충 거짓말로 하고 싶었다. 하지만 포기하지 않고 끝까지 시도해서 성공시키니 뿌듯하다. 내 목표는 프론트엔드 개발자가 되는 것이지만, 인프라도 구축에 대한 이해도도 어느 정도 있으면 플러스 요인이 될 것이라고 생각한다. 다음 뻔에는 blue/green 배포 같은 것도 한번 시도해 보고 싶다. 그리고 ssl과 도메인 적용을 하지 못한 것도 너무 아쉽다...! 지금 사용한 기술들은 정말 인프라 구축의 아주 기본적인 것이라고 생각해서 이를 기반으로 다음엔 더 좋은 서비스를 구축하는 게 목표이다!

(이렇게 힘들게 구축했는데 프로젝트 기간 끝나서 다 삭제해야 되는 게 너무 아쉽다ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ) 

728x90
반응형
Comments