Configuration injection with Kotlin & Spring Boot for Google App Engine Standard
Application configuration often contains sensitive information (like database credentials) that should be treated with care — preferably not checked in to version control, but injected into the application via a separate channel.
GAE Standard offers very little in this space — developers can either decide to use environment variables that are set in
appengine-web.xml, or just choose one of the configuration files they bundle with the application, based e.g on the current project name.
Either way, the configuration files/settings will be part of the deployed artifact, and storing them separately from the application source code heavily complicates the build process, and makes it difficult to change the configuration options.
One, conceptually different alternative is to use Google Cloud Datastore for storing service configuration. While there is certainly more to be wished when it comes to auditing/access control to GCD, this solution at least makes it possible to produce a deployment artifact that is environment (GCP project) agnostic, without baking in any configuration.
- the deployment artifact should not contain any configuration that is related to environments (test/QA/prod/…)
- the configuration should provide basic information about the current running container (GCP project name, local/server mode, …)
- the configuration should be extendable by custom items
- the configuration code should be centralized, preferably retrieved via a single object
- the solution should support unit/integration testing
- the application should work seamlessly both on developer machines and on GCP
Environment based configuration
Often we need to be able to retrieve if the code is running in production or in a different environment. As a practical example, one might want to use different colors on a UI that is served from the application, or use different keys for validating JWT tokens, etc.
It is also useful to know if the running container is the local development server, or App Engine itself.
All these are easily retrieved using the App Engine API:
These settings do not change (and actually none of the other kind of configurations do either) while the application is running, so they are good candidates for lazy property delegates.
The two example properties are:
environmentName— assuming the projects are named consistently, this will return the current environment’s name. The example above expects that the projects are called like
environmentMode— this one tells if the code is running on App Engine, or on the local development server.
EnvironmentConfiguration is a Spring bean, so it can be @Autowire’d anywhere.
Datastore based configuration
Communicating with Google Coud Datastore can happen either by using the official SDK, or via an ORM framework.
The official documentation names Objectify as the main candidate, so let’s try that to create a simple DTO that stores some settings:
The idea was to use one specific Datastore Kind —
Configuration — which then contains an entry with a given ID, holding all the configuration options.
This also makes it possible to split the configuration into multiple parts — in case e.g you want to configure App Engine services separately.
Configuration should be loaded at startup, so let’s also define a method that returns the populated DTO as a Spring bean:
With this, configuration from Cloud Datastore can also be @Autowire’d.
The name sounds a bit contradictory (configuration can be changed, so why static…?), but this one is different only when tests are run — and exists only so that test/mock values can be injected.
Spring Boot offers (and prefers) the @ConfigurationProperties annotation, which is used to map file based settings to classes, in a typesafe manner.
Let’s create a simple property file with a corresponding Kotlin class:
The example uses two settings — an URL for JWT validation certificates, and its cache timeout.
Having these three different sources for application settings might make using them harder than it needs to be.
Kotlin’s delegation comes to the rescue — it is possible to call public methods of a given class from another, without boilerplate code.
Delegation requires that the delegated methods are specified with an interface — so let’s extract the necessary properties of
EnvironmentConfiguration and make them implement the respective interface:
Finally, the static configuration can be extended so that it delegates the two other type of configuration options to the respective underlying objects:
By autowiring this single bean, it becomes possible to retrieve all the available configuration options, regardless of their type.
The local development server for App Engine offers the same API as the one in App Engine itself, so there should be no problems with the environment based configuration. Same goes for static configuration — nothing App Engine specific there.
Dynamic configuration, however, relies on the Datastore contents. Luckily, the local development server offers a simple implementation of Cloud Datastore that saves the data in a file under the build folder.
The local Datastore will be empty by default, so there are two possibilities — bypass the Datastore entirely for local development, or bootstrap it based on e.g a local configuration file.
Since it is wise to make the local environment as similar to App Engine as possible, let’s do the latter — reusing the
DynamicConfiguration DTO in Jackson (to be populated from a JSON file) and saving it to the local Datastore on application start should do it:
First, we are making use of
Environmentconfiguration to be able to determine if we are in local mode. If so, we retrieve the file system path for the servlet, and using that we figure out the path for the file
This is just one way of doing it — there are surely other possibilities/sources for bootstrapping the local datastore.
local.json file should have the same fields as the
DynamicConfigurationImpl DTO — in the example, I was using the Jackson Kotlin module that reads the given file into the specified type.
The same DTO then can be saved to the local Datastore so that it can be read back — just as in App Engine itself.
To be able to run tests, the code preferably should not rely on external services. Since all our configuration classes are Spring beans, it is quite easy to replace them using a bean factory that is instantiated only when testing:
The @Primary annotations are useful for marking these beans as primary candidates for autowiring — this way we override the ones used normally.
By setting the @Profile we also tell Spring that this factory is used only in the
test profile — which is set by
application-test.properties. The same file can be used to set static configuration options for tests.
In summary — even though App Engine Standard is limited when it comes to configuring the applications, it is possible to use Cloud Datastore as configuration store.
There are certain shortcomings — for example, configuration is not versioned, services can read each other’s configuration, and any change to dynamic configuration requires that the service is restarted.
Despite these, using the demonstrated setup would remove the need to rebuild the application to change configuration options (and they can live outside of version control).