Skip to content

[재하] 1123(목) 개발기록

박재하 edited this page Dec 10, 2023 · 6 revisions

체크리스트

  • 트러블 슈팅 : Object Storage 에러 => 해결
  • AWS-SDK를 이용한 NCP Object Storage 파일 업로드, 다운로드
  • HTTPS 적용
  • GitHub Actions를 활용한 자동 배포

기본적인 사용법

API 키 발급 (NCP)

스크린샷 2023-11-22 오후 9 21 20 스크린샷 2023-11-22 오후 9 36 17

Object Storage 이용신청, bucket 생성

스크린샷 2023-11-22 오후 9 16 04 스크린샷 2023-11-22 오후 9 19 05 스크린샷 2023-11-22 오후 10 47 35

S3와 호환되므로 AWS-SDK 모듈 등에서 Endpoint를 kr.object.ncloudstorage.com으로 설정해서 사용하면 된다.

config

import * as AWS from 'aws-sdk';

import { configDotenv } from 'dotenv';
configDotenv();

export const awsConfig = {
	endpoint: new AWS.Endpoint(process.env.AWS_S3_ENDPOINT),
	region: process.env.AWS_REGION,
	credentials: {
		accessKeyId: process.env.AWS_ACCESS_KEY_ID,
		secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
	},
};

export const bucketName = process.env.AWS_BUCKET_NAME;

upload

// NCP Object Storage 업로드
AWS.config.update(awsConfig);
const result = await new AWS.S3()
	.putObject({
		Bucket: bucketName,
		Key: filename,
		Body: buffer,
		ACL: 'public-read',
	})
	.promise();
Logger.log('uploadFile result:', result);

download

// NCP Object Storage 다운로드
AWS.config.update(awsConfig);
const result = await new AWS.S3()
	.getObject({
		Bucket: bucketName,
		Key: filename,
	})
	.promise();
Logger.log(`downloadFile result: ${result.ETag}`);

트러블 슈팅 : Object Storage 에러 => 해결

S3 Endpoint 주소 문제

NCloud 공식문서에 나와있는 S3 Endpoint 주소가 문제였다..

스크린샷 2023-11-23 오후 12 42 10

여기 나와있는 주소를 썼는데 ACCESS KEY가 없다고 나와 뭔가 잘못한 줄 알고 한참을 헤맸는데

스크린샷 2023-11-23 오후 12 42 49

혹시나 싶어 다른 문서에 나와있는

스크린샷 2023-11-23 오후 12 42 54

이 주소를 보니까 다른거야.. 그래서 이걸로 했더니 됨. ㅋ

알고보니 내가 보던 공식문서는 financial용 ncloud로 서버도 인증키도 따로 관리되는 거였다... 내가 바보 ㅇㅈ

업로드 로직 추가

이제 주석처리해 두었던 업로드 로직을 다시 추가해줬다.

async createBoard(
  createBoardDto: CreateBoardDto,
  userData: UserDataDto,
  files: Express.Multer.File[],
): Promise<Board> {
  const { title, content } = createBoardDto;

  const user = await this.userRepository.findOneBy({ id: userData.userId });

  const images: Image[] = [];
  for (const file of files) {
    const image = await this.uploadFile(file);
    images.push(image);
  }

  const board = this.boardRepository.create({
    title,
    content: encryptAes(content), // AES 암호화하여 저장
    user,
    images,
  });
  const createdBoard: Board = await this.boardRepository.save(board);

  createdBoard.user.password = undefined; // password 제거하여 반환
  return createdBoard;
}

파일 목록에서 순서대로 읽어와 uploadFile() 호출

async uploadFile(file: Express.Multer.File): Promise<Image> {
  if (!file.mimetype.includes('image')) {
    throw new BadRequestException('Only image files are allowed');
  }

  const { mimetype, buffer, size } = file;

  const filename = uuid();

  // NCP Object Storage 업로드
  AWS.config.update(awsConfig);
  const result = await new AWS.S3()
    .putObject({
      Bucket: bucketName,
      Key: filename,
      Body: buffer,
      ACL: 'public-read',
    })
    .promise();
  Logger.log('uploadFile result:', result);

  const updatedImage = await this.imageRepository.save({
    mimetype,
    filename,
    size,
  });

  return updatedImage;
}

업로드 파일 로직은 uuid로 생성된 식별자를 파일이름으로 Object Storage에 업로드하고, 나머지 파일에 대한 정보는 관계형 DB 이미지 테이블에 저장한다.

다운로드 로직 추가

yarn workspace server add form-data

폼데이터로 응답을 해야하기 때문에 form-data 모듈을 설치해줬다.

async downloadFile(filename: string): Promise<Buffer> {
  // NCP Object Storage 다운로드
  AWS.config.update(awsConfig);
  const result = await new AWS.S3()
    .getObject({
      Bucket: bucketName,
      Key: filename,
    })
    .promise();
  Logger.log(`downloadFile result: ${result.ETag}`);

  return result.Body as Buffer;
}
@Get(':id')
@UseGuards(CookieAuthGuard)
async findBoardById(
  @Param('id', ParseIntPipe) id: number,
  @Res() res,
): Promise<void> {
  const found = await this.boardService.findBoardById(id);
  // AES 복호화
  if (found.content) {
    found.content = decryptAes(found.content); // AES 복호화하여 반환
  }

  // 폼 데이터 만들어 반환
  const formData = new FormData();
  formData.append('id', found.id.toString());
  formData.append('title', found.title);
  formData.append('content', found.content);
  formData.append('author', found.user.nickname);
  formData.append('created_at', found.created_at.toString());
  formData.append('updated_at', found.updated_at.toString());
  formData.append('like_cnt', found.like_cnt.toString());

  // NCP Object Storage 다운로드
  const files = [];
  for (let image of found.images) {
    const file: Buffer = await this.boardService.downloadFile(image.filename);
    console.log(file);
    formData.append('file', file, {
      filename: image.filename,
      contentType: image.mimetype,
    });
  }

  res.set({
    'Content-Type': 'multipart/form-data',
  });
  formData.pipe(res);
  // return found;
}

다운로드는 컨트롤러에서 개별 파일 다운로드하는 서비스 메소드인 downloadFile을 호출해서 순서대로 폼데이터에 넣고, @Res() 데코레이터로 가져온 Response 객체에 전달한다.

결과 화면

  • 업로드
스크린샷 2023-11-23 오후 2 08 43
  • 다운로드
스크린샷 2023-11-23 오후 2 08 35

GitHub Actions를 활용한 자동 배포

on:
  push:
    branches:
      - main

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v1
        with:
          node-version: '20.x'

      - name: Install yarn
        run: npm install -g yarn

      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_PASSWORD }}

      - name: Test and Build with yarn
        run: |
          yarn install
          yarn workspace client build

      - name: Build and Push Docker image
        run: |
          docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-fe:${{ github.sha }} -f ./Dockerfile-web . 
          docker push ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-fe:${{ github.sha }}
          docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-be:${{ github.sha }} -f ./Dockerfile-was .
          docker push ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-be:${{ github.sha }}

      - name: make docker-compose file
        run: |
          sed -i "s/GITHUB_SHA/${{ github.sha }}/g" docker-compose.yml

      - name: send docker-compose file with scp
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          password: ${{ secrets.SSH_PASSWORD }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: docker-compose.yml
          target: /app

      - name: make .env file
        run: |
          echo ${{ secrets.ENV }} > .env

      - name: send .env file with scp
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          password: ${{ secrets.SSH_PASSWORD }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: .env
          target: /app

      - name: Deploy with SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          password: ${{ secrets.SSH_PASSWORD }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            cd /app
            docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-fe:${{ github.sha }}
            docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/web16-b1g1-be:${{ github.sha }}
            docker stop $(docker ps -a -q)
            docker rm $(docker ps -a -q)
            docker-compose up -d
            rm -rf .env docker-compose.yml

앞선 GitHub Actions 스크립트 학습과, 각종 AI들의 도움을 받아 스크립트를 작성해봤다. 중간중간에 에러가 나는 부분을 잡아주느라 main PR을 여러번 올리며 고생을 좀 했다 ㅎ

주요 에러 요인들은 다음과 같다.

  • SCP로 파일 전송 시 이미 있는 파일이면 에러 발생
    • 배포 후 ���경설정 파일(.env)이나 docker-compose.yml을 지워주는 것으로 해결
  • 우리는 yarn berry(zero install) 모노레포를 사용해 docker build 전 의존성 설치과정이 필요없었는데, esbuild 패키지는 플랫폼에 종속적이라 macOS에서 개발중이던 프로젝트가 ubuntu에서 build시 에러 발생
    • 리눅스 환경에서 yarn install를 한 번 더 실행하여 linux/amd64용 esbuild 설치
  • .env 파일 생성 시 echo로 여러 줄의 입력을 넣으면 echo ${{ secrets.ENV }} > .env 명령으로 파일이 정상적으로 입력되지 않음
    • secrets.ENV의 내용을 -e "A=...\n" \ "B=...\n" \ ... ""형태로 넣어서 스크립트 변경 없이 해결. 다음으로 해석됨
    echo -e "A=...\n" \ 
    "B=...\n" \ 
    ...
    "" > .env`
    
스크린샷 2023-11-24 오후 2 12 26

secrets 변수들은 프로젝트 설정의 Secrets and variables => Actions에 직접 넣어준다.

스크린샷 2023-11-24 오후 2 14 06

Actions 탭에서 결과 확인 가능

스크린샷 2023-11-24 오후 2 14 45

새로운 tag의 web, was 컨테이너가 잘 돌아가고 있는 것을 확인할 수 있다!

HTTPS 적용

다양한 자료를 참고했지만, 결정적으로 다음의 boilerplate를 활용해 Let's Encrypt 인증서를 발���했다.

서버 인스턴스에 위 repository를 클론해 온 뒤

server {
    listen 80;
    server_name www.xn--bj0b03z.site;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name www.xn--bj0b03z.site;
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/www.xn--bj0b03z.site/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.xn--bj0b03z.site/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass  http://www.xn--bj0b03z.site;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    }
}

/data/nginx/app.conf의 도메인을 구매한 서버 도메인으로 지정한다.

#!/bin/bash

if ! [ -x "$(command -v docker-compose)" ]; then
  echo 'Error: docker-compose is not installed.' >&2
  exit 1
fi

domains=(www.xn--bj0b03z.site)
rsa_key_size=4096
data_path="./data/certbot"
email="적절한 이메일 입력" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
...

다음으로 init-letsencrypt.sh의 도메인도 적절히 변경, email 입력, staging=0으로 설정 후 실행한다.

./init-letsencrypt.sh

알아서 nginx와 certbot docker 이미지를 다운받고 실행시켜 인증서를 받아준다.

스크린샷 2023-11-24 오후 2 23 54

인증서가 잘 받아졌는지 확인하고,

version: '3'

services:
  was:
    container_name: was
    image: qkrwogk/web16-b1g1-be:GITHUB_SHA
    ports:
      - 3000:3000
    env_file:
      - .env
    networks:
      - b1g1-network
  web:
    container_name: web
    image: qkrwogk/web16-b1g1-fe:GITHUB_SHA
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    networks:
      - b1g1-network

networks:
  b1g1-network:
    driver: bridge

이제 우리 걸 실행한다. nginx가 있는 web 컨테이너에는 인증서 파일이 동일하게 들어가게 volumes 속성을 부여해줘야 한다. (data 폴더를 container 실행환경으로 옮겨준다)

# nginx.conf with https
server {
    listen 80;
    server_name www.xn--bj0b03z.site;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name www.xn--bj0b03z.site;
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/www.xn--bj0b03z.site/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.xn--bj0b03z.site/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

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

    location /api {
        proxy_pass  http://was:3000;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        
        rewrite ^/api(/.*)$ $1 break;
    }   
}

참고로 nginx conf도 https를 받을 수 있도록, ssl 인증서도 등록해주고 80으로 오면 443으로 리다이렉트 해주고 해야 한다! web 컨테이너는 이 파일을 이용해서 다시 빌드했음

스크린샷 2023-11-24 오후 2 29 38

이제 안전한 사이트라고 잘 뜬다!

스크린샷 2023-11-24 오후 2 30 13

https 적용 성공! (뿌듯)

학습메모

  1. 잘못된 ncloud 공식문서 (ncloud financial)
  2. 올바른 endpoint 주소(일반 ncloud)

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally