diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-03-15 00:50:34 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-03-15 00:50:34 -0700 |
commit | fb7e6890d8516618fa3baec0edf84048e2b6601d (patch) | |
tree | a7bc5cfce71288ab69e8fa590d0f02df90c55385 /playbooks/roles/docker | |
download | infra-fb7e6890d8516618fa3baec0edf84048e2b6601d.tar.gz infra-fb7e6890d8516618fa3baec0edf84048e2b6601d.zip |
a docker swarm
Diffstat (limited to 'playbooks/roles/docker')
-rw-r--r-- | playbooks/roles/docker/files/docker-compose@.service | 19 | ||||
-rwxr-xr-x | playbooks/roles/docker/files/docker-rollout | 204 | ||||
-rw-r--r-- | playbooks/roles/docker/handlers/main.yml | 8 | ||||
-rw-r--r-- | playbooks/roles/docker/tasks/main.yml | 55 |
4 files changed, 286 insertions, 0 deletions
diff --git a/playbooks/roles/docker/files/docker-compose@.service b/playbooks/roles/docker/files/docker-compose@.service new file mode 100644 index 0000000..77e8892 --- /dev/null +++ b/playbooks/roles/docker/files/docker-compose@.service @@ -0,0 +1,19 @@ +[Unit] +Description=%i service with docker compose +Requires=docker.service +After=docker.service + +[Service] +RemainAfterExit=true +WorkingDirectory=/etc/docker/compose/%i +ExecStartPre=/bin/bash -c "/usr/bin/docker compose pull || true" +ExecStart=/usr/bin/docker compose up +ExecStop=/usr/bin/docker compose down +Restart=always +RestartSec=5 +StartLimitInterval=500 +StartLimitBurst=3 + +[Install] +WantedBy=multi-user.target + diff --git a/playbooks/roles/docker/files/docker-rollout b/playbooks/roles/docker/files/docker-rollout new file mode 100755 index 0000000..c15d5a8 --- /dev/null +++ b/playbooks/roles/docker/files/docker-rollout @@ -0,0 +1,204 @@ +#!/bin/sh +set -e + +# Defaults +HEALTHCHECK_TIMEOUT=60 +NO_HEALTHCHECK_TIMEOUT=10 +WAIT_AFTER_HEALTHY_DELAY=0 + +# Print metadata for Docker CLI plugin +if [ "$1" = "docker-cli-plugin-metadata" ]; then + cat <<EOF +{ + "SchemaVersion": "0.1.0", + "Vendor": "Karol Musur", + "Version": "v0.9", + "ShortDescription": "Rollout new Compose service version" +} +EOF + exit +fi + +# Save docker arguments, i.e. arguments before "rollout" +while [ $# -gt 0 ]; do + if [ "$1" = "rollout" ]; then + shift + break + fi + + DOCKER_ARGS="$DOCKER_ARGS $1" + shift +done + +# Check if compose v2 is available +if docker compose >/dev/null 2>&1; then + # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments + COMPOSE_COMMAND="docker $DOCKER_ARGS compose" +elif docker-compose >/dev/null 2>&1; then + COMPOSE_COMMAND="docker-compose" +else + echo "docker compose or docker-compose is required" + exit 1 +fi + +usage() { + cat <<EOF + +Usage: docker rollout [OPTIONS] SERVICE + +Rollout new Compose service version. + +Options: + -h, --help Print usage + -f, --file FILE Compose configuration files + -t, --timeout N Healthcheck timeout (default: $HEALTHCHECK_TIMEOUT seconds) + -w, --wait N When no healthcheck is defined, wait for N seconds + before stopping old container (default: $NO_HEALTHCHECK_TIMEOUT seconds) + --wait-after-healthy N When healthcheck is defined and succeeds, wait for additional N seconds + before stopping the old container (default: 0 seconds) + --env-file FILE Specify an alternate environment file + +EOF +} + +exit_with_usage() { + usage + exit 1 +} + +healthcheck() { + # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments + docker $DOCKER_ARGS inspect --format='{{json .State.Health.Status}}' "$1" | grep -v "unhealthy" | grep -q "healthy" +} + +scale() { + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --scale "$1=$2" --no-recreate "$1" +} + +main() { + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + if [ -z "$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE")" ]; then + echo "==> Service '$SERVICE' is not running. Starting the service." + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE" + exit 0 + fi + + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + OLD_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | tr '\n' '|' | sed 's/|$//') + OLD_CONTAINER_IDS=$(echo "$OLD_CONTAINER_IDS_STRING" | tr '|' ' ') + SCALE=$(echo "$OLD_CONTAINER_IDS" | wc -w | tr -d ' ') + SCALE_TIMES_TWO=$((SCALE * 2)) + echo "==> Scaling '$SERVICE' to '$SCALE_TIMES_TWO' instances" + scale "$SERVICE" $SCALE_TIMES_TWO + + # Create a variable that contains the IDs of the new containers, but not the old ones + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + NEW_CONTAINER_IDS=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | grep -Ev "$OLD_CONTAINER_IDS_STRING" | tr '\n' ' ') + + # Check if first container has healthcheck + # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments + if docker $DOCKER_ARGS inspect --format='{{json .State.Health}}' "$(echo $OLD_CONTAINER_IDS | cut -d\ -f 1)" | grep -q "Status"; then + echo "==> Waiting for new containers to be healthy (timeout: $HEALTHCHECK_TIMEOUT seconds)" + for _ in $(seq 1 "$HEALTHCHECK_TIMEOUT"); do + SUCCESS=0 + + for NEW_CONTAINER_ID in $NEW_CONTAINER_IDS; do + if healthcheck "$NEW_CONTAINER_ID"; then + SUCCESS=$((SUCCESS + 1)) + fi + done + + if [ "$SUCCESS" = "$SCALE" ]; then + break + fi + + sleep 1 + done + + SUCCESS=0 + + for NEW_CONTAINER_ID in $NEW_CONTAINER_IDS; do + if healthcheck "$NEW_CONTAINER_ID"; then + SUCCESS=$((SUCCESS + 1)) + fi + done + + if [ "$SUCCESS" != "$SCALE" ]; then + echo "==> New containers are not healthy. Rolling back." >&2 + + docker $DOCKER_ARGS stop $NEW_CONTAINER_IDS + docker $DOCKER_ARGS rm $NEW_CONTAINER_IDS + + exit 1 + fi + + if [ "$WAIT_AFTER_HEALTHY_DELAY" != "0" ]; then + echo "==> Waiting for healthy containers to settle down ($WAIT_AFTER_HEALTHY_DELAY seconds)" + sleep $WAIT_AFTER_HEALTHY_DELAY + fi + else + echo "==> Waiting for new containers to be ready ($NO_HEALTHCHECK_TIMEOUT seconds)" + sleep "$NO_HEALTHCHECK_TIMEOUT" + fi + + echo "==> Stopping and removing old containers" + + # shellcheck disable=SC2086 # DOCKER_ARGS and OLD_CONTAINER_IDS must be unquoted to allow multiple arguments + docker $DOCKER_ARGS stop $OLD_CONTAINER_IDS + # shellcheck disable=SC2086 # DOCKER_ARGS and OLD_CONTAINER_IDS must be unquoted to allow multiple arguments + docker $DOCKER_ARGS rm $OLD_CONTAINER_IDS +} + +while [ $# -gt 0 ]; do + case "$1" in + -h | --help) + usage + exit 0 + ;; + -f | --file) + COMPOSE_FILES="$COMPOSE_FILES -f $2" + shift 2 + ;; + --env-file) + ENV_FILES="$ENV_FILES --env-file $2" + shift 2 + ;; + -t | --timeout) + HEALTHCHECK_TIMEOUT="$2" + shift 2 + ;; + -w | --wait) + NO_HEALTHCHECK_TIMEOUT="$2" + shift 2 + ;; + --wait-after-healthy) + WAIT_AFTER_HEALTHY_DELAY="$2" + shift 2 + ;; + -*) + echo "Unknown option: $1" + exit_with_usage + ;; + *) + if [ -n "$SERVICE" ]; then + echo "SERVICE is already set to '$SERVICE'" + + if [ "$SERVICE" != "$1" ]; then + exit_with_usage + fi + fi + + SERVICE="$1" + shift + ;; + esac +done + +# Require SERVICE argument +if [ -z "$SERVICE" ]; then + echo "SERVICE is missing" + exit_with_usage +fi + +main diff --git a/playbooks/roles/docker/handlers/main.yml b/playbooks/roles/docker/handlers/main.yml new file mode 100644 index 0000000..2db0186 --- /dev/null +++ b/playbooks/roles/docker/handlers/main.yml @@ -0,0 +1,8 @@ +--- + +- name: Enable docker + ansible.builtin.service: + name: docker + state: restarted + enabled: true + diff --git a/playbooks/roles/docker/tasks/main.yml b/playbooks/roles/docker/tasks/main.yml new file mode 100644 index 0000000..8b91f6a --- /dev/null +++ b/playbooks/roles/docker/tasks/main.yml @@ -0,0 +1,55 @@ +--- + +- name: Install dependencies + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg-agent + - software-properties-common + state: present + update_cache: true + +- name: Docker GPG key + become: true + ansible.builtin.apt_key: + url: > + https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg + state: present + +- name: Repository docker + ansible.builtin.apt_repository: + repo: > + deb https://download.docker.com/linux/{{ ansible_distribution | lower }} + {{ ansible_distribution_release }} stable + state: present + +- name: Install docker + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + update_cache: true + notify: + - Enable docker + +- name: Copy docker rollout script + ansible.builtin.copy: + src: docker-rollout + dest: /usr/local/bin/docker-rollout + mode: 0755 + +- name: Copy docker-compose@.service + ansible.builtin.copy: + src: docker-compose@.service + dest: /etc/systemd/system/docker-compose@.service + +- name: Ensure /etc/docker/compose exist + ansible.builtin.file: + path: /etc/docker/compose + state: directory + mode: 0700 + |