Step 6-7. 환경변수/Secrets + Nginx 리버스 프록시
인프라 파이프라인 핸즈온의 여섯·일곱 번째 단계. 비밀값을 안전하게 관리하고, 트래픽 입구를 분리하는 경험을 한다.
Step 6. 환경변수와 Secrets — ⑨ Secrets/Config (비밀값 관리)
왜 비밀값 관리가 필요한가?
코드에 비밀번호를 직접 쓰면 이렇게 된다:
1) 코드에 DB_PASSWORD = "abc123" 을 작성
2) git push → GitHub에 올라감
3) GitHub은 공개 저장소면 전세계 누구나 볼 수 있음
4) 비밀번호가 노출됨 → 해킹 위험
이것을 **하드코딩(Hardcoding)**이라 한다. 절대 하면 안 되는 행위다. 해결책: 비밀값을 코드 바깥에 두고, 실행 시점에 주입한다.
용어 정리
Secret — 영어 뜻: “비밀”. IT 의미: API 키, DB 비밀번호, 토큰 등 절대 노출되면 안 되는 정보. 비유: 금고에 보관하는 귀중품. 금고 번호(코드)에 귀중품 이름을 적어놓지 않는 것처럼, 코드에 비밀값을 적지 않는다.
Environment Variable — 영어 뜻: “환경 변수”. IT 의미: 프로그램 실행 시 운영체제 수준에서 주입하는 설정값. 비유: 식당의 오늘의 메뉴판. 식당(앱) 자체를 바꾸지 않아도, 메뉴판(환경변수)만 교체하면 오늘의 메뉴(동작)가 달라진다.
실무 용어: “시크릿 관리(Secret Management)”, “환경변수 주입(Environment Variable Injection)” (이 문서의 “슬롯”은 학습용 비유이며, 실무에서는 위 용어를 쓴다.)
.env 파일 만들기
프로젝트 루트(my-infra-lab/)에 .env 파일을 생성한다.
# .env 파일을 만든다 (프로젝트 루트에서)
cat <<'EOF' > .env
# 서버가 사용할 포트 번호
PORT=3001
# Redis 접속 주소 (docker compose에서 서비스명 'redis'가 호스트명이 됨)
REDIS_URL=redis://redis:6379
# 앱 이름 (비밀은 아니지만 환경별로 다를 수 있는 설정값)
APP_NAME=my-infra-lab
EOF
.gitignore에 .env 추가
# .gitignore 파일에 .env를 추가한다
echo ".env" >> .gitignore
app.js를 환경변수 방식으로 수정
하드코딩된 값을 process.env로 교체한다.
// Express 모듈을 불러온다
const express = require("express");
// Express 앱 인스턴스를 생성한다
const app = express();
// 환경변수에서 포트를 읽는다. 없으면 기본값 3001 사용
const PORT = process.env.PORT || 3001;
// 환경변수에서 Redis URL을 읽는다
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
// 환경변수에서 앱 이름을 읽는다
const APP_NAME = process.env.APP_NAME || "my-infra-lab";
// GET /health — 서버 상태 확인 엔드포인트
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
// GET /info — 앱 이름과 Redis 접속 정보를 반환
app.get("/info", (req, res) => {
res.json({
app: APP_NAME, // 환경변수에서 가져온 앱 이름
redis: REDIS_URL, // 환경변수에서 가져온 Redis 주소
version: "1.0.0",
});
});
// 서버를 시작한다
app.listen(PORT, () => {
console.log(`${APP_NAME} running on http://localhost:${PORT}`);
});
docker-compose.yml에 env_file 추가
services:
app:
build: .
# .env 파일의 모든 변수를 컨테이너에 자동 주입한다
env_file:
- .env
ports:
- "3001:3001"
redis:
# Redis 공식 이미지 (경량 Alpine 버전)
image: redis:alpine
ports:
- "6379:6379"
확인: 환경변수가 잘 들어갔는지 테스트
# 컨테이너를 빌드하고 실행한다
docker compose up -d --build
# info 엔드포인트를 호출해서 환경변수 값이 나오는지 확인
curl http://localhost:3001/info
# 기대 결과: {"app":"my-infra-lab","redis":"redis://redis:6379","version":"1.0.0"}
GitHub Actions에서 Secrets 사용하기
로컬 .env는 개발용이고, CI/CD(GitHub Actions)에서는 GitHub Secrets를 사용한다.
GitHub Secrets 등록 방법:
1) GitHub 저장소 페이지 → Settings 탭 클릭
2) 왼쪽 메뉴에서 Secrets and variables → Actions 클릭
3) "New repository secret" 버튼 클릭
4) Name: GHCR_TOKEN (Step 4에서 만든 Personal Access Token)
Value: ghp_xxxx... (실제 토큰 값)
5) "Add secret" 클릭
GitHub Actions workflow에서 사용하는 법:
# .github/workflows/ci.yml 안에서
jobs:
build:
steps:
- name: GHCR 로그인
run: |
# GitHub Secrets에 등록한 GHCR_TOKEN을 ${{ secrets.GHCR_TOKEN }}으로 참조
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
핵심 원리: 비밀값은 코드에 없고, GitHub 서버의 금고(Secrets)에만 존재한다. workflow 실행 시점에 자동 주입된다.
Step 7. Nginx 리버스 프록시 — ⑧ Networking/Traffic (트래픽 관리)
왜 리버스 프록시가 필요한가?
Express 앱을 인터넷에 직접 노출하면:
사용자 → Express (포트 3001) ← 직접 노출
문제: SSL 처리 불가, 정적 파일 비효율, 보안 취약, 로드 밸런싱 불가
Nginx를 앞에 두면:
사용자 → Nginx (포트 80) → Express (포트 3001) ← 외부에서 접근 불가
해결: SSL 처리, 정적 파일 캐싱, 보안 강화, 로드 밸런싱 가능
용어 정리
Reverse Proxy — 영어 뜻: “역방향 대리자”. IT 의미: 사용자의 요청을 대신 받아서 뒤에 있는 실제 서버로 전달하는 중간자. 비유: 호텔 프론트 데스크. 손님(사용자)은 프론트(Nginx)에 요청하고, 프론트가 적절한 방(서버)으로 안내한다. 손님이 직접 방을 찾아다니지 않아도 된다.
Load Balancer — 영어 뜻: “부하 분산기”. IT 의미: 들어오는 요청을 여러 서버에 골고루 나눠주는 역할. 비유: 놀이공원 입구에서 “1번 줄 가세요, 2번 줄 가세요” 안내하는 직원. 한 줄만 길어지지 않도록 분산한다.
실무 용어: “리버스 프록시(Reverse Proxy)”, “API 게이트웨이(API Gateway)” (이 문서의 “슬롯”은 학습용 비유이며, 실무에서는 위 용어를 쓴다.)
Nginx 설정 파일 만들기
mkdir -p nginx 후 nginx/default.conf 파일을 생성한다:
# Nginx 서버 블록 — 하나의 가상 호스트 설정
server {
# 80번 포트에서 요청을 받는다 (HTTP 기본 포트)
listen 80;
# 모든 경로(/)에 대한 처리 규칙
location / {
# 받은 요청을 app 서비스의 3001번 포트로 전달한다
# "app"은 docker-compose.yml의 서비스명이 DNS 이름이 됨
proxy_pass http://app:3001;
# 원래 클라이언트의 Host 헤더를 뒤쪽 서버에 전달한다
proxy_set_header Host $host;
# 원래 클라이언트의 IP 주소를 뒤쪽 서버에 전달한다
proxy_set_header X-Real-IP $remote_addr;
}
}
docker-compose.yml에 Nginx 서비스 추가
services:
app:
build: .
env_file:
- .env
# 포트를 외부에 노출하지 않는다 (Nginx가 대신 받으므로)
# ports: 제거 — 이제 외부에서 직접 접근 불가
expose:
- "3001" # 컨테이너 내부에서만 3001 포트 사용 선언
redis:
image: redis:alpine
nginx:
# Nginx 공식 이미지 (경량 Alpine 버전)
image: nginx:alpine
# 외부 포트 80번을 Nginx 컨테이너의 80번에 연결
ports:
- "80:80"
# 로컬의 설정 파일을 컨테이너 안에 마운트한다
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
# app 서비스가 먼저 시작된 후에 nginx를 시작한다
depends_on:
- app
실행 및 확인
# 전체 서비스를 빌드하고 실행한다
docker compose up -d --build
# Nginx를 통해 접근 (포트 80)
curl http://localhost
# 기대 결과: 404 error
# info 엔드포인트도 확인
curl http://localhost/info
# 기대 결과: {"app":"my-infra-lab","redis":"redis://redis:6379","version":"1.0.0"}
# 포트 3001으로 직접 접근 — 실패해야 정상 (외부 노출 제거됨)
curl http://localhost:3001
# 기대 결과: Connection refused (Nginx만 외부와 통신)
이것이 트래픽 입구가 분리되는 경험이다. 사용자는 포트 80(Nginx)으로 접근하고, Nginx가 내부의 Express(포트 3001)로 전달한다. Express는 외부에서 직접 접근할 수 없다.
전체 흐름 다이어그램
사용자 → Nginx(:80) → Express(:3001) → Redis(:6379)
↑ 외부 접근 ↑ 내부만 ↑ 내부만
트래픽 입구 .env 주입 데이터 저장
GitHub Secrets → CI/CD 주입
체크리스트
-
.env파일을 만들고PORT,REDIS_URL,APP_NAME을 정의했다 -
.gitignore에.env가 포함되어 있다 -
app.js에서process.env로 환경변수를 읽고 있다 -
docker-compose.yml에env_file: .env가 설정되어 있다 - GitHub Secrets에 비밀값을 등록하는 방법을 이해했다
-
nginx/default.conf설정 파일을 만들었다 -
docker-compose.yml에 nginx 서비스를 추가했다 -
curl http://localhost(포트 80)로 Express 응답을 받았다 -
curl http://localhost:3001이 실패하는 것을 확인했다 (외부 노출 차단) - 트래픽 흐름(사용자 → Nginx → Express)을 이해했다
다음 단계: 05-observability-and-security.md — 모니터링과 보안
Comments
// admin login