diff --git a/assets/img/news/get-started-microservices-on-kubernetes/simple-microservice-client-part3-1.png b/assets/img/news/get-started-microservices-on-kubernetes/simple-microservice-client-part3-1.png new file mode 100644 index 00000000..ea84c130 Binary files /dev/null and b/assets/img/news/get-started-microservices-on-kubernetes/simple-microservice-client-part3-1.png differ diff --git a/guides/get-started-microservices-on-kubernetes.adoc b/guides/get-started-microservices-on-kubernetes.adoc index e4def41c..e487995a 100644 --- a/guides/get-started-microservices-on-kubernetes.adoc +++ b/guides/get-started-microservices-on-kubernetes.adoc @@ -18,14 +18,22 @@ We will start building a link:https://docs.docker.com/[Docker Image, window="_bl === Guides in this series -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-part1[{simple-microservice-part1}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-part2[{simple-microservice-part2}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-database-part1[{simple-microservice-database-part1}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-database-part2[{simple-microservice-database-part2}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-infinispan-part1[{simple-microservice-infinispan-part1}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-infinispan-part2[{simple-microservice-infinispan-part2}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-jms-part1[{simple-microservice-jms-part1}] -* link:/guides/get-started-microservices-on-kubernetes/simple-microservice-jms-part2[{simple-microservice-jms-part2}] +* **{simple-microservice-header}** +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-part1[{simple-microservice-part1}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-part2[{simple-microservice-part2}] +* **{simple-microservice-database-header}** +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-database-part1[{simple-microservice-database-part1}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-database-part2[{simple-microservice-database-part2}] +* **{simple-microservice-infinispan-header}** +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-infinispan-part1[{simple-microservice-infinispan-part1}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-infinispan-part2[{simple-microservice-infinispan-part2}] +* **{simple-microservice-jms-header}** +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-jms-part1[{simple-microservice-jms-part1}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-jms-part2[{simple-microservice-jms-part2}] +* **{simple-microservice-client-header}** +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part1[{simple-microservice-client-part1}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part2[{simple-microservice-client-part2}] +** link:/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part3[{simple-microservice-client-part3}] //* link:get-enterprise-ready[{get-enterprise-ready}] [[references]] diff --git a/guides/get-started-microservices-on-kubernetes/_includes/_constants.adoc b/guides/get-started-microservices-on-kubernetes/_includes/_constants.adoc index 92b883f6..d1da0262 100644 --- a/guides/get-started-microservices-on-kubernetes/_includes/_constants.adoc +++ b/guides/get-started-microservices-on-kubernetes/_includes/_constants.adoc @@ -2,14 +2,14 @@ :jaxrs-example-project-groupId: org.wildfly.examples :jaxrs-example-project-artifactId: jaxrs :jaxrs-example-project-version: 11.0.0.Final-SNAPSHOT -:version-bootable-jar: 11.0.0.Beta1 -:version-wildfly-galleon-pack: 32.0.0.Final +:version-wildfly: 34.0.1.Final +:version-wildfly-galleon-pack: 34.0.1.Final :jakarta-jakartaee-api-version: 10.0.0 :version-junit-jupiter-api: 5.10.2 :version-arquillian-junit5-container: 1.8.0.Final :version-org-wildfly-arquillian-wildfly-arquillian: 5.1.0.Beta1 :version-resteasy-client: 6.2.7.Final -:version-wildfly-cloud-galleon-pack: 7.0.0.Final +:version-wildfly-cloud-galleon-pack: 7.0.2.Final :version-wildfly-maven-plugin: 5.0.0.Final :my-jaxrs-app-docker-image-name: my-jaxrs-app :my-jaxrs-app-db-docker-image-name: my-jaxrs-app-db @@ -46,3 +46,19 @@ :artemis-console-port-name: artemis-console-port :my-jms-app-docker-image-name: my-jms-app :podman-network-name: demo-network +:my-jaxrs-app-docker-image-name-client: my-jaxrs-app-client +:my-jaxrs-app-docker-image-name-server: my-jaxrs-app-server +:simple-microservice-client-secured: simple-microservice-client-secured +:simple-microservice-server-secured: simple-microservice-server-secured +:keycloak-external: keycloak-external +:keycloak-internal: keycloak-internal +:keycloak-realm: keycloak-realm +:keycloak-data-import: keycloak-data-import +:keycloak-admin-user: admin +:keycloak-admin-pws: admin +:keycloak-user1: alice +:keycloak-user1-pws: 123 +:keycloak-user2: bob +:keycloak-user2-pws: 123 +:keycloak-role1: user +:keycloak-role2: admin diff --git a/guides/get-started-microservices-on-kubernetes/_includes/_titles.adoc b/guides/get-started-microservices-on-kubernetes/_includes/_titles.adoc index 5504ff2f..1bab34bc 100644 --- a/guides/get-started-microservices-on-kubernetes/_includes/_titles.adoc +++ b/guides/get-started-microservices-on-kubernetes/_includes/_titles.adoc @@ -1,9 +1,17 @@ +:simple-microservice-header: WildFly Java Microservice :simple-microservice-part1: WildFly Java Microservice - PART 1: Docker Image :simple-microservice-part2: WildFly Java Microservice - PART 2: Kubernetes +:simple-microservice-database-header: Connecting to a DB :simple-microservice-database-part1: Connecting to a DB - PART 1: Docker Image :simple-microservice-database-part2: Connecting to a DB - PART 2: Kubernetes +:simple-microservice-jms-header: Using a Message Broker :simple-microservice-jms-part1: Using a Message Broker - PART 1: Docker Image :simple-microservice-jms-part2: Using a Message Broker - PART 2: Kubernetes +:simple-microservice-infinispan-header: Using Infinispan remote cache :simple-microservice-infinispan-part1: Using Infinispan remote cache - PART 1: Docker Image :simple-microservice-infinispan-part2: Using Infinispan remote cache - PART 2: Kubernetes +:simple-microservice-client-header: Invoke one Microservices from another +:simple-microservice-client-part1: Invoke one Microservices from another - PART 1: Docker Images +:simple-microservice-client-part2: Invoke one Microservices from another - PART 2: Kubernetes +:simple-microservice-client-part3: Invoke one Microservices from another - PART 3: Propagate Authentication :get-enterprise-ready: Get “Enterprise” ready \ No newline at end of file diff --git a/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part1.adoc b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part1.adoc new file mode 100644 index 00000000..20272a42 --- /dev/null +++ b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part1.adoc @@ -0,0 +1,290 @@ += {simple-microservice-client-part1} +:summary: Invoke one microservice from another +:includedir: ../_includes +include::{includedir}/_attributes.adoc[] +include::./_includes/_titles.adoc[] +include::_includes/_constants.adoc[] +// you can override any attributes eg to lengthen the +// time to complete the guide +:prerequisites-time: 10 + +In this guide, you will learn HOW-TO invoke one microservice from another; + +[[prerequisites]] +== Prerequisites + +To complete this guide, you need: + +* Complete link:simple-microservice-part1[{simple-microservice-part1}] + +== Introduction + +This guide is the first in a series of three: + +1. In link:simple-microservice-client-part1[{simple-microservice-client-part1}] (this guide), we explain HOW-TO invoke one microservice from another; +2. In link:simple-microservice-client-part2[{simple-microservice-client-part2}], as usual, we explain HOW-TO run the whole thing on Kubernetes +3. In link:simple-microservice-client-part3[{simple-microservice-client-part3}] we explain HOW-TO propagate user authentication and authorization data from the calling microservice to the invoked microservice; this is most useful in a scenario where you have a "chain" of microservices ("**A -> B -> C -> etc.**") and you want the user's authentication and authorization data to be propagated from one microservice to the next; + +== This Guide + +In these guides, we work with a simple invocation chain composed by +++two+++ microservices: + +* **Microservice A**: acting as **client** +* **Microservice B**: acting as **server** + +Our invocation chain is then: "**Microservice A -> Microservice B**": when working with https://microprofile.io/[Microprofile], this is achieved by using the https://github.com/eclipse/microprofile-rest-client[microprofile-rest-client]; + +Specifically, **Microservice A** will use the https://github.com/eclipse/microprofile-rest-client[microprofile-rest-client] to invoke the Jakarta REST service exposed by **Microservice B**; + +For both services, we will start from the microservice we built in link:simple-microservice-part1[{simple-microservice-part1}] (complete code in {source-code-git-repository}/simple-microservice); + +== Microservice B - the server + +We start from the server because we need the server's API for the client later on; + +=== Maven Project + +Copy {source-code-git-repository}/simple-microservice into a new folder named *simple-microservice-server* and: + +* remove folder *src/test* +* remove all test scope dependencies + +NOTE: we remove tests because, since we are going to introduce service to service invocation, they wouldn't be much useful anymore + +==== pom.xml + +Update the `artifactId` to `simple-microservice-server`; + +NOTE: **Microservice B** is basically unchanged, we will modify it in link:simple-microservice-client-part3[{simple-microservice-client-part3}] + +==== Build the application + +[source,bash] +---- +mvn clean package +---- + +=== Docker Image + +==== Dockerfile + +Since you copied {source-code-git-repository}/simple-microservice[simple-microservice], the Dockerfile from link:https://github.com/wildfly/wildfly-s2i/blob/main/examples/docker-build/Dockerfile[examples/docker-build/Dockerfile, window="_blank"] should already be at the root of your project; + +==== Build the Docker Image + +[source,bash,subs="normal"] +---- +podman build -t {my-jaxrs-app-docker-image-name-server}:latest . +---- + +==== Run the Docker Image + +First we create a network for our containers: + +[source,bash,subs="normal"] +---- +podman network create {podman-network-name} +---- + +Then we run our container using this network: + +[source,bash,subs="normal"] +---- +podman run --rm -p 8180:8080 -p 10090:9990 \ + --network={podman-network-name} \ + --name={my-jaxrs-app-docker-image-name-server} \ + {my-jaxrs-app-docker-image-name-server} +---- + +== Microservice A - the client + +=== Maven Project + +Copy {source-code-git-repository}/simple-microservice into a new folder named *simple-microservice-client* and: + +* remove folder *src/test* +* remove all test scope dependencies + +NOTE: we remove tests because, since we are going to introduce service to service invocation, they wouldn't be much useful anymore + +==== pom.xml + +Set `simple-microservice-client`; + +Add the following to `dependencyManagement`: + +[source,xml,subs="normal"] +---- + + org.wildfly.bom + wildfly-microprofile + ${version.wildfly.bom} + pom + import + +---- + +Add the following to `dependencies`: + +[source,xml,subs="normal"] +---- + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + provided + + + org.eclipse.microprofile.config + microprofile-config-api + provided + +---- + +Add the following `layers` in the `wildfly-maven-plugin`: + +[source,xml,subs="normal"] +---- + microprofile-config + microprofile-rest-client +---- + +Later on, we will use: + +* **microprofile-config** to make the URL to **Microservice B** configurable +* **microprofile-rest-client** to actually invoke **Microservice B** + +==== microprofile-config.properties + +As anticipated, we use **microprofile-config** to make the URL to **Microservice B** configurable; + +Add file `src/main/resources/META-INF/microprofile-config.properties` with the following content: + +.microprofile-config.properties: +[source,properties] +---- +simple-microservice-server/mp-rest/uri=${simple-microservice-server-uri:http://127.0.0.1:8080} +simple-microservice-server/mp-rest/connectTimeout=3000 +---- + +NOTE: `simple-microservice-server-uri` would pick up its value, whenever set, from the environment variable named `SIMPLE_MICROSERVICE_SERVER_URI` (see https://download.eclipse.org/microprofile/microprofile-config-3.0/microprofile-config-spec-3.0.html#default_configsources.env.mapping[env.mapping]) + +==== Java code + +Add the following interface: + +.GettingStartedEndpointInterface.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey="simple-microservice-server") +@Path("/hello") +public interface GettingStartedEndpointInterface { + @GET + @Path("/{name}") + @Produces(MediaType.TEXT_PLAIN) + public Response sayHello(final @PathParam("name") String name); +} +---- + +NOTE: this class is used to define the API to be invoked by the Rest Client; the actual URL where the remote service is +located, comes from the `microprofile-config.properties` file we just added; + +Remove the `src/main/java/org/wildfly/examples/GettingStartedService.java` file and replace the content of +`src/main/java/org/wildfly/examples/GettingStartedEndpoint.java` with the following: + +.GettingStartedEndpoint.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/") +public class GettingStartedEndpoint { + + @Inject + @RestClient + private GettingStartedEndpointInterface service; + + @GET + @Path("/{name}") + @Produces(MediaType.TEXT_PLAIN) + public Response sayHello(final @PathParam("name") String name) { + return service.sayHello(name); + } +} +---- + +NOTE: as anticipated, we use **microprofile-rest-client** to actually invoke **Microservice B** + +==== Build the application + +[source,bash] +---- +mvn clean package +---- + +=== Docker Image + +==== Dockerfile + +Since you copied {source-code-git-repository}/simple-microservice[simple-microservice], the Dockerfile from link:https://github.com/wildfly/wildfly-s2i/blob/main/examples/docker-build/Dockerfile[examples/docker-build/Dockerfile, window="_blank"] should already be at the root of your project; + +==== Build the Docker Image `{my-jaxrs-app-docker-image-name-client}:latest` with the following command: + +[source,bash,subs="normal"] +---- +podman build -t {my-jaxrs-app-docker-image-name-client}:latest . +---- + +==== Run the Docker Image + +[source,bash,subs="normal"] +---- +podman run --rm -p 8080:8080 -p 9990:9990 \ + --network={podman-network-name} \ + --env "SIMPLE_MICROSERVICE_SERVER_URI=http://{my-jaxrs-app-docker-image-name-server}:8080/hello" \ + --name={my-jaxrs-app-docker-image-name-client} \ + {my-jaxrs-app-docker-image-name-client} +---- + +NOTE: The **{my-jaxrs-app-docker-image-name-server}** container can be reached, inside the **{podman-network-name}** network, using the DNS name **{my-jaxrs-app-docker-image-name-server}** + +== Test + +Open http://localhost:8080[http://localhost:8080] in your browser: this web page is served by the **{my-jaxrs-app-docker-image-name-client}** container; + +Write something in the "Name" input box and then press "Say Hello": the response you'll see will come from **{my-jaxrs-app-docker-image-name-server}** container! + +The complete invocation chain is "**web browser** -> **{my-jaxrs-app-docker-image-name-client}** -> **{my-jaxrs-app-docker-image-name-server}**" + +== What's next? + +link:simple-microservice-client-part2[{simple-microservice-client-part2}] + +[[references]] +== References + +* https://microprofile.io/specifications/microprofile-rest-client[microprofile-rest-client] +* https://microprofile.io/specifications/microprofile-config[microprofile-config] +* Source code for this guide: +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-client +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-server + + diff --git a/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part2.adoc b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part2.adoc new file mode 100644 index 00000000..b1f713ef --- /dev/null +++ b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part2.adoc @@ -0,0 +1,293 @@ += {simple-microservice-client-part2} +:summary: Invoke one microservice from another on Kubernetes +:includedir: ../_includes +include::{includedir}/_attributes.adoc[] +include::./_includes/_titles.adoc[] +include::_includes/_constants.adoc[] +// you can override any attributes eg to lengthen the +// time to complete the guide +:prerequisites-time: 10 + +In this guide, you will learn HOW-TO run the Docker Images you built in link:simple-microservice-client-part1[{simple-microservice-client-part1}] on Kubernetes. + +[[prerequisites]] +== Prerequisites + +To complete this guide, you need: + +* Complete link:simple-microservice-client-part1[{simple-microservice-client-part1}] + +== Introduction + +In this guide, we will deploy on Kubernetes the two container images we created in link:simple-microservice-client-part1[{simple-microservice-client-part1}]; + +== Minikube + +You can use whatever Kubernetes cluster you have available; in this guide, and in the following, we will use link:https://minikube.sigs.k8s.io/docs/[minikube, window="_blank"]. + +== Microservice B - the server + +=== Image Registry + +To make the `{my-jaxrs-app-docker-image-name-server}:latest` Docker Image available to Kubernetes, you need to push it to some Image Registry that is accessible by the Kubernetes cluster you want to use. + +==== Quay.io + +There are many Image Registries you can use: in this guide, we will push the `{my-jaxrs-app-docker-image-name-server}:latest` Docker Image, to the link:https://quay.io[quay.io, window="_blank"] Image Registry. + +Create a public repository named `{my-jaxrs-app-docker-image-name-server}` on link:https://quay.io[quay.io, window="_blank"] (e.g. link:https://quay.io/repository/{quay-io-account-name}/my-jaxrs-app[https://quay.io/repository/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-server}, window="_blank"]). + +NOTE: replace `{quay-io-account-name}` with the name of your account in all the commands that will follow + +Tag the Docker image: + +[source,bash,subs="normal"] +---- +podman tag {my-jaxrs-app-docker-image-name-server} quay.io/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-server} +---- + +Push the `{my-jaxrs-app-docker-image-name-server}` Docker Image to it: + +[source,bash,subs="normal"] +---- +podman push quay.io/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-server} +---- + +At this point, the `{my-jaxrs-app-docker-image-name-server}:latest` Docker Image should be publicly available and free to be consumed by any Kubernetes Cluster; + +=== Deploy on Kubernetes + +Create a file named `{my-jaxrs-app-docker-image-name-server}-deployment.yaml`: + +.{my-jaxrs-app-docker-image-name-server}-deployment.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {my-jaxrs-app-docker-image-name-server}-deployment + labels: + app: {my-jaxrs-app-docker-image-name-server} +spec: + replicas: 1 + selector: + matchLabels: + app: {my-jaxrs-app-docker-image-name-server} + template: + metadata: + labels: + app: {my-jaxrs-app-docker-image-name-server} + spec: + containers: + - name: {my-jaxrs-app-docker-image-name-server} + image: quay.io/tborgato/{my-jaxrs-app-docker-image-name-server} + ports: + - containerPort: 8080 + - containerPort: 9990 + livenessProbe: + httpGet: + path: /health/live + port: 9990 + readinessProbe: + httpGet: + path: /health/ready + port: 9990 + startupProbe: + httpGet: + path: /health/started + port: 9990 +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {my-jaxrs-app-docker-image-name-server}-deployment.yaml +---- + +=== Create Kubernetes ClusterIP Service + +We create a service to consume the services exposed by **{my-jaxrs-app-docker-image-name-server}** from inside Kubernetes; + +Create a file named `{my-jaxrs-app-docker-image-name-server}-service.yaml`: + +.{my-jaxrs-app-docker-image-name-server}-service.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: v1 +kind: Service +metadata: + name: {my-jaxrs-app-docker-image-name-server}-service + labels: + app: {my-jaxrs-app-docker-image-name-server} +spec: + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + app: {my-jaxrs-app-docker-image-name-server} + type: ClusterIP +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {my-jaxrs-app-docker-image-name-server}-service.yaml +---- + +=== Check Kubernetes Service + +[source,bash,subs="normal"] +---- +kubectl run --rm -it --tty curl-{my-jaxrs-app-docker-image-name-server} --image=curlimages/curl --restart=Never ‐‐ {my-jaxrs-app-docker-image-name-server}-service:8080/hello/pippo +---- + +== Microservice A - the client + +=== Image Registry + +To make the `{my-jaxrs-app-docker-image-name-client}:latest` Docker Image available to Kubernetes, you need to push it to some Image Registry that is accessible by the Kubernetes cluster you want to use. + +==== Quay.io + +There are many Image Registries you can use: in this guide, we will push the `{my-jaxrs-app-docker-image-name-client}:latest` Docker Image, to the link:https://quay.io[quay.io, window="_blank"] Image Registry. + +Create a public repository named `{my-jaxrs-app-docker-image-name-client}` on link:https://quay.io[quay.io, window="_blank"] (e.g. link:https://quay.io/repository/{quay-io-account-name}/my-jaxrs-app[https://quay.io/repository/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-client}, window="_blank"]). + +NOTE: replace `{quay-io-account-name}` with the name of your account in all the commands that will follow + +Tag the Docker image: + +[source,bash,subs="normal"] +---- +podman tag {my-jaxrs-app-docker-image-name-client} quay.io/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-client} +---- + +Push the `{my-jaxrs-app-docker-image-name-client}` Docker Image to it: + +[source,bash,subs="normal"] +---- +podman push quay.io/{quay-io-account-name}/{my-jaxrs-app-docker-image-name-client} +---- + +At this point, the `{my-jaxrs-app-docker-image-name-client}:latest` Docker Image should be publicly available and free to be consumed by any Kubernetes Cluster; + +=== Deploy on Kubernetes + +Create a file named `{my-jaxrs-app-docker-image-name-client}-deployment.yaml`: + +.{my-jaxrs-app-docker-image-name-client}-deployment.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {my-jaxrs-app-docker-image-name-client}-deployment + labels: + app: {my-jaxrs-app-docker-image-name-client} +spec: + replicas: 1 + selector: + matchLabels: + app: {my-jaxrs-app-docker-image-name-client} + template: + metadata: + labels: + app: {my-jaxrs-app-docker-image-name-client} + spec: + containers: + - name: {my-jaxrs-app-docker-image-name-client} + image: quay.io/tborgato/{my-jaxrs-app-docker-image-name-client} + ports: + - containerPort: 8080 + - containerPort: 9990 + livenessProbe: + httpGet: + path: /health/live + port: 9990 + readinessProbe: + httpGet: + path: /health/ready + port: 9990 + startupProbe: + httpGet: + path: /health/started + port: 9990 + env: + - name: SIMPLE_MICROSERVICE_SERVER_URI + value: "http://{my-jaxrs-app-docker-image-name-server}-service:8080" +---- + +NOTE: The environment variable `SIMPLE_MICROSERVICE_SERVER_URI` allows **{my-jaxrs-app-docker-image-name-client}** to invoke **{my-jaxrs-app-docker-image-name-server}** through the service **{my-jaxrs-app-docker-image-name-server}-service** + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {my-jaxrs-app-docker-image-name-client}-deployment.yaml +---- + +=== Create Kubernetes NodePort Service + +We create a service to consume the services exposed by **{my-jaxrs-app-docker-image-name-client}** from outside Kubernetes; + +Create a file named `{my-jaxrs-app-docker-image-name-client}-service.yaml`: + +.{my-jaxrs-app-docker-image-name-client}-service.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: v1 +kind: Service +metadata: + name: {my-jaxrs-app-docker-image-name-client}-service + labels: + app: {my-jaxrs-app-docker-image-name-client} +spec: + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + app: {my-jaxrs-app-docker-image-name-client} + type: NodePort +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {my-jaxrs-app-docker-image-name-client}-service.yaml +---- + +=== Check your application + +Find out on what IP address/port, link:https://minikube.sigs.k8s.io/docs/[minikube, window="_blank"] is exposing your service: + +[source,bash,subs="normal"] +---- +$ minikube service {my-jaxrs-app-docker-image-name-client}-service --url +http://192.168.39.143:30347 +---- + +Verify it's working as expected: + +[source,bash,subs="normal"] +---- +$ curl http://192.168.39.143:30347/hello/pippo +Hello 'pippo'. +---- + +== What's next? + +link:simple-microservice-client-part3[{simple-microservice-client-part3}] + +[[references]] +== References + +* Source code for this guide: +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-client +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-server \ No newline at end of file diff --git a/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part3.adoc b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part3.adoc new file mode 100644 index 00000000..94d77c09 --- /dev/null +++ b/guides/get-started-microservices-on-kubernetes/simple-microservice-client-part3.adoc @@ -0,0 +1,735 @@ += {simple-microservice-client-part3} +:summary: Invoke one microservice from another on Kubernetes propagating users authentication +:includedir: ../_includes +include::{includedir}/_attributes.adoc[] +include::./_includes/_titles.adoc[] +include::_includes/_constants.adoc[] +// you can override any attributes eg to lengthen the +// time to complete the guide +:prerequisites-time: 10 + +In this guide, you will learn HOW-TO propagate user authentication and authorization data between two microservices; + +[[prerequisites]] +== Prerequisites + +To complete this guide, you need: + +* Complete link:simple-microservice-client-part2[{simple-microservice-client-part2}] + +== Introduction + +The scenario consists of: + +* a *Service* deployed on a Kubernetes cluster which is not exposed outside the cluster +* a *Web Application* deployed on a Kubernetes cluster which is exposed outside the cluster and consumes the *Service* + +The user is required to authenticate before using the *Web Application* and, after authentication happens, we want authentication data to be available, not only to the *Web Application*, but also tho the *Service*; authentication is delegated to *Keycloak* using link:https://www.keycloak.org/securing-apps/oidc-layers[OpenID Connect] protocol; + +The overall architecture is: + +image::get-started-microservices-on-kubernetes/simple-microservice-client-part3-1.png[] + +We will start from the two microservices we built in link:simple-microservice-client-part2[{simple-microservice-client-part2}] and: + +* *Microservice A* will be the basis for *Web Application* +* *Microservice B* will be the basis for *Service* + +=== How it works + +This is how it works: + +1. The user tries to access *Web Application* (*Microservice A*) from a web browser +2. The browser is redirected to *Keycloak* where, by providing username and password, the user authenticates itself +3. The browser is redirected back to *Web Application*: this time the request contains a *JWT Access Token* (and a few more tokens actually) provided by *Keycloak*, containing authentication and authorization data +4. *Web Application* validates the *JWT Access Token* and grants access to the user +5. *Web Application* invokes *Service* (*Microservice B*) forwarding to it the *JWT Access Token* it just received +6. *Service* validates the *JWT Access Token* and grants access to *Web Application* + +== Keycloak + +First, we install and configure Keycloak with users, groups etc.. + +=== Run Keycloak on Kubernetes + +Download link:{source-code-git-repository}/simple-microservice-rest-client/simple-microservice-client-secured/kubernetes/keycloak-realm-realm.json[keycloak-realm-realm.json] and create a `configmap` using it as its content: + +[source,bash,subs="normal"] +---- +kubectl create configmap {keycloak-data-import} --from-file=keycloak-realm-realm.json=keycloak-realm-realm.json +---- + +Create a file named `keycloak.yaml`: + +.keycloak.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: v1 +kind: Service +metadata: + name: {keycloak-external} + labels: + app: keycloak +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: keycloak + type: NodePort +--- +apiVersion: v1 +kind: Service +metadata: + name: {keycloak-internal} + labels: + app: keycloak +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: keycloak + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + volumes: + - name: {keycloak-data-import}-volume + configMap: + name: {keycloak-data-import} + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:26.0 + args: [ "start-dev", "--import-realm" ] + env: + - name: KEYCLOAK_ADMIN + value: "{keycloak-admin-user}" + - name: KEYCLOAK_ADMIN_PASSWORD + value: "{keycloak-admin-pws}" + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /realms/master + port: 8080 + volumeMounts: + - name: {keycloak-data-import}-volume + mountPath: /opt/keycloak/data/import +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f keycloak.yaml +---- + +To access the Keycloak console, find out on what IP address/port, link:https://minikube.sigs.k8s.io/docs/[minikube, window="_blank"] is exposing your **{keycloak-external}** service: + +[source,bash,subs="normal"] +---- +$ minikube service keycloak-external --url +http://192.168.39.190:31950 +---- + +Open the link in your web browser and login to Keycloak with username/password "*{keycloak-admin-user}*/*{keycloak-admin-pws}*"; + +NOTE: since we are using minikube, we expose *Keycloak* outside the cluster with a `NodePort` service (**{keycloak-external}**) and inside the cluster with a `ClusterIP` service (**{keycloak-internal}**) + +==== optional alternative: configure Keycloak manually + +As an alternative to using *Keycloak* auto import feature (see the "--import-realm" command argument above), you can configure *Keycloak* manually: remove `volumes` and `volumeMounts` and follow these steps: + +1. Create a realm called **{keycloak-realm}** +2. Create a client called **{simple-microservice-client-secured}**; in the Capability config, turn on +++Client authentication+++. +3. For the **{simple-microservice-client-secured}** client, we also need to set the valid redirect URIs to ***** and set the Web origins to **+** to permit all origins of Valid Redirect URIs. +4. For the **{simple-microservice-client-secured}** client, note down the +++Client Secret+++ in the +++Credentials+++ tab (e.g. `KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB`); +5. Now, click on Realm roles and create two roles, **{keycloak-role1}** and **{keycloak-role2}**. +6. Create a user called **{keycloak-user1}** and assign her the **{keycloak-role1}** and **{keycloak-role2}** roles; set password **{keycloak-user1-pws}** for **{keycloak-user1}** +7. Create a user called **{keycloak-user2}** and assign him only the **{keycloak-role1}** role; set password **{keycloak-user2-pws}** for **{keycloak-user2}** + +NOTE: in case you want to go deeper, find more information and examples in link:https://wildfly-security.github.io/wildfly-elytron/blog/bearer-only-support-openid-connect/[Setting up your Keycloak OpenID provider] + +== Web Application (Microservice A) + +=== Maven Project + +Copy link:{source-code-git-repository}/simple-microservice-rest-client/simple-microservice-client[simple-microservice-client] +into a new folder named **{simple-microservice-client-secured}**; + +==== pom.xml + +Set `{simple-microservice-client-secured}`; + +Add the following to `dependencies`: + +[source,xml,subs="normal"] +---- + + org.wildfly.security + wildfly-elytron-http-oidc + provided + + + jakarta.servlet + jakarta.servlet-api + provided + +---- + +Add the following `layers` in the `wildfly-maven-plugin`: + +[source,xml,subs="normal"] +---- + elytron-oidc-client +---- + +==== web.xml + +Create file `src/main/webapp/WEB-INF/web.xml` with the following content: + +.src/main/webapp/WEB-INF/web.xml: +[source,xml,subs="normal"] +---- + + + + + + secured + /* + + + {keycloak-role1} + {keycloak-role2} + + + + + OIDC + + + + {keycloak-role1} + + + {keycloak-role2} + + +---- + +==== oidc.json + +Create file `src/main/webapp/WEB-INF/oidc.json` with the following content: + +.src/main/webapp/WEB-INF/oidc.json: +[source,json,subs="normal"] +---- +{ + "client-id" : "{simple-microservice-client-secured}", + "provider-url" : "${env.OIDC_PROVIDER_URL:http://localhost:8080}/realms/{keycloak-realm}", + "ssl-required" : "EXTERNAL", + "credentials" : { + "secret" : "${env.OIDC_CLIENT_SECRET:KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB}" + } +} +---- + +replace `KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB` with the +++Client Secret+++ you previously noted down; + + +=== Java code + +==== GettingStartedEndpointInterface + +Add the following interface: + +.org.wildfly.examples.GettingStartedEndpointInterface.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterClientHeaders +@RegisterRestClient(configKey="simple-microservice-server") +@Path("/hello") +public interface GettingStartedEndpointInterface { + @GET + @Path("/{name}") + @Produces(MediaType.TEXT_PLAIN) + public Response sayHello(@HeaderParam("Authorization") String authorization, final @PathParam("name") String name); +} +---- + +==== GettingStartedEndpoint + +Modify the following class: + +.org.wildfly.examples.GettingStartedEndpoint.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.wildfly.security.http.oidc.OidcSecurityContext; + +import java.io.IOException; + +@Path("/") +public class GettingStartedEndpoint { + + @Context + private HttpServletRequest httpServletRequest; + + @Inject + @RestClient + private GettingStartedEndpointInterface service; + + @GET + @Path("/{name}") + @Produces(MediaType.TEXT_PLAIN) + public Response sayHello(final @PathParam("name") String name) throws IOException { + Response response; + OidcSecurityContext oidcSecurityContext = getOidcSecurityContext(httpServletRequest); + if (oidcSecurityContext != null) { + String authzHeaderValue = "Bearer " + oidcSecurityContext.getTokenString(); + System.out.println("\n\n[JWT] service Token: " + authzHeaderValue + "\n\n"); + return service.sayHello(authzHeaderValue, name); + } else { + System.out.println("\n\n[JWT] No token :(\n\n"); + return service.sayHello(null, name); + } + } + + private OidcSecurityContext getOidcSecurityContext(HttpServletRequest req) { + return (OidcSecurityContext) req.getAttribute(OidcSecurityContext.class.getName()); + } +} +---- + +=== Build and push the image to Quay.io + +Build the application: + +[source,bash] +---- +mvn clean package +---- + +Build the Docker image: + +[source,bash,subs="normal"] +---- +podman build -t {simple-microservice-client-secured}:latest . +---- + +Create a public repository named `{simple-microservice-client-secured}` on link:https://quay.io[quay.io, window="_blank"] (e.g. link:https://quay.io/repository/{quay-io-account-name}/{simple-microservice-client-secured}[https://quay.io/repository/{quay-io-account-name}/{simple-microservice-client-secured}, window="_blank"]). + +NOTE: replace `{quay-io-account-name}` with the name of your account in all the commands that will follow + +Tag the Docker image: + +[source,bash,subs="normal"] +---- +podman tag {simple-microservice-client-secured} quay.io/{quay-io-account-name}/{simple-microservice-client-secured} +---- + +Push the `{simple-microservice-client-secured}` Docker Image: + +[source,bash,subs="normal"] +---- +podman push quay.io/{quay-io-account-name}/{simple-microservice-client-secured} +---- + +=== Deploy to Kubernetes + +Create file `{simple-microservice-client-secured}-deployment.yaml`: + +.{simple-microservice-client-secured}-deployment.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {simple-microservice-client-secured}-deployment + labels: + app: {simple-microservice-client-secured} +spec: + replicas: 1 + selector: + matchLabels: + app: {simple-microservice-client-secured} + template: + metadata: + labels: + app: {simple-microservice-client-secured} + spec: + containers: + - name: {simple-microservice-client-secured} + image: quay.io/{quay-io-account-name}/{simple-microservice-client-secured} + ports: + - containerPort: 8080 + - containerPort: 9990 + livenessProbe: + httpGet: + path: /health/live + port: 9990 + readinessProbe: + httpGet: + path: /health/ready + port: 9990 + startupProbe: + httpGet: + path: /health/started + port: 9990 + env: + - name: SIMPLE-MICROSERVICE-SERVER_MP_REST_URI + value: "http://{simple-microservice-server-secured}-service:8080" + - name: OIDC_PROVIDER_URL + # replace with the outcome of "minikube service keycloak-external --url" + value: "http://192.168.39.190:31950" + - name: OIDC_CLIENT_SECRET + value: "KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB" +---- + +Then: + +* replace "http://192.168.39.190:31950" with the outcome of command `minikube service keycloak-external --url` +* replace "{quay-io-account-name}" with your account name on link:quay.io[quay.io] +* replace `KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB` with the +++Client Secret+++ you previously noted down; + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {simple-microservice-client-secured}-deployment.yaml +---- + +Create file `{simple-microservice-client-secured}-service.yaml`: + +.{simple-microservice-client-secured}-service.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: v1 +kind: Service +metadata: + name: {simple-microservice-client-secured}-service + labels: + app: {simple-microservice-client-secured} +spec: + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + app: {simple-microservice-client-secured} + type: NodePort +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {simple-microservice-client-secured}-service.yaml +---- + +== Service (Microservice B) + +=== Maven Project + +Copy link:{source-code-git-repository}/simple-microservice-rest-client/simple-microservice-server[simple-microservice-server] +into a new folder named **simple-microservice-server-secured**; + +==== pom.xml + +Set `simple-microservice-server-secured`; + +Add the following to `dependencyManagement`: + +[source,xml,subs="normal"] +---- + + org.wildfly.bom + wildfly-microprofile + ${version.wildfly.bom} + pom + import + +---- + +Add the following to `dependencies`: + +[source,xml,subs="normal"] +---- + + org.eclipse.microprofile.config + microprofile-config-api + provided + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + provided + +---- + +Add the following `layers` in the `wildfly-maven-plugin`: + +[source,xml,subs="normal"] +---- + microprofile-config + microprofile-jwt +---- + +=== microprofile-config.properties + +Add file `src/main/resources/META-INF/microprofile-config.properties` with the following content: + +.microprofile-config.properties: +[source,properties] +---- +mp.jwt.verify.publickey.location=http://localhost:8080/realms/keycloak-realm/protocol/openid-connect/certs +---- + +=== Java code + +==== GettingStartedApplication + +Modify the following class: + +.org.wildfly.examples.GettingStartedApplication.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; +import org.eclipse.microprofile.auth.LoginConfig; + +@LoginConfig(authMethod="MP-JWT") +@ApplicationPath("/hello") +public class GettingStartedApplication extends Application { + +} +---- + +==== GettingStartedEndpoint + +Modify the following class: + +.org.wildfly.examples.GettingStartedEndpoint.java: +[source,java] +---- +package org.wildfly.examples; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; + +@Path("/") +public class GettingStartedEndpoint { + + @Inject + @ConfigProperty(name = "mp.jwt.verify.publickey.location") + private String publicKeyLocation; + + @Inject + JsonWebToken jwt; + + @GET + @Path("/{name}") + @Produces(MediaType.TEXT_PLAIN) + @RolesAllowed({"admin"}) + public Response sayHello(final @PathParam("name") String name) { + System.out.println("mp.jwt.verify.publickey.location=" + publicKeyLocation); + + String response = + "Hello " + name + + (jwt != null ? (" Subject:" + jwt.getSubject()) : null) + + (jwt != null ? (" Issuer: " + jwt.getIssuer()) : null); + + return Response.ok(response).build(); + } +} +---- + +=== Build and push the image to Quay.io + +Create a public repository named `{simple-microservice-server-secured}` on link:https://quay.io[quay.io, window="_blank"] (e.g. link:https://quay.io/repository/{quay-io-account-name}/my-jaxrs-app[https://quay.io/repository/{quay-io-account-name}/{simple-microservice-server-secured}, window="_blank"]). + +NOTE: replace `{quay-io-account-name}` with the name of your account in all the commands that will follow + +Tag the Docker image: + +[source,bash,subs="normal"] +---- +podman tag {simple-microservice-server-secured} quay.io/{quay-io-account-name}/{simple-microservice-server-secured} +---- + +Push the `{simple-microservice-server-secured}` Docker Image: + +[source,bash,subs="normal"] +---- +podman push quay.io/{quay-io-account-name}/{simple-microservice-server-secured} +---- + +=== Deploy to Kubernetes + +Create file `{simple-microservice-server-secured}-deployment.yaml`: + +.{simple-microservice-server-secured}-deployment.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {simple-microservice-server-secured}-deployment + labels: + app: {simple-microservice-server-secured} +spec: + replicas: 1 + selector: + matchLabels: + app: {simple-microservice-server-secured} + template: + metadata: + labels: + app: {simple-microservice-server-secured} + spec: + containers: + - name: {simple-microservice-server-secured} + image: quay.io/tborgato/{simple-microservice-server-secured} + ports: + - containerPort: 8080 + - containerPort: 9990 + livenessProbe: + httpGet: + path: /health/live + port: 9990 + readinessProbe: + httpGet: + path: /health/ready + port: 9990 + startupProbe: + httpGet: + path: /health/started + port: 9990 + env: + - name: MP_JWT_VERIFY_PUBLICKEY_LOCATION + value: "http://keycloak-internal:8080/realms/{keycloak-realm}/protocol/openid-connect/certs" +---- + +Then: + +* replace "{quay-io-account-name}" with your account name on link:quay.io[quay.io] + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {simple-microservice-server-secured}-deployment.yaml +---- + +Create file `{simple-microservice-server-secured}-service.yaml`: + +.{simple-microservice-server-secured}-service.yaml: +[source,yaml,subs="normal"] +---- +apiVersion: v1 +kind: Service +metadata: + name: {simple-microservice-server-secured}-service + labels: + app: {simple-microservice-server-secured} +spec: + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + app: {simple-microservice-server-secured} + type: ClusterIP +---- + +Deploy to your Kubernetes Cluster: + +[source,bash,subs="normal"] +---- +kubectl apply -f {simple-microservice-server-secured}-service.yaml +---- + +== Test + +[source,bash,subs="normal"] +---- +$ minikube service {simple-microservice-client-secured}-service --url +http://192.168.39.190:32225 +---- + +Open that URL in your browser, log in as *{keycloak-user1}*/*{keycloak-user1-pws}* and try it out! + +After pressing the "Say Hello" button, you should see something like: + +[source,text] +---- +Hello ddd Subject:aaef43ee-4005-4d2d-a5f0-0e0d11a1f831 Issuer: http://192.168.39.190:31950/realms/keycloak-realm +---- + + +[[references]] +== References + +* https://wildfly-security.github.io/wildfly-elytron/blog/bearer-only-support-openid-connect/ +* https://github.com/wildfly-security-incubator/elytron-examples/tree/main/oidc-with-bearer +* https://www.keycloak.org/getting-started/getting-started-kube +* Source code for this guide: +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-client-secured +** {source-code-git-repository}/simple-microservice-rest-client/simple-microservice-server-secured \ No newline at end of file