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
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에
'⭐project > petiary' 카테고리의 다른 글
[트러블슈팅] Jenkins와 Github Actions로 CI/CD 구축 중 무한 빌드 문제 해결하기 (0) | 2024.12.01 |
---|