Images

В проекте Docker впервые предложили паковать контейнеры в послойные образы в 2013. Это позволило переносить контейнеры между машинами.

skopeo, jq

Проверим работу с образами на утилите skopeo и пробном контейнере:

skopeo copy docker://saschagrunert/mysterious-image oci:mysterious-image
sudo apt install tree
tree mysterious-image

Видим, что мы скачали индекс образа (image index) и blob. Изучим индекс:

jq . mysterious-image/index.json
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:bc2baac64f1088c56c259a21c280cde5a0110749328454a2b931df6929315acf",
      "size": 559
    }
  ]
}

По сути индекс есть манифест более высокого уровня, который содержит указатели на конкретные манифесты для определённых ОС (linux) и архитектур (amd).

jq . mysterious-image/blobs/sha256/bc2baac64f1088c56c259a21c280cde5a0110749328454a2b931df6929315acf
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa",
      "size": 2789742
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:6d8c9f2df98ba6c290b652ac57151eab8bcd6fb7574902fbd16ad9e2912a6753",
      "size": 120
    }
  ]
}

Image manifest указывает на расположение конфига и набора слоёв для образа контейнера на конкретной ОС и архитектуре. Поле size указывает общий размер объекта. Теперь можно исследовать далее:

jq . mysterious-image/blobs/sha256/0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa
{
  "created": "2019-08-09T12:09:13.129872299Z",
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "/bin/sh"
    ]
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:1bfeebd65323b8ddf5bd6a51cc7097b72788bc982e9ab3280d53d3c613adffa7",
      "sha256:56e2c46a3576a8c1a222f9a263dc57ae3c6b8baf6a68d03065f4b3ea6c0ae8d1"
    ]
  },
  "history": [
    {
      "created": "2019-07-11T22:20:52.139709355Z",
      "created_by": "/bin/sh -c #(nop) ADD file:0eb5ea35741d23fe39cbac245b3a5d84856ed6384f4ff07d496369ee6d960bad in / "
    },
    {
      "created": "2019-07-11T22:20:52.375286404Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
      "empty_layer": true
    },
    {
      "created": "2019-08-09T14:09:12.848554218+02:00",
      "created_by": "/bin/sh -c echo Hello",
      "empty_layer": true
    },
    {
      "created": "2019-08-09T12:09:13.129872299Z",
      "created_by": "/bin/sh -c touch my-file"
    }
  ]
}

Распакуем базовый первый слой из архива и изучим его:

$ mkdir rootfs
$ tar -C rootfs -xf mysterious-image/blobs/sha256/0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa
$ tree -L 1 rootfs/
rootfs/
├── bin
├── dev
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var

Это файловая система ОС! Можно изучить версию дистрибутива

$ cat rootfs/etc/issue 
Welcome to Alpine Linux 3.10
Kernel \r on an \m (\l)

$ cat rootfs/etc/os-release 
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.10.1
PRETTY_NAME="Alpine Linux v3.10"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

Распакуем следующий слой и изучим его:

$ tar -C layer -xf mysterious-image/blobs/sha256/6d8c9f2df98ba6c290b652ac57151eab8bcd6fb7574902fbd16ad9e2912a6753 
$ tree -L 1 layer/
layer/
└── my-file

1 directory, 1 file

Тут лежим доп файл, созданный командой "/bin/sh -c touch my-file" - это можно увидеть в секции history. По сути исходный Dockerfile выглядел так:

FROM alpine:latest
RUN echo Hello
RUN touch my-file

Buildah

В 2017 году Red Hat разработали инструмент для создания образов контейнеров по стандарту OCI - как аналог docker build.

Создадим Dockerfile vim Dockerfile и впишем в него:

FROM alpine:latest
RUN echo Hello
RUN touch my-file

Запустим сборку контейнера на базе этого файла - buildah bud Buildah поддерживает много команд:

buildah images # список образов
buildah rmi # удалить все образы
buildah ps # показать запущенные контейнеры

Почему вдруг buildah ps показ запущенных контейнеров, когда это инструмент для их СОЗДАНИЯ? А потому что в процессе создания как в buildah, так и в docker идёт запуск промежуточных контейнеров, их модификация в runtime. Каждый шаг модификации создаёт записи в history. Это потенциальная проблема ИБ: можно влезть в контейнер, пока он собирается (и запущен), если там что-то большое, и модифицировать его.

Например, приготовим манифест:

FROM debian:buster
RUN apt-get update -y && \
    apt-get install -y clang

Начнём сборку:

docker build -t clang

Залезем в работающий контейнер:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
05f4aa4aa95c        54da47293a0b        "/bin/sh -c 'apt-get…"   11 seconds ago      Up 11 seconds                           interesting_heyrovsky

> docker exec -it 05f4aa4aa95c sh
# echo "Hello world" >> my-file

После сборки можно удостовериться, что созданный файл на месте:

> docker run clang cat my-file
Hello world

Создание контейнеров без Dockerfile

У buildah есть императивные команды к любой команде из Dockerfile, типа RUN или COPY. Это даёт огромную гибкость, т.к вместо огромных Dockerfile можно делить процесс создания контейнеров на части, между ними запускать любые вспомогательные инструменты UNIX.

Создадим базовый контейнер с Alpine Linux и посмотрим как он работает:

buildah from alpine:latest
buildah ps

Можно запускать команды в контейнере, а также создать в нёс файл:

> buildah run alpine-working-container cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.3
PRETTY_NAME="Alpine Linux v3.20"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

echo test > file
> buildah copy alpine-working-container test-file file
86f68033be6f25f54127091bb410f2e65437c806e7950e864056d5c272893edb

По-умолчанию, buildah не делает записи history в контейнер, это значит порядок команд и частота их вызова не влияют на итоговые слои. Можно поменять это поведение ключом --add-history или переменной ENV BUILDAH_HISTORY=true.

Сделаем коммит нового контейнера в образ для финализации процесса:

buildah commit alpine-working-container my-image
buildah images # новый образ теперь в локальном реестре

Можно выпустить образ в реестр Docker, либо на локальный диск в формате OCI:

> buildah push my-image oci:my-image
> jq . my-image/index.json
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:bd3f673eb834eb86682a100768a14ae484b554fb42083666ba1b88fe4fe5c1ec",
      "size": 1189
    }
  ]
}

Теперь сделаем в обратную сторону: удалим образ my-image из реестра и вытащим его с диска из формата OCI:

buildah rmi my-image
buildah images
buildah pull oci:my-image

Контейнер alpine-working-container при этом ещё работает. Запустим CLI в контейнере:

buildah run -t alpine-working-container sh
ls
cat file

Проведём mount файл-системы контейнера на локальную (В ДРУГОМ ТЕРМИНАЛЕ):

> buildah unshare --mount MOUNT=alpine-working-container
> echo it-works > "$MOUNT"/test-from-mount
> buildah commit alpine-working-container my-new-image
  • buildah unshare создаёт новое пространство имён, что позволяет подключить файловую систему как текущий не-root пользователь;
  • --mount автоматически подключает, путь кладём в переменную среды MOUNT;
  • Далее мы делаем commit на изменения, и mount на автомате убирается при покидании buildah unshare сессии.

Мы успешно модифицировали файловую систему контейнера через локальный mount. Проверим наличие файла:

> buildah run -t alpine-working-container cat test-from-mount
it-works

Вложенные контейнеры

У buildah нет даемона, значит, не нужно подключать docker.sock в контейнер для работы с Docker CLI. Это даёт гибкость и возможность делать вложенные схемы: установим buildah в контейнер, созданный buildah:

> buildah from opensuse/tumbleweed
tumbleweed-working-container
> buildah run -t tumbleweed-working-container bash
# zypper in -y buildah

Теперь вложенный buildah готов к использованию. Нужно указать драйвер хранения VFS в контейнере, чтобы получить рабочий стек файловой системы:

# buildah --storage-driver=vfs from alpine:latest
# buildah --storage-driver=vfs commit alpine-working-container my-image

Получили вложенный контейнер. Заложим его в хранилище на локальной машине -> для начала заложим образ в /my-image:

buildah --storage-driver=vfs push my-image oci:my-image

Выходим из вложенного контейнера (В ДРУГОМ ТЕРМИНАЛЕ) и копируем образ из рабочего контейнера путём mount его файловой системы:

> buildah unshare
> export MOUNT=$(buildah mount tumbleweed-working-container)
> cp -R $MOUNT/my-image .
> buildah unmount tumbleweed-working-container

Теперь мы вытаскиваем образ контейнера из директории прямо в локальный реестр buildah:

> buildah pull oci:my-image
> buildah images my-image

ВАЖНОЕ ЗАМЕЧАНИЕ: все действия с buildah не потребовали sudo. Buildah создаёт всё необходимое для каждого пользователя в папках:

  • ~/.config/containers, конфигурация
  • ~/.local/share/containers, хранилища контейнеров

Декомпозиция Dockerfile в несколько разных с помощью CPP макросов.

podman

Инструмент для замены Docker. podman использует buildah как API для создания Dockerfile с помощью podman build. Это значит, что они разделяют одно хранилище под капотом. А это значит, что podman может запускать созданные buildah контейнеры:

buildah images
podman run -it my-image sh
/ # ls