"Life is Full of Possibilities" - Soul, 2020

우아한테크코스

개발 서버와 운영 서버의 환경변수만 다른데 운영 서버 배포만 안되는 이유?! (AWS S3, CDN, CodePipeline, CodeBuild)

m2ndy 2024. 11. 17. 14:30

 

EC2와 Nginx를 사용한 첫 번째 버전의 배포 과정은 여기서 확인하실 수 있습니다.

 

Devel Up 프로젝트의 브랜치 전략

Devel Up 프로젝트에서는 dev 브랜치main 브랜치를 활용해 각각 개발 서버운영 서버의 파이프라인을 독립적으로 구축하고 배포하는 것을 목표로 했습니다.

 

 

 

처음에는 GitHub Actions를 사용하려 했지만, 제공받은 AWS 계정의 S3 정책 설정 상 access key 발급이 불가능했습니다. 이에 따라 AWS CodePipelineCodeBuild를 사용해 파이프라인을 구축하기로 결정했습니다.

 

 

 

AWS를 선택한 이유와 장단점

 

AWS는 GUI를 통해 파이프라인을 간편하게 설정하고 실행할 수 있는 장점이 있습니다. 디버깅 과정에서도 CloudWatch를 활용하면 문제 상황을 쉽게 확인할 수 있습니다.
GitHub Actions와 AWS를 모두 사용해 본 결과, AWS가 더 편리하다는 점을 느꼈습니다. 하지만 AWS는 비용이 발생하기 때문에 프로젝트 상황에 따라 적합한 서비스를 선택하는 것이 중요합니다.

 

 

파이프라인 설정

개발 서버와 운영 서버의 파이프라인은 동일한 방식으로 구축하고, 환경 변수와 빌드 명령어만 다르게 설정했습니다. 파이프라인은 3단계로 구성됩니다. (추후 CDN 캐시 무효화를 위한 Lambda 설정이 추가될 예정입니다.)

 

1. Source

Github와 연동하여 프로젝트의 소스 파일을 가져옵니다.

 

2. Build

AWS CodeBuild를 이용하여 가져온 소스 파일을 빌드합니다.

 

3. Deploy

S3를 통해 빌드 결과물을 배포합니다.

 

 

 

 

Build 단계 설정

 

개발 서버와 운영 서버에 따라 환경 변수와 빌드 명령어를 아래와 같이 설정했습니다.

 

개발 서버

  • 환경 변수 : NODE_ENV=development
  • 빌드 명령어 : npm run start

운영 서버

  • 환경 변수 : NODE_ENV=production
  • 빌드 명령어 : npm run build

하지만, 운영 서버의 경우 빌드가 실패하는 문제가 발생했습니다.

 

CodePipeline에서 프로젝트 빌드 시 사용되는 CodeBuild에서 자꾸만 실패를 했습니다.

 

빌드 명령어의 경우, 개발 서버와 운영 서버에서 사용하는 Webpack 모드가 달라 개발 서버는 `start`로, 운영 서버는 `build` 명령어를 입력합니다. start 명령어를 입력하는 경우, webpack.common.js를 거쳐 webpack.dev.js를 참조하고, build 명령어를 입력하는 경우 webpack.common.js를 거쳐 webpack.prod.js를 참조합니다.

 

webpack 설정 코드는 아래와 같습니다.

// webpack.common.js

const { sentryWebpackPlugin } = require('@sentry/webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const Dotenv = require('dotenv-webpack');

module.exports = {
  entry: './src/index.tsx',

  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.svg$/i,
        issuer: /\.[jt]sx?$/,

        use: ['@svgr/webpack'],
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
        generator: {
          publicPath: 'http://localhost:3000/assets/',
          outputPath: 'assets/',
        },
      },
      {
        test: /\.(css|scss)$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'assets/fonts/[name][ext][query]',
        },
      },
    ],
  },

  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  output: {
    filename: 'bundle.[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'index.html',
      publicPath: '/',
    }),
    new ForkTsCheckerWebpackPlugin({
      async: false,
      typescript: {
        configFile: path.resolve(__dirname, 'tsconfig.json'),
      },
    }),
    new Dotenv(),
    sentryWebpackPlugin({
      authToken: process.env.SENTRY_AUTH_TOKEN,
      org: 'develupteam',
      project: 'javascript-react',
    }),
    new MiniCssExtractPlugin(),
  ],

  devtool: 'source-map',
  optimization: {
    minimize: true,
    minimizer: ['...', new CssMinimizerPlugin()],
  },
};
// webpack.dev.js

const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    historyApiFallback: true,
    port: 3000,
    hot: true,
  },
});
// webpack.prod.js

const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = merge(common, {
  mode: 'production',
  devtool: 'hidden-source-map',
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: true,
      excludeAssets: [/node_modules/],
    }),
  ],
});

 

 

문제 원인 분석

1. CloudWatch를 통한 디버깅

CloudWatch는 AWS에서 제공하는 로깅 툴입니다. 실제 터미널에서 빌드할 때의 환경처럼 화면에 출력되는 (무수한..) 메시지들을 AWS에서 확인할 수 있도록 도와줍니다. 평소 에러가 발생했을 때 로그를 자세히 살펴보면 답이 나온다고 생각하는 편이었기에, CloudWatch를 통해 로그를 확인하기로 했습니다.

 

CloudWatch를 활용해 로그를 확인한 결과, 운영 서버 배포 시 webpack-cli가 설치되지 않아 빌드가 실패하는 것을 발견했습니다.

운영 서버 배포 시 webpack-cli가 설치되지 않아 webpack-cli 설치 여부를 묻는 메시지가 출력되는데, buildspec 스크립트에서는 npm run build 명령어 이후 별다른 조치를 취하지 않아 빌드에 실패했던 것이었습니다.

 

 

 

초기에 설정했던 buildspec은 다음과 같습니다.

// buildspec

version: 0.2

phases:
  install:
    commands:
      - echo Installing dependencies...
      - cd frontend
      - npm install
  build:
    commands:
      - echo Building the project...
      - npm run build

artifacts:
  files:
    - '**/*'
  base-directory: frontend/dist
  name: techcourse-project-2024/develup/frontend-deploy

 

깃허브로부터 받은 소스 파일에서 npm install을 한 뒤, npm run build를 통해 빌드를 진행합니다.

 

 

 

2. package.json의 devDependencies 문제

 

webpack-cli 등 설치된 패키지를 관리하는 package.json을 확인했을 때, webpack과 관련된 패키지들은 모두 devDependencies에 설치되어 있었습니다.

 

// package.json

{
  // ... 기타 설정들
  
  "scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js --open --hot",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack --config webpack.dev.js",
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.24.1",
    "styled-components": "^6.1.11"
  },
  "devDependencies": {
    "dotenv-webpack": "^8.1.0",
    "html-webpack-plugin": "^5.6.0",
    "webpack": "^5.92.1",
    "webpack-bundle-analyzer": "^4.10.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  },
}

 

 

 

buildspec에서 npm install을 하는 과정에서, CodeBuild의 NODE_ENV 환경 변수가 production으로 지정된 경우 devDependencies를 설치하지 않고, dependencies만을 설치합니다. 따라서 webpack 관련 패키지들이 정상적으로 설치되지 않았던 것입니다.

 

 

 

문제 해결 방법

 

환경 변수와 buildspec 파일을 하나씩 조정하며 테스트한 결과, 아래와 같은 방식으로 해결할 수 있었습니다.

 

개발 서버 설정 : 기존과 동일

 

운영 서버 설정

  • 환경변수 : CodeBuild에서 환경변수 NODE_ENV=production은 설정하지 않음
  • buildspec : npm install 후, 환경변수 NODE_ENV=production을 설정하여 빌드 진행

 

개선된 buildspec은 다음과 같습니다.

version: 0.2

phases:
  install:
    commands:
      - echo Installing dependencies...
      - cd frontend
      - npm install
  build:
    commands:
      - echo Setting NODE_ENV to production...
      - export NODE_ENV=production
      - echo Building the project...
      - npm run build

artifacts:
  files:
    - '**/*'
  base-directory: frontend/dist
  name: techcourse-project-2024/develup/frontend-deploy

 

환경변수를 따로 설정하지 않으면 npm은 알아서 모든 패키지를 다운받습니다.

npm install로 모든 패키지를 설치한 뒤 NODE_ENV를 주입하여 build시 API가 운영 서버로 호출되도록 하고, npm run build를 통해 webpack mode가 production으로 설정되도록 했습니다.

 

 

 

 

마무리

 

배포 파이프라인에서 발생했던 문제를 해결하며 저만의 문제 해결 방식을 확립할 수 있었습니다. 가장 먼저 로그를 적극적으로 활용하고, 비교 대상 간의 차이점을 파악한 뒤, 차이점을 하나씩 변경하며 단계적으로 검증하는 방식입니다. 앞으로도 유사한 문제가 발생하면 이와 같은 방법으로 해결하려 합니다.

 

감사합니다.