A build environment is composed of many moving parts.
- Source control environment
- Build process
- Build environment
- Build Orchestration
- Build Results
- Build Artifacts
All of these require their own specific setups, and there can be many different ways to accomplish each. I’ll talk about my opinionated way, with detailed setups.
Source Control Environment
First, there is the source code itself. Generally, this will be stored in something like Git/GitHub/GitLab, SVN, RTC, etc. For most open source environments, GitHub is currently the king, but I’m seeing quite alot of movement to GitLab as well. Even for closed source, GitHub is a pretty cheap environment to use, and cuts down on one more system to setup and manage at the tool level.
All of my code is currently stored in a variety of repositories within GitHub. Quite a few are stored as private repositories, especially before I decide that I’m ready to ‘make it public’.
There is an infinite number of ways that the build process can be set up, but I generally like to break it into 4 stages.
- Code Compiling
- Unit Testing
- Integration Testing
When it comes to compiling the code, I’m generally a fan of Maven, but there’s, obviously, alot of different choices such as SBT, Gradle, Ant, etc. I highly recommend that you try to have one consistent tooling, since it keeps things consistent. Regardless of choice, I do recommend that you leverage the Maven-style artifact repository, since managing dependencies manually is too error-prone. Fortunately, just about every build tool today supports it.
Within Maven, I like using a specific directory structure that works well with Eclipse, and that means that all pom’s need to be in ‘non-recursive’ folders (ie. one pom cannot be in a child folder of another pom). Thus, my project structures tend to look like:
MyProject/ root/ pom.xml services/ root/ pom.xml service1/ pom.xml
In this structure, each level has a ‘root’ folder that contains a parent pom. Building the entire project simply means going into the top level root folder, and running
While I tend to be somewhat hit-and-miss around my consistent application of unit testing, I do heavily believe in them. There are many different unit testing frameworks for each language you work in. For Java, JUnit is the most well known.
I generally put all my unit testing within each project, and have them run as part of the compiling process. Thus, each project doesn’t complete compiling until the unit tests are complete.
Unit tests are meant to be reasonably quick. The entire suite shouldn’t take more than 5-10 minutes to run. Anything more complicated should be located with the Integration Testing.
Once all the pieces are compiled and unit tested, the next step is to assembly into the final structure. These days, that usually means Docker for me. This is where I build the docker image. This isn’t a complex stage, since it’s almost always just following a Dockerfile which is checked into source control, but, it’s critical to have the final assembly done before Integration Testing happens.
This is where more complex and longer running testing happens. This can range anywhere from a few minutes to days of testing. I’ll generally set up my testing environment to only do ONE integration test in parallel. This means that if we’re still running an integration test when a new build comes along, the old integration test is cancelled, and the new integration test is started.
There are many different tools to use here, and I generally use multiple different ones. Things like Gatling, …
Over the years, I’ve used many different build environments, but I’m currently moving towards standardizing on Jenkins. It’s well known, easy to set up, and easy to customize.
Like everything else I do, I’ll only run tools within a Docker environment. Currently, I’m using the out-of-the-box build, so
docker run -d -v /var/run/weave/weave.sock:/var/run/docker.sock -v /data/docker:/usr/bin/docker -v /data/jenkins:/var/jenkins_home -t --name jenkins jenkins:latest
Because I’m going to use Docker during the build process, I want to expose the docker socket (in this case the Weave.Works socket, since I also do everything in a weave controlled environment). Additionally, I need the docker binary to be available. NOTE: Make sure that you mount a statically compiled docker binary, since the majority of ones installed by default are NOT statically bound, and your Jenkins container won’t have all the missing libraries. Finally, I’ve mounted a data folder to hold all the updates.
NOTE: That because this Jenkins is running in my isolated Weave environment, I do need to expose it to the outside world so that GitHub hooks, etc can reach it.
In my case, since I’m usually working at my house, which is NAT’d behind a firewall/router, I find it easier to just expose all my different servers under specific port numbers. Eventually, I’ll probably set up a good reverse proxy for it all, but until then, I’ve decided to expose Jenkins under port 1234. Additionally, enp0s3 happens to be the linux network interface on my box, most other people probably have eth0 or eth1.
iptables -t nat -A PREROUTING -p tcp -i enp0s3 --dport 1234 -j DNAT --to-destination $(weave dns-lookup jenkins):8080
With a small change to my DNS Provider (ie. Cloudflare), I know have http://jenkins.mydomain.com:1234 now available to everyone.
I’ve just started playing with Jenkinsfile’s and the Multi-branch Pipeline code, but so far, it’s been very good and easy to use (although I still don’t have the GitHub change hook working after fighting with it for 5 hours).
Groovy has never been a language of interest, but it’s close enough to Java, that I don’t generally have a problem. Of course, every example uses Groovy’s DSL structure instead of just plain functions, so it always “looks” weirder then it is.
One of the things I like to do is to have all my POM’s use a consistent XXX-SNAPSHOT version, and then, as part of the build, replace the versions with the latest build number. Within my Jenkinsfile, I’m currently using this:
// First, let's read the version number def pom = readFile 'root/pom.xml' def project = new XmlSlurper().parseText(pom) def version = project.version.toString() //set this to null in order to stop Jenkins serialisation of job failing project = null // Update the version to contain the build number version = version.replace("-SNAPSHOT", "") def lastOffset = version.lastIndexOf("-"); if (lastOffset != -1) version = version.substring(0, lastOffset); version = version + "-" + env.BUILD_NUMBER env.buildVersion = version
Two key issues here.
1) In Jenkins2’s new security Sandbox, the use of XmlSlurper causes a whole bunch of run errors, so you’ll have to ‘run this’ about 3 or 4 times, each time approving a new method call (the new(), the parseText(), the getProperty(), etc.). A little annoying, but once you do it once, it won’t bother you again.
2) Any non-serializable object, such as the XmlSlurper results, can’t be kept around since, some of the later “special functions”, actually serialize the entire context so that the data can be restored later. Thus, a simple solution is to just assign null to those variables (such as the project variable above). There’s a @NonCPS annotation as well, but I don’t really understand it, and this works fine for these simple variables.
I generally like to make available the different build results, such as the test results, etc. The Jenkinsfile has a couple of step commands to make that pretty easy.
step([$class: 'ArtifactArchiver', artifacts: '**/target/*.jar', fingerprint: true]) step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
In this case, I’m storing all the JARs as artifact results, and the JUnit results as JUnit Results (Jenkins has some nice tooling to make the JUnit results display nicely).
Finally, I want to store the artifacts, such as the Maven artifacts or Docker images into a repository, either public or private. I’ll talk more later about hooking up to the public Maven repository or standing up your own personal Docker repository.