Практика. Многоэтапная сборка, сборка с использованием 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 на предмет наличия в ней следов секретных данных.