Go services often keep both config.yaml and config.yaml.example. A common problem is committing the real config.yaml with production secrets. Choose between reading environment variables and retaining a file supplied securely at runtime according to the service's needs.
When to use this guide
- The repository contains both
config.yamlandconfig.yaml.example. - The Go application loads YAML configuration at startup.
- Values such as
database.password,api.token, orjwt.secretare written directly inconfig.yaml.
Recommended structure
text
config.yaml.example # 샘플 설정만 포함, Git 커밋 가능
config.yaml # 실제 운영값, Git 커밋 금지
.gitignore # config.yaml 포함
Examples
yaml
server:
port: 8080
database:
host: prod-db.internal
user: app_user
password: super-secret-password
jwt:
secret: hardcoded-jwt-secret
Pattern A: Environment-variable injection
This is a simple option for services that can use environment variables.
When to choose this pattern
- There are relatively few secrets and environment variables are suitable.
- The deployment system provides environment-variable injection.
- You do not need to preserve the existing configuration-file structure.
config.yaml.example
yaml
server:
port: 8080
database:
host: ${DB_HOST}
user: ${DB_USER}
password: ${DB_PASSWORD}
jwt:
secret: ${JWT_SECRET}
Go code example
This code reads environment variables directly. yaml.Unmarshal does not expand ${ENV_VAR} automatically; a loader that reads the YAML above needs separate substitution logic.
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: Inject a mounted config.yaml
If the application must read config.yaml, retain the format but mount the actual file at runtime, for example at /etc/app/config.yaml.
When to choose this pattern
- The application depends on file-based configuration loading.
- You need to preserve a hierarchical YAML structure.
- Kubernetes Secret volumes or secure file delivery are the operational standard.
Repository file: config.yaml.example
yaml
server:
port: 8080
database:
host: prod-db.internal
user: app_user
password: "<runtime-provided>"
jwt:
secret: "<runtime-provided>"
Example runtime path
text
/etc/app/config.yaml
Go code example
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
}
The application must fail immediately if the file is missing or a required secret is empty.
When the Go service must use config.yaml
- Do not commit the real
config.yaml. - Keep only
config.yaml.examplein the repository. - Supply the actual file at runtime through a Kubernetes Secret volume, CSI/Vault Agent, or secure file delivery.
- Do not include production
config.yamlin a Docker image. - Minimize file permissions and prevent world-readable access.
- Fail closed when the file or required secrets are missing.
- Redact secrets from configuration dumps, panics, and debug logs.
- Document file replacement and reload or restart steps for rotation.
Developer tasks
- Choose environment variables or a mounted file to suit the service.
- Make
os.Getenv()handling or the file loader fail closed when required secrets are missing. - Do not log entire configuration objects.
- For file-based configuration, document the runtime path and remove hard-coded development secrets.
Infrastructure and platform tasks
- Configure deployment-time environment-variable injection when using Pattern A.
- Supply secret files at a standard path such as
/etc/app/config.yamlwhen using Pattern B. - Do not distribute production files through Docker images, ConfigMaps, or repository commits.
- Include rotation and redeployment or reload steps in the operations guide.
Verification
- Check that
config.yaml.examplecontains no production secrets. - Confirm that the actual
config.yamlis not tracked by Git. - Confirm that a missing file or environment variable causes the application to fail.
- Check logs, panics, and debug output for secret values.
Common mistakes
- Committing production secrets in
config.yamlfor development convenience - Mounting configuration but also including the real file in the Docker image
- Allowing an empty secret so the application starts in an invalid state
- Storing secrets in a ConfigMap and treating its file mount as secret protection
Remediation steps
- Remove actual secrets from
config.yaml. - Leave only key names and example formats in
config.yaml.example. - Add the real
config.yamlto.gitignore. Stop tracking it if it is already tracked;.gitignoredoes not remove values from previous commits. - For environment variables, read values through
os.Getenv()or the configuration loader. - For a mounted file, read it from a runtime path such as
/etc/app/config.yaml. - On Kubernetes, choose environment injection or a Secret volume to suit operations.
- Treat previous secret values as exposed: revoke them and issue replacements.
Example instructions for the owner
- “Remove actual secrets from Go's
config.yamland keep only example formats inconfig.yaml.example.” - “Use
os.Getenv()when environment variables are suitable. Ifconfig.yamlis required, mount the actual file at/etc/app/config.yamlat runtime.” - “If
config.yamlwas committed, treat its secrets as exposed and rotate them immediately.”
Additional checks
- Have production values been copied into
config.yaml.example? - Does the loader accept empty secrets?
- Do debug logs print the entire configuration object?
- Is production
config.yamlincluded in a Docker image or ConfigMap?