Docker, Compose, and Helm
To help make our lives easier, we build Docker images of our services so we can use them in a number of ways: as dependencies when developing other services, in our CI/DI pipeline for automated integration testing, and then in actual test and production environments.
In general, we currently have two ways of running our services as Docker images:
In docker-compose as a dev, test, and CI/CD dependency
In helm in a Kubernetes pod as live services in test and production environments
Talking To or Mocking Out Third Parties
When interacting with third-party services, we often have intricacies to handle. We’ll have environment requirements around whether or not our service should actually call out and, if not, whether or not we need it to be stateful. For example, when developing locally against the service when it’s running in docker-compose, we generally do not want it reaching out to any third parties ever. But we might need a way to emulate the actual service so it can return useful enough data for whatever’s needed to develop the feature.
ScannerService & Widget
We’ll walk through a couple solutions as examples. For these examples, we’re going to have a
ScannerService that has to integrate with a service called Widget using an API Key.
Special Values w/ Env Vars
Generally, for third party API Keys, we define an environment variable for Spring to read in via Spring’s application properties – in this case we’ll call it
WIDGET_API_KEY. We would then set this on a per-environment basis, and we can overload it with a special “short circuit” value that can be used in code.
In the main
For docker-compose, which is mainly our dev/test/ci runtime, we don’t have to do anything because the ‘fake’ default value will be used and those will never call out.
But for running via helm in Kubernetes, we have a config yml per environment and would just simply override with the pointer to that env’s key in our secrets manager:
NOTE: We don’t actually put api keys in yml files because it’s bad practice. In reality, we use a runtime secrets manager that can do a key lookup against a secure vault, but that’s outside the scope of this article.
Finally in our code, we can do something like this:
Explicit Flag w/Spring Profiles
Similarly to a special value, we can use an explicit flag in
In this case, we used Spring profiles to set this value. That way, the value is defined once but shared across anything that uses that profile. That way we don’t have to remember all the places we’ve set that value, we can group these kinds of values in the profile, and it can be shared by whatever use the profile.
If we had a profile called ‘ci’, then we’d do something like this in
If we need that profile to run in docker-compose, we set it in the
ScannerService config like so:
For helm, since the default is true, it will make the third party calls. But if we needed a specific profile, it’d look like this:
Our code then becomes a little clearer in that we have a single, well-defined and understood flag rather than overloading something else with the magic value:
Using special overloaded values or explicit flags solves the problem, but adds complexity to the code, which makes it more difficult in general to add functionality and also to track down runtime issues. Another downside we ran into was when someone added more calls against Widget, they didn’t know that they needed to take the special value/explicit flag into consideration. In the examples above it’s trivial to see what’s going on, but in a bigger class with more going on, it wasn’t clear. In that case, the overall system behavior became difficult to understand because that service was making some calls to the third party but not all.
Up until recently, our third party integrations just needed to be ‘off’ or ‘on’ for a given environment and the above approaches worked. Then we had to add some new functionality that required a service to be stateful but not actually hit the third party when running in certain test and dev modes. In order to completely develop, test, and deliver the solution across our platform, the service needed to persist and return data but not actually reach out to the third party service in some environments.
The third party in question here had a Dockerized mock service that seemed at first to solve our issue. But unfortunately, that mock service wasn’t stateful and a lot of things weren’t mutable – it was unable to set certain states because the real service didn’t allow it…but we needed to dev and test with those statuses.
Profile Based Mock Implementation
Luckily, we already had the pieces needed to provide a solution in that we were using Spring Profiles in both our docker-compose and helm strategies. The strategy was to use Spring profiles to provide either the real client or a statefully mocked one. This gave us two benefits: it completely separates out the two codepaths between ‘live’ and ‘mock’ mode, and also gives ‘mock’ mode the ability to keep state where needed.
Here’s what that might look like in an interface and two implementing classes that use the Spring
@Profile annotation to specify which one will be used:
WidgetClientImpl is blissfully unaware of any sort of mock/test codepaths and just assumes it’s doing the real thing. The
WidgetClientMock then doesn’t know anything about what’s supposed to happen and just focuses on local, mocked data. Both are easier to understand and much easier to test.
If something isn’t configured correctly and the wrong environment gets the wrong
WidgetClient implementation, it becomes quickly apparent that something’s wrong. For example, since the profiles that do mocking don’t have a real key, if they get the
WidgetClientImpl and start making calls against Widget, it will fail early and loudly. If the opposite happens and an env gets the mock implementation, it’s obvious that nothing is actually calling out to Widget.
Having to work with third-party services is commonly requested functionality these days, and having multiple options is key. Using a single overloaded value or an explicit flag with conditional checks keeps your logic contained in a single service, but makes that service more complicated. Letting the runtime pick an implementation based on a runtime value like a Spring Profile lets you split the test code from an actual implementation, allows a mock implementation to have a test state, but adds more configuration to your application.