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처럼 사용
담당자 조치 절차
config.yaml에서 실제 Secret 값을 제거합니다.config.yaml.example에는 키 이름과 예시 형식만 남깁니다.- 실제
config.yaml은.gitignore에 추가합니다. 이미 추적 중인 파일은 Git 추적에서도 제거해야 하며,.gitignore만으로 과거 커밋의 값이 사라지지는 않습니다. - env 기반이면
os.Getenv()또는 설정 로더로 값을 읽도록 수정합니다. - file 기반이면
/etc/app/config.yaml같은 런타임 경로에서 파일을 읽도록 수정합니다. - Kubernetes 사용 서비스라면 환경 변수 또는 Secret volume 중 운영 패턴에 맞는 방식으로 주입합니다.
- 기존 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에 포함되어 있지 않은가