Moving to containerized microservices is not an easy transition for developers who have been building applications using more traditional methods. There are a lot of new concepts and details developers need to consider and become familiar with when they design a distributed application, which is what a microservice application is. Throw Docker and Kubernetesinto the mix and it becomes clear why many developers struggle to adapt to this new world.Developers want to focus on the development of the logic, not on the code necessary to handle the execution environment where the microservice will be deployed. APIs have always been a productive way to connect services, and this is still true for microservices on Kubernetes (K8s). In this article, we will lay out why you can benefit from an API-first approach for building microservices applications on Kubernetes. Before we can dive into the how, let’s have a quick review of what API-first means and what one commonly refers to services in K8s.
What Does API-First Mean?
This previous DZone article describes what API-first means: you first start designing and implementing an API that can be consumed by other microservices before you actually start implementing the actual microservice itself. Along with the API design itself, you typically provide mocks and documentation for an API. Those artifacts are then used to facilitate discussions with other teams that will be consumers of the microservices that your team is planning to build. In other words, the approach allows you to validate your API design before investing too much in writing the actual microservice.However, an API-first approach is not just useful during the development phase. Once a microservice has been built, other teams who want to consume the microservice will benefit from the documentation and mocking capabilities. The good news is that there are plenty of tools available that support an API-first approach. The most common specifications to support an API-first approach are OpenAPI and API Blueprint.You can then use tools like Swagger or Apiary to design your API, generate mocks, documentation, and even client libraries.
All of this becomes particularly useful for applications that require independence and loose coupling, such as microservices applications, as it helps teams be more productive when it comes to consuming services built by other teams. But, how does this approach translate to a modern microservice architecture that relies on an orchestrator, such as Kubernetes, to handle the deployment and execution of each microservice? Before explaining this approach, it is worthwhile to recap whatK8s services are.
What Are Services in K8S?
As mentioned earlier, a lot of developers are a bit overwhelmed with all the new concepts they need to learn. For developers who are new to K8s, the concept of a K8s service is very confusing as it does not technically relate to the microservice’s code itself. Below is an example of a K8s service:
apiVersion: v1 kind: Service metadata: name: githubstats labels: app: githubstats spec: ports: - port: 9000 name: http selector: app: githubstats
As you can see, a K8s service has nothing to do with the microservice you develop. In fact, it is just an endpoint with a port number that provides information on how to access your microservice inside a pod.
Under the cover, a K8s service creates a persistent IP address and DNS name entry so that the targeted microservice can always be reached.
K8s uses label selectors to know which pod the service needs to point to, in this example app: githubstats. The microservice you develop is typically packaged inside a container image and deployed to K8s. The example below shows the container image repo/githubstats:0.0.1 as part of a deployment with the label app: githubstats.
apiVersion: apps/v1beta2 # for versions before 1.8.0 use apps/v1beta1 kind: Deployment metadata: name: githubstats spec: replicas: 3 selector: matchLabels: app: githubstats template: metadata: labels: app: githubstats spec: containers: - name: githubstats image: repo/githubstats:0.0.1 ports: - containerPort: 9000
The real advantage of using a K8s service is that they provide a steady endpoint to access the microservice itself, no matter where the scheduler places it inside the cluster. It does not take a lot to see that by just looking at K8s services developers do not get the information they need in order to consume a microservice. Let’s say the githubstats microservice, shown in the previous example, is developed by team A. Now another team, team B, is building a microservice, call it UI service, that is supposed to consume the githubstats microservice.The only information team B gets is the githubstats microservice name and the endpoint information. Whatis completely missing is information on the microservice itself, such as what methods one can call.
Why You Should Use an API-First Approach on K8S
As mentioned in the beginning the big advantage of an API-first approach is that you always start with the API design, create mock services, documentation and client libraries. From a K8s perspective, an API-first approach allows you to up-level the discussion for most of your developers, so that they do not need to understand the inner working of K8s right away. To make this approach useful on K8s you need to somehow bind an API withK8s services. The rest of this article focuses on how you can approach this.
The API-First Workflow
The first step of the process involves creating a “formal” description of the APIs. There are various format and tools that can be used. One, for instance, is Oracle’s Apiary. The Apiary website offers an environment where a team of developers can design and document APIs as shown in Figure 1.
Figure 1 - Apiary UI.
Most of the time these tools are being used during“design time” of the overall workflow, where they are creating the APIs with some additional documentation.As microservices applications are highly dynamic innature, it makes sense to also transfer the API-first part into the “runtime” part of the process when APIs are actually going to be used.
To accomplish that, you need to create a “bind” relationship that makes a K8s service more than just a hostname and TCP Port, as it is currently. By binding APIs to a K8s service, developers can get important information on the service right away without having to go through the extra hoops of finding the documentation for the APIs and write the code to process the request/response based on the schema defined in the APIs. This binding information can be kept in a simple data store with an UI on top of it as shown in Figure 2.
Figure 2 - Simple UI to show which APIs are bound to a K8S service.
The last part of an API-first approach is how to consume the service. Ideally, developers would like to avoid having to implement parsing/ marshaling of the response and add the code that can handle the HTTP calls. A more productive way is to provide a client library for a service, at least for the most common languages. In some more advanced organizations, client libraries can be generated as part of the CI (ContinuousIntegration) process. There are tools such as swagger–codgen that can be used to generate clients based on the specification and make it part of your CI process, or even include the client generation in your custom binding UI.What is missing to achieve a real “API-first” approach in the context of microservice architectures is to include the logic that makes it possible for the generated code to discover, at runtime, where the service is running.Having the ability to determine where the service is running at the time it is needed (when a service is making a call to a remote service) makes the API-First approach a better solution than existing best practices, where some aspects of the discovery phase of the process is hardcoded when the service is deployed.
This article laid out how you can combine an API-first approach with K8s. You can make an API-first approach part of your existing environment if you are willing to put a little effort into the “binding” and code generation experience. The advantage is not only that developers can focus on writing code, while only a few need to understand the inner workings of K8s, but also that you fulfill some of the governance requirements you need to have in place for successful microservices projects, such as proper documentation and correct versions for APIs.