Centralized Configuration with Spring Cloud Config Server

Managing configuration for 10 services across 3 environments means 30 separate config files. Spring Cloud Config Server centralizes all of them — one place to change a database URL, one place to rotate secrets, and services pick up changes without redeployment.

Config Server Setup

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# application.yml — config-server
server:
  port: 8888

spring:
  application:
    name: config-server

  cloud:
    config:
      server:
        git:
          uri: https://github.com/devops-monk/config-repo
          default-label: main
          search-paths: '{application}'    # look for config in a directory named after the service
          clone-on-start: true
          # For private repos:
          username: ${GIT_USERNAME}
          password: ${GIT_TOKEN}

The Config Server reads configuration files from a Git repository and serves them over HTTP.

Config Repository Structure

config-repo/
├── application.yml           # shared across all services
├── order-service/
│   ├── application.yml       # order-service defaults
│   ├── application-dev.yml   # dev profile
│   └── application-prod.yml  # prod profile
├── payment-service/
│   ├── application.yml
│   └── application-prod.yml
└── customer-service/
    └── application.yml

File naming convention: {application}/{application}-{profile}.yml

Resolved configuration order (last wins):

  1. application.yml (shared defaults)
  2. {service-name}/application.yml (service defaults)
  3. {service-name}/application-{profile}.yml (profile-specific)

Config Client Setup

Each service adds:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
# bootstrap.yml (loaded before application.yml — needed for Config Server URL)
# OR set via application.yml with spring.config.import:

spring:
  application:
    name: order-service    # ← determines which config files are fetched
  config:
    import: optional:configserver:http://localhost:8888
  cloud:
    config:
      fail-fast: true       # crash if config server unreachable
      retry:
        max-attempts: 6
        initial-interval: 1000
        multiplier: 1.5
        max-interval: 2000

On startup, order-service fetches its configuration from http://localhost:8888/order-service/default (or /order-service/prod for the prod profile) and merges it with local application.yml.

Config Server Endpoints

# Get config for order-service, default profile
GET http://localhost:8888/order-service/default

# Get config for order-service, prod profile
GET http://localhost:8888/order-service/prod

# Get config for order-service, prod profile, main branch
GET http://localhost:8888/order-service/prod/main

# Get a specific file
GET http://localhost:8888/order-service/default/main/application.yml

Response includes all properties resolved for that service/profile combination.

Encrypting Secrets

Never store secrets in plaintext in Git. Config Server can encrypt/decrypt values:

# config-server application.yml
encrypt:
  key: ${ENCRYPTION_KEY}   # symmetric key (AES)
  # Or use RSA key pair:
  # key-store:
  #   location: classpath:config-server.jks
  #   password: ${KEYSTORE_PASSWORD}
  #   alias: configserver

Encrypt a value:

curl -X POST http://localhost:8888/encrypt -d 'my-secret-password'
# Returns: AQA+9Kd5X8mGhY...

Store in config repo:

# order-service/application-prod.yml
spring:
  datasource:
    password: '{cipher}AQA+9Kd5X8mGhY...'   # encrypted — decrypted at fetch time

The Config Server decrypts {cipher}... values before serving them to clients. The secret is never exposed in Git.

Dynamic Refresh with @RefreshScope

When config changes in Git, services can pick it up without restarting:

<!-- client service -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>

Mark beans that should reload on config change:

@Service
@RefreshScope   // bean is re-created when /actuator/refresh is called
@RequiredArgsConstructor
public class PaymentGatewayService {

    @Value("${payment.gateway.api-key}")
    private String apiKey;    // refreshed when config changes

    @Value("${payment.gateway.url}")
    private String gatewayUrl;
}

Trigger refresh on a specific service:

POST http://order-service:8080/actuator/refresh
# Refreshes @RefreshScope beans in this instance

Or broadcast refresh to all instances via Spring Cloud Bus (Kafka):

POST http://config-server:8888/actuator/busrefresh
# Broadcasts refresh event to all services on the bus

All services receive the refresh event via Kafka and reload their @RefreshScope beans — no restarts needed.

Automatic Refresh with Webhooks

Configure Git to send a webhook to Config Server on every push:

GitHub/GitLab webhook:
  Push → POST http://config-server:8888/actuator/busrefresh

Config Server → publishes refresh event to Kafka
Kafka → all services consume event → refresh @RefreshScope beans

Configuration changes are live within seconds of a Git push.

Spring Cloud Vault (Alternative for Secrets)

For production secret management, use HashiCorp Vault instead of Config Server’s encryption:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
spring:
  config:
    import: vault://
  cloud:
    vault:
      uri: https://vault.devopsmonk.com
      authentication: KUBERNETES    # use Kubernetes service account token
      kubernetes:
        role: order-service
      kv:
        enabled: true
        default-context: order-service

Vault provides: secret rotation, dynamic credentials (DB passwords that expire), audit logs, fine-grained access control. For production microservices, Vault is the better choice for secrets.

Config Server with Kubernetes ConfigMaps

In Kubernetes, you can use ConfigMaps and Secrets as the config source instead of Git:

spring:
  config:
    import: kubernetes:
  cloud:
    kubernetes:
      config:
        enabled: true
        sources:
          - name: order-service-config
            namespace: default
      secrets:
        enabled: true
        sources:
          - name: order-service-secrets
            namespace: default

Kubernetes ConfigMaps and Secrets mount as Spring configuration — no Config Server process needed.

What You’ve Learned

  • Config Server serves configuration from a Git repository over HTTP
  • Services fetch config on startup by declaring spring.config.import: configserver:... and their spring.application.name
  • Encrypt secrets with {cipher}... — Config Server decrypts at fetch time, never exposes plaintext in Git
  • @RefreshScope beans reload when POST /actuator/refresh is called
  • Spring Cloud Bus broadcasts refresh to all instances via Kafka — no per-instance restarts
  • For production secrets, Vault provides rotation, dynamic credentials, and audit logs
  • On Kubernetes, use ConfigMaps/Secrets as native config sources instead of a Config Server process

Next: Article 50 — Resilience Patterns with Resilience4j — circuit breakers, retries, rate limiters, and bulkheads for resilient microservices.