
Для моего пет-проекта потребовалось запускать сборку документации с помощью Hugo. Использовать бинарник показалось не самым удобным вариантом — хотелось большей универсальности. Здесь на помощь приходят контейнеры.
Контейнеры можно применять как универсальные модули, расширяющие функциональность системы. Данные можно передать в контейнер через подключенные каталоги или файлы, а результат работы получить в других файлах. Именно поэтому контейнеры так популярны в пайплайнах сборки практически любого современного CI/CD.
Общая схема работы проста как две копейки:
- Берём готовый контейнер для задачи или публикуем собственный.
- Пишем код, который запускает контейнер и обрабатывает результаты его работы.
В этой статье я расскажу о втором пункте — как запускать контейнеры из Go-кода. Есть два основных способа:
- Использовать классический подход с
exec.Command
:cmd := exec.Command("docker", "run", "chainguard/hugo", "arg1", "arg2")
- Воспользоваться API для работы с контейнерами.
Запуск Docker через API
Начнём с запуска контейнеров через Docker API. Хотя стоит отметить, что контейнеры можно запускать и без самого Docker (но об этом позже).
Для работы потребуется пакет:
"github.com/docker/docker/client"
Создаём экземпляр клиента:
import (
"github.com/docker/docker/client"
"github.com/docker/docker/api/types/container"
)
// ...
cli, err := client.NewClientWithOpts(
client.WithVersion("1.41"), // Явно указываем версию API
client.WithHost("unix:///var/run/docker.sock"),
client.FromEnv,
)
if err != nil {
return fmt.Errorf("error creating docker client: %s", err)
}
Готовим конфигурацию контейнера. Нужно указать используемый образ и подключить необходимые директории:
config := &container.Config{
Image: "klakegg/hugo", // Образ для запуска
Cmd: []string{}, // Команда (если нужна)
Tty: true,
}
hostConfig := &container.HostConfig{
Binds: []string{
"/tmp/example:/src", // Только чтение
},
AutoRemove: true, // Автоудаление после остановки (--rm)
}
Затем создаём и запускаем контейнер:
// Создаём контейнер
resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, "")
if err != nil {
return fmt.Errorf("error creating container: %s", err)
}
// Запускаем контейнер
if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
return fmt.Errorf("error starting container: %s", err)
}
return nil
Docker — это просто и понятно, и в сети много материалов на эту тему. Поэтому перейдём к чему-то более интересному.
Запуск Podman через API
Docker уже не так актуален. Для своих пет-проектов я постепенно перехожу на Podman. Он предоставляет схожую среду для запуска контейнеров и поддерживает большинство команд Docker.
В отличие от Docker, Podman не требует отдельного демона, что делает его легче и безопаснее. Также он лучше поддерживает запуск контейнеров без прав root.
Хотя Podman менее распространён, он активно развивается Red Hat с упором на open-source и совместимость с Kubernetes. Правда, сообщество пока меньше.
Для работы с Podman из Go потребуется пакет:
"github.com/containers/podman/v5/pkg/bindings"
Подготовим соединение и загрузим образ:
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/bindings/containers"
"github.com/containers/podman/v5/pkg/bindings/images"
"github.com/containers/podman/v5/pkg/specgen"
spec "github.com/opencontainers/runtime-spec/specs-go"
// ...
conn, err := bindings.NewConnection(context.Background(), "unix:///run/podman/podman.sock")
if err != nil {
return fmt.Errorf("error connecting to podman: %w", err)
}
_, err = images.Pull(conn, "mirror.gcr.io/chainguard/hugo", nil)
if err != nil {
return fmt.Errorf("error pulling image: %w", err)
}
Создаём контейнер и настраиваем подключение директории:
s := specgen.NewSpecGenerator("mirror.gcr.io/chainguard/hugo", false)
s.Mounts = []spec.Mount{
{
Source: "/tmp/example",
Destination: "/hugo",
Type: "bind",
},
}
s.Command = []string{}
s.Remove = &remove // (--rm)
create, err := containers.CreateWithSpec(conn, s, nil)
if err != nil {
return fmt.Errorf("error creating container: %w", err)
}
Запускаем контейнер:
if err := containers.Start(conn, create.ID, nil); err != nil {
return fmt.Errorf("error starting container: %w", err)
}
Готово! Самый большой недостаток работы с Podman через Go — это отсутствие внятной документации. Кроме README в репозитории и старой статьи на официальном сайте, полезных материалов почти нет.