Containers. They’re what all the cool kids talk about these days.
But really, if you haven’t been moving everything you’re doing to either containerized or serverless deployments you may want to make a commitment to putting that at the forefront of your 2020 objectives.
And yeah, we’re already well into that internally at Jamf (more on that in the future).
For the Jamf admin who has yet to migrate to Jamf Cloud and still operates their own environment, you may have already considered moving off your virtual machine (or, shudder, bare metal) installs.
But where to start?
Let’s start with the basics. We’re going to use Docker on your laptop to get Jamf Pro up and running without having to install Java, Tomcat, or MySQL.
You’re Going to Need Docker
Before continuing, you’re going to need Docker Desktop. 17.09 is the latest at time of posting. Finish this before continuing (you won’t need anything else!).
Mac: https://docs.docker.com/docker-for-mac/install/
Windows 10: https://docs.docker.com/docker-for-windows/install/
Building a Jamf Pro Docker Image
First thing we need is a Docker image we can deploy.
What is an image? A Docker image is an artifact that contains everything needed to run an application. Think of it like a virtual machine snapshot, but immutable and highly portable. Containers are the running processes launched using an image.
To build an image we need a Dockerfile
that contains all the instructions on what to install and configure. Engineers at Jamf maintain a “starter” image that we will be using for our environment.
The source code is located here: https://github.com/jamf/jamfpro
But, we don’t need to build this image ourselves. The latest version of it is already available on DockerHub: https://hub.docker.com/r/jamfdevops/jamfpro
DockerHub is a public repository of already built images you can pull down and use! You’ll find all sorts of images readily available from members of the open source community as well as software vendors.
The jamfdevops/jamfpro
image is going to be the foundation of our jamfpro
image we launch.
Some of you might be thinking at this point, “Why are we building another image from this one?” True, we can use the image on its own and provide a
ROOT.war
file at run time, but this is only helpful for one-off testing.In a production, or production-like, setting we won’t be running
docker
commands to launch our instances. We’ll be following best practices with pipelines, and infrastructure and configuration as code. To accomplish this we need an immutable deployment artifact.
Pull a copy of this image down to your computer before continuing (it doesn’t have a latest
tag so you have to specify – 0.0.10
is the latest at time of posting).
docker pull jamfdevops/jamfpro:0.0.10
Download the ROOT.war
We need the WAR file for a manual installation of Jamf Pro first. Customers cant obtain one from their Assets page on Jamf Nation: https://www.jamf.com/jamf-nation/my/products
Yes, you have to be a customer for this step. ¯\_(ツ)_/¯
Click Show alternative downloads and then Jamf Pro Manual Installation. Extract the zip file into an empty directory.
Build the Deployable Image
Here’s a quick script that will use the jamfdevops/jamfpro
image as a base for our output deployment image. The VERSION
variable can be changed for what you are using (10.17.0 is the current at time of posting).
# Run from a directory that _only_ contains your ROOT.war file. # Change VERSION to that of the ROOT.war being deployed VERSION=10.17.0 docker build . -t jamfpro:${VERSION} -f - <<EOF FROM jamfdevops/jamfpro:0.0.10 ADD ROOT.war /data/ EOF
Grab this script on my GitHub as a Gist: build_jamfpro_docker_version.py
I’m using an inline Dockerfile
for this script. FROM
tells it what the base image is. ADD
is the command that will copy the ROOT.war
file into the image’s /data
directory. This is where the jamfdevops/jamfpro
startup script checks for a WAR file to extract into the Tomcat webapps directory.
You should see the following after a successful build of the image.
Sending build context to Docker daemon 227.4MB Step 1/2 : FROM jamfdevops/jamfpro:0.0.10 ---> f87f303293bc Step 2/2 : ADD ROOT.war /usr/local/tomcat/webapps/ ---> 83cd51f8b622 Successfully built 83cd51f8b622 Successfully tagged jamfpro:10.17.0
To learn more, see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
Create a Docker Network
Our environment is going to need two running containers: the Jamf Pro web app, and a MySQL database. We’re going to create a virtual network for them to launch in that will allow communication between the web app and the database.
It’s a quick one-liner.
docker network create jamfnet
Our new jamfnet
network is a bridge
network (the default). We don’t need to do any additional configuration for our purposes today.
To learn more, see https://docs.docker.com/network/bridge/
Start a MySQL Container
Now to start a database. We’re going to use the official mysql:5.7
image from DockerHub, and this image gives us some handy shortcuts to make our local environment easier to setup, but let’s talk about what we’re about to do and why you shouldn’t do this in production, or production-like environments.
- We’re running a database in a container.
This isn’t necessarily something that you shouldn’t do, but the way we’re doing this is not production grade. Running databases in Docker is fantastic for having the service available without needing to install and configure it on your own. You should really, really, really know what you’re doing.
Protip: use managed database services from your provider of choice (which we’re going to explore in the near future). - We’re not using a volume to persist the database.
Containers are ephemeral. They don’t preserve state or data without the use of some external volumes (either on the host or remote). You can preserve the data on disk by mounting a local directory into the running container using an addtional argument:-v /my/local/dir:/var/lib/mysql
. - We’re allowing the image startup scripting to create the Jamf Pro database.
The MySQL image has a handy feature exposed through theMYSQL_DATABASE
environment variable to create a default database. This is a shortcut for setting up Jamf Pro as we don’t have to connect afterwards to create it, but this means the only user available isroot
which leads us to the last point… - We’ll be using the root MySQL user for Jamf Pro.
Never do this in production. For our local test environment it’s fine – we’re not hosting customer or company data and it’s temporary.
Refer to this Jamf Nation KB on how to properly setup the Jamf Pro database on MySQL: https://www.jamf.com/jamf-nation/articles/542/manually-creating-the-jamf-pro-database
With all that said, let’s take a look at the docker run
command to start up our test MySQL database.
docker run --rm -d \ --name jamf_mysql \ --net jamfnet \ -e MYSQL_ROOT_PASSWORD=jamfsw03 \ -e MYSQL_DATABASE=jamfsoftware \ -p 3306:3306 \ mysql:5.7
The --rm
argument it so delete the container once it stops. If you omit this then you can stop and restart containers while preserving their last state (going back to the issue of not preserving our MySQL data in a mounted volume: it would still persist while the container was stopped).
-d
is short for --detach
and will start the container in a new process without tying up the current shell. If you omit this you will remain attached to the running process and view the log stream.
These two options are specific to us treating this as a short lived test environment. In a production container deployment you wouldn’t even think about this because you won’t be deploying the containers with the
docker
command.
The --name
argument provides a friendly name we can use to interact with our container instead of the randomized ones that will be generated otherwise. This is important when it comes to the internal networking to Docker.
Which, with --network
, the container will launch attached to our jamfnet
network, and it will be discoverable via DNS by any other container in the same network. Our web app will be able to reach its database by connecting to mysql://jamf_mysql:3306
.
The two -e
arguments set the values for environment variables in the running container. Most configurable options for a containerized service are handled through environment variables. You can use the same image across multiple environments by changing the values that are passed.
In addition to the web app container, we’re going to want to be able to interact with the data being written to MySQL from our own command line or utilities. In order to do so, we must expose the service’s ports on the host. The -p
or --publish
argument is a mapping of host ports to container ports. MySQL communication is over port 3306
so we are mapping that port on our computer to the same port on our container.
Run the command and you will see a long randomized ID printed out. You can verify what containers are running by typing docker ps -a
.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES bd2e35ebcdd1 mysql:5.7 "docker-entrypoint.s…" 40 seconds ago Up 39 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp jamf_mysql
To learn more about the MySQL Docker image, view the documentation on DockerHub: https://hub.docker.com/_/mysql
Connect Using MySQL CLI or MySQL Workbench
Now that MySQL is up an running we can connect to it either on the command line using mysql
or with a GUI application such as MySQL Workbench.
If you want to use the CLI, you don’t need to install anything addtional. You already have everything you need with the mysql:5.7
Docker image!
docker run --rm -it \ --net jamfnet \ mysql:5.7 \ mysql -u root -h jamf_mysql -p
There are a few new things here. Instead of running a service and detaching it, we are telling it to launch the container with an interactive terminal with the -it
arguments (--interactive
and --tty
respectively).
We’re also passing a command after the name of the image we want to use. This overrides the entrypoint
for the image (I’ve been referring to this as a “startup” script up until now). The entrypoint is the default command that runs for an image if another is not provided.
You can also see that we’ve attached to the jamfnet
network again and are passing the name of our MySQL container as the host argument for mysql
. If we didn’t have port 3306
exposed this method allows us to launch a shell to access private resources.
But, because we do have port 3306
exposed, we can run this container in a slightly different way.
docker run --rm -it \ --net host \ mysql:5.7 \ mysql -u root -h 127.0.0.1 -p
The host
network pretty much does as it sounds. This container will come up without network isolation that bridge
networks provide and access resources much like other clients would. Here you can see we pass the localhost IP instead of the container name and the MySQL connection will be established.
Launch a Jamf Pro Container
We’re finally ready to launch the Jamf Pro web app container itself. The command for this is going to look very similar to what we did with MySQL.
docker run --rm -d \ --name jamf_app \ --net jamfnet \ -e DATABASE_USERNAME=root \ -e DATABASE_PASSWORD=jamfsw03 \ -e DATABASE_HOST=jamf_mysql \ -p 80:8080 \ jamfpro:10.17.0
MySQL doesn’t talk to Jamf Pro, so naming it doesn’t really do anything for us here, but it does make managing the container using docker
a little easier.
We have a different set of environment variables specific to our Jamf Pro image. Again, an image is a static artifact we use for a deployment. The deployment is customized through the use of environment variable values that are applied on startup (the entrypoint scripting). You could easily spin up numerous local Jamf Pro instances all pointing to their own unique databases just by switching out those values all from the one image.
We’re also passing theMySQL container name for the DATABASE_HOST
like in our previous example with the mysql
CLI.
There’s one difference with out -p
argument. We’re mapping port 80
on our host to port 8080
of the container (which is what Jamf Pro uses when it comes up). This is a handy feature of publishing ports: you can effectively perform port forwarding from the host to the container. Instead of interacting with the web app at http://localhost:8080
in our browser we can just use http://localhost
.
Run the command and you will again see another long randomized ID printed. Verify the running containers using docker ps -a
as before.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 382d3fd60afe jamfpro:10.17.0 "/startup.sh" 24 seconds ago Up 22 seconds 0.0.0.0:80->8080/tcp jamf_app bd2e35ebcdd1 mysql:5.7 "docker-entrypoint.s…" 43 minutes ago Up 42 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp jamf_mysql
Access in Your Browser
Open up Safari, enter localhost
into the address bar, and you’ll be greeted by the warm glow of a Jamf EULA.
Consistency, Repeatability
You’ve achieved your first major milestone: you’re running Jamf Pro in a containerized environment. Now, how do we take what we have done above and ensure we can repeat it several hundred (thousand) times exactly as we did now when we spun everything manually.
Infrastructure/configuration as code is the means to achieve this. The way you implement this is going to be different depending on where it is you’re deploying to. For Docker, the tool we want to use to define how to bring up Jamf Pro is Docker Compose (which I’ve presented about).
If you installed Docker Desktop you will already have this CLI tool available to you! Docker Compose uses YAML files to define the elements of your application (services, networks, volumes) and allows you to spin up and manage environments from them.
Here’s a Docker Compose file that automates everything we went through in this post.
version: "3" services: mysql: image: "mysql:5.7" networks: - jamfnet ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "jamfsw03" MYSQL_DATABASE: "jamfsoftware" app: image: "jamfpro:10.17.0" networks: - jamfnet ports: - "80:8080" environment: DATABASE_USERNAME: "root" DATABASE_PASSWORD: "jamfsw03" DATABASE_HOST: "mysql" depends_on: - mysql networks: jamfnet:
Grab this file on my GitHub as a Gist: jamfpro-docker-compose.yml
Notice anything about the attributes on our services
? Our definitions map almost 1:1 with the CLI arguments to docker run
. The service’s key becomes the name
which we are able to use as a reference just as before. In the app
service (previously jamf_app
) we are passing mysql
as the DATABASE_HOSTNAME
– Docker’s DNS magic continuing to do the work for us. There’s also a Docker Compose specific option in here, depends_on
, that references it. This tells Docker Compose that the mysql
service must finish starting before it brings up the app
.
The beauty of Docker Compose is that it takes very little explaining to understand once you’ve already done some work with the docker
CLI. From the manual commands we ran as a part of this post you can understand what the YAML file is defining and what will happen when we run it.
To do, save the above into to a docker-compose.yml
file and run the following command from the same directory (I’m in a directory called build
):
/build % docker-compose up --detach Creating build_mysql_1 ... done Creating build_app_1 ... done /build %
Fast, consistent, and repeatable deployments from a single definition. To tear this stack down and clean up, run:
/build % docker-compose down Stopping build_app_1 ... done Stopping build_mysql_1 ... done Removing build_app_1 ... done Removing build_mysql_1 ... done Removing network build_jamfnet /build %
The Next Phase
From here your Jamf Pro server is up, running, and ready for whatever testing you have in store. Ideally, as you progress on this journey, the Docker image you use at this step would be the one that you are ultimately deploying to production. The running application itself is identical at each stage it moves through your deployment processes.
You’ve taken your first steps with running Jamf Pro containerized on your computer, but as alluded to in the beginning this is only an exercise in the basics. A production environment is not going to be a laptop with Docker installed (…at least, I certainly hope not). What you’re most likely looking at in that case is a managed container service in the cloud from one of the big three: Amazon Web Services, Google Cloud Platform, or Microsoft Azure.
If you’ve followed me long enough you’ll know that I’m an AWS developer. That’s where my production environment would be, and I can define in CloudFormation templates (AWS’s infrastructure as code implementation) the things I’m going to need:
- Virtual Private Cloud
- Application Load Balancer
- Fargate Cluster
- Aurora MySQL Database (with a twist)
So stay tuned for the next phase in our containerized journey:
“So You Want to Run Serverless Jamf Pro”
Wonderful article!!! Thanks for putting them all together and sharing it.
LikeLike