Docker vs. Podman
В этой статье мы не будем подробно разбирать базовые функции Podman и отличия этого проекта от Docker.
Такой информации хватает в интернете.
Вместо этого, мы на конкретном примере (простое Java приложение) разберем как можно заменить Docker на Podman в конвейере CI/CD,
и какие это может принести дивиденды.
Podman – это инструмент с открытым исходным кодом для работы с образами контейнеров.
Является утилитой командной строки с аналогичными docker командами, однако не требует дополнительный сервис для работы и может работать без привилегий root.
Установка
Пакеты Podman доступны для большинства дистрибутивов Linux и не требуют настройки дополнительных репозиториев:
yum install podman buildah skopeo
Сборка Podman из исходного кода гораздо проще, чем компиляция бинарных файлов Docker.
Компоненты Podman
Компонент | Описание |
|---|---|
libpod | Управление жизненным циклом контейнеров |
crun/runc | Среда выполнения контейнеров |
containers/image | Управление образами контейнеров |
containers/storage | Управление томами, файловой системой и хранилищем данных для контейнеров и образов |
Buildah | Сборка контейнеров |
Conmon | Мониторинг среды выполнения контейнеров |
Container Network Interface (CNI) | Управление сетями реализовано на основе спецификаций Кубернетес CNI |
Отношение между компонентами Podman:

Как Podman запускает контейнеры:

Docker Compose
Podman полностью совместим с Docker Compose, однако для это необходимо активировать сервис сокета в системе:
systemctl enable --now podman.socket
docker-compose нужен docker.sock, поэтому Podman при установке создает символическую ссылку на этот файл:
ls -al /run/docker.sock
API
После активации сервиса сокета можно проверить REST API Podman с помощью curl:
curl --unix-socket /run/podman/podman.sock http://d/v3.0.0/libpod/images/json
В данном случае вы должны получить список со всеми образами контейнеров в системе. Большинство функции Podman CLI доступны через REST API.
Перемещение образов между машинами
Podman позволяет перемещать образы контейнеров между хостами, без использования реестров контейнеров. Сначала нужно добавить новое SSH соединение:
podman system connection add <CONNECTION> -i ~/.ssh/id_ed25519 REMOTE_USER_NAME@<IP_ADDRESS>/run/user/1000/podman/podman.sock
Затем копируем образ с помощью команды podman image scp:
podman image scp my_image <CONNECTION>
Тестовый проект
В качестве примера будет использоваться тестовый проект на Java Spring Boot. Для сборки Java будет использоваться Gradle.
Для оркестрации Jenkins.
Генерируем исходный код проекта Spring Boot
Spring Boot дает возможность с помощью встроенных средств создавать скелет проекта: директории и исходный код для сборных инструментов экосистемы Java – Maven и Gradle. Для этого можно использовать интерфейс командной строки, веб-форму, в данном случае вы будете использовать утилиту curl:
curl https://start.spring.io/starter.tgz \
-d applicationName=HelloApplication \
-d artifactId=demo \
-d baseDir=hello \
-d bootVersion=2.7.0 \
-d dependencies=web,actuator \
-d description='Demo project for Spring Boot' \
-d groupId=com.example \
-d javaVersion=17 \
-d language=java \
-d name=demo \
-d packageName=com.example.demo \
-d packaging=jar \
-d type=gradle-project \
-d version=0.0.1-SNAPSHOT \
| tar -xzvf -
После выполнения этой команды, в текущей директории должен появится каталог приложения под названием hello:
Для того, чтобы вывести список с файлами в директории выполните команду ls hello.
Планируемый результат:
HELP.md build.gradle gradle gradlew gradlew.bat settings.gradle src
Нужно будет добавить код в директорию сгенерированного приложения, для того, чтобы скомпилировать проект.
Добавляем логику
Ниже приведен пример кода, который нужно добавить в скелет проекта.
HelloApplication.java:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class HelloApplication {
@RequestMapping("/")
public String home() {
return "Hello world\n";
}
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
Добавите этот код в проект:
cp -f HelloApplication.java hello/src/main/java/com/example/demo/
Выполнить команду: ls hello/src/main/java/com/example/demo/
Планируемый результат:
HelloApplication.java
Dockerfile
Для работы вы будете использовать следующий Dockerfile:
cat Dockerfile:
## Stage 1 : Gradle компилирует исходный код в исполняемый jar
FROM openjdk:17-oraclelinux8 as builder
RUN microdnf install findutils
WORKDIR /home/gradle/src
COPY . .
RUN ./gradlew build
## Stage 2 : Финальный образ который содержит JRE и jar
FROM openjdk:17-oraclelinux8
# Копируем jar в образ контейнера
COPY --from=builder /home/gradle/src/build/libs/demo-0.0.1-SNAPSHOT.jar demo.jar
EXPOSE 8080
# Запускаем приложение используя исполняемый jar файл
CMD ["java", "-jar", "demo.jar"]
Следующая стадия определяет контейнер который содержит только среду выполнения Java (Java runtime).
Jar-файл из предыдущей стадии копируется в новый контейнер, а остальные инструкции нужны, чтобы запустить с нужными параметрами приложение Spring Boot, которое мы создали.
Скопируйте Dockerfile в проект:
cp -f Dockerfile hello
Выполнить команду: ls hello/Dockerfile
Планируемый результат:
hello/Dockerfile
Build
Buildah это инструмент который работает в связке с Podman. Кодовая база Podman включает в себя пакеты Buildah.
Buildah не нужны привилегии root для сборки образов, что является несомненным плюсом с точки зрения безопасности.
В отличии от Docker, Buildah позволяет автоматизировать сборку образа с помощью Bash скриптов, а не только Dockerfile.
Директивы Dockerfile/Containerfile можно использовать в CLI:
buildah from
buildah run
buildah config --entrypoint/entrypoint.sh
buildah add
buildah copy
В данном случае мы используем для сборки стандартного Dockerfile:
stage('Build image') {
steps {
dir('k8s-course/podman/helloPodman') {
sh 'buildah build -f Dockerfile -t antonvkrylov/k8s-course:helloPodman'
}
}
}
После запуска сборки в логах можно увидеть вывод Gradle:

Основные конфигурационные файлы, которые используются Buildah:
/etc/containers/registries.conf - Реестры контейнеров.
/usr/share/containers/policy.json - Верификация сигнатур.
/usr/share/containers/mounts.conf - Директории и файлы которые автоматически монтируются в контейнерах.
/usr/share/containers/seccomp.json - Системные вызовы которые разрешены для выполнения в контейнерах.
Push
Загрузка собранного образа в реестр ничем не отличается от Docker:
stage('Push image') {
steps {
dir('k8s-course/podman/helloPodman') {
sh 'buildah push antonvkrylov/k8s-course:helloPodman'
}
}
}
Так же как и с Docker для аутентификации используется команда podman/buildah login
Buildah так же как и Podman умеет работать c аутентификационными файлами:
buildah push --authfile /auth.json registry.example.com
Test
Podman позволяет проверять состояние контейнера и приложения с помощью специальных проверок (healthchecks).
Механизм проверок похож на то, что было реализовано в последних версиях Docker Compose.
С помощью этой функции, можно встраивать в конвейер CI простые функциональные и интеграционные тесты.

stage('Test') {
/////////
/// Jenkins позволяет выполнять стадии или шаги внутри одной стадии параллельно
/////////
parallel {
stage('Test ports') {
steps {
/////////
/// Мы создаем проверку (healthcheck) с определенными параметрами, которая выполняет запрос с помощью curl внутри контейнера.
/////////
sh"""
podman run --rm -dt --name test_ports \
--healthcheck-start-period 10s \
--healthcheck-retries 1 \
--healthcheck-command "CMD-SHELL curl -f http://127.0.0.1:8080 || exit 1" docker.io/antonvkrylov/k8s-course:helloPodman
"""
/////////
/// Если таймаут в 2 минуты будет привешен то конвейер прервется с ошибкой и статусом «Aborted»
/////////
timeout(time: 2, unit: 'MINUTES') {
/////////
/// В этом скрипте мы проверяем статус проверки через каждые 3 секунды – если статус «healthy» то цикл завершается.
/////////
sh"""
#!/bin/bash
while [ "`podman healthcheck run test_ports`" = "unhealthy" ]; do
sleep 3
done
"""
}
}
}
stage('Test Java') {
steps {
/////////
/// Мы создаем проверку (healthcheck) с определенными параметрами, которая выполняет запрос с помощью curl внутри контейнера.
/////////
sh"""
podman run --rm -dt --name test_java \
--healthcheck-start-period 10s \
--healthcheck-retries 1 \
--healthcheck-command "CMD-SHELL java --version | grep 'openjdk 17.0.2' || exit 1" docker.io/antonvkrylov/k8s-course:helloPodman
"""
timeout(time: 2, unit: 'MINUTES') {
sh"""
#!/bin/bash
while [ "`podman healthcheck run test_java`" = "unhealthy" ]; do
sleep 3
done
"""
}
}
}
}
}
Scan
Для сканирования собранного образа на уязвимости можно использовать один из популярных инструментов для статического анализа уязвимостей (SAST):
stage('Scan image') {
steps {
dir('k8s-course/podman/helloPodman') {
sh 'trivy image --security-checks vuln --severity CRITICAL antonvkrylov/k8s-course:helloPodman'
}
}
}
Результаты сканирования для каждой сборки будут сохранятся в Jenkins:

Archive
Skopeo используется для копирования и перемещения образов между реестрами контейнеров. В данном случае образ контейнера из публичного реестра с помощью skopeo сохраняется для архивации на диске:
stage('Archive') {
steps {
sh"""
skopeo copy docker://antonvkrylov/k8s-course:helloPodman --dest-compress --dest-compress-format gzip docker-archive:./helloPodman.tar.gz
"""
}
}
}
Skopeo можно использовать для реверс-инжиниринга образов, в случаях, когда не доступа к Dockerfile:
skopeo inspect docker://camunda/camunda-bpm-platform:wildfly-SNAPSHOT
skopeo copy docker://camunda/camunda-bpm-platform:wildfly-SNAPSHOT dir://tmp/camunda
tree /tmp/camunda/
/tmp/camunda/
├── 0cdfa0c98ed79707cd91c5dd7ebd282aa2b976d86a9e699d7fc188cdb6be390e
├── 16e3566755e5dc026d1a6bcc28c8edc7a7aa6d068af8a5b9e10478c1dd900682
├── 410f32425c8a73d3e62330ab499ae85a1ce97fec74e572c2b50af4d654d44649
├── 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
├── c2005d18c6a3b605fc54bf733127284e0e46adbcd1180532d8d0f06b684525ae
├── dd2f5668f681c59da620c20459eca51d05ac3412e3e37ab24420329452e5fec9
├── manifest.json
└── version
0 directories, 8 files
cat /tmp/camunda/16e3566755e5dc026d1a6bcc28c8edc7a7aa6d068af8a5b9e10478c1dd900682 | jq .
Шифрование
В ситуациях когда необходимо использовать публичный реестр контейнеров для работы с закрытой кодовой базой, skopeo позволяет использовать шифрование:
openssl genrsa -out private_key.pem
openssl rsa -inform pem -outform pem -pubout -in private_key.pem -out public_key.pem
skopeo copy --encryption-key jwe:public_key.pem docker://antonvkrylov/k8s-course:helloPodman docker://antonvkrylov/k8s-course:helloPodman_encrypted
Этот инструмент также можно использовать для изучения структуры образов:
skopeo inspect docker://antonvkrylov/k8s-course:helloPodman
Ответ
{
"Name": "docker.io/antonvkrylov/k8s-course",
"Digest": "sha256:fdede2de3187a7bf0b83cc40567f6f9e71dddb5c0f8840b410c2e628c28a8371",
"RepoTags": [
"helloPodman",
"helloPodman_copy"
],
"Created": "2023-07-15T15:55:52.490530483Z",
"DockerVersion": "",
"Labels": {
"io.buildah.version": "1.29.1"
},
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:38a980f2cc8accf69c23deae6743d42a87eb34a54f02396f3fcfd7c2d06e2c5b",
"sha256:de849f1cfbe60b1c06a1db83a3129ab0ea397c4852b98e3e4300b12ee57ba111",
"sha256:a7203ca35e75e068651c9907d659adc721dba823441b78639fde66fc988f042f",
"sha256:b94c8bc1022b7129e7b55a7e9c522f0bb60f798cc25ef0eab20b368a71dc75b2"
],
"LayersData": [
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:38a980f2cc8accf69c23deae6743d42a87eb34a54f02396f3fcfd7c2d06e2c5b",
"Size": 42114164,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:de849f1cfbe60b1c06a1db83a3129ab0ea397c4852b98e3e4300b12ee57ba111",
"Size": 13525123,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:a7203ca35e75e068651c9907d659adc721dba823441b78639fde66fc988f042f",
"Size": 187528104,
"Annotations": null
},
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:b94c8bc1022b7129e7b55a7e9c522f0bb60f798cc25ef0eab20b368a71dc75b2",
"Size": 18163581,
"Annotations": null
}
],
"Env": [
"PATH=/usr/java/openjdk-17/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_HOME=/usr/java/openjdk-17",
"LANG=C.UTF-8",
"JAVA_VERSION=17.0.2"
]
}
Artifacts
Инструктируем Jenkins сохранять сборочные артефакты и останавливать все запущенные контейнеры после прогона конвейера:
post {
// Clean after build
always {
cleanWs()
}
success {
echo "Build success"
//archiveArtifacts artifacts: '**/*.tar.gz', fingerprint: true
}
failure {
echo "Build failed"
sh "podman stop --all"
}
aborted {
echo "Build aborted"
sh "podman stop --all"
}
}
