Devops - 2 ( 프론트 엔드 서버 CI/CD 구축하기 )

HD.Y·2024년 2월 20일
0
post-thumbnail

실습 전 초기구성 🧐

  • AWS EC2 1대 준비

  • EC2 방화벽 끄기 : sudo ufw disable

  • EC2에 도커 설치

    1) 패키지 목록 업데이트 : sudo apt-get update

    2) 도커에 필요한 패키지 설치
      ➡ sudo apt-get install ca-certificates curl gnupg lsb-release

    3) curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

    4) 도커 저장소 추가

    echo \
    "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    5) 도커 저장소 추가 후 패키지 목록 재 업데이트 : sudo apt-get update

    6) 도커 설치 : sudo apt-get install docker-ce docker-ce-cli containerd.io

    7) 도커가 정상적으로 설치됬는지 확인 : sudo docker --version



🐮 VSCode에서 수정 후 깃허브 Push 시 자동으로 프론트엔드 서버에 배포하기

1. 도커 컴포즈 파일 작성 : vi devops.yml ( 야멜 파일 이름은 원하는 대로 지정 )

version: '3'
services:
  frontend:
    container_name: frontend  // 컨테이너 이름을 frontend로 설정 ✅
    ports:
      - 8888:80   // 프론트엔드 서버를 원하는 포트에 포트 포워딩 설정 ✅
    // 도커 허브에 있는 이미지, 이때 버전은 latest 로 한다. ✅   
    image: [도커허브 계정명]/[레포지토리명]:latest
    depends_on:
      - backend

  backend:
    container_name: backend  // 컨테이너 이름을 backend로 설정 ✅
    ports:
      - 8080:8080   // 백엔드 서버를 원하는 포트에 포트 포워딩 설정 ✅
    image: [도커허브 계정명]/[레포지토리명]:[버전]
    environment:    // 환경변수 설정 ✅
      APP_PASSWORD: 
      AWS_S3_ACCESS_KEY: 
      AWS_S3_SECRET_KEY: 
      BRAND_BUCKET: 
      CLIENT_ID: 
      EXPIRED_TIME: 
      JWT_SECRET_KEY: 
      MAIL_SENDER: 
      MASTER: 
      MASTER_PW: 
      MASTER_URL: 
      PORTONE_KEY: 
      PORTONE_SECRETKEY: 
      PRODUCT_BUCKET: 
      PRODUCT_INTROD_BUCKET: 
      REGION: 
      REVIEW_BUCKET: 
      SLAVE: 
      SLAVE_PW: 
      SLAVE_URL:
      

2. EC2 보안그룹 - 인바운드 규칙 편집
  ➡ 80번 및 도커 컴포즈 파일에서 프론트서버 포트 포워딩 설정한 포트 ( 8888번 ) 허용


3. 현재 사용자 도커 그룹에 추가 : sudo usermod -aG docker $USER

  ➡ 이부분은 실습 간 아래와 같은 오류가 등장하게 되어 해결하게된 내용이다.

오류 내용
err: If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
err: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
err:
err: If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.


4. VSCode 에서 백엔드 호출 URL 수정 : const backend = "http://[EC2 IP]:8888/api";

5. VSCode 에서 nginx 폴더 생성 및 nginx 설정파일 ( default.conf ) 생성

server {
  listen       80;
  server_name  localhost;
  #access_log  /var/log/nginx/host.access.log  main;
  location /api {
      rewrite ^/api(.*)$ $1 break;
      proxy_pass http://backend:8080;  // 실행할 백엔드 컨테이너의 이름 
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
  }
  location / {
      alias   /usr/share/nginx/html/;
      try_files $uri $uri/ /index.html;
  }
  #error_page  404              /404.html;
  # redirect server error pages to the static page /50x.html
  #
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
      root   /usr/share/nginx/html;
  }
  # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  #
  #location ~ \.php$ {
  #    proxy_pass   http://127.0.0.1;
  #}
  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  #
  #location ~ \.php$ {
  #    root           html;
  #    fastcgi_pass   127.0.0.1:9000;
  #    fastcgi_index  index.php;
  #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
  #    include        fastcgi_params;
  #}
  # deny access to .htaccess files, if Apache's document root
  # concurs with nginx's one
  #
  #location ~ /\.ht {
  #    deny  all;
  #}
}
  • 항상 문제가 일어나는 nginx의 프록시 설정 부분이다. 이번에도 역시나, 쉽지 않았다.

  • 먼저, 지금까지 EC2에서 서버를 배포했을때, 프론트엔드 서버와 백엔드 서버가 동일한 EC2에서 실행되다 보니, 프록시 설정에서 백엔드 호출 url을 http://localhost:8080 으로 했을때, 호출이 잘되었다.

  • 하지만, 밑에서 GitHub Workflow를 실행시키고, EC2에서 도커 컴포즈 파일이 실행되어 프론트엔드 서버와 백엔드 서버가 정상적으로 동작은 하는데, 백엔드 서버 호출에서 502 Bad GateWay가 등장하였다.

  • 그동안, 많은 실습으로 이 에러는 nginx의 프록시 서버 설정 문제라는 것은 알고 있었다.

  • 그래서, localhost를 테스트겸 EC2의 IP주소로 설정해봤다. 그랬더니, 504 타임아웃 에러가 등장하였다. 프록시 설정 문제는 아니라는 것을 깨닫고, EC2의 인바운드 규칙 편집에서 8080 포트를 허용시켜줘 봤더니 정상적으로 통신이 되는 것이다.

  • 여기서 든 의문, localhost나 EC2의 IP 주소나 어차피 동일한 컴퓨터이기 때문에 똑같은거 아니었나? 그동안 그렇게 이해하고 사용했었는데, 왜 이번에는 안되는 걸까?

  • 그때 문득 머릿속을 스쳐지나가는게 도커 컨테이너를 통한 서버 실행이었다.
    즉, 도커 컴포즈 파일로 서버를 실행시킨다는 것은 EC2에서 서버를 실행시키는게 아니라, EC2 안에서 도커 컨테이너를 실행시켜서 서버를 실행시키는 것이다.

  • 각각의 도커 컨테이너는 내부 IP 주소를 가지고 있고, 그렇다 보니 localhost를 호출한다는건 프론트엔드 도커 컨테이너 자신을 호출하는 것이다.

    그리고, EC2의 IP주소를 직접 호출 했을때는, 8080 포트를 포트포워딩으로 설정하여 백엔드 도커 컨테이너의 내부 IP로 찾아가게 되어 요청이 됬던 것이다.

  • EC2에서 서버를 실행한다고 생각한 나의 패착이었다... 그래서 지난 도커 실습때로 되돌아가서, 도커 컴포즈로 컨테이너를 실행 시 컨테이너 내부끼리는 컨테이너 이름으로 통신이 가능하다는 것을 알고 있었기에, 컨테이너 이름으로 호출을 해보니 정상적으로 호출이 되는 것을 확인할 수 있었다.


6. VSCode에서 Dockerfile 생성 및 작성 ( dist 폴더 안 모든 파일 nginx 서버에 추가 )

FROM nginx:latest
 ADD ./dist/css /usr/share/nginx/html/css
 ADD ./dist/fonts /usr/share/nginx/html/fonts
 ADD ./dist/img /usr/share/nginx/html/img
 ADD ./dist/js /usr/share/nginx/html/js
 ADD ./dist/styles.css /usr/share/nginx/html/styles.css
 ADD ./dist/logo.png /usr/share/nginx/html/logo.png
 RUN rm -rf /usr/share/nginx/html/index.html
 ADD ./dist/index.html /usr/share/nginx/html/index.html
 RUN rm -rf /etc/nginx/conf.d/default.conf
 ADD ./nginx/default.conf /etc/nginx/conf.d/default.conf
 CMD ["nginx", "-g", "daemon off;"]

7. VSCode 에서 .github/workflows 폴더 생성 후 GitHub Workflow 작성

name: frontend devops

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: frontend test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository    // git clone 과 같은 역할 ✅
        uses: actions/checkout@v3

      - name: Install dependencies   // npm i 수행 ✅
        run: npm install

      - name: Build                  // npm run build 수행 ✅
        run: npm run build

      - name: Docker Build and Push   // 도커 이미지 빌드 및 푸쉬 ✅
        script: |
          cd devops
          sudo docker build --tag hyungdoyou/fe:latest .
          sudo docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }}
          sudo docker push hyungdoyou/fe:latest

      - name: Deploy Docker Compose   // EC2에 접속하여, 도커 컴포즈 파일 실행 ✅
        uses: appleboy/ssh-action@v0.1.3
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.REMOTE_PORT }}
          script: |
            cd devops
            docker-compose -f /home/ubuntu/devops.yml pull
            docker-compose -f /home/ubuntu/devops.yml up --force-recreate

  ➡ 환경변수는 지난 글에서 작성한 4개의 환경변수에 도커 허브 로그인을 위한 변수 2개만
    추가하였다.

  ➡ 수업시간에 실습할 때는, 워크플로우를 이렇게 세분화 시키지 않고 EC2 안에서 위의
    모든 과정을 실행토록 했었다. 그랬더니, 무료 EC2를 사용하다 보니 사양이
    좋지 않아서, EC2가 계속 멈췄다.

  ➡ 그래서 이 부분을 해결하고자 EC2에서는 도커 컴포즈 파일 실행만 시키고, 나머지는
    깃허브에서 동작토록 작성한 것이다.

  ➡ 그 결과, 아주 잘 동작하였다. 하지만 깃허브 액션도 일정 크기 이상 실행시키면 돈을
    내야 된다고 한다.


🐯 Selenium 으로 프론트 엔드 테스트 코드 작성하기

  • Selenium 이란❓
    Selenium은 웹 애플리케이션을 자동화하기 위한 도구 중 하나로, 웹 브라우저를 제어하여 테스트를 자동으로 수행할 수 있다. 따라서, Vue.js와 Selenium을 함께 사용하여 Vue.js 애플리케이션을 자동으로 테스트할 수 있다.

  • Selenium 설치하기 : npm i selenium-webdriver

  • Selenium 실습하기 - 1단계💡

1. test.js 파일 작성

const {Builder} = require('selenium-webdriver');

(async function example() {
    let driver = await new Builder()
    .forBrowser('chrome')
    .build();
    await driver.get('https://테스트할 페이지 URL/');
})();
 

2. 테스트 파일 실행 : node test.js

  ➡ 그러면 해당 URL 화면이 등장할 것이다.


3. 테스트해볼 페이지로 이동하여 기능들을 테스트 해본다.

const {Builder, By} = require('selenium-webdriver');

(async function example() {
    let driver = await new Builder()
    .forBrowser('chrome')
    .build();
    await driver.get('https://테스트할 페이지 URL/UserLogIn');
    // 웹브라우저 화면 크기를 조정하고 싶을때 설정
    await driver.manage().window().setRect({ width: 1200, height: 900 });

    // 해당 id와 동일한 이름을 갖는 요소를 찾음
    const input_id = await driver.findElement(By.id('custId'));
    // 찾은 부분에 해당 내용을 입력
    input_id.sendKeys('tester01@gmail.com');

    const input_pw = await driver.findElement(By.id('custPw'));
    input_pw.sendKeys('Tester01!');

    // 해당 클래스명과 동일한 이름을 갖는 요소들을 찾음
    const login_btn = await driver.findElements(By.className('btn full_width black'));
    // 찾은 요소들 중 첫번째 요소를 클릭
    login_btn[0].click();

    // await driver.manage().setTimeouts({implicit: 5000});
})();

  ➡ 이것은 ID와 PW를 입력 후 로그인 버튼을 클릭하는 테스트 코드이다.

  ➡ 중요한것은 DOM과 마찬가지로, 테스트할 부분을 찾아서 처리해주는 것인데, Selenium
    자체적으로 쓰는 문법들이 있어서 공식 홈페이지에서 잘 찾아봐야 되는 것 같다.


  • Selenium 실습하기 - 2단계 💡

1. 패키지 설치 : npm i jest

2. package.json에서 scripts 마지막 줄에 아래처럼 추가

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test": "jest"  // 추가한 부분 🔥
  },

3. 테스트 파일 생성 : login.test.js

4. 테스트 파일 작성 ( 로그인 테스트에서 정상 케이스에 대한 테스트 코드 )

import { describe, expect, test, beforeAll, afterAll } from "@jest/globals";
const {Builder, By, until} = require('selenium-webdriver');
const chrome = require("selenium-webdriver/chrome");

describe("로그인", () => {
    let driver;
    beforeAll(async () => {
      driver = await new Builder()
        .forBrowser("chrome")
        .setChromeOptions(
          new chrome.Options().addArguments("--headless")  // 크롬을 띄우지 않도록 설정
        )
        .build();
  
      await driver.get("https://테스트할 페이지 URL/UserLogIn");
      // 이부분은 내가 테스트하는 페이지에 문제가 있어서 설정한 것이라 안해줘도 된다.
      await driver.manage().window().setRect({ width: 1200, height: 900 });
    }, 30000);
  
    afterAll(async () => {
      await driver.quit();
    }, 40000);

    test("정상 케이스 테스트", async () => {
            const input_id = await driver.findElement(By.id('custId'));
            input_id.sendKeys('tester01@gmail.com');
        
            const input_pw = await driver.findElement(By.id('custPw'));
            input_pw.sendKeys('Tester01!');
        
            const login_btn = await driver.findElements(By.className('btn full_width black'));
            login_btn[0].click();
        
            // 이부분은 로그인 성공 시 화면이 너무 빠르게 전환되어
            // 해당 클래스 이름이 화면에 등장할때까지 기다리도록 설정하는 것이다.
            await driver.wait(until.elementLocated(By.className('category-title')), 3000); 
            // 로그인에 성공하면 메인 페이지로 이동하고, 
            // 그러면 해당 이름의 클래스가 로드되기 때문에 이것으로 로그인 성공여부를 결정해봤다.
            const main_page = await driver.findElement(By.className('category-title'));
            // 가져온 요소가 NULL이 아니라면 성공으로 판단
            expect(main_page).not.toBeNull();            
    });
})

5. 테스트 실행 : npm test
 ➡ 지금은 만든 테스트 파일이 login.test.js 1개지만, 이 명령어 하나로 test.js의 이름을
   가지고 있는 모든 테스트 파일이 실행된다.


  • Selenium 실습하기 - 3단계 💡

1. 테스트 결과를 파일로 만들어 주는 패키지 설치 : npm i jest-junit

2. jest.config.js 파일 작성

const config = {
  reporters: [
    'default',
    ['jest-junit', {outputDirectory: 'test-results', outputName: 'report.xml'}],
  ],
};
module.exports = config;

  ➡ test-results 라는 폴더 밑에 report.xml 파일을 생성토록 하는 설정
    ( 폴더 및 생성되는 파일이름은 설정해주기 나름 )

  ➡ 설정한대로 test-results 폴더를 생성한다.

3. 테스트 파일 작성 ( 위의 테스트 파일에 로그인 실패 테스트 파일 추가 )

import { describe, expect, test, beforeAll, afterAll } from "@jest/globals";
const {Builder, By, until} = require('selenium-webdriver');
const chrome = require("selenium-webdriver/chrome");

describe("로그인", () => {
    let driver;
    beforeAll(async () => {
      driver = await new Builder()
        .forBrowser("chrome")
        .setChromeOptions(
          new chrome.Options().addArguments("--headless")
        )
        .build();
  
      await driver.get("https://테스트할 페이지 URL/UserLogIn");
      await driver.manage().window().setRect({ width: 1200, height: 900 });
    }, 30000);
  
    afterAll(async () => {
      await driver.quit();
    }, 40000);

    test("패스워드 오류 테스트", async () => {
            const input_id = await driver.findElement(By.id('custId'));
            input_id.sendKeys('tester01@gmail.com');
        
            const input_pw = await driver.findElement(By.id('custPw'));
            input_pw.sendKeys('Tester02!');
        
            const login_btn = await driver.findElements(By.className('btn full_width black'));
            login_btn[0].click();
        
            // alert 창 뜨는 것 처리
            await driver.wait(until.alertIsPresent());

            let alert = await driver.switchTo().alert();

            // alert 창의 메시지 뽑아냄
            let alertText = await alert.getText();

            // alert 창수락 버튼
            await alert.accept();
        
            // alert창에 해당 내용을 포함하고 있으면 로그인 실패 테스트 성공
            expect(alertText).toContain('이메일과 비밀번호가 일치하지 않습니다.');   
    });
})

  ➡ 로그인 실패 시 alert창을 띄우도록 개발하였었는데, 처음에 이 alert창 때문에 테스트가
    계속 실패하였다. 그러다가 공식 홈페이지에서 처리하는 방법을 찾아서 위 처럼 작성한
    것이다.

  ➡ 테스트 실행 결과는 아래와 같다.


  • Selenium 실습하기 - 최종 💡

기존에 작성한 GitHub Workflow 에 테스트 관련 내용 추가

name: frontend devops

on:
  push:
    branches: [main]

permissions: write-all    // 추가한 부분 🔥

jobs:
  deploy:
    name: frontend test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "v20.8.1"

      - name: Install dependencies
        run: npm install

      - name: Build
        run: npm run build

      - name: result   // 추가한 부분 🔥
        uses: EnricoMi/publish-unit-test-result-action@v1
        with:
          files: 'test-results/*.xml'

      - name: Docker Build and Push
        run: |
          sudo docker build --tag hyungdoyou/fe:latest .
          sudo docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }}
          sudo docker push hyungdoyou/fe:latest

      - name: Deploy Docker Compose
        uses: appleboy/ssh-action@v0.1.3
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.REMOTE_PORT }}
          script: |
            sudo docker-compose -f /home/ubuntu/devops.yml pull
            sudo docker-compose -f /home/ubuntu/devops.yml up --force-recreate

  ➡ 깃허브로 푸쉬해보면 정상적으로 실행되고, Unit Test Results 를 클릭하면 아래처럼
    테스트 결과까지 추가로 나타나게 된다.

  ➡ 지금은 테스트 파일을 2개만 만들었는데, 각 페이지마다 테스트 파일을 만들어 놓으면,
    코드 수정 후 푸쉬 시 자동으로 테스트를 실행하고, 서버를 배포하게 되므로 최종적으로
    프론트엔드 서버의 CI/CD 환경 구축이 완성된다.

profile
개발자가 되기까지의 과정

0개의 댓글