본문 바로가기
⭐project/petiary

Petiary: 프로젝트 배포하기(S3)

by 킁킁잉 2024. 12. 24.

petiary는 창업 아이템으로 진행하던 프로젝트인데, 이번 전공 과목 프로젝트 중  AWS를 활용한 텀프로젝트가 있어서 MMP 제작 겸 과제 제출을 위해 AWS를 활용하는 방향으로 리팩토링을 진행하였다. 

 

배포하기

초기 프로젝트 세팅 후 바로 배포부터 도전했다. Jenkins를 활용한  cicd 파이프라인 구축을 도전해보고 싶어서, 먼저 배포 후 퍼블리싱을 진행하기로 결심한 것. 프로젝트 개발은 Next.js를 사용하여 진행하였고, 배포는 S3, Cloudfront를 사용하였다. 

 

1. AWS CLI 설치

먼저 aws-cli를 설치해주었다. npm으로는 설치가 안되고, window는 사이트에서 설치 파일을 통해 다운받아야 한다는데 귀찮아서 pip install awscli 하니 설치가 되었다. 

설치 확인

 

AWS CLI란?

명령줄 셸의 명령을 사용하여 aws 서비스와 상호작용할 수 있게 해주는 통합 도구

 

본 프로젝트 배포에서 aws 기능들을 사용하기 때문에, 터미널에서 aws 명령어를 사용하기 위해 CLi 를 설치했다. Jenkins나 Github actions를 활용하여 ci/cd까지 도전해 볼 생각이기 때문에 aws 명령어들은 더욱 필수적이다. 

 

 2. IAM 사용자 생성

IAM은 AWS 리소스에 대한 접근을 관리하고 제어하기 위해 필요하다.

 

AWS 계정의 루트 사용자로 모든 작업을 수행하는 것은 보안상 위험한 행동이다 . 따라서 특정 작업을 수행할 수 있는 권한을 가진 IAM 사용자를 생성하여 보안을 강화한다.


사용자 이름은 특정 작업이나 역할을 수행하는 사용자 계정을 식별하는데 사용된다. 페티어리 서비스 배포 작업을 수행하는 사용자임을 나타내기 위해 petiary_deploy 사용자를 생성하였다.

이미지 관리를 위해 미리 생성해두었던 사용자

 

 

사용자 그룹 petiary를 생성하고, 권한  AmazonS3FullAccess,  CloudFrontFullAccess 정책을 연결해주었다.

사용자 그룹 정책

 

3. AWS CLI 설정

   aws configure
   
   # 다음 정보 입력:
   AWS Access Key ID: [위에서 생성한 액세스 키]
   AWS Secret Access Key: [위에서 생성한 시크릿 키]
   Default region name: [ap-northeast-2] # 서울 리전
   Default output format: json

aws cli 설정

4. S3 버킷 생성

배포 파일 업로드를 위한 S3 버킷을 생성해주었다. 퍼블릭 엑세스 차단 설정은 모두 해제하고, 아래와 같이 권한 정책을 입력해주었다.

버킷 정책 설정

   {
       "Version": "2012-10-17",
       "Statement": [
           {
               "Sid": "PublicReadGetObject",
               "Effect": "Allow",
               "Principal": "*",
               "Action": "s3:GetObject",
               "Resource": "arn:aws:s3:::bucket-name/*"
           }
       ]
   }

 

그리고 정적 웹 호스팅 설정을 활성화 해주었다.

속성->정적 웹 사이트 호스팅

 

 

5. 배포 스크립트 작성

배포를 위한 스크립트 작성 전에 우선 의존성을 추가해주었다.

   npm install --save-dev @aws-sdk/client-s3
   npm install --save-dev dotenv

 

  • @aws-sdk/client-s3는 AWS S3와 상호작용하기 위한 클라이언트 라이브러리이다. S3에 접근하여 파일을 업로드 하기 위해 설치하여 사용했다.
  • dotenv는 환경 변수 로더로, .env 파일에 저장된 환경 변수를 Node.js 애플리케이션에서 사용 가능하도록 로드해주는 패키지이다.

 

스크립트는 다음과 같이 작성했다. 우선 package.json에 배포 실행을 위한 명령어를 추가해주고, 명령어 입력 시 실행될 스크립트를 작성해주었다.

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "deploy": "npm run build && ts-node scripts/deploy.ts",
    "start": "next start",
    "lint": "next lint"
  },

 

AWS S3를 활용한 폴더 업로드 자동화 코드

이 코드는 로컬 디렉토리에 있는 파일과 폴더를 AWS S3 버킷에 업로드하는 스크립트이다. 파일의 MIME 타입(Content-type)을 자동을 지정하여 적절한 형식으로 업로드하며, 재귀적으로 하위 디렉토리까지 처리한다. 직접 S3 버킷에 배포 파일 업로드 하는 번거로움 없이, 명령어만으로 자동 업데이트 되도록 자동화 코드를 작성하였다.

 

코드 동작

  • 환경 변수 설정: AWS 자격 증명과 버킷 정보를 .env 파일에서 읽어온다.
  • S3 클라이언트 생성: AWS SDK를 사용하여 S3와 통신할 클라이언트를 설정한다.
  • 디렉토리 탐색: 지정된 디렉토리 out/의 파일과 폴더를 탐색하여 모든 파일을 업로드한다.
  • 파일 MIME 타입 설정: 파일 확장자에 따라 Content-type을 설정하여 S3에 적합한 형식으로 저장한다.
  • 파일 업로드: PutObjectCommand를 사용하여 S3 버킷으로 파일을 전송한다.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";
import path from "path";
import dotenv from "dotenv";

//환경변수 로드
dotenv.config();

//S3 클라이언트 생성
const s3Client = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
  },
});

interface ContentTypes {
  [key: string]: string;
}

async function uploadDir(s3Path: string, bucketName: string): Promise<void> {

  //현재 디렉토리의 파일과 폴더 목록을 동기적으로 읽음
  const files = fs.readdirSync(s3Path);

  for (const file of files) {
    const filePath = path.join(s3Path, file);
    
    //경로가 파일인지, 디렉토리인지 확인하고, 디렉토리일 경우 재귀적으로 uploadDir 호출
    if (fs.statSync(filePath).isDirectory()) {
      await uploadDir(filePath, bucketName);
    } else {
      //파일 내용을 읽어 Body로 전달
      const fileStream = fs.createReadStream(filePath);
      const uploadParams = {
        Bucket: bucketName,
        //업로드 경로에서 루트 디렉토리 out/을 제거하여 S3 버킷 내 상대 경로를 설정
        Key: filePath.replace("out/", ""),
        Body: fileStream,
        ContentType: getContentType(filePath),
      };

      try {
        await s3Client.send(new PutObjectCommand(uploadParams));
        console.log(`Successfully uploaded ${filePath}`);
      } catch (err) {
        console.error(`Error uploading ${filePath}:`, err);
      }
    }
  }
}

//확장자에 따른 MIME 타입 매핑
function getContentType(filePath: string): string {
  const ext = path.extname(filePath).toLowerCase();
  const contentTypes: ContentTypes = {
    ".html": "text/html",
    ".css": "text/css",
    ".js": "application/javascript",
    ".json": "application/json",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".svg": "image/svg+xml",
  };
  return contentTypes[ext] || "application/octet-stream";
}

//S3 버킷 업로드
async function deploy(): Promise<void> {
  //S3_BUCKET 미정의 시 에러 발생
  if (!process.env.S3_BUCKET) {
    throw new Error("S3_BUCKET environment variable is not defined");
  }

  try {
    //uploadDir 함수로 out/ 디렉토리 내용을 S3로 업로드
    await uploadDir("out", process.env.S3_BUCKET);
    console.log("Deployment completed successfully");
  } catch (err) {
    console.error("Deployment failed:", err);
    process.exit(1);
  }
}

deploy();

 

next.config.mjs 설정은 아래와 같이 해주었다. S3는 정적 파일을 호스팅하는 서비스이기 때문에, Next.js를 S3에서 배포하기 위해서는 정적 HTML 파일로 생성해야 했다. 이를 위해 ouput: 설정을 사용하였다. 

 

output: "export"로 설정하여 Next.js를 정적 사이트 생성 모드로 전환하여, next build 명령 실행 시 정적 HTML 파일로 생성되도록 설정하였다. 

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  /* config options here */
};

export default nextConfig;

 

npm run deploy를 실행하고, S3에서 확인한 결과 파일들이 S3에 정상적으로 업로드 된 것을 확인할 수 있었다.


트러블 슈팅

오류 1

npm run deploy를 실행하니 아래와 같은 에러 메시지가 출력되었다.

해결방법

이 오류는 typescript 파일을 실행할 때 발생하는 문제였다. ES 모듈을 사용하기 위한 설정이 추가적으로 필요해서, ts-node를 사용하여 typescript 실행 시 CommonJS 모듈 시스템을 사용하도록 설정을 해주었다.

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "CommonJS", //commonJS로 변경
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

 

CommonJS 모듈 시스템에서는 require을 사용하기 때문에, deploy.ts에서 import 대신 require을 사용하도록 설정을 변경해주었다.

const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const fs = require("fs");
const path = require("path");
require("dotenv").config(); //import -> require로 변경

 

 

오류 2

S3에 파일이 업로드 된 것을 확인한 후, URL로 접속하였으나 404 Not Found 에러가 발생했다.

 

이 오류는 S3 버킷에 index.html 파일이 없어서 발생한 오류였다. 하지만 분명히 S3 버킷에 파일이 업로드 된 것을 확인했고, index.html 파일의 존재도 확인했는데,,,계속해서 404 Not Found가 발생하는걸 이해할 수 없었다😡

 

일단 cloudfront를 붙여보면 뭔가 달라질까? 해서 cloudfront를 붙여보았지만, 문제는 해결되지 않았는데 사실 굉장히 간단한 문제였다.

 

해결방법

배포하는 index.html 파일은 S3 버킷의 루트에 위치해야 하는데, out/index.html 경로로 업로드 되어있었던 것이다.  

 

next.config.ts에서 output: "export" 설정을 해주어서, out 폴더에 정적 파일이 생성되는데, deploy.ts에서 이 경로 처리를 잘못 하고 있었다. deploy.ts에서 다음과 같이 out/ 폴더 이후의 경로만 사용하도록 변경하고, index.html이 버킷의 루트에 위치할 수 있게 해주었다.

async function uploadDir(s3Path: string, bucketName: string): Promise<void> {
  const files = fs.readdirSync(s3Path);

  for (const file of files) {
    const filePath = path.join(s3Path, file);
    if (fs.statSync(filePath).isDirectory()) {
      await uploadDir(filePath, bucketName);
    } else {
      const fileStream = fs.createReadStream(filePath);
      const uploadParams = {
        Bucket: bucketName,
        // 수정: S3 키 경로 처리 방식 변경
        Key: filePath.split(path.sep).slice(1).join('/'),  // out 폴더 이후의 경로만 사용
        Body: fileStream,
        ContentType: getContentType(filePath),
      };

      try {
        await s3Client.send(new PutObjectCommand(uploadParams));
        console.log(`Successfully uploaded ${filePath}`);
      } catch (err) {
        console.error(`Error uploading ${filePath}:`, err);
      }
    }
  }
}

 

 

더 간단하게는, 

 

Key: path.relative("out", filePath),

 

배포 성공👍


AWS S3를 활용하여 정적 파일 배포를 자동화 할 수 있었다. 굳이 S3에 접속하여 직접 업로드 하지 않아도, npm run deploy 명령어 하나면 최신 변경사항을 손쉽게 S3에 업로드 할 수 있게 된 것이다. 배포 자동화를 위한 첫 걸음이고, 여기서 멈추지 않고 jenkins, github actions 를 사용하여 완벽한 배포 자동화 프로세스를 구축했는데, 이건 next time에