Key Takeaways
- Micronaut provides seamless integration with several distributed tracing solutions, such as Zipkin and Jaeger
- Several security solutions are provided "out-of-the-box" with the framework, such as JWT-based authentications.
- Micronaut provides features such as “Token Propagation” to ease secure communication between microservices.
- Thanks to its low memory footprint, Micronaut is capable of running in Function as a Service (FaaS) serverless environments.
In the first article within this series, we developed and deployed three microservices with the JVM-based Micronaut framework. In this second tutorial article we are going to add several features to our app: distributed tracing, security via JWT and a serverless function. Moreover, we will discuss the user input validation capabilities offered by Micronaut.
Distributed tracing
Breaking our system up into smaller, fine-grained microservices results in multiple benefits, but it also adds complexity when it comes to monitoring the system in production.
You should assume that your networks are plagued with malevolent entities ready to unleash their ire on a whim. ― Sam Newman, Building Microservices
Micronaut integrates natively with Jaeger and Zipkin -- the top open-source distributed tracing solutions.
Zipkin is a distributed tracing system. It helps gather timing data needed to troubleshoot latency problems in microservice architectures. It manages both the collection and lookup of this data.
An easy way to start Zipkin is via Docker:
$ docker run -d -p 9411:9411 openzipkin/zipkin
The app is composed of three microservices. ( gateway, inventory, books)
which we developed in the first article.
You will need to do these changes to all three microservices.
Modify build.gradle
to add tracing dependency:
build.gradle
compile "io.micronaut:micronaut-tracing"
Add the following dependencies to build.gradle to send tracing spans to Zipkin.
build.gradle
runtime 'io.zipkin.brave:brave-instrumentation-http'
runtime 'io.zipkin.reporter2:zipkin-reporter'
compile 'io.opentracing.brave:brave-opentracing'
Configure tracing:
src/main/resources/application.yml
tracing:
zipkin:
http:
url: http://localhost:9411
enabled: true
sampler:
probability: 1
Setting tracing.zipkin.sample.probability=1
means we want to trace 100% of requests. In production, you probably would want set a lower percentage.
Disable tracing in tests:
src/test/resources/application-test.yml
tracing:
zipkin:
enabled: false
That is it. With minimum configuration changes you are able to integrate distributed tracing into Micronaut.
Running the app
Let us run the app and see the distributed tracing integration action. In the first article, we integrated Consul for service discovery into our app. Because of this, you need to start both Zipkin and Consul before starting the microservices. When we start the microservices, they will register themselves at Consul service discovery. When we engage them with a request, they will send spans to Zipkin.
To start the microservices, Gradle has a handy flag (-parallel) for that:
./gradlew -parallel run
You can run a cURL command to engage the three microservices:
$ curl http://localhost:8080/api/books
[{"isbn":"1680502395","name":"Release It!","stock":3},
{"isbn":"1491950358","name":"Building Microservices","stock":2}]
You can then navigate to http://localhost:9411
to access the Zipkin UI.
Security via JWT
Micronaut ships with several security options out of the box. You can configure basic authentication, session based authentication, JWT authentication, Ldap authentication etc. JSON Web Token (JWT) is an open, industry standard RFC 7519 method for representing claims securely between two parties.
Micronaut ships out-of-the-box with capabilities to generate, sign and/or encrypt, and verify JWT tokens.
We are going to integrate JWT authentication into our app.
Changes in gateway
to support JWT
The gateway microservice will be responsible for generating and propagating JWT tokens.
Modify build.gradle
to add micronaut-security-jwt
dependency to each microservice ( gateway, inventory
and books
):
gateway/build.gradle
compile "io.micronaut:micronaut-security-jwt"
annotationProcessor "io.micronaut:micronaut-security"
Modify application.yml
:
gateway/src/main/resources/application.yml
micronaut:
application:
name: gateway
server:
port: 8080
security:
enabled: true
endpoints:
login:
enabled: true
oauth:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne
writer:
header:
enabled: true
propagation:
enabled: true
service-id-regex: "books|inventory"
We have made several important configuration changes which are worth discussing:
micronaut.security.enable=true
turns on security and secures every endpoint by default.micronaut.security.endpoints.login.enable=true
enables the/login
endpoint which we will use shortly to authenticate.micronaut.security.endpoints.oauth.enable=true
enables a /oauth/access_tokenendpoint which we could use to obtain a new JWT access token once the issued token expires.micronaut.security.jwt.enable=true
enables JWT capabilities.- We configure our app to issue signed JWTs with a secret configuration. Please check the JWT token Generation documentation to learn about the different signing and encrypting options at your disposal.
micronaut.security.token.propagation.enabled=true
means we are turning on Token Propagation. This is a feature which simplifies working with JWT or other token security mechanism in a microservices architecture. Please, read Token Propagation tutorial to learn more.micronaut.security.writer.header.enabled
enables a token writer which will write the JWT tokens transparently for the developer in a HTTP header.micronaut.security.token.propagation.service-id-regex
sets a regular expression which matches the services targeted for token propagation. We are matching the other two services in the app.
With Micronaut, you can use @Secured
annotation to configure access at Controller or Controller's Action level.
Annotate BookController.java
with @Secured("isAuthenticated()"
). It permits access only to authenticated users. Remember to annotate with
@Secured("isAuthenticated()")
also the BookController
class of both inventory and books microservices.
The exposed /login
endpoint, when invoked, attempts to authenticate the user against any available AuthenticationProvider
bean. To keep things simple, we are going to allow access to two users sherlock and watson ; an homage to Sir Arthur Conan Doyle characters. Create a SampleAuthenticationProvider
:
gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.java
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.security.authentication.AuthenticationFailed;
import io.micronaut.security.authentication.AuthenticationProvider;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
@Requires(notEnv = Environment.TEST)
@Singleton
public class SampleAuthenticationProvider implements AuthenticationProvider {
@Override
public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
if (authenticationRequest.getIdentity() == null) {
return Flowable.just(new AuthenticationFailed());
}
if (authenticationRequest.getSecret() == null) {
return Flowable.just(new AuthenticationFailed());
}
if (Arrays.asList("sherlock", "watson").contains(authenticationRequest.getIdentity().toString()) && authenticationRequest.getSecret().equals("elementary")) {
return Flowable.just(new UserDetails(authenticationRequest.getIdentity().toString(), new ArrayList<>()));
}
return Flowable.just(new AuthenticationFailed());
}
}
Changes in inventory
and books
to support JWT
For services inventory
and books
, in addition to adding a micronaut-security-jwt
dependency and annotating the controller with @Secured
, we need to modify application.yml
to create a configuration which enables us to validate the JWT tokens generated and signed at the gateway microservice.
Modify application.yml
:
inventory/src/main/resources/application.yml
micronaut:
application:
name: inventory
server:
port: 8081
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
validation:
secret: pleaseChangeThisSecretForANewOne
Note we use the same secret as we did in the gateway
configuration so that we can validate the JWT tokens signed by the gateway
microservice.
Running the secured app
Once you have Zipkin and Consul running you can start the three microservices in parallel. Gradle has a handy flag (-parallel) for that:
./gradlew -parallel run
You can run a cURL command and you receive a 401. Unauthorized!
$ curl -I http://localhost:8080/api/books HTTP/1.1 401 Unauthorized
Date: Mon, 1 Oct 2018 18:44:54 GMT transfer-encoding: chunked connection: close
We need to login first and obtain a valid JWT access token:
$ curl -X "POST" "http://localhost:8080/login" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{ "username": "sherlock", "password": "password" }'
{"username":"sherlock","access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWI iOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYX Rld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiOjE1Mzg0MTI0MDl9.1W4CXbN1bJgM CQlCDKJtm7zHWzyZeIr1rHpTuDy6h0","refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ zaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2 F5IiwiaWF0IjoxNTM4NDEyNDA5fQ.l72msZKwHmYeLs7T0vKtRxu7_DZr62rPCILNmC 7UEZ4","expires_in":3600,"token_type":"Bearer"}
Micronaut supports RFC 6750 Bearer Token specification out-of-the-box. We can invoke the /api/books
endpoint supplying the JWT we received in the /login response via the HTTP Authorization header.
curl "http://localhost:8080/api/books" \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOS wicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiO jE1Mzg0MTI0MDl9.1W4CXbN1bJgMCQlCDKJtm7zHWz-yZeIr1rHpTuDy6h0'
[{"isbn":"1680502395","name":"Release It!","stock":3}, {"isbn":"1491950358","name":"Building Microservices","stock":2}]
Serverless
We are going to add a function deployed to AWS Lambda to validate the books' ISBN codes.
mn create-function example.micronaut.isbn-validator
Note: We used the create-function command included in the Micronaut CLI.
Validation
We are going to create a Singleton to deal with ISBN 10 validation.
Create an interface to encapsulate the operation:
package example.micronaut;
import javax.validation.constraints.Pattern;
public interface IsbnValidator {
boolean isValid(@Pattern(regexp = "\\d{10}") String isbn);
}
The previous code listing contains a javax.validation.constraint
.
Micronaut's validation is built on with the standard framework – JSR 380, also known as Bean Validation 2.0.
Hibernate Validator is a reference implementation of the validation API.
Add the next snippet to build.gradle
isbn-validator/build.gradle
compile "io.micronaut.configuration:micronaut-hibernatevalidator"
Create a Singleton which implements IsbnValidator.java
isbn-validator/src/main/java/example/micronaut/DefaultIsbnValidator.java
package example.micronaut;
import io.micronaut.validation.Validated;
import javax.inject.Singleton;
import javax.validation.constraints.Pattern;
@Singleton
@Validated
public class DefaultIsbnValidator implements IsbnValidator {
/**
* must range from 0 to 10 (the symbol X is used for 10), and must be such that the sum of all the ten digits, each multiplied by its (integer) weight, descending from 10 to 1, is a multiple of 11.
* @param isbn 10 Digit ISBN
* @return whether the ISBN is valid or not.
*/
@Override
public boolean isValid(@Pattern(regexp = "\\d{10}") String isbn) {
char[] digits = isbn.toCharArray();
int accumulator = 0;
int multiplier = 10;
for (int i = 0; i < digits.length; i++) {
char c = digits[i];
accumulator += Character.getNumericValue(c) * multiplier;
multiplier--;
}
return (accumulator % 11 == 0);
}
}
As in the previous code listing, in Micronaut you will add the Validated annotation to any class that requires validation.
Create a test to verify the validation works:
isbn-validator/src/test/java/example/micronaut/IsbnValidatorTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.DefaultApplicationContext;
import io.micronaut.context.env.Environment;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import javax.validation.ConstraintViolationException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IsbnValidatorTest {
private static ApplicationContext applicationContext;
@BeforeClass
public static void setupContext() {
applicationContext = new DefaultApplicationContext(Environment.TEST).start();
}
@AfterClass
public static void stopContext() {
if (applicationContext!=null) {
applicationContext.stop();
}
}
@Rule public ExpectedException thrown = ExpectedException.none();
@Test public void testTenDigitValidation() {
thrown.expect(ConstraintViolationException.class);
IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);
isbnValidator.isValid("01234567891");
}
@Test
public void testControlDigitValidationWorks() {
IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);
assertTrue(isbnValidator.isValid("1491950358"));
assertTrue(isbnValidator.isValid("1680502395"));
assertFalse(isbnValidator.isValid("0000502395"));
}
}
The previous code listing verifies that if we attempt to invoke the method with a eleven digits string a javax.validation.ConstraintViolationException
is thrown.
Function input and output
The function will accept a single argument (a ValidationRequest
which is a POJO encapsulating a ISBN number)
isbn-validator/src/main/java/example/micronaut/IsbnValidationRequest.java
package example.micronaut;
public class IsbnValidationRequest {
private String isbn;
public IsbnValidationRequest() {
}
public IsbnValidationRequest(String isbn) {
this.isbn = isbn;
}
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
}
And returns a single result ( ValidationResponse
a POJO encapsulating a ISBN Number and a Boolean flag indicating whether the ISBN is valid).
isbn-validator/src/main/java/example/micronaut/IsbnValidationResponse.java
package example.micronaut;
public class IsbnValidationResponse {
private String isbn;
private Boolean valid;
public IsbnValidationResponse() {
}
public IsbnValidationResponse(String isbn, boolean valid) {
this.isbn = isbn;
this.valid = valid;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public Boolean getValid() {
return valid;
}
public void setValid(Boolean valid) {
this.valid = valid;
}
}
Function Testing
When we run create-function
command, the Micronaut command line interface created a class located at src/main/java/example/micronaut/IsbnValidatorFunction
. Modify it and implement java.util.Function
to accommodate to the input and output described in the previous section.
isbn-validator/src/main/java/example/micronaut/IsbnValidatorFunction.java
package example.micronaut;
import io.micronaut.function.FunctionBean;
import java.util.function.Function;
import javax.validation.ConstraintViolationException;
@FunctionBean("isbn-validator")
public class IsbnValidatorFunction implements Function<IsbnValidationRequest, IsbnValidationResponse> {
private final IsbnValidator isbnValidator;
public IsbnValidatorFunction(IsbnValidator isbnValidator) {
this.isbnValidator = isbnValidator;
}
@Override
public IsbnValidationResponse apply(IsbnValidationRequest req) {
try {
return new IsbnValidationResponse(req.getIsbn(), isbnValidator.isValid(req.getIsbn()));
} catch(ConstraintViolationException e) {
return new IsbnValidationResponse(req.getIsbn(),false);
}
}
}
The previous code listing shows several things:
- The
@FunctionBean
annotation is used on a method that returns the function. - You can use Micronaut compile-time dependency injection in your functions to. We inject
IsbnValidator
via constructor injection.
Functions can also be run as part of the Micronaut application context for ease of testing. The app includes already the function-web and an HTTP server dependency on the classpath for tests:
isbn-validator/build.gradle
testRuntime "io.micronaut:micronaut-http-server-netty"
testRuntime "io.micronaut:micronaut-function-web"
To consume the function in our tests, we need to modify IsbnValidatorClient.java
isbn-validator/src/test/java/example/micronaut/IsbnValidatorClient.java
package example.micronaut;
import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
import javax.inject.Named;
@FunctionClient
public interface IsbnValidatorClient {
@Named("isbn-validator")
Single<IsbnValidationResponse> isValid(@Body IsbnValidationRequest isbn);
}
Modify also IsbnValidatorFunctionTest.java
. We test for different scenarios (valid ISBN, invalid ISBN, ISBN with more than 10 digits and ISBN with less than 10 digits).
isbn-validator/src/test/java/example/micronaut/IsbnValidatorFunctionTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IsbnValidatorFunctionTest {
@Test public void testFunction() {
EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);
IsbnValidatorClient client = server.getApplicationContext().getBean(IsbnValidatorClient.class);
assertTrue(client.isValid(new IsbnValidationRequest("1491950358")).blockingGet().getValid());
assertTrue(client.isValid(new IsbnValidationRequest("1680502395")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("0000502395")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("01234567891")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("012345678")).blockingGet().getValid());
server.close();
}
}
Deploy to AWS Lambda
We are ready to deploy the function. Assuming you have an Amazon Web Services (AWS) account, you can go to AWS Lambda and create a new function.
Select Java 8 runtime. Name: isbn-validator
and create a new role form template(s). I give the role name lambda_basic_execution
.
Run ./gradlew shadowJar
to generate a fat Jar.
shadowJar
is a gradle task exposed by Gradle ShadowJar plugin.
$ du -h isbn-validator/build/libs/isbn-validator-0.1-all.jar 11M isbn-validator/build/libs/isbn-validator-0.1-all.jar
Upload the JAR and enter as Handler value
io.micronaut.function.aws.MicronautRequestStreamHandler
I allocated just 256Mb and a timeout of 25s.
Consume the function/AWS Lambda from another microservice.
We are going to consume the lambda in the gateway
microservice. Modify build.gradle
at gateway
microservice. Add micronaut-function-client
and
com.amazonaws:aws-java-sdk-lambda dependencies:
build.gradle
compile "io.micronaut:micronaut-function-client"
runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'
Modify src/main/resources/application.yml
and the define a function with the same name isbn-validator
as the one we deployed to AWS Lambda:
src/main/resources/application.yml
aws:
lambda:
functions:
vat:
functionName: isbn-validator
qualifer: isbn
region: eu-west-3 # Paris Region
Create an interface to abstract the collaboration with the function:
src/main/java/example/micronaut/IsbnValidator.java
package example.micronaut;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
public interface IsbnValidator {
Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req);
}
Create a @FunctionClient
src/main/java/example/micronaut/FunctionIsbnValidator.java
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
import javax.inject.Named;
@FunctionClient
@Requires(notEnv = Environment.TEST)
public interface FunctionIsbnValidator extends IsbnValidator {
@Override
@Named("isbn-validator")
Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req);
}
Several things about the previous code worth mentioning:
- The
FunctionClient
annotation allows applying introduction advice to an interface such that methods defined by the interface become invokers of remote functions configured by the application. - Use the function name isbn-validator as we did in application.yml .
The last step is to modify the gateway BookController
to call the function.
src/main/java/example/micronaut/BooksController.java
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.reactivex.Flowable;
import java.util.List;
@Secured("isAuthenticated()")
@Controller("/api")
public class BooksController {
private final BooksFetcher booksFetcher;
private final InventoryFetcher inventoryFetcher;
private final IsbnValidator isbnValidator;
public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher, IsbnValidator isbnValidator) {
this.booksFetcher = booksFetcher;
this.inventoryFetcher = inventoryFetcher;
this.isbnValidator = isbnValidator;
}
@Get("/books")
Flowable<Book> findAll() {
return booksFetcher.fetchBooks()
.flatMapMaybe(b -> isbnValidator.validateIsbn(new IsbnValidationRequest(b.getIsbn()))
.filter(IsbnValidationResponse::getValid)
.map(isbnValidationResponse -> b)
)
.flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn())
.filter(stock -> stock > 0)
.map(stock -> {
b.setStock(stock);
return b;
})
);
}
}
As you can see in the previous code we are injection a bean conforming to IsbnValidator
via constructor injection. Using the remote function is transparent for the programmer.
Conclusion
The image below illustrates the application after we developed through these articles:
- We have three microservices (a Java, Groovy and a Kotlin Microservice).
- Those microservices use Consul for service discovery.
- Those microservices use Zipkin as a distributed tracing service.
- We have added a fourth microservice, a function deployed to AWS Lambda
- Communications between microservices are secured. Every request allowed through the network includes a JWT token in the Authorization Http header. JWT tokens are propagated automatically through the internal requests.
Please, visit the website to learn more about Micronaut.
About the Author
Sergio del AmoCaballero is a developer specialized in the development of mobile phone apps ( iOS, Android) powered by Grails / Micronaut backends. Since 2015, Sergio del Amo writes a newsletter, Groovy Calamari, around the Groovy Ecosystem and Microservices. Groovy, Grails, Micronaut, Gradle,...