BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Micronaut Tutorial: Part 2: Easy Distributed Tracing, JWT Security and AWS Lambda Deployment

Micronaut Tutorial: Part 2: Easy Distributed Tracing, JWT Security and AWS Lambda Deployment

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,...

Rate this Article

Adoption
Style

BT