Single task (machine) runner
Machine runner can be configured in two separate “modes”. By default runner is in “continuous” mode, whereby it will endlessly poll for new jobs. However, it can also operate in “single-task” mode, whereby the runner process will exit after a single job has ran. This mode of operation can be leveraged for some unusual and esoteric use cases.
One farily trivial example of how this mode can be used is in combination with docker. Using the runner docker image, we can quite easily accomplish a setup whereby each job has a “clean” execution environment that is untouched from any previous jobs, just like on our cloud executors.
Take the following docker command (any config removed for berevity):
docker run --rm circleci/runner:launch-agent
Leveraging the docker run command, we can easily spin up a container that will run machine runner. When we kill the container, it’ll be removed automatically due to the use of the --rm
flag.
By setting the mode of runner to single-task
, the container will automatically exit and be removed after the execution of a single job. This can conveniently be configured via an env var, e.g:
docker run --rm --env LAUNCH_AGENT_RUNNER_MODE=single-task circleci/runner:launch-agent
A more complete example, with authentication config, might look like:
docker run --rm --env CIRCLECI_API_TOKEN=auth-token --env CIRCLECI_RESOURCE_CLASS=your-namespace/your-runner --env LAUNCH_AGENT_RUNNER_MODE=single-task --env LAUNCH_AGENT_RUNNER_NAME=your-runner-agent-name --name your-container-name circleci/runner:launch-agent
By default however, we’d need to keep running this command every time a job completes, which is ridiculous.
However, we can combine this behaviour with, for example, systemd to restart this process upon exit, so that when a job is complete and the container exits and is removed, the system will simply start a new one.
For reference a complete systemd unit might look something like:
[Unit]
Description=CircleCI Runner Service
After=network.target
StartLimitIntervalSec=0
# You might also want to provide flags like this to the container
# --network my-docker-network --memory="512m" --cpus=".5" --cpu-shares="512"
# Further, the container is barebones (doesn't even include git), so you might want to use it as a starting point and install your own
# dependencies etc, then reference that image here.
[Service]
SyslogIdentifier=circleci-runner
ExecReload=docker restart your-container-name
ExecStart=docker run --rm --env CIRCLECI_API_TOKEN=auth-token --env CIRCLECI_RESOURCE_CLASS=your-namespace/your-runner --env LAUNCH_AGENT_RUNNER_MODE=single-task --env LAUNCH_AGENT_RUNNER_NAME=your-runner-agent-name --name your-container-name circleci/runner:launch-agent
Type=simple
Restart=always
User=circleci-runner
[Install]
WantedBy=multi-user.target
We now have an example whereby runner is being used to provide a clean execution environment to every job.
- It’s neat
- It provides low-level insight into how we can use docker to provide a sort of fine-tuned docker environment to machine jobs with isolation
- It only requires a machine (e.g vm) to get working, no k8s etc
- Requires the user to maintain a VM with some finicky config, most customers do not want this
- Provides no advantages to container runner, as far as job execution is concerned
- It’s still machine runner - it cannot be used with
docker:
syntax - the image is fixed to the resource class - By default only provides 1 concurrency, no ability to auto-scale
Single-task mode could probably be used in conjunction with an AWS auto scaling group to provide a clean, root-access VM to every job. Requires further research.