Dockerizing The Application

The following article will take our two services, the BFF and the core service, as well as RabbitMQ, and put them into what are called Docker containers. These containers will be identical to what is in production, so understanding them and how to use them locally is important. As the complexity of your work grows, Docker containers provide us an isolated environment that guarantees our local stack will function as intended on the production stack.

Docker

Docker is a platform of containers. These containers can, and should be thought of, as immutable servers. These containers do not care if they are run on your machine or if they are run on a remote machine in the cloud. This affords us the obvious portability as found in the JVM, but it also provides to work directly on a deliverable, only releasing it when ready. By leveraging this concept and with proper orchestration measures in place, we reduce the develop to production release cliff, allowing us to perform better as developers.

What is a Docker container?

A Docker container differs from a virtual machine in the sense that it is not a full operating system. The container has only the required libraries to run, where as a virtual machine has the entire operating system. This means Docker containers are lightweight and efficient, which allows us to replicate, or even restart them, very quickly.

Docker Machine

Docker itself has been quickly adapting to the ever changing software development lifecycle, and because of so, there are many options how to provide automated orchestration among containers. We have decided to use Docker Machine. Docker Machine uses Virtualbox to orchestrate our containers. Because it uses Virtualbox, we can then not worry about the quality of the desktop or laptop, nor do we have to worry about what operation system and version is being run. Once configured, we can then run standard Docker commands to manage our containers.

Configuring Docker Machine

Docker Machine will require a local registry. The registry is simply a repository of local images. While the Docker Machine documentation suggests building a default image, we will replace that with a registry image instead.

docker-machine create -d virtualbox \
    --virtualbox-memory "4096" \
    --virtualbox-hostonly-cidr "192.168.131.1/24" \
    registry

Upon success, if you run docker-machine ls, you will see your newly minted machine.

With this, we now have a machine running, but need to tell Docker to work with it. The following bash commands will link the two

eval $(docker-machine env registry)
REG_IP=$(docker-machine ip registry)
docker run -d -p 80:5000  --restart=always --name registry-srv \
    -v /Users/$(whoami)/Documents/Docker/registry:/var/lib/registry \
    registry:2

With the machine and Docker linked, you will next need to ssh into the machine and update some configurations.

docker-machine ssh registry

Once in the machine, edit /var/lib/boot2docker/profileto look like the following.

Once saved, make sure to exit from the registry machine's ssh session. Changing this configuration will require you to restart the registry.

docker-machine restart registry

Installing the Registry Web UI

One of the great things about Docker images is that each new Docker machine builds upon a previous Docker machine by creating a new layer. Not only will the Web UI provide insight to what machines you have registered, it will also provide you the layers of the machine over time. So let us install the Docker container for the Web UI.

docker run -d -p 5080:8080 --restart=always --name registry-web -e REGISTRY_URL=http://$REG_IP:80/v2 -e REGISTRY_NAME=$REG_IP:80 hyper/docker-registry-web

Once complete, visit the Web UI

open -a Safari.app http://$REG_IP:5080

Creating a Dockerized Application

You will notice that there is no images in the registry, so for example's sake, let us create a quick Grails 3 application

grails create-app demo

We will dockerize this application and deploy it to the registry. To do so, we need to have a Dockerfile in the root of the application.

touch demo/Dockerfile

Now open that file in your editor and add the following commands

FROM openjdk:8-jdk-alpine

# exposed ports
EXPOSE 8080

# create a tmp folder
RUN mkdir /app_home

# copy java war file into image
COPY build/libs/demo-0.1.war /app_home

# set working folder
WORKDIR /app_home

# set perms
RUN chmod -R 700 /app_home
RUN chown -R daemon /app_home

# run as user
USER daemon

# run container
CMD ["java", "-jar","demo-0.1.war", "run"]

FROM command

The FROM command tells Docker which layer to build upon. In this case we will use openjdk:8-jdk-alpine. This will provide us the libraries needed to run a JVM.

EXPOSE command

Since Docker applications are container based applications, we need to map ports. Our quick demo application only has port 8080, but remember that our addressbook application has ports 8080 and 777. When a request comes into the ip address set up by Docker Machine, eventually we will map the requested port to the applications port. Exposing the port allows us to do that.

RUN command

The RUN command simply tells Docker to execute a command inside the container. In the case of RUN mkdir /app_home, we are simply telling Docker to make a directory in which we will place our applications binary image.

COPY command

Again, the COPY command is pretty simple. We are telling Docker to copy a file from one place to another, which in this case is our binary from the build location to the where we decided the application should execute from.

WORKDIR command

The WORKDIR command tells Docker that the working directory is the argument passed. In our example, that is the application home we decided upon earlier.

USER command

The USER command tells Docker which user context we should execute commands as.

CMD command

When we run the Docker image from our shell, the CMD command is the command that will execute. All other commands are for building the image and configuration. When we run our demo application, we are telling Docker to execute our war image with the java command.

Building a Dockerized Image

Now that we have a Dockerfile in our demo application, we want to use Docker to use the Dockerfile to build the container. From the demo applications root directory, we first need to compile the source code.

./gradlew assemble

When the compilation completes successfully, you will have demo.war located in build/libs. This is our target file we set in the Dockerfile.

Now we can build our Docker image.

docker build -t demo -f Dockerfile .

As the image builds, Docker will print to the terminal each step we supplied it. If you ever run into a build issue, you can use this output to help debug the issue.

The build command builds our image. It does not run it. You can verify this by using docker images to see the build image and use docker ps to note that our image is not running.

Tagging an Image

Because we are using our own registry, we want to be able to push our images to it. This means we need to tag the image before hand. This is almost identical in concept to tagging a release image on a repository.

docker tag demo $REG_IP:80/demo:latest

The above command tags demo with our docker-machine registry $REG_IP:80 reference. This is for identification purposes only, and we also give it a version of latest. We could just as easily give a semantic version if needed.

Pushing an Image

Now that we have successfully built and tagged our demo application Docker image, we need to push it to our registry.

docker push $REG_IP:80/demo:latest

Once completed, you can revisit the registry and see our demo application image.

open -a Safari.app http://$REG_IP:5080

Running a Dockerized Image

To run a Docker image, we simply need to tell it to run.

docker run -d --restart=always --name demoapp -p 9999:8080 $REG_IP:80/demo:latest

To understand the arguments passed, lets cover them one by one.

run

This argument runs our CMD command found in the Dockerfile

-d

This argument tells Docker to detach from the shell (run in background).

--restart=always

Restarts our container

--name

We name our container demoapp for our own reference

-p

Tells the port mapping used. In our case, we will use port 9999 and it will map to our applications port 8080

Conclusion

We could run the application before pushing our image to the registry, but that defeats the purpose. When we use the registry as we have, after we have brought down the docker-machine registry for whatever reason, we would only need to restart the registry to run each container versus needing to start each one manually without. You can visit our demo application with the following bash command.

open -a Safari.app http://$REG_IP:9999

Dockerizing the Distributed AddressBook Application

To dockerize our addressbook application, we will need three containers. The first is RabbitMQ and the other two are our BFF and core service. Because each container is unique, working from localhost will no longer make any sense. Because of so, we will also need to update our configurations to match.

Dockerizing RabbitMQ

Dockerizing RabbitMQ is very straight forward. We will simply use their image.

docker run -d --restart=always --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:alpine

Once started, check the RabbitMQ image logs to see if the plugins have started. To do this, you will need to execute docker ps, recording the container id given for the image. Then execute docker logs [container id]. If it says the server started the plugins, you may skip the next part. If not, we will need to manually drive the plugins with the following.

docker exec -it [container id] rabbitmq-plugins enable rabbitmq_management rabbitmq_shovel rabbitmq_shovel_management

Unfortunately, RabbitMQ will only accept the default username/password pair of guest when accessed by localhost. Since we will now be accessing a remote machine, we will need to use the admin portal for RabbitMQ to build a virtual host with permissions. The port 15672 we mapped provides this interface.

open -a Safari.app http://$REG_IP:15672

When prompted for username and password, use guest for both of them. Once logged in, go the Admin tab on the top bar. From here, we can create a vhost by clicking Virtual Host on the right a-side. When you enter the Virtual Hosts page, you will notice a default guest. Under that listing is Add a new virtual host.

Clicking that accordion will provide the following form. Add a vhost named addressbook and click the Add virtual host button.

Now that we have a new vhost, we need to add users to it. Return to the Admin panel and add a user named addressbookuser.Once the user is added, click the new user in All users and update their vhost from / to addressbook.

Clicking Set permission will not provide us a vhost and username/password pair to use RabbitMQ remotely.

RabbitMQ Conclusion

We created a vhost with a unique username/password pair to allow our two services to communicate. Why creating a new vhost is wise over reusing some default one is because while it make seem like we are adding clutter, we are actually keeping it organized. As we add many more services, having a discrete vhost for the contextual domain will allow other developers to quickly pick up on what the vhost is intended for. Of course, as we use the vhost, we can have topics unique to new services. Try to keep this in mind when administering RabbitMQ.

Dockerizing the AddressBook-BFF

Because we are now using remote services, we are going to have to update the configuration as well as create the Dockerfile, tag the image, and push to the registry.

Updating the configuration

Our first stop to updating the configuration is within the application.yaml file found in resources. We need to add the username/password pair for the user we created and also inject the vhost and ipaddress. For the ipaddress, make sure to use echo $REG_IP to get the ipaddress of your registry. While there, we can also delete the server.port configuration since we will not have conflicts anymore.

# rabbitmq configs
rabbitmq:
  username: addressbookuser
  password: 9Yr-EJN-G7w-hYa
  port: 5672
  ipaddress: 192.168.131.100
  vhost: addressbook

# camel/spring
camel:
 springboot:
   main-run-controller: true

# messaging routes
addressbook:
 person:
   findAll: addressbook.person.findAll
   findOne: addressbook.person.findOne

Next we will have to update our source code to handle the new dependency injections. In PersonRoute, update our class variables to the following.

...
...
...

@Service('personRouteService')
class PersonRoute extends RouteBuilder {

    private final String apiVersionPath = '/v1'

    @Value('${rabbitmq.ipaddress')
    private String ipaddress

    @Value('${rabbitmq.port')
    private Integer port

    @Value('${rabbitmq.username}')
    private String username

    @Value('${rabbitmq.password}')
    private String password

    @Value('${rabbitmq.vhost}')
    private String vhost

    @Value('${addressbook.person.findAll}')
    private String findAllTopic

    @Value('${addressbook.person.findOne}')
    private String findOneTopic

    ...
    ...
    ...

}

We will also have to update the configure method to reflect our injections.

@Override
void configure() throws Exception {

    // add in vhost to options
    def options = "username=${username}&password=${password}&vhost=${vhost}"
    def findOneRoute = "rabbitmq://${ipaddress}:${port}/${findOneTopic}?${options}"
    def findAllRoute = "rabbitmq://${ipaddress}:${port}/${findAllTopic}?${options}"

}

Create the Dockerfile

To dockerize anything, we need a Dockerfile. The Dockerfile for our BFF will look as follows.

FROM openjdk:8-jdk-alpine

# exposed ports
EXPOSE 8080
EXPOSE 7777

# create a tmp folder
RUN mkdir /app_home

# copy java war file into image
COPY target/addressbook-bff-0.0.1-SNAPSHOT.jar /app_home

# set working folder
WORKDIR /app_home

# set perms
RUN chmod -R 700 /app_home
RUN chown -R daemon /app_home

# run as user
USER daemon

# run container
CMD ["java", "-jar","addressbook-bff-0.0.1-SNAPSHOT.jar", "run"]

You will need to run mvn install to create the .jar image used in runtime. Once that compilation is completed, you can then build the docker image, tag it, and push it to the registry.

docker build -t addressbook-bff -f Dockerfile .
docker tag addressbook-bff $REG_IP:80/addressbook-bff:latest
docker push $REG_IP:80/addressbook-bff:latest

Verify you new image on the regsitry

open -a Safari.app http://$REG_IP:5080

And finally run your image, but before we do, let's stop and remove our previous demo app. You will need to get the container id of the image with the command docker ps

Here, my demo app has a container id of cdce4df10aa. I will simply do the two docker commands

docker stop cdce4df10aa
docker rm cdce4df10aa

This will free me up to run my addressbook-bff. Simply use docker as follows.

docker run -d --restart=always -p 7777:7777 -p 8080:8080 $REG_IP:80/addressbook-bff:latest

Conclusion

As of now we have dockerized both RabbitMQ and the AddressBook-BFF. We still to complete dockerizing our AddressBook service. If we were to access the BFF as is right now, we will get errors because we have no core service to use. But to check the status, you can use docker ps to see it running or use docker logs [container id] to see the log output.

Dockerizing the AddressBook

Similar to the AddressBook-BFF, we need to update configurations to use RabbitMQ remotely. So again, let's update application.yml and have the rabbitmq config look as follows.

# rabbitmq configs
rabbitmq:
  username: addressbookuser
  password: 9Yr-EJN-G7w-hYa
  port: 5672
  ipaddress: 192.168.131.100
  vhost: addressbook

Again, use the ipaddress given to you in echo $REG_IP in place of my registry's ipaddress. After updating the application's configuration, update the PersonRouteService class variables to use our newly injected values.

...
...
...

class PersonRouteService extends RouteBuilder {

    private final String apiVersionPath = '/v1'

    @Value('${rabbitmq.ipaddress}')
    private String ipaddress

    @Value('${rabbitmq.port}')
    private Integer port

    @Value('${rabbitmq.username}')
    private String username

    @Value('${rabbitmq.password}')
    private String password

    @Value('${rabbitmq.vhost}')
    private String vhost

    @Value('${addressbook.person.findAll}')
    private String findAllTopic

    @Value('${addressbook.person.findOne}')
    private String findOneTopic

    ...
}

And finally, update our configure method to reflect these injections.

@Override
void configure() throws Exception {

    def options = "username=${username}&password=${password}&vhost=${vhost}"
    def findOneRoute = "rabbitmq://${ipaddress}:${port}/${findOneTopic}?${options}"
    def findAllRoute = "rabbitmq://${ipaddress}:${port}/${findAllTopic}?${options}"

    from(findOneRoute)
        .unmarshal().json(JsonLibrary.Jackson, PersonDTO.class)
        .to('log:persons-findall?level=INFO&showAll=false&multiline=true')
        .to('bean:personService?method=findOne(${body})')
        .marshal().json(JsonLibrary.Jackson)

    from(findAllRoute)
        .to('log:person-findall?level=INFO&showAll=false&multiline=true')
        .to('bean:personService?method=findAll()')
        .marshal().json(JsonLibrary.Jackson)

}

This will finish our configuration changes, now we just need to create the Dockerfile. Create this Dockerfile in your application's root directory.

FROM openjdk:8-jdk-alpine

# exposed ports
EXPOSE 8080
EXPOSE 7777

# create a tmp folder
RUN mkdir /app_home

# copy java war file into image
COPY build/libs/addressbook-0.war /app_home

# set working folder
WORKDIR /app_home

# set perms
RUN chmod -R 700 /app_home
RUN chown -R daemon /app_home

# run as user
USER daemon

# run container
CMD ["java", "-jar","addressbook-0.1.war", "run"]

You will notice that it is pretty identical to our BFF, but also realize the paths to COPY are different. This is because mvn install and ./gradlew assemble build to different targets. Being said, build the .war image as well.

./gradlew assemble

With a success compile, you should now be able to build, tag and push the docker image.

docker build -t addressbook-core -f Dockerfile .
docker tag addressbook-core $REG_IP:80/addressbook-core:latest
docker push $REG_IP:80/addressbook-core:latest

Now run our newly newly named addressbook-core.

docker run -d --restart=always -p 8081:8080 $REG_IP:80/addressbook-core:latest

Realize we are shifting the access port for up by one value. Remember that the ports we are using around 8xxx values are for administration purposes only. Our publically facing RESTful API is on port 7777.

results matching ""

    No results matching ""