Документация

Краткий обзор сборщиков

Многоэтапная сборка

Ключевая задача контейнеризации — создание окружения с предсказуемыми параметрами. То есть чтобы мы могли с достаточной степенью уверенности утверждать, что в рамках контейнера нам будут доступны определённые библиотеки, будут доступны конкретные приложения. При этом созданный образ мы можем поместить в хранилище, извлекать его из хранилища в любой момент времени (сейчас, через 5 лет) и создавать на его основе контейнеры.

Данный подход является удобным способом решения множества задач:

  • Обеспечения единой платформы для сборки приложения и их эксплуатации.
  • Обеспечения единой платформы для работы разработчиков на своих (зачастую сильно отличающихся) рабочих станциях.
  • Обеспечения неизменности окружения для работы приложения.

Однако мы не можем объединить в рамках одного контейнера окружения для разработчиков и окружение для развёртывания приложения. Это связано в первую очередь с тем, что в рамках процесса разработки требуются приложения, которые не нужны для его работы. Дополнительные приложения представляют собой не только проблему в размере получающегося образа, но также и с точки зрения безопасности. Чем больше установлено приложений, тем больше векторов атаки может быть реализовано.

Подход с многоэтапной сборкой образов позволяет отделить приложения, необходимые для компиляции целевого приложения, от зависимостей, которые необходимы для его исполнения.

Ознакомьтесь с документацией по использованию многоэтапной сборки.

Передача секретной информации

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

  • Информация о расположении ресурсов внутри компании.
  • Пароли для доступа к приватным ресурсам.
  • Другие элементы, которые не должны быть явно доступны во время анализа образа.

Как Вы знаете, информацию о сборке образа можно получить:

  • из состояния файловой системы;
  • из истории создания образа, image history.

Для решения этой задачи была разработана новая система сборки, которая постепенно внедряется в качестве основной системы сборки образов. Данная система позволяет:

  • ускорить процесс сборки;
  • передавать секретную информацию с помощью --secret;
  • использовать ключи SSH-ключи хост-системы для доступа к репозиториям.

Ознакомьтесь с документацией. Убедитесь, что понимаете:

  • Как перейти на использование новой сборочной системы.
  • Как передать секретную информацию в контекст сборки с помощью ключа --secret.

Задачи

Задача № 1: многоэтапная сборка C++-приложения

Многоэтапная сборка приложений наиболее привлекательна для компилируемых приложений, так как для сборки приложения необходим компилятор, а для его работы этот компонент не нужен. В рамках такого приложения разделение на этапы становится очевидным:

  1. В рамках первого этапа устанавливается компилятор и происходит сборка приложения.
  2. В рамках второго этапа устанавливаются зависимости для работы приложения и происходит копирование файла-приложения из контейнера первого этапа во второй.

  3. Создайте файл приложения main.c со следующим содержимым:

    #include <stdio.h>
    
    int main() {
      printf("Hello, world!\n");
      return 0;
    }
    
  4. Создайте многоэтапную сборку Dockerfile на основе образа debian:buster.
  5. В рамках первого этапа установите средства сборки приложения.
    • Скопируйте файл main.c в контейнер первого этапа.
    • Скомпилируйте из него исполняемый файл: gcc main.c -o hello-world.
  6. В рамках второго этапа скопируйте исполняемый файл hello-world из контейнера первого этапа в каталог /usr/bin в контейнер второго этапа.
  7. Соберите образ на основании сформированного Dockerfile.
  8. Запустите приложение hello-world в рамках контейнера, созданного с помощью данного образа.
  9. Изучите историю создания данного образа.

Задача № 2: многоэтапная сборка ruby-приложения

Многоэтапная сборка может быть применена и для Ruby-приложений.

  • Интерпретатор языка Ruby может быть скомпилирован из исходных кодов.
  • Часть джемов для достижения эффективности выполнения реализованы с использованием компилируемых языков.

Рассмотрим вторую схему по установке зависимостей приложения. Для работы приложения требуется установить джемы, которые зависят от нативной реализации.

В качестве приложения для тестирования данного подхода используйте собственное веб-приложение с курса по Ruby.

Для того, чтобы установить джемы не в интерпретатор Ruby, а в каталог с приложением, используйте команду bundle install --deployment. В результате данной команды джемы будут установлены в подкаталог vendor/bundle. Более детальную информацию можно подчерпнуть в официальной документации на bundle install.

Создайте многоэтапную сборку для вашего ruby-приложения.

  1. В качестве базового образа первого этапа используйте образ ruby:3.0-buster.
  2. В рамках сборки добавьте исходные коды своего приложения в первый образ.
  3. Выполните установку всех зависимостей bundle install --deployment.
  4. В качестве базового образа второго этапа используйте образ ruby:3.0-slim-buster.
  5. Перенесите каталог своего приложения из образа первого этапа в образ второго этапа.
  6. Настройте образ так, чтобы в качестве приложения по умолчанию запускалось ваше веб-приложение. Обеспечьте доступ к порту, по которому будет работать веб-приложение.
  7. Удостоверьтесь, что ваше приложение запускается.
  8. Оцените объём получившегося образа. Посмотрите на историю его создания.

Задача № 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

  1. Создайте Dockerfile, описывающий одноступенчатую сборку.
  2. Оформите передачу секретной информации в виде аргумента сборки, ARG.
  3. Выполните копирование файла main.c путём копирования с хост-машины.
  4. Сохраняйте файл secret.h на основании аргумента, переданного сборке.
  5. Добавьте шаг компиляции приложения.
  6. Настройте запуск приложения hello-world при старте контейнера из данного образа.

Посмотрите на историю созданного образа. Насколько легко получить секретную информацию, которая была использована во время сборки.

Подход № 2

  1. Настройте Docker на использование нового сборочного механизма.
  2. Исправьте Dockerfile, созданный на предыдущем этапе, чтобы использовался механизм секретов для передачи скрытой информации.
  3. Используя полученный Dockerfile, выполните сборку образа.

Посмотрите на историю создания образа. Можно ли из истории сборки получить секретную информацию?

Подход № 3

  1. Возьмите Dockerfile, реализованный на подходе № 1.
  2. Переработайте данный файл, чтобы он реализовывал концепцию многоступенчатой сборки.
  3. Используйте аргумент ARG, аргумент с секретной информацией, в рамках первого этапа сборки.
  4. Используя полученный Dockerfile создайте образ, содержащий переданную секретную информацию.

Проанализируйте историю создания Dockerfile на предмет наличия в ней следов секретных данных.