Category: Docker

Scenario

Let’s say you built spring-boot executable jar and uploaded it into nexus repository. To deploy your spring-boot application you create a Dockerfile that downloads your jar using nexus rest search and download API. For example:

FROM openjdk:8-jre-alpine

MAINTAINER itzap <mailer@itzap.com>

WORKDIR /

RUN wget -O app-service.jar  \
	'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=des'

EXPOSE 8080

ENTRYPOINT [ "java", "-jar","app-service.jar"]

When you run docker container you see the following error message:

no main manifest attribute, in app-service.jar

Diagnostic

When you build and run jar locally everything is working fine. app-service.jar inside docker container appears to be corrupted. To diagnose the issue run the wget command from the terminal:

wget -O app-service.jar  \
	'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=des'

Notice the following results on the command line:

$ wget -O app-service.jar 'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=desc'

--2019-10-21 21:48:27--  https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=desc

Распознаётся nexus.compnyUrl.com (nexus.companyUrl.com)… 00.00.00.00

Подключение к nexus.compnyUrl.com (nexus.compnyUrl.com)|00.00.00.00|:443... соединение установлено.

HTTP-запрос отправлен. Ожидание ответа… 302 Found

Адрес: https://nexus.compnyUrl.com/nexus/repository/company-snapshots/com/mycompany/service/app-service/0.0.1-SNAPSHOT/app-service-0.0.1-20191021.220127-1-javadoc.jar [переход]

--2019-10-21 21:48:28--  https://nexus.compnyUrl.com/nexus/repository/app-snapshots/com/mycompany/service/app-service/0.0.1-SNAPSHOT/app-service-0.0.1-20191021.220127-1-javadoc.jar

Повторное использование соединения с nexus.compnyUrl.com:443.

HTTP-запрос отправлен. Ожидание ответа… 200 OK

Длина: 247463 (242K) [application/java-archive]

Сохранение в: «app-service.jar»

Do not get all these messages in Russian fool you. This is not a Russian hacker attack. Pay attention to what is actually being downloaded. app-service-0.0.1-20191021.220127-1-javadoc.jar javadoc!!!

Solution

Solution to this issue can be found in sonatype Search API. Looking through documentation I came across this query parameter maven.classifier This issue happens when your repository contains javadoc jar alnog side your artifact. &sort=version&direction=desc parameters are also critical to insure you are downloading the latest version, but not enough to uniquely identify artifact. Final URL that works looks like this:

'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=desc&maven.classifier'

Vision Statement

Create a very lightweight email micro-service that will be self-contained, easy to deploy and integrate with. Micro-service could not use any out of the box email servers or email as a service solutions.

Use Case

Suppose, you have an application that needs to send user emails to update a forgotten password or to invite new users to the system. The email capability of your application should be designed as a shared component that can be later extended to send text messages. Email templates should be consistent across different applications and maintained in one place. Email server configuration can be complex, so it needs to be isolated and easily reproducible.

Design Considerations

Spring boot or no spring boot, that is the question. For the itzap-message micro-service, I choose Jersey with hk2 as dependency injection framework. It turns out, that it is quite easy to build executable Jar micro-service with embedded Tomcat without spring/spring-boot. As a result, executable Jar is a lot smaller, faster to load, and deploy. Another component of the system is an email server. To solve email server configuration problems I looked for a Docker container that provides an email server. The Docker container that I found was boky/postfix . Now, to build the application, I can use docker-compose to put everything together.

Implementation Details

Jersey, Tomcat, and HK2

To start building micro-service using Jersey, Tomcat, and HK2 drop theses dependencies in your pom.xml file.

<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-core</artifactId>
  <version>${tomcat.version}</version>
</dependency>

<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-jasper</artifactId>
  <version>${tomcat.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-jasper</artifactId>
  <version>${tomcat.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-jasper-el</artifactId>
  <version>${tomcat.version}</version>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
  <version>${jersey2.version}</version>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
  <version>${jersey2.version}</version>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
  <version>${jersey2.version}</version>
</dependency>

<dependency>
  <groupId>org.glassfish.jersey.bundles</groupId>
  <artifactId>jaxrs-ri</artifactId>
  <version>${jersey2.version}</version>
</dependency>

<dependency>
  <groupId>org.glassfish.hk2</groupId>
  <artifactId>hk2-extras</artifactId>
  <version>${hk2.version}</version>
</dependency>

Application Class

The main application class looks like this:

public class EmailApplication
{
  private static final Logger LOGGER = LoggerFactory.getLogger(EmailApplication.class);

  public static void main(String[] args) {
    try {
      start();
    } catch (Exception e) {
      LOGGER.error("ITZap Email application failed to run", e);
    }
  }

  private static void start() throws Exception {

    String contextPath = "";
    String appBase = ".";
    String port = System.getenv("PORT");
    if (port == null || port.isEmpty()) {
      port = "8025";
    }

    Tomcat tomcat = new Tomcat();
    tomcat.setPort(Integer.parseInt(port));
    tomcat.getHost().setAppBase(appBase);

    Context context = tomcat.addWebapp(contextPath, appBase);

    Tomcat.addServlet(context, "jersey-container-servlet",
                      new ServletContainer(resourceConfig()));
    context.addServletMappingDecoded(UDecoder.URLDecode("/v1/itzap/message/*", Charset.defaultCharset()),
                                     "jersey-container-servlet");

    // initialize Tomcat. (Since version 9.xx need to add tomcat.getConnector() here)
    tomcat.getConnector();
    tomcat.start();
    tomcat.getServer().await();
  }

  private static ResourceConfig resourceConfig() {
    return new JerseyConfig();
  }
}

HK2 Dependency Injection

I drop all factories as static nested classes in one Factories class for convenience.

public final class Factories {
  private static final Config CONFIG = ConfigFactory.load();

  private Factories() {}

  public static class MailServiceFactory extends AbstractFactory<EMailService> {
    public MailServiceFactory() {
    }

    @Override
    public EMailService provide() {
      return new EMailService(CONFIG);
    }
  }
}

Put it all together.

public class JerseyConfig extends ResourceConfig {
  JerseyConfig() {
    register(JacksonFeature.class);

    // register endpoints
    packages("com.itzap.message.rest");

    register(new ContainerLifecycleListener()
             {
               public void onStartup(Container container)
               {
                 // access the ServiceLocator here
                 // serviceLocator = container.getApplicationHandler().
                 ServiceLocator serviceLocator = ((ImmediateHk2InjectionManager)container
                                                  .getApplicationHandler().getInjectionManager()).getServiceLocator();
                 enableTopicDistribution(serviceLocator);
                 // ... do what you need with ServiceLocator ...
               }

               public void onReload(Container container) {/*...*/}
               public void onShutdown(Container container) {/*...*/}
             });

    register(new AbstractBinder() {
      @Override
      protected void configure() {
        // hk2 bindings
        bindFactory(Factories.MailServiceFactory.class)
          .to(EMailService.class)
          .in(Singleton.class);
      }
    });
  }
}

More Jersey

One of the features of Jersey that I like is @Provider mechanism. Here is an example of how to centralize error handling in the Jersey application.

@Provider
public class IZapExceptionMapper implements ExceptionMapper<IZapException> {
  @Override
  public Response toResponse(IZapException e) {
    ErrorResponse response = MapperUtils.toReturn(e);

    return Response.status(response.getStatus())
      .entity(response)
      .type(MediaType.APPLICATION_JSON_TYPE)
      .build();
  }
}

Not shown here, but Jersey can actually inject beans into @Provider classes.

Other

I think one of the undervalued configuration/properties management libraries is com.typesafe I like this library for the ease of use, handling property hierarchies, flexibility, and out of the box property loading convention.

<dependency>
  <groupId>com.typesafe</groupId>
  <artifactId>config</artifactId>
</dependency>
private static final Config CONFIG = ConfigFactory.load();
// do some stuff
String emailFrom = CONFIG.getString("email.from");
  

Build

Some manual work needs to be done to package an application into a single executable Jar. Take a look at the following maven plugin:

<plugin>
  <artifactId>maven-assembly-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <id>make-assembly</id> <!-- this is used for inheritance merges -->
      <phase>package</phase> <!-- bind to the packaging phase -->
      <goals>
        <goal>single</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <archive>
      <manifest>
        <mainClass>com.itzap.message.app.EmailApplication</mainClass>
      </manifest>
    </archive>
    <descriptors>
      <descriptor>src/main/resources/assembly/itzap-message.xml</descriptor>
    </descriptors>
    <appendAssemblyId>false</appendAssemblyId>
  </configuration>
</plugin>

This plugin is using itzap-message.xml descriptor file

<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
    <id>itzap-message</id>
    <formats>
        <format>jar</format>
    </formats>

    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>src</directory>
            <excludes>
                <exclude>**</exclude>
            </excludes>
        </fileSet>
    </fileSets>

    <dependencySets>
        <dependencySet>
            <unpack>true</unpack>
            <unpackOptions>
                <excludes>
                    <exclude>*.log</exclude>
                    <exclude>assembly/**</exclude>
                    <exclude>databases/**</exclude>
                </excludes>
            </unpackOptions>
        </dependencySet>
    </dependencySets>
</assembly>

I usually, unpack all dependencies and merge them into a final jar. There are other strategies for building executable Jar that can be configured in the descriptor file.

Deployment

I already mentioned adocker-compose as a mechanism for running itzap-message micro-service. Here is an example of the docker-compose.yml

version: '3.2'

services:

  mail:
    image: boky/postfix
    environment:
      ALLOWED_SENDER_DOMAINS: ${ALLOWED_DOMAINS}

    ports:
      - 1587:587

  message:
    # Builds message service
    build: "."
    image: message-service:latest
    environment:

      MAIL_HOST: mail
      MAIL_FROM: ${MAIL_FROM}
    privileged: true
    ports:
      - 8025:8025
    links:
      - mail

    depends_on:
      - mail

Here, you can see two containers mail and message. Mail container comes straight from the Docker Hub boky/postfix while message is a simple java contaner defined in the following Dockerfile

# Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine
COPY message-impl/target/message-impl-0.0.1-SNAPSHOT.jar /opt/lib/
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/message-impl-0.0.1-SNAPSHOT.jar"]
EXPOSE 8025

Notes

itzap-message micro-service application contains two modules: message-api and message-impl. Assuming application consuming itzap-message micro-service is a Java application, having message-api jar streamlines the development of the Java client. API library provides common POJOs and a service interface that can be implemented on the client-side.

Readme

itzap-message

itzap-message micro-service project designed to send email messages. Visit my ITZap blog to read more about this project.

Usage

itzap-message desinged to run in a Docker container along with postfix that is also running in a Docker. Using docker-compose itzap-message can be deployed anyware and ready to send emails.

How To Build

  • Clone the following projects:
    • git clone git@github.com:avinokurov/itzap-parent.git
    • git clone git@github.com:avinokurov/itzap-common.git
    • git clone git@github.com:avinokurov/itzap-rxjava.git
    • git clone git@github.com:avinokurov/itzap-message.git
  • Build all projects
    • cd itzap-parent && mvn clean install
    • cd ../itzap-common && mvn clean install
    • cd ../itzap-rxjava && mvn clean install
    • cd ../itzap-message && mvn clean install
  • Running
    • Before running set environment variables export MAIL_FROM = mailer@test.com to whater email address will be used to send messages from and export ALLOWED_DOMAINS=test.com domain used to send emails.
    • To run the itzap-message micro-service docker-compose up
  • Testing
    • Once both Docker containers are running open Postman and call micro-service API to send an email POST http://localhost:8025/v1/itzap/message/email Body
     	{
     		"messageId": "new-user",
     		"subject": "test",
     		"addresses": ["avinokurov@itzap.com"],
     		"transport": "email"
     	}

Code