#16 Spring Boot – Cloud Config

Spring Cloud we współpracy z Netflixem w bardzo pozytywny sposób ułatwiają pracę w architekturze mikroserwisów. Dostarczają oni wiele mechanizmów które pozwalają spełnić założenia jakie stawia poprawne tworzenie mikroserwisów. Pierwszym narzędziem Cloudowym który przedstawię jest Cloud Config.

12 Factor manifesto

12 Factor manifesto zawiera 12 punktów które powinna spełnia aplikacja w środowisku rozproszonym. Jeden z punktów mówi o tym, że ustawienia specyficzne dla danego środowiska powinny znajdować się tylko na nim. Domyślna implementacja application.properties spełnia te założenia, jednakże rozwiązanie to ma kilka wad.

Brak historii

Aktualnie (bez korzystania z Spring Cloud Config) najczęściej trzymamy nasze ustawienia razem z kodem w jakiś plikach typu properties (najgorzej jeśli te wartości są zahardkodowane!). Nie jest to dobre rozwiązaniem, ponieważ może zdarzyć się, że przez przypadek zacommitujemy nasz plik z hasłami lub ustawieniami z którymi nie chcemy dzielić się z innymi. Jeśli nie przechowujemy naszych plików z konfiguracją na jakimś serwerze z systemem kontroli wersji tracimy możliwość przeglądania historii tych plików. Potem możemy stracić dużo czasu na rozmyślanie dlaczego ktoś zmienił jakąś wartość w propertiesach.

Brak możliwości współdzielenia informacji

Kolejnym minusem trzymania konfiguracji w lokalnych plikach jest brak możliwości dzielenia się ustawieniami pomiędzy mikroserwisami. Bez serwera z konfiguracjami musimy kopiować zawartość plików które można by uwspólnić.

Bezpieczeństwo

W kwestii bezpieczeństwa przechowywanie haseł czystym plain textem nie jest bezpiecznym rozwiązaniem.

Zmiany w plikach

Problemem jest także przeładowywanie aplikacji po każdej zmianie w properties. Killer featurem który rozwiązuje ten problem jest @RefreshScope. Spring Cloud Config dostarcza mechanizm który w łatwy sposób może odświeżyć kontekst aplikacji bez potrzeby ponownego jej uruchomienia.

Config Server

Korzystanie z Spring Cloud Config zaczynamy od stworzenia serwera który będzie dostarczał propertiesy. Adnotacja @EnableConfigServer uruchamia naszą aplikację jako serwer, według konwencji port 8888 jest portem jaki powinniśmy ustawiać dla serwera. W kolejnym kroku dodajemy wpisy do pliku application.properties. Cloud Config domyślnie korzysta z Git’a dlatego też wszystkie propertiesy powinniśmy przechowywać właśnie tam. Dla treningu można stworzyć sobie lokalne repozytorium w którym umieszczamy dwa pliki konfiguracyjne: application.properties oraz client-service.properties. Ścieżkę do repozytorium ustawiamy pod wpisem spring.cloud.config.server.git.uri. Minimalna konfiguracja:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigServerApplication.class, args);
   }
}
#Applicatione name
spring.application.name=config-server
#Conventional port number
server.port=8888
#URI to GIT with all properties
spring.cloud.config.server.git.uri=GIT-URI-HERE

Po uruchomieniu serwera wystarczy wejść pod adres http://localhost:8888/application/master aby zobaczyć wpisy dla naszego pliku application.properties. Natomiast każde inne property poprzedzamy nazwą pliku. Dla client-service.properties będzie to adres http://localhost:8888/client-service/master. GitHub dla serwera: https://github.com/kchrusciel/spring-boot-config-server-example.

Config Client

Aby móc korzystać z serwera konfiguracji musimy wykorzystać fazę bootstrapowania aplikacji. Aby to zrobić musimy utworzyć plik bootstrap.properties, który jest ładowany w tej fazie. Jest on identyczny jak plik application.properites jednakże ładowany jest w innej fazie uruchamiania.

A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for the main application. Out of the box it is responsible for loading configuration properties from the external sources, and also decrypting properties in the local external configuration files.

W tym pliku ustawiamy nazwę aplikacji spring.application.name=config-client. Nazwa ta powinna być odpowiednia dla nazwy pliku properties który znajduje się w repozytorium (w naszym przypadku jest to config-client). Kolejnym elementem jest ustawienie adresu serwera konfiguracji, spring.cloud.config.uri=http://localhost:8888. Wartość http://localhost:8888 jest wartością domyślną, więc tak naprawdę wpis ten jest nadmiarowy (zostawiam dla czytelności).

#Application name
spring.application.name=client-service
#Config server address by default is 8888
spring.cloud.config.uri=http://localhost:8888

Refresh Scope

@RefreshScope jest adnotacją która pozwala nam odświeżyć wykorzystywane przez nas property bez potrzeby przeładowywania aplikacji. Jest to rozwiązanie które eliminuje wadę wymienioną we wstępie. Na początek do naszego config-client dodajemy endpoint /message:

@RestController
@RefreshScope
public class ConfigClientController {

    @Value("${info.property}")
    private String property;

    @GetMapping("/message")
    String property() {
        return property;
    }

}

Który wyświetla nam zawartość property info.property. Dodajmy teraz ten wpis do naszego config-client.properties, przykładowo info.property=CodeCouple (należy zacommitować tą zmianę jeśli korzystamy z GIT’a) a następnie uruchamiamy aplikację. Pod endpointem /message wyświetli nam się napis CodeCouple. Teraz zmieniamy znów tą wartość w pliku config-client.properties na info.property=CodeCouple roxx!!. Po ponownym odświeżeniu /message nadal widzimy starą wartość. Aby wykorzystać możliwości @RefreshScope musimy wykorzystać acutatory i wysłać puste żądanie POST na endpoint /refresh, teraz bez potrzeby restartu aplikacji powinniśmy widzieć nową zawartość (odśwież przeglądarkę jeśli nie widzisz zmiany).

Szyfrowanie

Uwaga! Aby to zadziałało należy zainstalować JCE.

W bardzo prosty sposób możemy rozwiązać problem szyfrowania haseł. Na naszym serwerze konfiguracji ustawiamy wartość dla encrypt.key:

#Cipher value, this value should comes from ENV variables
encrypt.key=secret

Wartość secret jest ustawiona dla przykładu, jednakże wartość ta powinna pochodzić ze zmiennych środowiskowych lub z jakiegoś jks z kluczami. Następnie mamy dostępne dwa endpointy, decrypt oraz encrypt. Pod adres encrypt uderzamy POST’em z naszą wartością którą chcemy zaszyfrować kluczem:

curl localhost:8888/encrypt -d szyfrujemy
bfed366b135b6d96d736571d2131e330fc34e0396cde0ab430429fe9373ba9d0

W odpowiedzi dostajemy zaszyfrowaną wartość która do enkrypcji wykorzystała nasz klucz z encrypt.key. Teraz na naszym serwerze Gitowym wystarczy poprzedzić wartość kluczem {cipher}:

info.encrypted.property={cipher}bfed366b135b6d96d736571d2131e330fc34e0396cde0ab430429fe9373ba9d0

Teraz możemy używać tej wartości która w momencie użycia zostanie odszyfrowana. Dla testu możemy wysłać POST’a na adres decrypt:

curl localhost:8888/decrypt -d bfed366b135b6d96d736571d2131e330fc34e0396cde0ab430429fe9373ba9d0
szyfrujemy

More

Więcej informacji na temat dobrych praktyk znajdziecie TUTAJ. Linki do GitHuba.