Deploy a Dropwizard Unikernel to AWS
Part 2: Automated Maven build

Dropwizard to AWS using Maven

In part 1 of this series we looked at how to fuse a Dropwizard application into a secure unikernel image. We then tested it locally on VirtualBox and finally we deployed it unchanged to AWS.

Today we are going to continue where we left off and automate the steps from part 1 directly from our Maven build. In addition, we are going to automatically execute an integration test against a test instance deployed on VirtualBox before pusing the image out to AWS.

Maven phases

Prerequisites

This post assumes you have everything set up as described in part 1.

Step 1: Automatically fusing the unikernel image

Instead of manually having to type some commands, we now automatically fuse our Dropwizard app into a unikernel image using the CloudCaptain Maven plugin as part every build. The image effectively supplants the jar file as the final product of our build.

It however shares all the essential qualities of the jar file:

  • one immutable artifact
  • regenerated after every changed
  • promoted unchanged from environment to environment

And it avoids the classic mistake of building a separate artifact per environment, where the artifact you run in production is different from the one you tested in test. In other words it avoids the very mistake that happens with classic provisioning of systems.

Maven phases - step 1

For this we first need to add the CloudCaptain Maven repository to our pom.xml:

<pluginRepositories>
    <pluginRepository>
        <id>central</id>
        <url>https://repo1.maven.org/maven2</url>
    </pluginRepository>
    <pluginRepository>
        <id>boxfuse-repo</id>
        <url>https://files.cloudcaptain.sh</url>
    </pluginRepository>
</pluginRepositories>

We now add the CloudCaptain Maven plugin after the shade plugin and define one execution in the package phase to fuse the image:

<!-- must be placed after the shade plugin to guarantee the correct execution order -->
<plugin>
    <groupId>com.boxfuse.client</groupId>
    <artifactId>boxfuse-maven-plugin</artifactId>
    <version>1.33.0.1460</version>
    <configuration>
        <user>your-boxfuse-client-user</user>
        <secret>your-boxfuse-client-secret</secret>
    </configuration>
    <executions>
        <execution>
            <id>fuse-image</id>
            <goals>
                <goal>fuse</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

Don't forget to replace the CloudCaptain user and secret with the ones specified on your download page of the CloudCaptain Console.

Executing the build now gives us this:

> mvn clean package
...
[INFO] --- boxfuse-maven-plugin:1.33.0.1460:fuse (fuse-image) @ dwunikernel ---
[INFO] Fusing Image for dwunikernel-1.0.jar ...
[INFO] Image fused in 00:07.273s (54017 K) -> axelfontaine/dwunikernel:1.0

And there we have it! A new image generated as part of every build.

Step 2: Adding an integration test against VirtualBox

Now let's automatically

  • run an instance of our new image on VirtualBox in the pre-integration-test phase
  • execute an integration test against it in the integration-test phase
  • kill the instance in the post-integration-test phase
  • verify the results in the verify phase
Maven phases - step 2

Let's start by creating our integration test. For that we'll need to add junit and fluent-hc (the fluent interface for the Apache HTTP client) to the test scope of the pom.xml:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>fluent-hc</artifactId>
    <version>4.3.6</version>
    <scope>test</scope>
</dependency>

Now let's add an integration test called dwunikernel.resources.HelloWorldResourceTest to src/test/java:

package dwunikernel.resources;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.fluent.Request;
import org.junit.Test;
import java.util.Map;

import static org.junit.Assert.assertEquals;

public class HelloWorldResourceTest {
    @Test
    public void hello() throws Exception {
        String url = System.getProperty("instanceUrl") + "?name=IntegrationTest";
        String json = Request.Get(url).execute().returnContent().asString();
        Map<String, Object> result = new ObjectMapper().readValue(json, new TypeReference<Map<String, Object>>(){});
        assertEquals("Hello, IntegrationTest!", result.get("hello"));
    }
}

As the url of the instance may change based on port availability, you can see it is being passed in dynamically using the system property instanceUrl. It is then being used to connect to the running instance, where the response of the request is then converted from a json string into a map object and compared against our expectations.

Next add additional executions of the CloudCaptain Maven plugin to the pom.xml to automatically run and kill the instance:

<execution>
    <id>run-on-virtualbox</id>
    <goals>
        <goal>run</goal>
    </goals>
    <phase>pre-integration-test</phase>
</execution>
<execution>
    <id>kill-on-virtualbox</id>
    <goals>
        <goal>kill</goal>
    </goals>
    <phase>post-integration-test</phase>
</execution>

And finally configure the Surefire and Failsafe plugins to run our test and verify the results:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <excludes>
            <exclude>**/*ResourceTest.java</exclude>
        </excludes>
    </configuration>
</plugin>
<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <includes>
            <include>**/*ResourceTest.java</include>
        </includes>
        <systemPropertyVariables>
            <instanceUrl>${boxfuse.instances.0.url}</instanceUrl>
        </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <id>integration-test</id>
            <goals>
                <goal>integration-test</goal>
            </goals>
        </execution>
        <execution>
            <id>verify</id>
            <goals>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

As you can see we pass in the variable ${boxfuse.instances.0.url} as the value of the instanceUrl System property we referred to in out test. After executing run, CloudCaptain will automatically set the ${boxfuse.instances.0.url} to the correct url of the instance that was just started. We then use that url in our test to connect to the Dropwizard application.

Time to fire up Maven and see the results:

> mvn clean verify
...
[INFO] --- boxfuse-maven-plugin:1.3.2.576:run (run-on-virtualbox) @ dwunikernel ---
[INFO] Launching Instance of axelfontaine/dwunikernel:1.0 on VirtualBox ...
[INFO] Forwarding admin-http port localhost:50001 -> vb-fc55a898:8081
[INFO] Forwarding http port localhost:8888 -> vb-fc55a898:80
[INFO] Instance launched in 00:03.851s -> vb-fc55a898
[INFO] Waiting for Payload to start on Instance vb-fc55a898 ...
[INFO] Payload started in 00:07.025s -> https://127.0.0.1:8888
[INFO]
[INFO] --- maven-failsafe-plugin:2.18.1:integration-test (integration-test) @ dwunikernel ---
[INFO] Failsafe report directory: C:\Workspaces\dwunikernel\target\failsafe-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running dwunikernel.resources.HelloWorldResourceTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.557 sec - in dwunikernel.resources.HelloWorldResourceTest

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- boxfuse-maven-plugin:1.3.2.576:kill (kill-on-virtualbox) @ dwunikernel ---
[INFO] Killing Instance vb-fc55a898 on VirtualBox ...
[INFO] Instance killed in 00:02.349s
[INFO]
[INFO] --- maven-failsafe-plugin:2.18.1:verify (verify) @ dwunikernel ---
[INFO] Failsafe report directory: C:\Workspaces\dwunikernel\target\failsafe-reports

Success! Our test has verified our HelloWorldResource is behaving as we expect and we have now automatically tested the exact same image we'll later run on AWS. This provides us with very strong guarantees that it will work there too.

Step 3: Deploying the tested image to AWS

After we have successfully tested our image, we are now sufficiently confident to automatically push it out to our production environment on AWS. For that we need one final addition to the list of executions of the CloudCaptain Maven plugin in the pom.xml:

<execution>
    <id>run-on-aws</id>
    <goals>
        <goal>run</goal>
    </goals>
    <phase>deploy</phase>
    <configuration>
        <env>prod</env>
    </configuration>
</execution>

And also disable the deploy plugin as we have not configured a remote repository (and our images land in the CloudCaptain Vault):

<plugin>
    <artifactId>maven-deploy-plugin</artifactId>
    <version>2.8.2</version>
    <configuration>
        <skip>true</skip>
    </configuration>
</plugin>

Finally put it all together:

Maven phases
> mvn clean deploy
...
[INFO] Pushing axelfontaine/dwunikernel:1.0 ...
[INFO] Verifying axelfontaine/dwunikernel:1.0 ...
[INFO] Waiting for AWS to create an AMI for axelfontaine/dwunikernel:1.0 in eu-central-1 (this may take up to 50 seconds) ...
[INFO] AMI created in 00:31.703s -> ami-842d1299
[INFO] Creating security group boxfuse-sg_axelfontaine/dwunikernel:1.0 ...
[INFO] Launching t2.micro instance of axelfontaine/dwunikernel:1.0 (ami-842d1299) in eu-central-1 ...
[INFO] Instance launched in 00:22.192s -> i-0cd66acd
[INFO] Waiting for Payload to start on Instance i-0cd66acd at https://52.28.79.74:8081/healthcheck ...
[INFO] Remapping Elastic IP 52.28.20.51 to i-0cd66acd ...
[INFO] Waiting 15s for AWS to complete Elastic IP Zero Downtime transition ...
[INFO] Terminating instance i-2ed66aef ...
[INFO] Destroying Security Group sg-fc548d95 ...
[INFO] Deployment completed successfully. axelfontaine/dwunikernel:1.0 is up and running at https://dwunikernel-axelfontaine.boxfuse.io:8081

And there we have it. We have successfully pushed the image to the CloudCaptain Vault, started a new instance on AWS and remapped the Elastic IP for a smooth Zero Downtime transition. No additional work is required. We now have this as part of every single build.

Done

Congratulations! We have taken our unikernel from part 1 and with just a few additions to the pom.xml you now have a fully automated deployment process. That includes both the dev environment, including automated testing against VirtualBox, and the production environment with Blue/Green deployments with zero downtime on AWS.

Now go ahead, dive deeper and read the Maven plugin documentation.

Continue to part 3, where we take this to 11 by creating a fully automated Continuous Deployment pipeline using GitHub and TravisCI.

« Deploy a Dropwizard Unikernel to AWS
Part 1: Up and running
Deploy a Dropwizard Unikernel to AWS »
Part 3: Continuous Deployment with GitHub and Travis CI