Go config.yaml

Go 서비스에서 config.yaml과 config.yaml.example를 함께 두는 패턴은 흔하지만, 실제 운영 Secret이 config.yaml에 들어간 채 저장소에 커밋되는 경우가 많습니다. Go 서비스는 환경 변수 기반으로 바꾸는 경우도 있고, 운영상 config.yaml 자체를 유지해야 하는 경우도 있으므로 두 패턴을 구분해서 안내하는 것이 좋습니다.

이런 경우에 참고

  • 저장소에 config.yaml과 config.yaml.example가 함께 있는 경우
  • Go 애플리케이션이 YAML 설정 파일을 읽어 초기화되는 경우
  • database.password, api.token, jwt.secret 같은 값이 config.yaml에 직접 적힌 경우

권장 구조

text
config.yaml.example   # 샘플 설정만 포함, Git 커밋 가능
config.yaml           # 실제 운영값, Git 커밋 금지
.gitignore            # config.yaml 포함

예시

yaml
server:
  port: 8080

database:
  host: prod-db.internal
  user: app_user
  password: super-secret-password

jwt:
  secret: hardcoded-jwt-secret

Pattern A: 환경 변수 기반 주입

환경 변수 사용이 가능한 서비스라면 이 패턴이 단순합니다.

언제 이 패턴을 선택하는가

  • Secret 개수가 많지 않고 환경 변수로 관리 가능한 경우
  • 기존 배포 시스템이 환경 변수 주입을 표준으로 제공하는 경우
  • 설정 파일 구조를 크게 유지할 필요가 없는 경우

config.yaml.example

yaml
server:
  port: 8080

database:
  host: ${DB_HOST}
  user: ${DB_USER}
  password: ${DB_PASSWORD}

jwt:
  secret: ${JWT_SECRET}

Go 코드 예시

아래 코드는 환경 변수를 직접 읽습니다. yaml.Unmarshal은 ${ENV_VAR}를 자동 치환하지 않으므로 위 YAML을 읽는 로더를 쓴다면 별도의 치환 처리가 필요합니다.

go
package config

import (
	"fmt"
	"os"
)

type Config struct {
	DBPassword string
	JWTSecret  string
}

func Load() (*Config, error) {
	cfg := &Config{
		DBPassword: os.Getenv("DB_PASSWORD"),
		JWTSecret:  os.Getenv("JWT_SECRET"),
	}

	if cfg.DBPassword == "" {
		return nil, fmt.Errorf("DB_PASSWORD is not configured")
	}
	if cfg.JWTSecret == "" {
		return nil, fmt.Errorf("JWT_SECRET is not configured")
	}

	return cfg, nil
}

Pattern B: mounted config.yaml 기반 주입

애플리케이션이 반드시 config.yaml 파일을 읽어야 한다면 형식은 유지하되, 실제 파일은 런타임에 /etc/app/config.yaml 같은 경로로 마운트합니다.

언제 이 패턴을 선택하는가

  • 애플리케이션이 파일 기반 설정 로딩에 강하게 의존하는 경우
  • 계층형 YAML 구조를 유지해야 하는 경우
  • Kubernetes Secret volume 또는 secure file delivery를 운영 표준으로 사용하는 경우

저장소에 두는 파일: config.yaml.example

yaml
server:
  port: 8080

database:
  host: prod-db.internal
  user: app_user
  password: "<runtime-provided>"

jwt:
  secret: "<runtime-provided>"

런타임 파일 경로 예시

text
/etc/app/config.yaml

Go 코드 예시

go
package config

import (
	"fmt"
	"os"

	"gopkg.in/yaml.v3"
)

type FileConfig struct {
	Database struct {
		Password string `yaml:"password"`
	} `yaml:"database"`
	JWT struct {
		Secret string `yaml:"secret"`
	} `yaml:"jwt"`
}

func LoadFromFile(path string) (*FileConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config file: %w", err)
	}

	var cfg FileConfig
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse config file: %w", err)
	}

	if cfg.Database.Password == "" {
		return nil, fmt.Errorf("database.password is not configured")
	}
	if cfg.JWT.Secret == "" {
		return nil, fmt.Errorf("jwt.secret is not configured")
	}

	return &cfg, nil
}

애플리케이션은 파일이 없거나 필수 Secret이 비어 있으면 즉시 실패해야 합니다.

Go 서비스에서 꼭 config.yaml을 써야 하는 경우

  • 실제 config.yaml은 저장소에 커밋하지 않습니다.
  • 저장소에는 config.yaml.example만 둡니다.
  • 실제 config.yaml은 Kubernetes Secret volume, CSI/Vault Agent, 또는 secure file delivery로 런타임에 공급합니다.
  • Docker image 안에 운영용 config.yaml을 포함시키지 않습니다.
  • 파일 권한은 최소화하고 world-readable 상태를 금지합니다.
  • 설정 파일 누락이나 필수 Secret 누락 시 애플리케이션이 fail closed 하도록 합니다.
  • 설정 dump, panic, debug 로그에 Secret이 노출되지 않도록 마스킹합니다.
  • rotation 시 파일 갱신 후 reload 또는 restart 절차를 운영 문서에 포함합니다.

개발자 해야 할 일

  • env 패턴 또는 mounted file 패턴 중 현재 서비스 구조에 맞는 방식을 선택합니다.
  • os.Getenv() 또는 파일 로딩 코드에서 필수 Secret 누락 시 fail closed 하도록 구현합니다.
  • 설정 객체 전체를 로깅하지 않도록 수정합니다.
  • 파일 기반 패턴이면 런타임 경로를 문서화하고 하드코딩된 개발용 Secret을 제거합니다.

인프라/플랫폼 해야 할 일

  • env 패턴이면 배포 환경 변수 주입 체계를 구성합니다.
  • mounted file 패턴이면 /etc/app/config.yaml 같은 표준 경로로 Secret file을 공급합니다.
  • Docker image, ConfigMap, 저장소 커밋을 통한 운영 파일 배포를 금지합니다.
  • Secret rotation과 재배포 또는 reload 절차를 운영 문서에 반영합니다.

검증 방법

  • config.yaml.example에 운영 Secret 값이 없는지 확인합니다.
  • 실제 config.yaml이 Git에 포함되지 않는지 확인합니다.
  • 파일 누락 또는 환경 변수 누락 시 애플리케이션이 정상적으로 실패하는지 확인합니다.
  • 로그, panic, 디버그 출력에 Secret이 보이지 않는지 확인합니다.

자주 하는 실수

  • 개발 편의를 위해 운영 Secret이 들어간 config.yaml을 그대로 커밋
  • mounted file 패턴을 쓰면서도 Docker image에 샘플이 아닌 실제 파일 포함
  • 설정 로더가 빈 Secret을 허용해 잘못된 상태로 기동
  • ConfigMap에 Secret을 넣고 file mount처럼 사용

담당자 조치 절차

  1. config.yaml에서 실제 Secret 값을 제거합니다.
  2. config.yaml.example에는 키 이름과 예시 형식만 남깁니다.
  3. 실제 config.yaml은 .gitignore에 추가합니다. 이미 추적 중인 파일은 Git 추적에서도 제거해야 하며, .gitignore만으로 과거 커밋의 값이 사라지지는 않습니다.
  4. env 기반이면 os.Getenv() 또는 설정 로더로 값을 읽도록 수정합니다.
  5. file 기반이면 /etc/app/config.yaml 같은 런타임 경로에서 파일을 읽도록 수정합니다.
  6. Kubernetes 사용 서비스라면 환경 변수 또는 Secret volume 중 운영 패턴에 맞는 방식으로 주입합니다.
  7. 기존 Secret 값은 노출된 것으로 보고 폐기 및 재발급합니다.

담당자에게 안내할 문구 예시

  • "Go 서비스의 config.yaml에 저장된 실제 Secret은 제거하고, config.yaml.example에는 예시 형식만 남기세요."
  • "환경 변수 사용이 가능하면 os.Getenv()로 읽고, config.yaml이 필수라면 실제 파일을 런타임에 /etc/app/config.yaml으로 마운트하세요."
  • "config.yaml이 이미 저장소에 커밋되었다면 해당 Secret은 노출된 것으로 보고 즉시 회전해야 합니다."

추가 확인 사항

  • config.yaml.example에 실제 운영값이 복사되어 있지 않은가
  • Go 설정 로더가 비어 있는 Secret을 허용하고 있지 않은가
  • 디버그 로그에서 전체 설정 객체를 출력하고 있지 않은가
  • 운영용 config.yaml이 Docker image 또는 ConfigMap에 포함되어 있지 않은가

관련 문서