Skip to content

Instantly share code, notes, and snippets.

@kriskbx
Created October 29, 2024 11:35
Show Gist options
  • Save kriskbx/fcfe9b11f1604ee267f70dcc62254e33 to your computer and use it in GitHub Desktop.
Save kriskbx/fcfe9b11f1604ee267f70dcc62254e33 to your computer and use it in GitHub Desktop.
Static ip-addresses for Docker Swarm services
/*
* This is a node.js script supposed to run regularly on a docker swarm node and allows
* containers in a macvlan (or ipvlan) network to have static ip addresses. This is currently
* not possible using Docker Swarm. It doesn't use any external dependencies and instead
* calls docker cli commands directly. So, if you want to run it in a container, make
* sure to bind the docker socket into the container as well.
*
* It works by reading a `macvlan-static-ip` label from the service, e.g. 192.168.0.80
* (This is the desired ip address for the resulting container). And then it "starts"
* and "stops" containers in the right order to ensure they get their configured ip.
*
* This means, your desired ip must be at the start of the configured ip-range of your
* macvlan/ipvlan network and there mustn't be any gaps between ip-addresses.
*
* Example:
*
* Let's say you configured this subnet for your macvlan: 192.168.0.240/29
* This includes ip-addresses from 192.168.0.241 to 192.168.0.246
* If you have 3 containers, they must use the following ips: 192.168.0.241, 192.168.0.242
* and 192.168.0.243. You cannot use *.241, *.244 and *.246, there cannot be any gaps and
* they must start at the beginning.
*/
const {execSync} = require('child_process');
const macvlanNetwork = 'macvlan-network';
const macvlanLabel = 'macvlan-static-ip';
function docker(command, multiple = false) {
const result = (execSync(`docker ${command}`) + '').trim();
if (multiple) {
return result.split('\n').filter(a => a);
} else {
return result;
}
}
function getContainer(serviceId) {
return docker(`service ps ${serviceId} --format '{{ .ID }}'`, true)
.map(taskId => {
const id = docker(`inspect ${taskId} --format '{{ .Status.ContainerStatus.ContainerID }}'`);
try {
const state = docker(`inspect ${id} --format '{{ .State.Status }}' 2>/dev/null`);
return {id, state};
} catch (e) {
return null;
}
})
.filter(c => c);
}
console.info('--------------------------------------');
console.info('Starting process…');
const services = docker(`service ls --filter label=${macvlanLabel} -q`, true)
.map(serviceId => {
const name = docker(`service inspect ${serviceId} --format "{{ .Spec.Name }}"`);
const ipWanted = docker(`service inspect ${serviceId} --format '{{ $label := index .Spec.Labels "${macvlanLabel}" }}{{ $label }}'`);
console.info(`Detected service ${name} with requested ip ${ipWanted}:`);
console.info(`| - Querying container for ${name}...`);
let container = getContainer(serviceId);
if (container.filter(c => c.state === "running").length === 0) {
console.info(`| - No running container found for service ${name}. Restarting service...`);
docker(`service scale ${name}=1`);
docker(`service update --force ${name}`);
container = getContainer(serviceId);
}
if (container.filter(c => c.state === "running").length === 0) {
console.error(`| - Still no running container found for service ${name}. Exiting...`);
process.exit();
}
console.log(`| - ${container.length} container found...`)
return {name, ipWanted, container};
});
console.info('Looking for missmatched macvlan ips...');
const missmatchedServices = docker(`network inspect ${macvlanNetwork} --format '{{ range $k, $v := .Containers }}{{ $all := print $k "," $v.IPv4Address }}{{ println $all }}{{ end }}'`, true)
.map(data => {
const [containerId, containerIp] = data.split(',');
const actualIp = containerIp.replace('/24', '');
console.info(`| - Found ${containerId} with ip ${actualIp}`)
const service = services.find(service =>
service.container.findIndex(container =>
container.id === containerId &&
service.ipWanted !== actualIp
) !== -1
);
if (!service) {
console.info(`| - Ip is matching the required one. Continuing...`);
return null;
}
console.info(`| - Ip missmatch detected for service ${service.name}. Actual: ${actualIp} Wanted: ${service.ipWanted}`);
return {
...service,
actualIp,
};
})
.filter(m => m)
.sort((a, b) => {
return a.ipWanted > b.ipWanted ? 1 : -1;
});
missmatchedServices
.forEach(service => {
console.info(`Shutting down service ${service.name}...`);
docker(`service scale ${service.name}=0`);
});
missmatchedServices.forEach(service => {
console.info(`Starting service ${service.name}...`);
docker(`service scale ${service.name}=1`);
});
if (missmatchedServices.length === 0) {
console.info('No ip missmatches detected. Exiting...')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment