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 nginxnginx/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.ymlenv_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

  • // 댓글을 불러오는 중...
main ⚠ 0 ✕ 0 Ln 1, Col 1 Spaces: 2 UTF-8 LF Markdown