Using Docker for executing ad-hoc scripts and cron-like jobs
Intro
People use Docker to package applications and infrastructure dependencies for easy shipment - and it shines bright while doing it.
A less talked about usecase for Docker is executing simple ad-hoc scripts and cron-type jobs, like nightly jobs on a build server. Since Docker isolates processes, we can be much more free to experiment with different languages and runtimes not only in our applications, but also our scripts without having to think about provisioning software to the target servers. As developers we should be able to use the language we want and abstract it away. The output is what matters.
The Case
First, let's look at the case. As readers of my blog know, I work with tools crossing the line between the Linux and Windows environments. That's why my team like Ansible. For provisioning Windows servers we use the win_chocolatey
module Ansible provides.
We pull all the Chocolatey packages directly from the central Chocolatey repository. To be able to support offline provisioning of servers (as well as letting me sleep good at night knowing we have a backup) I need to script some syncing of the Chocolatey packages we use to our local ProGet server. Sounds easy, just write up some PowerShell and call it a day right?
Nah. Too boring. Let's see if we can do something cool.
The Code
So grabbing the package names should be rather easy, just some string searching and digging in the Ansible files.
The next step is to download the packages (including the package dependencies) and upload them to our NuGet server. Here we can use the NuGet Command Line tool directly, or Chocolatey. Both are now cross platform and works as well on NIX systems as it does on Windows. Throw in a Docker image and we can mix some scripts.
A colleague of mine is working on a cool project called dotnet-script. In a nutshell, the project provides C# scripting based on dotnet core, with full debug support in Visual Studio Code (if you haven't checked it out, you should). As we all know, dotnet core means Linux support, so after a quick PR to the project it now have a Dockerfile as well. Parsing some Ansible task files is easy when C# is the weapon.
All Ansible roles installing Chocolatey packages looks like this:
- name: Installing dotnet 4.6 Target Pack
win_chocolatey: name=dotnet4.6-targetpack source="{{ ChocoFeedUrl }}"
So the following parser code returns all the package names:
#! "netcoreapp1.1"
#r "nuget:NetStandard.Library,1.6.1"
var rolesFolder = "roles/";
var packages = new HashSet<string>();
var allFiles = Directory.GetFiles(rolesFolder, "*.yaml", SearchOption.AllDirectories);
foreach(var yamlFile in allFiles)
{
var textLines = File.ReadAllLines(yamlFile);
foreach(var line in textLines)
{
if(line.Contains("win_chocolatey") && !line.ToLower().Contains("internalsoftware"))
{
packages.Add(FetchPackageName(line));
}
}
}
private string FetchPackageName(string line)
{
var words = line.Split(' ');
foreach(var word in words)
{
if(word.Contains("name="))
{
return word.Replace("name=", string.Empty);
}
}
return string.Empty;
}
File.WriteAllLines("scripts/nugetPackages.txt", packages);
Writing that without a single Console.Writeline()
for debug is a pretty nice experience.
To download the packages and upload them to the local NuGet server a simple shell script does the trick:
#! /bin/bash
filename="nugetPackages.txt"
while read -r line
do
nuget install -OutputDirectory . $line -Source https://chocolatey.org/api/v2/
echo $line
done < "$filename"
packages=$(find **/*.nupkg)
for package in $packages
do
choco push $package --api-key "SomeAPIKeyYouCantHaveDearBlog" --Source http://internalNugetServer/nuget/ExternalSoftware/ --force
done
Note that we use Nuget Command Line for downloading the package, while Chocolatey puts up less of a fuzz when pushing packages (throw in the --force
flag and it just pushes, no questions asked).
To glue it all together without having to install any dotnet tools on the Linux server, enter the power of Docker.
#! /bin/bash
docker run --name packages -v $(cd ../ && pwd):/scripts:z andmos/dotnet-script scripts/GeneratePackagesFile.csx
docker run --name packagesync --volumes-from=packages:z -w="/scripts/scripts" andmos/choco ./DownloadFromChocoPushToProget.sh
docker rm -v data
docker rm -v packagesync
Here we first use the the andmos/dotnet-script
image to execute the parser script, digging out all Chocolatey packages from the Ansible roles. The Ansible repo is shared from the host to the container via the -v
flag, mounting it in the workspace folder. Next, the workspace folder is mounted to the andmos/choco
container, giving it access to the nugetPackages.txt
. The andmos/choco
provides access to NuGet Command Line and Chocolatey itself.
Last but not least - we clean up after ourself. When writing self contained ad-hoc Docker pipelines like this it is important to take out the thrash.
Summary
This code runs each night via a TeamCity build, syncing packages as we provision out new Chocolatey packages. The agent running it has only Docker installed, and that is enough. Docker provides a nice abstraction when executing simple scripts like this, making it easy to try out new languages without having to install a thousand runtimes on the servers. If I want to try out F# or Python next, it is as simple as switching out an Image. It is also really cool to see traditional Windows tools running smoothly on Linux.