Guide To Micronaut Kubernetes
source link: https://piotrminkowski.wordpress.com/2020/01/07/guide-to-micronaut-kubernetes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Guide To Micronaut Kubernetes
Micronaut provides a library that eases development of applications deployed on Kubernetes or on a local single-node cluster like Minikube. The project Micronaut Kubernetes is relatively new in Micronaut family, its current release version is 1.0.3
. It allows you to integrate Micronaut application with Kubernetes discovery, and use Micronaut Configuration Client to read Kubernetes ConfigMap
and Secret
as a property sources. Additionally it provides health check indicator based on communication with Kubernetes API.
Thanks to that module you can simplify and speed up your Micronaut application deployment on Kubernetes during development. In this article I’m going to show how to use Micronaut Kubernetes together with some other interesting tools to simplify local development with Minikube. The topics covered in this article are:
- Using Skaffold together with Jib Maven Plugin to automatically publish application to Minikube after source code change
- Providing communication between applications using Micronaut HTTP Client basing on Kubernetes
Endpoints
name - Enabling Kubernetes
ConfigMap
andSecret
as Micronaut Property Sources - Using application health check
- Integrating application with MongoDB running on Minikube
Example
The source code with examples is as usual available on GitHub: https://github.com/piomin/sample-micronaut-kubernetes.git. Here’s the architecture of our example system consisting of three microservices built on top of Micronaut Framework.
Using Skaffold and Jit
Development with Minikube may is a little bit complicated in comparison to standard approach when you are testing application locally without running it on the platform. First you need to build your application from source code, then build its Docker image and finally redeploy application on Kubernetes using the newest image. Skaffold performs all these steps automatically for you. The only thing you need to do is to install it on machine and enable it for your maven project using command skaffold init
. The command skaffold init
just creates a file skaffold.yaml
in the root of project. Of course, you can create such a manifest by yourself, especially if you would like to use Skaffold together with Jib. Here’s my skaffold.yaml
manifest. We set name of Docker image, tagging policy to Git commit id and also enabled Jib.
apiVersion: skaffold/v2alpha1
kind: Config
build:
artifacts:
- image: piomin/employee
jib: {}
tagPolicy:
gitCommit: {}
Why we need to use Jib? By default, Skaffold bases on Dockerfile, so each change will be published to Kubernetes only after JAR file change. With Jib it is watching for changes in the source code and first automatically rebuild your Maven projects.
<
plugin
>
<
groupId
>com.google.cloud.tools</
groupId
>
<
artifactId
>jib-maven-plugin</
artifactId
>
<
version
>1.8.0</
version
>
</
plugin
>
Now you just need to run command skaffold dev
on selected Maven project, and your application will be automatically deployed to Kubernetes on every change in the source code. Additionally, Skaffold may apply Kubernetes manifest file if it is located in k8s
directory.
Implementation
Let’s begin from implementation. Each of our application uses MongoDB as a backend store. We are using synchronous Java client for integration with MongoDB. Micronaut comes with project micronaut-mongo-reactive
that provides auto-configuration for both reactive and non-reactive drivers.
<
dependency
>
<
groupId
>io.micronaut.configuration</
groupId
>
<
artifactId
>micronaut-mongo-reactive</
artifactId
>
</
dependency
>
<
dependency
>
<
groupId
>org.mongodb</
groupId
>
<
artifactId
>mongo-java-driver</
artifactId
>
</
dependency
>
It is based on mongodb.uri
property and allows you to inject preconfigured MongoClient
bean. Then, we use MongoClient
for save and find operations. When using it we first need to set current database and collection name. All required parameters uri, database and collection are taken from external configuration.
@Singleton
public
class
EmployeeRepository {
private
MongoClient mongoClient;
@Property
(name =
"mongodb.database"
)
private
String mongodbDatabase;
@Property
(name =
"mongodb.collection"
)
private
String mongodbCollection;
EmployeeRepository(MongoClient mongoClient) {
this
.mongoClient = mongoClient;
}
public
Employee add(Employee employee) {
employee.setId(repository().countDocuments() +
1
);
repository().insertOne(employee);
return
employee;
}
public
Employee findById(Long id) {
return
repository().find().first();
}
public
List<Employee> findAll() {
final
List<Employee> employees =
new
ArrayList<>();
repository()
.find()
.iterator()
.forEachRemaining(employees::add);
return
employees;
}
public
List<Employee> findByDepartment(Long departmentId) {
final
List<Employee> employees =
new
ArrayList<>();
repository()
.find(Filters.eq(
"departmentId"
, departmentId))
.iterator()
.forEachRemaining(employees::add);
return
employees;
}
public
List<Employee> findByOrganization(Long organizationId) {
final
List<Employee> employees =
new
ArrayList<>();
repository()
.find(Filters.eq(
"organizationId"
, organizationId))
.iterator()
.forEachRemaining(employees::add);
return
employees;
}
private
MongoCollection<Employee> repository() {
return
mongoClient.getDatabase(mongodbDatabase).getCollection(mongodbCollection, Employee.
class
);
}
}
Each application expose REST endpoints for CRUD operations. Here’s controller implementation for employee-service.
@Controller
(
"/employees"
)
public
class
EmployeeController {
private
static
final
Logger LOGGER = LoggerFactory.getLogger(EmployeeController.
class
);
@Inject
EmployeeRepository repository;
@Post
public
Employee add(
@Body
Employee employee) {
LOGGER.info(
"Employee add: {}"
, employee);
return
repository.add(employee);
}
@Get
(
"/{id}"
)
public
Employee findById(Long id) {
LOGGER.info(
"Employee find: id={}"
, id);
return
repository.findById(id);
}
@Get
public
List<Employee> findAll() {
LOGGER.info(
"Employees find"
);
return
repository.findAll();
}
@Get
(
"/department/{departmentId}"
)
public
List<Employee> findByDepartment(Long departmentId) {
LOGGER.info(
"Employees find: departmentId={}"
, departmentId);
return
repository.findByDepartment(departmentId);
}
@Get
(
"/organization/{organizationId}"
)
public
List<Employee> findByOrganization(Long organizationId) {
LOGGER.info(
"Employees find: organizationId={}"
, organizationId);
return
repository.findByOrganization(organizationId);
}
}
We may use Micronaut declarative HTTP client for communication with REST endpoints. We just need to create interface annotated with @Client
that declares calling methods.
@Client
(id =
"employee"
, path =
"/employees"
)
public
interface
EmployeeClient {
@Get
(
"/department/{departmentId}"
)
List<Employee> findByDepartment(Long departmentId);
}
It allows you to integrate Micronaut HTTP Clients with Kubernetes discovery in order to use the name of Kubernetes Endpoints as a service id. Then the client is injected into the controller. In the following code you may see the implementation of controller in the department-service that uses EmployeeClient
.
@Controller
(
"/departments"
)
public
class
DepartmentController {
private
static
final
Logger LOGGER = LoggerFactory.getLogger(DepartmentController.
class
);
private
DepartmentRepository repository;
private
EmployeeClient employeeClient;
DepartmentController(DepartmentRepository repository, EmployeeClient employeeClient) {
this
.repository = repository;
this
.employeeClient = employeeClient;
}
@Post
public
Department add(
@Body
Department department) {
LOGGER.info(
"Department add: {}"
, department);
return
repository.add(department);
}
@Get
(
"/{id}"
)
public
Department findById(Long id) {
LOGGER.info(
"Department find: id={}"
, id);
return
repository.findById(id);
}
@Get
public
List<Department> findAll() {
LOGGER.info(
"Department find"
);
return
repository.findAll();
}
@Get
(
"/organization/{organizationId}"
)
public
List<Department> findByOrganization(Long organizationId) {
LOGGER.info(
"Department find: organizationId={}"
, organizationId);
return
repository.findByOrganization(organizationId);
}
@Get
(
"/organization/{organizationId}/with-employees"
)
public
List<Department> findByOrganizationWithEmployees(Long organizationId) {
LOGGER.info(
"Department find: organizationId={}"
, organizationId);
List<Department> departments = repository.findByOrganization(organizationId);
departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
return
departments;
}
}
Discovery with Micronaut Kubernetes
Using serviceId for communication with Micronaut HTTP Client requires integration with service discovery. Since, we are running our applications on Kubernetes we are going to use its service registry. Here comes Micronaut Kubernetes. It integrates Micronaut application and Kubernetes discovery via Endpoints
object. First, let’s add the required dependency.
<
dependency
>
<
groupId
>io.micronaut.kubernetes</
groupId
>
<
artifactId
>micronaut-kubernetes-discovery-client</
artifactId
>
</
dependency
>
In fact we don’t have to do anything else, because after adding the required dependency integration with Kubernetes discovery is enabled. We may proceed to the deployment. In Kubernetes Service
definition the field metadata.name
should be the same as field id
inside @Client
annotation.
apiVersion: v1
kind: Service
metadata:
name: employee
labels:
app: employee
spec:
ports:
- port:
8080
protocol: TCP
selector:
app: employee
type: NodePort
Here’s YAML deployment manifest for Service
employee. The container is exposed on port 8080
and uses latest tag of image piomin/employee
, which is set in Skaffold manifest.
apiVersion: apps/v1
kind: Deployment
metadata:
name: employee
labels:
app: employee
spec:
replicas:
1
selector:
matchLabels:
app: employee
template:
metadata:
labels:
app: employee
spec:
containers:
- name: employee
image: piomin/employee
ports:
- containerPort:
8080
We can also increase log level for Kubernetes API client calls and for the whole Micronaut Kubernetes project to DEBUG
. Here’s the fragment of our logback.xml
.
<
logger
name
=
"io.micronaut.http.client"
level
=
"DEBUG"
/>
<
logger
name
=
"io.micronaut.kubernetes"
level
=
"DEBUG"
/>
Micronaut Kubernetes Discovery additionally allows us to filter the list of registered services. We may define the list of included or excluded services using property kubernetes.client.discovery.includes
or kubernetes.client.discovery.excludes
. Assuming we have many services registered in the same namespace, this feature may be applicable. Here’s the list of services registered in the default
namespace after deploying all our sample microservices and MongoDB.
Since one of our application department-service is communicating only with employee-service we may reduce the list of discovered services only to employee
.
kubernetes:
client:
discovery:
includes:
- employee
Configuration Client
The Configuration client is reading Kubernetes ConfigMaps
and Secrets
, and make them available as PropertySources
for your application. Since configuration parsing happens in the bootstrap phase, we need to define the following property in bootstrap.yml
in order to enable distributed configuration clients.
micronaut:
application:
name: employee
config-client:
enabled:
true
By default, the configuration client is reading all the ConfigMaps
and Secrets
for the configured namespace. You can filter the list of config map names by defining kubernetes.client.config-maps.includes
or kubernetes.client.config-maps.excludes
. Alternatively we may use Kubernetes labels, which give us more flexibility. This configuration also needs to be provided in bootstrap phase. Reading Secrets
is disabled by default. Therefore, we also need to enable it. Here’s the configuration for department-service, which is similar for all other apps.
kubernetes:
client:
config-maps:
labels:
- app: department
secrets:
enabled:
true
labels:
- app: department
Kubernetes ConfigMap
and Secret
also need to be labeled with app=department
.
apiVersion: v1
kind: ConfigMap
metadata:
name: department
labels:
app: department
data:
application.yaml: |-
mongodb:
collection: department
database: admin
kubernetes:
client:
discovery:
includes:
- employee
Here’s Secret
definition for department-service. We configure there mongodb.uri
property, which contains sensitive data like username or password. It is used by MongoClient
for establishing connection with server.
apiVersion: v1
kind: Secret
metadata:
name: department
labels:
app: department
type: Opaque
data:
mongodb.uri: bW9uZ29kYjovL21pY3JvbmF1dDptaWNyb25hdXRfMTIzQG1vbmdvZGI6MjcwMTcvYWRtaW4=
Running sample applications
Before running any application in default
namespace we need to set the appropriate permissions. Micronaut Kubernetes requires read access to pods, endpoints, secrets, services and config maps. For development needs we may set the highest level of permissions by creating ClusterRoleBinding
pointing to cluster-admin
role.
$ kubectl create clusterrolebinding admin --clusterrole=cluster-admin --serviceaccount=default:default
One of useful Skaffold features is an ability to print standard output of started container to a console. Thanks to that you don’t have to execute command kubectl logs
on a pod. Let’s take a closer look on the logs during application startup. After increasing a level of logging we may find here some interesting informations, for example client calls od Kubernetes API. As you see on the screen below our application tries to find ConfigMap
and Secret
with label departament following configuration provided in bootstrap.yaml
.
Let’s add some test data to our database by calling endpoints exposed by our applications running on Kubernetes. Each of them is exposed outside node thanks to NodePort
service type.
$ curl http:
//192
.168.99.100:32356
/employees
-d
'{"name":"John Smith","age":30,"position":"director","departmentId":2,"organizationId":2}'
-H
"Content-Type: application/json"
{
"id"
:1,
"organizationId"
:2,
"departmentId"
:2,
"name"
:
"John Smith"
,
"age"
:30,
"position"
:
"director"
}
$ curl http:
//192
.168.99.100:32356
/employees
-d
'{"name":"Paul Walker","age":50,"position":"director","departmentId":2,"organizationId":2}'
-H
"Content-Type: application/json"
{
"id"
:2,
"organizationId"
:2,
"departmentId"
:2,
"name"
:
"Paul Walker"
,
"age"
:50,
"position"
:
"director"
}
$ curl http:
//192
.168.99.100:31144
/departments
-d
'{"name":"Test2","organizationId":2}'
-H
"Content-Type: application/json"
{
"id"
:2,
"organizationId"
:2,
"name"
:
"Test2"
}
Now, we can test HTTP communication between department-service and employee by calling method GET /organization/{organizationId}/with-employees
that finds all departments with employees belonging to a given organization.
$ curl http:
//192
.168.99.100:31144
/departments/organization/2/with-employees
Here’s the current list of endpoints registered in the namespace default
.
Let’s take a look on the Micronaut HTTP Client logs from department-service. As you see below when it tries to call endpoint GET /employees/department/{departmentId}
it finds the container under IP 172.17.0.11
.
Health checks
To enable health checks for Micronaut application we first need to add the following dependency to Maven pom.xml
.
<
dependency
>
<
groupId
>io.micronaut</
groupId
>
<
artifactId
>micronaut-management</
artifactId
>
</
dependency
>
Micronaut configuration module provides a health check that probes communication with the Kubernetes API, and shows some information about the pod and application. To enable detailed view for unauthenticated user we need to set the following property.
endpoints:
health:
details-visible: ANONYMOUS
After that we can take an advantage of quite detailed information about application including MongoDB connection status or HTTP Client status as shown below. By default, a health check is available under path /health
.
Conclusion
Micronaut Kubernetes integrates with Kubernetes API in order to allow application to read components responsible discovery and configuration. Integration between Micronaut HTTP Client and Kubernetes Endpoints
or between Micronaut Configuration Client and Kubernetes ConfigMap
or Secret
are useful features. I’m looking to some other interesting features which may be included to Micronaut Kubernetes, since it is relatively new project within Micronaut.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK