Kubernetes의 ConfigMap과 Secret: 이미지에서 설정 분리하기
1. 개요
컨테이너 이미지로 애플리케이션을 패킹할 때, 데이터베이스 URL, API 키 같은 값을 이미지에 하드코딩해서는 안 됩니다. 이 글은 Kubernetes의 ConfigMap과 Secret에 대해 명확히 이해하게 해줄 것입니다 — 이 두 개체는 이미지에서 설정을 분리하는 데 사용되며, 생성 방법, 환경 변수 또는 volume을 통해 Pod에 로드하는 방법, 그리고 Spring Boot 애플리케이션과 통합하는 방법을 다룹니다. 이를 읽은 후, 값을 변경할 때마다 이미지를 다시 빌드할 필요 없이 동적으로 config를 읽는 Deployment을 자신 있게 설정할 수 있을 것입니다.
2. ConfigMap이란 무엇인가
ConfigMap은 암호화되지 않은 key-value 형식의 설정 데이터를 저장하는 object로, 비민감한 데이터에 사용됩니다: 서비스 이름, feature flag, properties 파일…
ConfigMap의 데이터는 Pod과 독립적으로 존재합니다 — 이미지를 다시 빌드할 필요 없이 ConfigMap을 수정할 수 있으며, Pod을 다시 시작하기만 하면 됩니다 (또는 애플리케이션이 지원하는 경우 reload 메커니즘을 사용).
3. ConfigMap 생성
3.1. YAML 파일로부터
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
data:
APP_ENV: "production"
LOG_LEVEL: "INFO"
application.properties: |
server.port=8080
spring.jpa.hibernate.ddl-auto=validate
여기서 data는 두 가지 종류를 포함합니다: 단순한 key (APP_ENV, LOG_LEVEL)와 파일의 전체 내용을 포함하는 key (application.properties). 다음으로 적용합니다:
kubectl apply -f order-service-config.yaml
3.2. Literal 또는 기존 파일로부터
kubectl create configmap order-service-config \
--from-literal=APP_ENV=production \
--from-file=application.properties=./config/application.properties
이 명령은 YAML을 작성할 필요 없이 명령줄에서 직접 ConfigMap을 생성하므로, CI/CD 스크립트에 편리합니다.
4. Pod에서 ConfigMap 사용
4.1. 환경 변수로 로드
apiVersion: v1
kind: Pod
metadata:
name: order-service
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
envFrom:
- configMapRef:
name: order-service-config
envFrom은 ConfigMap의 모든 key를 동일한 이름의 환경 변수로 로드합니다. Container는 일반적인 환경 변수처럼 APP_ENV, LOG_LEVEL을 읽습니다.
특정 key만 선택하려면:
env:
- name: APP_ENVIRONMENT
valueFrom:
configMapKeyRef:
name: order-service-config
key: APP_ENV
이 방법은 container의 환경 변수 이름(APP_ENVIRONMENT)을 ConfigMap의 원래 key(APP_ENV)와 다르게 변경할 수 있게 해줍니다.
4.2. Volume으로 로드
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: order-service-config
ConfigMap의 각 key는 /app/config에 있는 파일이 됩니다 (예: /app/config/application.properties). 이 방법은 애플리케이션이 개별 환경 변수가 아닌 전체 설정 파일을 읽어야 할 때 적합합니다.
참고: volume 형태로 mount할 때, Kubernetes는 ConfigMap이 변경되면 파일을 자동으로 업데이트합니다 (kubelet 동기화 주기에 따라 수십 초에서 몇 분의 지연이 있음). 환경 변수(
env/envFrom)의 경우, Pod은 자동으로 다시 로드되지 않습니다 — Pod을 다시 시작해야 합니다.
5. Secret이란 무엇이고 ConfigMap과의 차이
Secret은 ConfigMap과 유사한 구조를 가지지만 민감한 데이터를 위한 것입니다: 비밀번호, token, private key. 주요 차이점:
| 기준 | ConfigMap | Secret |
|---|---|---|
| 암호화 | 아니요 | Base64-encode (실제 암호화가 아님) |
| etcd에 저장 | 일반 텍스트 | 저장 중 암호화를 활성화할 수 있음 |
kubectl get을 통해 표시 |
값 표시 | 값 숨김 (base64만 표시) |
| 목적 | 일반 설정 | 민감한 데이터 |
참고: Secret은 base64로 인코딩되기만 하며 기본적으로 암호화되지 않습니다. namespace에서
get secret권한이 있는 사람은 누구나 디코딩할 수 있습니다. 진정한 보안을 원하면 etcd의 [encryption at rest]를 활성화하거나 External Secrets Operator를 통해 통합된 external secret manager (Vault, AWS Secrets Manager)를 사용해야 합니다.
6. Secret 생성
6.1. YAML로부터 (base64를 직접 인코딩해야 함)
echo -n "P@ssw0rd123" | base64
# UEBzc3cwcmQxMjM=
apiVersion: v1
kind: Secret
metadata:
name: order-service-secret
type: Opaque
data:
DB_PASSWORD: UEBzc3cwcmQxMjM=
data 필드는 base64로 이미 인코딩된 값이 필요합니다. 직접 인코딩하고 싶지 않으면 stringData를 사용하세요:
stringData:
DB_PASSWORD: "P@ssw0rd123"
Kubernetes가 저장할 때 자동으로 인코딩합니다. manifest를 수동으로 작성할 때는 읽기 쉽도록 stringData를 사용하세요.
6.2. 명령줄에서
kubectl create secret generic order-service-secret \
--from-literal=DB_PASSWORD=P@ssw0rd123 \
--from-literal=DB_USERNAME=order_app
7. Pod에서 Secret 사용
로드하는 방법은 ConfigMap과 완전히 동일합니다:
envFrom:
- secretRef:
name: order-service-secret
또는 volume을 mount하세요:
volumeMounts:
- name: secret-volume
mountPath: /app/secret
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: order-service-secret
Secret의 volume mount는 항상 readOnly: true을 설정해야 하며 필요하면 defaultMode를 사용하여 파일 접근 권한을 제한해야 합니다:
secret:
secretName: order-service-secret
defaultMode: 0400
8. Spring Boot와 통합
Spring Boot는 relaxed binding 표준에 따라 환경 변수를 읽으므로, 특별한 코드가 필요하지 않습니다 — 변수 이름만 규칙에 맞게 설정하면 됩니다.
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secret
env:
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: order-service-secret
key: DB_USERNAME
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: order-service-secret
key: DB_PASSWORD
Spring Boot는 자동으로 SPRING_DATASOURCE_USERNAME → spring.datasource.username을 매핑합니다. 코드에서는 일반적인 @ConfigurationProperties를 사용하면 됩니다:
@ConfigurationProperties(prefix = "spring.datasource")
public record DataSourceProperties(String url, String username, String password) {
}
@Service
public class PaymentService {
private final DataSourceProperties dataSourceProperties;
public PaymentService(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
public String describeConnection() {
return "Connecting as %s".formatted(dataSourceProperties.username());
}
}
PaymentService는 Kubernetes에 대해 아무것도 알지 못합니다 — ConfigMap/Secret에서 제공한 환경 변수로부터 Spring Boot가 미리 바인딩한 값만 받습니다.
8.1. 클러스터에 의존하지 않는 테스트
로직을 테스트할 때 @TestPropertySource를 통해 시뮬레이션된 환경 변수를 설정하여 실제 Kubernetes가 필요 없습니다:
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.username=order_app",
"spring.datasource.password=test-pass"
})
class PaymentServiceTest {
@Autowired
private PaymentService paymentService;
@Test
void shouldDescribeConnectionUsingBoundProperties() {
assertThat(paymentService.describeConnection())
.isEqualTo("Connecting as order_app");
}
}
이 테스트는 DataSourceProperties가 올바르게 바인드되고 PaymentService가 올바른 값을 사용하는지 확인합니다 — 그 값이 클러스터의 실제 ConfigMap이나 Secret에서 오는지와 완전히 분리됩니다.
PaymentServiceTest > shouldDescribeConnectionUsingBoundProperties() PASSED
9. Pod 재시작 없이 동적 업데이트
ConfigMap/Secret을 volume 형태로 mount하면 파일은 자동 업데이트되지만 Spring Boot 애플리케이션은 다음을 수행하지 않는 한 자동으로 다시 읽지 않습니다:
- Spring Cloud Kubernetes (
spring-cloud-starter-kubernetes-client-config)를 사용하여 ConfigMap을 polling하고@RefreshScopebean을 새로고침합니다. - 또는 mount된 파일을 다시 읽는 file-watcher를 직접 구현합니다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
spring.cloud.kubernetes.config.enabled=true
spring.cloud.kubernetes.reload.enabled=true
spring.cloud.kubernetes.reload.mode=polling
이 설정은 Spring Cloud Kubernetes가 ConfigMap을 주기적으로 poll하고 @RefreshScope로 표시된 bean을 새로고침하는 기능을 활성화하므로, Pod을 수동으로 다시 시작할 필요가 없습니다.
참고: 지속적인 polling은 API 서버 리소스를 소비합니다. 대규모 클러스터의 경우, polling 대신
spring.cloud.kubernetes.reload.mode=event(watch API)를 사용하는 것을 고려하세요.
10. 일반적인 오류 및 사용하지 말아야 할 경우
- ConfigMap에 비밀번호 저장: ConfigMap은 암호화되지 않으며,
kubectl get configmap -o yaml은 값을 명확히 표시합니다. 민감한 데이터는 항상 Secret을 사용하세요. - Secret YAML에서
data를 작성할 때 base64-encode를 잊음:illegal base64 data오류가 발생합니다. 직접 인코딩하고 싶지 않으면stringData를 사용하세요. - ConfigMap을 변경했지만 Pod이 새 값을 받지 않음:
env/envFrom을 통해 로드되면 자동 다시 로드되지 않기 때문입니다 —kubectl rollout restart deployment를 실행해야 합니다. - Secret volume을 mount했지만
readOnly: true를 잊음: container가 파일을 덮어쓸 수 있어 디버깅할 때 혼란을 일으킵니다. - Secret을 장기 보안 저장소로 사용: Kubernetes의 Secret은 runtime 설정에 적합하며, 감사 로그, 자동 rotation 또는 엄격한 versioning이 필요할 때 전문 secret manager (Vault, AWS Secrets Manager)를 대체할 수 없습니다.
- ConfigMap/Secret이 너무 큼: Kubernetes는 object 크기를 약 1 MiB로 제한합니다 (etcd 제한 때문). 대용량 설정 파일이 필요하면 PersistentVolume의 volume mount를 사용하거나 init container에서 object storage에서 로드하는 것을 고려하세요.
11. 결론
ConfigMap과 Secret은 이미지에서 설정을 분리하고, 설정 object만 변경하여 같은 이미지를 여러 환경(dev, staging, production)에 배포할 수 있게 해줍니다. ConfigMap은 공개 데이터용, Secret은 민감한 데이터용입니다 — 하지만 Secret은 기본적으로 인코딩일 뿐 진정한 암호화가 아님을 기억하세요. Spring Boot를 사용하면 relaxed binding을 활용하여 비즈니스 로직 코드를 변경하지 않고 환경 변수를 통해 값을 로드할 수 있으며, Pod을 다시 시작하지 않고 동적 설정 reload가 필요한 경우 Spring Cloud Kubernetes를 활성화할 수 있습니다.