Devops

Umgebungsabhängige Laufzeitkonfiguration mit Profil

Softwaresysteme laufen in der Regel in mehreren Umgebungen. Bei Webapplikation sind beispielsweise Entwicklungs-, Nighly Build-, Stage- und Produktionsumgebung üblich. Während die Geschäftslogik in allen Umgebungen gleich ist, unterscheiden sich die Konfigurationen mitunter erheblich. Bei der Konzeption und Architektur der Software muss diese Rahmenbedingung daher entsprechend berücksichtigt werden.

Ein häufig gewählter Ansatz hierfür ist die Parametrisierung des Buildprozess, wodurch sich für jede Umgebung ein einsprechend konfiguriertes Artefakt erstellen lässt; im Falle von Maven geschieht dieses zum Beispiel durch die Verwendung von Profilen. Neben einer verlängerten Buildzeit birgt dieses Vorgehen aber das Problem, dass bei jedem Lauf ein unterschiedliches Artefakt erzeugt wird, das aber immer mit denselben Maven-Koordinaten veröffentlicht wird und damit nach außen nicht unterscheidbar ist. Insbesondere beim Einsatz eines Artefakt-Repository ist das jedoch kein gangbares Verfahren, da für den Nutzer nicht ersichtlich ist, in welchen Umgebungen die Software lauffähig ist. Schlimmer noch, mit jedem Lauf kann das veröffentlichte Artefakt potentiell unterschiedlich konfiguriert sein.

Build once, run anywhere

Als Lösung für dieses Problem wird in der Maven Dokumentation die Verwendung von Classifiern empfohlen. Ein ähnlicher Weg mittels des Maven-Assembly-Plugin findet sich auch im Buch Agile ALM. Beiden gemeinsam ist jedoch, dass die eigentlichen Deploymentartefakte für jede Umgebung unterschiedlich sind, was streng genommen dem von Jez Humble formulierten Grundsatz „Deploy the same artifacts in every environment“ widerspricht. Dieses Ziel kann erreicht werden, wenn das Artefakt so konstruiert wird, dass es über einen Umgebungsparameter erkennt, welcher Parametersatz aktiviert werden soll. Dieses Prinzip möchte ich anhand der Implementierung im Springframework näher erläutern.

Spring Profiles

Mit Einführung der Umgebungsabstraktion in der Version 3.1 lassen sich in einem Spring Kontext über sogenannte Profile mehrere Konfigurationsvarianten definieren. Wie dieses Konzept in der Praxis genutzt werden kann, soll am Beispiel einer Spring-Bean vom Java-SE-Typ Calendar gezeigt werden, die je nach Umgebung, in der die Applikation läuft, mit der dafür konfigurierten Zeitzone und erstem Wochentag initialisiert wird. Das folgende Listing zeigt die entsprechende Spring-Konfiguration.

@Configuration
@PropertySource("classpath:/de/agiledojo/multienv/conf/default.properties")
@Import(ProductionEnvironmentConfiguration.class)
public class MultiEnvConfiguration {

@Value("${timezone.id}")
private String timeZoneID;

@Value("${firstDayOfWeek}")
private int firstDayOfWeek;

@Bean
public Calendar configuredCalendar() {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(timeZoneID));
cal.setFirstDayOfWeek(firstDayOfWeek);
return cal;
}

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}

Die Calendar-Bean wird mit den Werten der Membervariablen der Konfigurationsklasse erzeugt (Ziele 14). Da es sich bei der Konfigurationsklasse ebenfalls um eine Spring-Bean handelt, können diese Variablen mittels der @Value-Annotation vom Spring-Kontext initialisiert werden. Hierzu werden die angegebenen Property-Platzhalter verwendet. Die Werte hierfür stammen aus der in Zeile 2 definierten Property-Quelle.

timezone.id = CET
firstDayOfWeek = 0

Verwendet man, wie in diesem Beispiel, die Java-basierte Konfigurationsvariante, ist zu beachten, dass die Factory-Methode des PropertySourcesPlaceholderConfigurer statisch deklariert wird, da dieser vor der Instanziierung der anderen Beans bereits erzeugt werden muss. Weitere Details hierzu finden sich in der Spring-API-Dokumentation zur Bean-Annotation.

Die korrekte Konfiguration lässt sich einfach mit einem Unittest prüfen.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MultiEnvConfiguration.class })
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class DefaultConfigurationTest {
private static final int DEFAULT_FIRST_DAY_OF_WEEK = 0;

private static final String DEFAULT_TIMEZONE_ID = "CET";

@Autowired
Calendar cal;

@Test
public void timeZoneHasDefaultValue() {
final TimeZone actualTimeZone = cal.getTimeZone();
assertThat(actualTimeZone).isEqualTo(TimeZone.getTimeZone(DEFAULT_TIMEZONE_ID));
}

@Test
public void firstDayOFWeekHasDefaultValue() {
final int actualFirstDayOfWeek = cal.getFirstDayOfWeek();
assertThat(actualFirstDayOfWeek).isEqualTo(DEFAULT_FIRST_DAY_OF_WEEK);
}

}

Für die Konfiguration der Produktionsumgebung erfolgt in einer eigenen Javaconfig-Klasse, die per Import (siehe Zeile 2 aus obigem Listing) eingebunden wird.

@Configuration
@Profile("production")
@PropertySource("classpath:/de/agiledojo/multienv/conf/production.properties")
public class ProductionEnvironmentConfiguration {

}

Ebenso wie bei der Basiskonfigurationsklasse zuvor wird per @PropertySource-Annotation eine Konfigurationsdatei einbezogen. Durch die @Profile-Annotation in Zeile 2 kommt diese Konfiguration jedoch nur dann zum Tragen, wenn beim Start der Applikation das Umgebungsprofil „production“ aktiviert wird. Dieses kann durch Setzen einer System Property beim Applikationsstart geschehen.

-Dspring.profiles.active="production"

In unserem Beispel wird nur ein Wert in der Datei definiert und damit für dieses Profil überschrieben.

timezone.id = MET

Für einen Test lässt sich nun das Profile mit Hilfe der ActiveProfile-Annotation auswählen.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MultiEnvConfiguration.class })
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
@ActiveProfiles("production")
public class ProductionProfileTest {

private static final int DEFAULT_FIRST_DAY_OF_WEEK = 0;

private static final String PRODUCTION_TIMEZONE_ID = "MET";

@Autowired
private Calendar cal;

@Test
public void timeZoneHasProductionProfileValue() {
final TimeZone actualTimeZone = cal.getTimeZone();
assertThat(actualTimeZone).isEqualTo(TimeZone.getTimeZone(PRODUCTION_TIMEZONE_ID));
}

@Test
public void firstDayOFWeekHasDefaultValue() {
final int actualFirstDayOfWeek = cal.getFirstDayOfWeek();
assertThat(actualFirstDayOfWeek).isEqualTo(DEFAULT_FIRST_DAY_OF_WEEK);
}
}

Wie man sieht, wird die Zeitzone wie erwartet überschrieben, während der erste Wochentag weiterhin den Wert aus der Default-Konfiguration besitzt.

Fazit

Durch die Kombination von Springs Umgebungsprofilen und Unified Property Management kann über einfache Konfiguration der Laufzeitumgebung das entsprechende Parameterprofil ausgewählt werden. Die Deploymentartefakte sind damit in allen Umgebungen ohne Änderung lauffähig und können daher in einem einzigen Buildlauf erstellt und veröffentlicht werden.

Der komplette Sourcecode steht im Github-Repository zur Verfügung.

Teilen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Bist Du ein Bot? Dann nimm das: * Time limit is exhausted. Please reload CAPTCHA.