The point of this blog series is to cut through the hype and jargon and get to the pith of DevOps, and there is no better way to do that than with concrete examples.
Before you go looking for solutions, it’s helpful to understand what the problem is. And what the problem is that, somewhere in the deep past, your foundational engineers started writing software. They got a requirement, and they:
- Selected a language
- Selected a build tool
- Selected a revision control system
- Started writing code
That’s pretty much it. Life was good–the software probably did its job (which is why you’re in business, right?) Then, requirements got added, those engineers left and new ones joined, and they:
- Wrote new functionality (frequently in other languages)
— Added new build tools and processes and artifacts
- Wrote a bunch of helper scripts at 1:00 in the morning on a customer call that got integrated into the product
- Added support for new platforms and operating systems
- Wrote a bunch of QA and automation scripts
And now it’s no longer an easy task to even build the project. In fact, if you’re doing anything Enterprisey (TM), I would bet good money that fewer than 10% of your team understands how to build and package your product, much less run your automated system tests, etc. And I promise you that no one making purchasing decisions for DevOps solutions understands what your process is. So, let’s figure out how to figure out what your problem is.
A typical pipeline addresses the following steps (current in bold)
Now, your process can vary quite a bit. This series isn’t intended to teach or advocate a particular methodology, and it’s not prescriptive. There are frequently many sub-steps to each of these steps.
Step 1: Understand your Project’s Build Structure
You’re going to have to go beyond “well, we use Java and Lua and Make and Gradle.” You’re going to want to start by enumerating every single component of your project. Every last one. This will create a set of projects that we will call P (for projects). Draw them out, but give yourself plenty of space.
Here’s mine for Sunshower components:
Now, yours doesn’t have to look like this. You could get away with just creating a list, but I’d prefer that you drew it out: it’s a good exercise, it provides a lot of hints as to how things fit together. Now, understand that this is a pretty small project with just a few dependencies. Your project could take weeks to diagram out correctly and comprehensively–that’s ok. You’ll save time and money in the long run. Draw it out, but don’t get hung up on anything like UML or BPML–just try to visually understand the pieces of your project.
Information that should be present
- All of the tools on all of the platforms you need to support
— For instance, if you need to build on Windows Server 2012 and Ubuntu 14.04, and your build-tool is Make, you’ll need (for instance)
— MinGW on Windows and devtools on Ubuntu. I would just draw them out as separate tool boxes on your diagram. It’ll help
- Source information for your dependencies
— You may have to build behind a firewall. Make sure that your description of build environments includes what needs to be accessed and how
- If you’re using managed dependencies (via Ivy, Maven, Gradle, or Go), don’t necessarily worry about enumerating them here. It’ll clutter things up
— Unless: you need access to a private source for your dependencies (e.g. you’ve purchased a subscription to a library that requires authenticated access). In which case:
— Be sure to indicate somewhere on here how to access private sources (e.g. settings.xml for Maven)
- Specific versions for tools. This is really important. Don’t assume that different versions of tooling will produce the same results.
Some words to the wise:
- Do not buy and products or services at this point. You’ll get something that you’ll never implement correctly or completely.
- Get one of your engineers to do this. If you don’t have one who can, be absolutely certain that a consultant can build one out for you
- This isn’t the whole story by any stretch of the imagination. In subsequent posts, we’ll cover how to ensure that everything gets built
- Be sure that this is correct. Try it out for yourself and verify that you can at least build everything (don’t worry about getting it running or packaged or installed at this point)
- Institute a process wherein any changes to the project are reflected in this diagram. If this diagram is changing too frequently, you need to understand why. Too much churn on this diagram past a few weeks into the start of a project is a serious warning sign.
Step 2: Create a Template for Building your Project
This is step zero in terms of automation IMO. Sure, build tools like Gradle “automate” builds in that they can be used for many steps described in this series, but we’re only considering the “compile my component and generate some artifacts” portions of their capabilities. You will almost certainly be using your build tools to automate other tasks, but don’t worry about that now. It’s going to be a lot harder to implement a good automated workflow if you don’t clearly distinguish between types of tasks and how you (you dear reader, and your org) perform them.
The goal of this step is to create a template for getting properly configured build environments. This means that
- They will have all of the tools enumerated in your tool boxes in your diagrams
- They will have the correct versions of those tools
- They will have access to the resources (connectivity to Github, credentials to Maven repositories, etc.) required to build your project
Let’s look at some tools that can help us out here.
- VMs (Open Virtualization Format–OVA): VMWare, VirtualBox, etc.
- …etc. There are tons.
There are quite a few more, and each of these tools can do more than allow you to create build templates, but we’re only concerned with creating build templates at this stage. These can all be made to work with subsequent stages in your process, and some of them can use others to create templates (e.g Vagrant can generate OVAs). This series will only cover Docker, contact me or leave a comment if you want me to show you how to do the same thing in any of the others.
Example 1: Translating A Tools Box into a Docker Container
Let’s start with the tools box for Updraft:
Before we get started, there’s a bit of a chicken-and-egg situation we need to discuss here. It might seem natural to:
- Install Go in our container
- Install Git in our container
- Checkout Updraft into our container
- Build it, etc.
But this introduces a bit of a problem in that there’s not an easy, clean way to check out and build a specific version of Updraft. So, instead, what we’ll do is we’ll require the actual build machines (VM images or bare metal) to have
- Docker installed
- Git installed
Then, we’ll check out the desired version of the project from Github, build the container using Docker, then build Updraft inside the container. This allows us to easily build different versions of Updraft reliably and reproducibly. I promise that you won’t need much more than this on any actual instance of a Docker host.
So, having installed Docker and Git, let’s add our Dockerfile. A lot of people like to keep their Dockerfiles at the root of their projects, but I like to keep mine in a “docker” subdirectory in case I need more than one:
│ │ └── error_code.go
│ └── utils
│ ├── uuid.go
│ └── uuid_test.go
│ ├── Dockerfile
│ └── Dockerfile.windows
│ └── parser
│ ├── abstract_parser.go
│ ├── base_source.go
│ ├── base_source_test.go
│ ├── parser_message_listener.go
│ └── parser_test.go
Note It’ll probably take a few tries to get your Dockerfile right. You can build and run the container in one (chained) command thuslike:
docker build -t "updraft" -f docker/Dockerfile . && docker run -it --rm --name updraft updraft
Or, if you like, two commands:
docker build -t "updraft" -f docker/Dockerfile . docker run -it --rm --name updraft updraft
Each run will re-create the container in the new state and drop you into a bash shell inside the container, where you can execute more commands until you get it right. Remember to add each command necessary to build your project to your Dockerfile as a RUN statement (so that it becomes a layer in your container).
FROM golang:1.8 ## Super easy Go base image (first dependency in our tools box) WORKDIR /go/src/updraft ## current working directory inside the container COPY . . ## Copy the contents of the current directory into WORKDIR RUN go get ./... ## Get updraft's dependencies (dependencies in our dependencies box) RUN go build -o out/ucc ucc/main.go ## Build updraft and output it into $WORKDIR/out
And viola! Running that all together produces:
➜ updraft git:(master) ✗ docker build -t "updraft" -f docker/Dockerfile . && docker run -it --rm --name updraft updraft Sending build context to Docker daemon 33.59MB Step 1/5 : FROM golang:1.8 ---> 0e070ede84f7 Step 2/5 : WORKDIR /go/src/updraft ---> Using cache ---> 4d23d64b3b23 Step 3/5 : COPY . . ---> 8a9a904e4971 Removing intermediate container 996472d595ff Step 4/5 : RUN go get ./... ---> Running in 919addd0c758 ---> 1608bbf6258f Removing intermediate container 919addd0c758 Step 5/5 : RUN go build -o out/ucc ucc/main.go ---> Running in a0c9e2bd2112 ---> d2d8855e56ae Removing intermediate container a0c9e2bd2112 Successfully built d2d8855e56ae Successfully tagged updraft:latest root@caa586b9157c:/go/src/updraft# ls README.md backends common docker front middle out pascal ucc root@caa586b9157c:/go/src/updraft# cd out root@caa586b9157c:/go/src/updraft/out# ./ucc -h Updraft is a modern compiler framework and collection written in pure Go for portability, speed, and embeddability Usage: ucc [flags] ucc [command] Available Commands: help Help about any command trace trace various aspects of configuration and execution Flags: --config string config file (default is $HOME/.ucc.yaml) -e, --execute string select the execution model (default "interpreter") -f, --file string file(s) to compile -h, --help help for ucc -l, --language string select the target language (default "pascal") Use "ucc [command] --help" for more information about a command. root@caa586b9157c:/go/src/updraft/out#
Deploy to Dockerhub
Now that we’ve successfully built our image, let’s publish it so that anyone can use it:
➜ updraft git:(master) ✗ docker login Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username (jhaswell): Password: Login Succeeded ➜ updraft git:(master) ✗ docker tag updraft sunshower/updraft ➜ updraft git:(master) ✗ docker push sunshower/updraft The push refers to a repository [docker.io/sunshower/updraft] 34f0b9f7c17e: Pushed ae2fc6d3daac: Pushed 9fabb269895b: Pushed 233937e534e9: Pushed f847715687e8: Mounted from library/golang 056e99358ddc: Mounted from library/golang 237b592486ad: Mounted from library/golang 86ca4e4dcab9: Mounted from library/golang 44b57351135e: Mounted from library/golang 00b029f9aa09: Mounted from library/golang 18f9b4e2e1bc: Mounted from library/golang latest: digest: sha256:089e2aa54db28ce35428e8a37cb46f51461def857d812aa14f1060030be384ba size: 2635 ➜ updraft git:(master) ✗
And that’s it! Next time we’ll look at how to automate this process so that it happens with every checkin (introducing new tools) and how to make your artifacts available to everyone via the web and other means.