부스트캠프 멤버십 과정의 3주 팀 프로젝트를 시작하면서 가장 먼저 해야겠다고 생각한 작업은 배포 자동화 작업이었다. 그 이유는 팀 회의에서 배포 전략을 특정 일을 기준으로 작업을 모은 후 배포를 시키는 것이 아닌 기능이 완성될 때마다 꾸준히 지속적으로 배포를 시키는 것으로 정했고, 그러기 위해서는 배포를 쉽고 간단하게 진행할 수 있어야 하기 때문이다. (배포 과정이 복잡하고 어려우면 배포 하는 것을 꺼리게 된다..)
그래서 첫 1주는 틈틈이 GitHub Action과 Docker를 이용하여 배포 자동화 환경을 구축하는 작업을 진행했었는데, 그 과정에서 겪었던 이슈를 한번 정리해보려고 한다.
먼저 프로젝트의 기술 스택은 아래와 같이 구성되어 있다.
FrontEnd
- React
- Webpack + Babel
BackEnd
- Node.js
- Express
DB
- MySQL
CI/CD
- GitHub Action
- Docker
CI/CD 툴로 GitHub Action과 Docker를 선택한 이유
GitHub Action
은 자체적으로 workflow 실행을 위한 서버 환경을 제공해주고 Travis CI 와 같은 별도의 툴을 사용하지 않으면서 관리 포인트가 GitHub 하나로 통일되는 효과도 얻을 수 있기 때문이다.
Docker
는 상황에 따라 서버를 빠르게 바꿔서 재배포를 해야하는 상황에 대비하기 위해 (이번 프로젝트에서는 그럴 일은 없겠지만..) 배포 버전의 프로젝트를 Docker 이미지로 빌드하여 Push 해두고 배포할 서버에서 해당 이미지를 가져와 실행만 시키면 되도록 하기 위함이다.
문제상황
리액트로 개발한 프론트엔트 프로젝트는 웹팩으로 번들링 한 정적 파일을 NGINX로 호스팅하는 방식으로 배포된다(도커 이미지로 만들어 배포하고 있다). 그리고 백엔드의 API를 호출할때 필요한 API_BASE_URL 환경변수를 서버 환경에 맞게 주입 받도록 구현이 되어 있었다.
처음 배포를 했을 때는 프론트엔드에서 백엔드 API를 호출하는 코드가 없었고 개발 과정에서는 localhost:3000 같은 고정된 URL로 API를 호출했기 때문에 환경 변수를 주입해 줄 필요가 없어서 이 부분을 생각하지 못했었는데, 백엔드 API를 호출하는 코드가 추가되면서 NGINX로 호스팅 중인 정적 파일에 런타임 시점에서 환경 변수를 주입해 줄 수 없는 문제를 만나게 되었다.
해결과정1 - 번들링 시점에 환경변수 주입 (❌)
이 문제를 해결하기 위해 처음에는 Webpack을 사용하는 프로젝트 환경에서 환경 변수를 어떻게 주입해주는지 찾아봤는데, dotenv-webpack
패키지를 이용하거나 직접 webpack의 DefinePlugin을 이용하여 주입해주는 방법이 있다.
이 방법을 이용하여 GitHub Repository Secret에 API_BASE_URL 환경변수 값을 등록해두고 GitHub Action에서 빌드를 진행할때 해당 secret 값을 환경변수로 주입해주었다.
일단 이 방법을 통해 현재 배포할 서버의 URL로 API_BASE_URL을 설정해 줄 수 있었다. 그러나 얼마 못 가 깨닳은 점은 이 방식은 빌드 시점에 환경 변수가 고정이 되기 때문에 배포 할 서버가 변경되면 해당 이미지는 사용할 수 없게 되고, secret 값을 변경하여 다시 빌드를 해주어야 한다는 점이었다.
배포 과정에서 서버를 유연하게 변경할 수 있도록 Docker를 사용했는데 그 의미를 잃어버린 것이다.
dotenv-webpack 패키지를 이용하거나 DefinePlugin을 이용하여 주입해주는 방식은 환경변수를 이용하는 부분의 코드가 주입된 String값으로 대치된 번들 파일이 만들어진다. 그렇기 때문에 번들링 이후에는 환경 변수를 변경할 수 있는 방법은 없을 것 같다는 생각이 들었다.
1
2
3
4
5
//번들링 전
if(process.env.NODE_ENV === 'production')
//번들링 후
if('production' === 'production')
해결과정2 - NGINX proxy 설정 이용 (✅)
결국 다시 원점에서 문제 해결을 위한 방법을 찾던 중 아래 글에서 하나의 실마리를 찾을 수 있었다.
https://qastack.kr/server/577370/how-can-i-use-environment-variables-in-nginx-conf
NGINX는 특정 요청을 다른 곳으로 redirect 시키거나 proxy 시키는 설정이 가능하다. 이 점을 이용하면 프론트엔드에서 특정 path로 요청을 발생시키고 해당 요청을 백엔드 서버가 배포된 서버로 proxy 시켜주는 구조로 구현을 해볼 수 있을 것 같았다.
이 방법을 테스트 해보기 위해 production으로 빌드를 할 때 API_BASE_URL이 /api
로 주입되도록 설정해주었다.
webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
...
plugins: [
...
new webpack.DefinePlugin({
'process.env.API_BASE_URL': JSON.stringify(
process.env.NODE_ENV === 'production'
? '/api'
: 'http://localhost:3000',
)
})
]
};
axios.js
1
2
3
4
5
6
7
8
9
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.API_BASE_URL,
});
...
export default instance;
다음으로는 NGINX에서 /api
로 오는 요청에 대한 proxy 설정을 추가해주었다.
app.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
location /api/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://backend_server_url/;
}
}
http://backend_server_url/
부분엔 백엔드 서버가 배포된 서버의 주소를 입력해주면 된다. 한가지 주의할 점은 끝에 /
를 붙여주어야 proxy 되는 URL에도 /api/
이후 path를 이어서 붙여준다고 한다.
설정을 마치고 서버에 배포된 상황을 만들기 위해 도커를 이용하여 실행시켜 주고 테스트 해본 결과 프론트 컨테이너에서 /api/...
로 요청이 발생할 경우 백엔드 컨테이너로 요청이 잘 전달되고 있었다.
로컬에서 도커를 이용하여 충분히 테스트를 마친 후 배포 서버에서도 적용시켜 본 결과 마찬가지로 잘 동작하는 것을 확인할 수 있었다.
conf 파일은 배포 서버에서 도커 이미지를 실행하는 시점에 volume 옵션을 통해 넣어줄 수 있기 때문에 서버를 유연하게 변경하기 위한 목적에도 부합하는 방법이고 이후에 다른 API 서버가 추가될 경우 proxy 설정만 추가해주면 되기 때문에 확장성도 확보할 수 있다. 🤩 (야호)