Skip to content

Commit

Permalink
Improve usability of gRPC probes in Kubernetes
Browse files Browse the repository at this point in the history
Plus, I'm adding a new guide about the usage of gRPC services in Kubernetes.
Relates to quarkusio#33219 (comment)
  • Loading branch information
Sgitario committed May 17, 2023
1 parent a75acb1 commit e47a6a3
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/deploying-to-kubernetes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,7 @@ quarkus.kubernetes.env-vars.foo.configmap=myconfigmap
`quarkus.kubernetes.env-vars` are deprecated (though still currently supported as of this writing) and the new declaration style should be used instead.
See <<#env-vars>> and more specifically <<env-vars-backwards>> for more details.

[[deployment]]
== Deployment

To trigger building and deploying a container image you need to enable the `quarkus.kubernetes.deploy` flag (the flag is disabled by default - furthermore it has no effect during test runs or dev mode).
Expand Down
81 changes: 81 additions & 0 deletions docs/src/main/asciidoc/grpc-kubernetes.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
////
This guide is maintained in the main Quarkus repository
and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
////
= Deploying your gRPC Service in Kubernetes
include::_attributes.adoc[]
:categories: serialization
:summary: This guide explains how to deploy your gRPC services in Quarkus to Kubernetes.

This page explains how to deploy your gRPC service in Quarkus in Kubernetes.
We'll continue with the example from xref:grpc-getting-started.adoc[the Getting Started gRPC guide].

== Configuring your project to use the Quarkus Kubernetes extension

Edit the `pom.xml` file to add the Quarkus Kubernetes extension dependency (just under `<dependencies>`):

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kubernetes</artifactId>
</dependency>
----

Next, we want to expose our application using the Kubernetes Ingress resource:

[source,properties]
----
quarkus.kubernetes.ingress.expose=true
----

By default, the Quarkus Kubernetes will expose the HTTP port with name `http` which is bound to the Quarkus HTTP server, not the gRPC service. To expose the gRPC server instead, set the `quarkus.kubernetes.ingress.target-port=grpc` property in your application.properties:

[source,properties]
----
quarkus.kubernetes.ingress.target-port=grpc
----

Finally, we need to generate the Kubernetes manifests by running the Maven command in a terminal:

[source,shell]
----
$ mvn clean package
----

Once generated, you can look at the `target/kubernetes` directory:

[source,txt]
----
target/kubernetes
└── kubernetes.json
└── kubernetes.yml
----

You can find more information about how to deploy the application in Kubernetes in the xref:deploying-to-kubernetes.adoc#deployment[the Kubernetes guide].

== Using gRPC Health probes

By default, the Kubernetes resources do not contain readiness and liveness probes. To add them, edit the `pom.xml` to add the Smallrye Health extension:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
----

TIP: More information about the health extension can be found in xref:microprofile-health.adoc[the Microprofile Health guide].

By the default, this extension will configure the probes to use the HTTP server (which is provided by some extensions like the Quarkus RESTEasy reactive extension). Internally, this probe will also use xref:grpc-service-implementation.adoc#health[the generated gRPC Health services].

If your application does not use any Quarkus extension that exposes an HTTP server, you can still configure the probes to directly use the gRPC Health service by adding the property `quarkus.kubernetes.readiness-probe.grpc-action-enabled=true` into your configuration:

[source,properties]
----
quarkus.kubernetes.readiness-probe.grpc-action-enabled=true
----
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/grpc-service-implementation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public class StreamingService implements Streaming {
}
----

[[health]]
== Health Check
For the implemented services, Quarkus gRPC exposes health information in the following format:
[source,protobuf]
Expand Down
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/grpc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ Quarkus gRPC is based on https://vertx.io/docs/vertx-grpc/java/[Vert.x gRPC].
* xref:grpc-getting-started.adoc[Getting Started]
* xref:grpc-service-implementation.adoc[Implementing a gRPC Service]
* xref:grpc-service-consumption.adoc[Consuming a gRPC Service]
* xref:grpc-kubernetes.adoc[Deploying your gRPC Service in Kubernetes]
* xref:grpc-xds.adoc[Enabling xDS gRPC support]
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ private static ContainerBuilder convert(String name, ContainerConfig c) {
c.command.ifPresent(w -> b.withCommand(w.toArray(new String[0])));
c.arguments.ifPresent(w -> b.withArguments(w.toArray(new String[0])));
if (c.readinessProbe != null && c.readinessProbe.hasUserSuppliedAction()) {
b.withReadinessProbe(ProbeConverter.convert(c.readinessProbe));
b.withReadinessProbe(ProbeConverter.convert(name, c.readinessProbe));
}
if (c.livenessProbe != null && c.livenessProbe.hasUserSuppliedAction()) {
b.withLivenessProbe(ProbeConverter.convert(c.livenessProbe));
b.withLivenessProbe(ProbeConverter.convert(name, c.livenessProbe));
}
b.addAllToEnvVars(c.convertToEnvs());
c.ports.entrySet().forEach(e -> b.addToPorts(PortConverter.convert(e)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,10 +985,11 @@ private static Optional<DecoratorBuildItem> createLivenessProbe(String name, Str
Optional<KubernetesHealthLivenessPathBuildItem> livenessPath) {
if (livenessProbe.hasUserSuppliedAction()) {
return Optional.of(
new DecoratorBuildItem(target, new AddLivenessProbeDecorator(name, ProbeConverter.convert(livenessProbe))));
new DecoratorBuildItem(target,
new AddLivenessProbeDecorator(name, ProbeConverter.convert(name, livenessProbe))));
} else if (livenessPath.isPresent()) {
return Optional.of(new DecoratorBuildItem(target, new AddLivenessProbeDecorator(name,
ProbeConverter.builder(livenessProbe).withHttpActionPath(livenessPath.get().getPath()).build())));
ProbeConverter.builder(name, livenessProbe).withHttpActionPath(livenessPath.get().getPath()).build())));
}
return Optional.empty();
}
Expand All @@ -997,10 +998,10 @@ private static Optional<DecoratorBuildItem> createReadinessProbe(String name, St
Optional<KubernetesHealthReadinessPathBuildItem> readinessPath) {
if (readinessProbe.hasUserSuppliedAction()) {
return Optional.of(new DecoratorBuildItem(target,
new AddReadinessProbeDecorator(name, ProbeConverter.convert(readinessProbe))));
new AddReadinessProbeDecorator(name, ProbeConverter.convert(name, readinessProbe))));
} else if (readinessPath.isPresent()) {
return Optional.of(new DecoratorBuildItem(target, new AddReadinessProbeDecorator(name,
ProbeConverter.builder(readinessProbe).withHttpActionPath(readinessPath.get().getPath()).build())));
ProbeConverter.builder(name, readinessProbe).withHttpActionPath(readinessPath.get().getPath()).build())));
}
return Optional.empty();
}
Expand All @@ -1009,10 +1010,10 @@ private static Optional<DecoratorBuildItem> createStartupProbe(String name, Stri
Optional<KubernetesHealthStartupPathBuildItem> startupPath) {
if (startupProbe.hasUserSuppliedAction()) {
return Optional.of(new DecoratorBuildItem(target,
new AddStartupProbeDecorator(name, ProbeConverter.convert(startupProbe))));
new AddStartupProbeDecorator(name, ProbeConverter.convert(name, startupProbe))));
} else if (startupPath.isPresent()) {
return Optional.of(new DecoratorBuildItem(target, new AddStartupProbeDecorator(name,
ProbeConverter.builder(startupProbe).withHttpActionPath(startupPath.get().getPath()).build())));
ProbeConverter.builder(name, startupProbe).withHttpActionPath(startupPath.get().getPath()).build())));
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public class ProbeConfig {
@ConfigItem
Optional<String> grpcAction;

/**
* If enabled and `grpc-action` is not provided, it will use the generated service name and the gRPC port.
*/
@ConfigItem(defaultValue = "false")
boolean grpcActionEnabled;

/**
* The amount of time to wait before starting to probe.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@

package io.quarkus.kubernetes.deployment;

import org.eclipse.microprofile.config.ConfigProvider;

import io.dekorate.kubernetes.config.Probe;
import io.dekorate.kubernetes.config.ProbeBuilder;

public class ProbeConverter {

public static Probe convert(ProbeConfig probe) {
return builder(probe).build();
public static Probe convert(String name, ProbeConfig probe) {
return builder(name, probe).build();
}

public static ProbeBuilder builder(ProbeConfig probe) {
public static ProbeBuilder builder(String name, ProbeConfig probe) {
ProbeBuilder b = new ProbeBuilder();
probe.httpActionPath.ifPresent(b::withHttpActionPath);
probe.execAction.ifPresent(b::withExecAction);
probe.tcpSocketAction.ifPresent(b::withTcpSocketAction);
probe.grpcAction.ifPresent(b::withGrpcAction);
if (probe.grpcAction.isPresent()) {
b.withGrpcAction(probe.grpcAction.get());
} else if (probe.grpcActionEnabled) {
b.withGrpcAction(getQuarkusGrpcPort() + ":" + name);
}

b.withInitialDelaySeconds((int) probe.initialDelay.getSeconds());
b.withPeriodSeconds((int) probe.period.getSeconds());
b.withTimeoutSeconds((int) probe.timeout.getSeconds());
b.withSuccessThreshold(probe.successThreshold);
b.withFailureThreshold(probe.failureThreshold);
return b;
}

private static int getQuarkusGrpcPort() {
return ConfigProvider.getConfig().getOptionalValue("quarkus.grpc.server.port", Integer.class)
.orElse(9000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.it.kubernetes;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.ProdBuildResults;
import io.quarkus.test.ProdModeTestResults;
import io.quarkus.test.QuarkusProdModeTest;

public class KubernetesWithGrpcProbeEnabledTest {

private static final String APP_NAME = "kubernetes-with-grpc-probe";

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class))
.setApplicationName(APP_NAME)
.setApplicationVersion("0.1-SNAPSHOT")
.overrideConfigKey("quarkus.kubernetes.readiness-probe.grpc-action-enabled", "true")
.setLogFileName("k8s.log")
.setForcedDependencies(List.of(
Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()),
Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion())));

@ProdBuildResults
private ProdModeTestResults prodModeTestResults;

@Test
public void assertGeneratedResources() throws IOException {

final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes");
assertThat(kubernetesDir)
.isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json"))
.isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml"));
List<HasMetadata> kubernetesList = DeserializationUtil
.deserializeAsList(kubernetesDir.resolve("kubernetes.yml"));
assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> {
assertThat(d.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(APP_NAME);
});

assertThat(d.getSpec()).satisfies(deploymentSpec -> {
assertThat(deploymentSpec.getTemplate()).satisfies(t -> {
assertThat(t.getSpec()).satisfies(podSpec -> {
assertThat(podSpec.getContainers()).singleElement()
.satisfies(container -> {
assertThat(container.getReadinessProbe()).isNotNull().satisfies(p -> {
assertEquals(p.getGrpc().getPort().intValue(), 9000);
assertEquals(p.getGrpc().getService(), APP_NAME);
});
});
});
});
});
});
}
}

0 comments on commit e47a6a3

Please sign in to comment.