Практика. Многоэтапная сборка, сборка с использованием BuildKit
Документация
- Use multi-stage builds
- Использование многоэтапных (multi-stage) сборок в Docker
- Build images with BuildKit
- Docker BuildKit: faster builds, new features, and now it’s stable
Краткий обзор сборщиков
Многоэтапная сборка
Ключевая задача контейнеризации — создание окружения с предсказуемыми параметрами. То есть чтобы мы могли с достаточной степенью уверенности утверждать, что в рамках контейнера нам будут доступны определённые библиотеки, будут доступны конкретные приложения. При этом созданный образ мы можем поместить в хранилище, извлекать его из хранилища в любой момент времени (сейчас, через 5 лет) и создавать на его основе контейнеры.
Данный подход является удобным способом решения множества задач:
- Обеспечения единой платформы для сборки приложения и их эксплуатации.
- Обеспечения единой платформы для работы разработчиков на своих (зачастую сильно отличающихся) рабочих станциях.
- Обеспечения неизменности окружения для работы приложения.
Однако мы не можем объединить в рамках одного контейнера окружения для разработчиков и окружение для развёртывания приложения. Это связано в первую очередь с тем, что в рамках процесса разработки требуются приложения, которые не нужны для его работы. Дополнительные приложения представляют собой не только проблему в размере получающегося образа, но также и с точки зрения безопасности. Чем больше установлено приложений, тем больше векторов атаки может быть реализовано.
Подход с многоэтапной сборкой образов позволяет отделить приложения, необходимые для компиляции целевого приложения, от зависимостей, которые необходимы для его исполнения.
Ознакомьтесь с документацией по использованию многоэтапной сборки.
Передача секретной информации
Зачастую для создания образа необходимо передать информацию, которая не должна быть доступна во время запуска контейнера из данного образа. Это могут быть:
- Информация о расположении ресурсов внутри компании.
- Пароли для доступа к приватным ресурсам.
- Другие элементы, которые не должны быть явно доступны во время анализа образа.
Как Вы знаете, информацию о сборке образа можно получить:
- из состояния файловой системы;
- из истории создания образа,
image history
.
Для решения этой задачи была разработана новая система сборки, которая постепенно внедряется в качестве основной системы сборки образов. Данная система позволяет:
- ускорить процесс сборки;
- передавать секретную информацию с помощью
--secret
; - использовать ключи SSH-ключи хост-системы для доступа к репозиториям.
Ознакомьтесь с документацией. Убедитесь, что понимаете:
- Как перейти на использование новой сборочной системы.
- Как передать секретную информацию в контекст сборки с помощью ключа
--secret
.
Задачи
Задача № 1: многоэтапная сборка C++-приложения
Многоэтапная сборка приложений наиболее привлекательна для компилируемых приложений, так как для сборки приложения необходим компилятор, а для его работы этот компонент не нужен. В рамках такого приложения разделение на этапы становится очевидным:
- В рамках первого этапа устанавливается компилятор и происходит сборка приложения.
-
В рамках второго этапа устанавливаются зависимости для работы приложения и происходит копирование файла-приложения из контейнера первого этапа во второй.
-
Создайте файл приложения main.c со следующим содержимым:
#include <stdio.h> int main() { printf("Hello, world!\n"); return 0; }
- Создайте многоэтапную сборку Dockerfile на основе образа
debian:buster
. - В рамках первого этапа установите средства сборки приложения.
- Скопируйте файл
main.c
в контейнер первого этапа. - Скомпилируйте из него исполняемый файл:
gcc main.c -o hello-world
.
- Скопируйте файл
- В рамках второго этапа скопируйте исполняемый файл
hello-world
из контейнера первого этапа в каталог/usr/bin
в контейнер второго этапа. - Соберите образ на основании сформированного Dockerfile.
- Запустите приложение hello-world в рамках контейнера, созданного с помощью данного образа.
- Изучите историю создания данного образа.
Задача № 2: многоэтапная сборка ruby-приложения
Многоэтапная сборка может быть применена и для Ruby-приложений.
- Интерпретатор языка Ruby может быть скомпилирован из исходных кодов.
- Часть джемов для достижения эффективности выполнения реализованы с использованием компилируемых языков.
Рассмотрим вторую схему по установке зависимостей приложения. Для работы приложения требуется установить джемы, которые зависят от нативной реализации.
В качестве приложения для тестирования данного подхода используйте собственное веб-приложение с курса по Ruby.
Для того, чтобы установить джемы не в интерпретатор Ruby, а в каталог с приложением, используйте команду bundle install --deployment
. В результате данной команды джемы будут установлены в подкаталог vendor/bundle
. Более детальную информацию можно подчерпнуть в официальной документации на bundle install.
Создайте многоэтапную сборку для вашего ruby-приложения.
- В качестве базового образа первого этапа используйте образ
ruby:3.0-buster
. - В рамках сборки добавьте исходные коды своего приложения в первый образ.
- Выполните установку всех зависимостей
bundle install --deployment
. - В качестве базового образа второго этапа используйте образ
ruby:3.0-slim-buster
. - Перенесите каталог своего приложения из образа первого этапа в образ второго этапа.
- Настройте образ так, чтобы в качестве приложения по умолчанию запускалось ваше веб-приложение. Обеспечьте доступ к порту, по которому будет работать веб-приложение.
- Удостоверьтесь, что ваше приложение запускается.
- Оцените объём получившегося образа. Посмотрите на историю его создания.
Задача № 3: использование секретной информации
Выполним имитирование передачи секретной информации для сборки приложения при компиляции приложения на языке Си. Предположим, что приложение устроено следующим образом.
main.c
:
#include <stdio.h>
#include "secret.h"
int main() {
printf("Hello, world!\n");
printf("Secret data: %s\n", SECRET);
return 0;
}
secret.h
:
#define SECRET "put secret here"
В рамках сборки необходимо в файл secret.h
поместить секретную информацию. Будем считать, что эта информация является критически важной для работы приложения.
Подход № 1
- Создайте Dockerfile, описывающий одноступенчатую сборку.
- Оформите передачу секретной информации в виде аргумента сборки,
ARG
. - Выполните копирование файла
main.c
путём копирования с хост-машины. - Сохраняйте файл
secret.h
на основании аргумента, переданного сборке. - Добавьте шаг компиляции приложения.
- Настройте запуск приложения
hello-world
при старте контейнера из данного образа.
Посмотрите на историю созданного образа. Насколько легко получить секретную информацию, которая была использована во время сборки.
Подход № 2
- Настройте Docker на использование нового сборочного механизма.
- Исправьте Dockerfile, созданный на предыдущем этапе, чтобы использовался механизм секретов для передачи скрытой информации.
- Используя полученный Dockerfile, выполните сборку образа.
Посмотрите на историю создания образа. Можно ли из истории сборки получить секретную информацию?
Подход № 3
- Возьмите Dockerfile, реализованный на подходе № 1.
- Переработайте данный файл, чтобы он реализовывал концепцию многоступенчатой сборки.
- Используйте аргумент ARG, аргумент с секретной информацией, в рамках первого этапа сборки.
- Используя полученный Dockerfile создайте образ, содержащий переданную секретную информацию.
Проанализируйте историю создания Dockerfile на предмет наличия в ней следов секретных данных.