[DevOps, Облачные сервисы, Kubernetes] Как сократить время сборки образов Docker в GitLab CI (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Делаем контейнерные CI среды по-настоящему практичными, ускорив сборку образов Docker.Современный цикл разработки программного обеспечения зачастую подразумевает, что ваши приложения регулярно упаковываются в контейнеры. Эта задача может занимать много времени, чем может значительно замедлять ваше тестирование или развертывание. Проблема становится особенно очевидной в контексте процесса непрерывной интеграции и развертывания, когда образы пересобираются при каждом изменении в кодеВ этой статье мы обсудим различные способы ускорения сборки образов Docker в конвейере непрерывной интеграции путем реализации различных стратегий.Локальная упаковка приложенияВ качестве примера мы возьмем достаточно простое приложение Python Flask:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
Написание DockerfileДавайте напишем соответствующий Dockerfile:
FROM python:3.7-alpine as builder
# установка зависимостей, необходимых для сборки пакетов python
RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip
# настройка venv и загрузка или сборка зависимостей
ENV VENV="/venv"
ENV PATH="${VENV}/bin:${PATH}"
COPY requirements.txt .
RUN python -m venv ${VENV} \
&& pip install --no-cache-dir -r requirements.txt
FROM python:3.7-alpine
# настройки venv с зависимостями с этапа компоновки
ENV VENV="/venv"
ENV PATH="${VENV}/bin:$PATH"
COPY --from=builder ${VENV} ${VENV}
# копирование файлов приложения
WORKDIR /app
COPY app .
# запуск приложения
EXPOSE 5000
ENV FLASK_APP="hello.py"
CMD [ "flask", "run", "--host=0.0.0.0" ]
Здесь вы можете наблюдать классический многоступенчатыйпроцесссборки:
- Мы начинаем с легкого базового образа, в который мы устанавливаем инструменты сборки и загружаем или компилируем зависимости в виртуальную среду Python.
- На втором этапе мы копируем виртуальную среду с нашими зависимостями в целевой образ и, наконец, добавляем файлы приложения.
Почему этот процесс выполняется в два этапа? Во-первых, потому что вы получаете секьюрный процесс сборки, поскольку он выполняется в контейнере без вмешательства среды хоста. Во-вторых, вы получаете облегченный финальный образ без всех библиотек сборки, а только с тем, что требуется для запуска приложения.Запуск и тестирование образа.Убедитесь, что все работает должным образом:
docker build -t hello .
docker run -d --rm -p 5000:5000 hello
curl localhost:5000
Hello, World!
Если вы запустите команду docker build во второй раз:
docker build -t hello .
...
Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip
---> Using cache
---> 24d044c28dce
...
Как видите, вторая сборка происходит намного быстрее, так как слои кэшируются в вашей локальной службе Docker и используются повторно, если в них не вносили изменений.Отправка образаДавайте опубликуем наш образ во внешнем реестре и посмотрим, что произойдет:
docker tag hello my-registry/hello:1.0
docker push my-registry/hello:1.0
The push refers to repository [my-registry/hello]
8388d558f57d: Pushed
77a59788172c: Pushed
673c6888b7ef: Pushed
fdb8581dab88: Pushed
6360407af3e7: Pushed
68aa0de28940: Pushed
f04cc38c0ac2: Pushed
ace0eda3e3be: Pushed
latest: digest: sha256:d815c1694083ffa8cc379f5a52ea69e435290c9d1ae629969e82d705b7f5ea95 size: 1994
Обратите внимание, как каждый из промежуточных слоев идентифицируется хэшем. Мы можем насчитать 8 слоев, потому что у нас есть ровно 8 docker команд в Dockerfile поверх нашей последней инструкции FROM.Важно понимать, что слои из нашего базового образа компоновщика не отправляются в удаленный реестр Docker, когда мы пушим наш образ, пушатся только слои из последнего этапа. Однако промежуточные слои по-прежнему кэшируются в локальном демоне Docker и их можно повторно использовать для вашей следующей локальной команды сборки.С локальной сборкой все достаточно просто, давайте теперь посмотрим, как это будет работать в CI среде.Сборка образа Docker в контексте CI конвейера В реальных условиях сборка и отправка образов Docker не всегда выполняется локально, как мы только что это делали. Обычно она выполняется внутри платформы непрерывной интеграции и развертывания. Перед развертыванием приложения вам нужно собирать и пушить свой образ при каждом изменении в коде. Естественно время сборки имеет решающее значение, так как вам нужен максимально быстрый цикл обратной связи.Тестовая CI среда Мы реализуем CI среду, используя:
- Kubernetes Executor для хостинга GitLab Runner
Последний пункт важен, потому что наши CI задачи будут выполняться в контейнерной среде. Учитывая это, каждая задача создается в виде пода Kubernetes. Каждое современное CI решение использует контейнерные задачи, и при создании Docker контейнеров все они сталкиваются с одной и той же проблемой: вам нужно заставить Docker команды работать внутри Docker контейнера.Чтобы все прошло гладко, у вас есть два пути:
- Забиндить /var/run/docker.sock, который слушает демон Docker, сделав демон хоста доступным для нашего контейнера задач.
- Использовать дополнительный контейнер, запускающий «Docker in Docker» (также известный как dind) вместе с вашей задачей. Dind - это особый вариант Docker, работающий с привилегиями и настроенный для работы внутри самого Docker ?
Для нашего примера мы будем использовать второй вариант.Реализация GitLab конвейера В GitLab конвейере обычно вы создаете служебные контейнеры, такие как DinD, с помощью ключевого слова service.В приведенном ниже фрагменте конвейера и задача docker-build, и служебный dind контейнер будут выполняться в одном и том же поде Kubernetes. Когда в сценарии задачи используется docker, он отправляет команды вспомогательному dind контейнеру благодаря переменной среды DOCKER_HOST.
stages:
- build
- test
- deploy
variables:
# отключаем проверку Docker TLS
DOCKER_TLS_CERTDIR: ""
# адрес localhost используется как контейнером задачи, так и dind контейнером (поскольку они используют один и тот же под)
# Таким образом, при выполнении команд Docker эта конфигурация делает службу dind нашим демоном Docker
DOCKER_HOST: "tcp://localhost:2375"
services:
- docker:stable-dind
docker-build:
image: docker:stable
stage: build
script:
- docker build -t hello .
- docker tag my-registry/hello:${CI_COMMIT_SHORT_SHA}
- docker push my-registry/hello:${CI_COMMIT_SHORT_SHA}
Запуск конвейераЭтот конвейер должен работать нормально. Запустив его один раз и проверив вывод задачи, мы получим:
docker build -t hello .
Step 1/15 : FROM python:3.7-alpine as builder
...
Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip
---> Running in ca50f59a21f8
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
...
Поскольку мы впервые создаем наш контейнер, каждый слой создается путем выполнения команд. Общее время выполнения задачи составляет около 1 минуты.Если вы запустите конвейер во второй раз, ничего не меняя, вы должны увидеть то же самое: каждый слой перестраивается! Когда мы запускали наши команды сборки локально, кэшированные слои использовались повторно. А здесь - нет. Для такого простого образа это на самом деле не имеет значения, но в реальной жизни, где для создания некоторых образов могут потребоваться десятки минут, это может стать настоящей проблемой.Почему так происходит? Просто потому, что в этом случае dind - это временный контейнер, который создается вместе с задачей и умирает после ее завершения, поэтому все кэшированные данные теряются. К сожалению, вы не можете легко сохранить данные между двумя запусками конвейера.Как же нам получить выгоду от кэширования и по-прежнему использовать Dind-контейнер?Использование кэша Docker вместе с Docker in DockerПервое решение: Pull/Push танцыПервое решение довольно простое: мы будем использовать наш удаленный реестр (тот, в который мы пушим) в качестве удаленного кэша для наших слоев.Если быть точнее:
- Мы начинаем с извлечения (pull) самого актуального образа (т. е. последнего) из удаленного реестра, который будет использоваться в качестве кэша для последующей docker команды сборки.
- Затем мы создаем образ, используя извлеченный образ в качестве кэша (аргумент --cache-from), если он доступен. Мы помечаем эту новую сборку в качестве последней и коммитим SHA.
- Наконец, мы помещаем оба образа с тегами в удаленный реестр, чтобы их также можно было использовать в качестве кэша для последующих сборок.
stages:
- build
- test
- deploy
variables:
# отключаем проверку Docker TLS
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: "tcp://localhost:2375"
services:
- docker:stable-dind
docker-build:
image: docker:stable
stage: build
script:
- docker pull my-registry/hello:latest || true
- docker build --cache-from my-registry/hello:latest -t hello:latest .
- docker tag hello:latest my-registry/hello:${CI_COMMIT_SHORT_SHA}
- docker tag hello:latest my-registry/hello:latest
- docker push my-registry/hello:${CI_COMMIT_SHORT_SHA}
- docker push my-registry/hello:latest
Если вы запустите этот новый конвейер два раза, разница от использования кэша все равно будет неудовлетворительной.Все слои из базового образа компоновщика пересобираются. Только первые 2 слоя (8 и 9) заключительного этапа используют кэш, но последующие слои перестраиваются.Как мы видели ранее, при локальной отправке нашего образа слои базового образа компоновщика не помещаются в удаленный реестр и фактически теряются. Следовательно, когда мы извлекаем последние образы, их там нет, и их нужно строить заново.Затем, когда наш финальный образ собран (шаги с 8 по 15), первые два слоя присутствуют в образе, который мы извлекли и использовали в качестве кэша. Но на шаге 10 мы получаем зависимости образа компоновщика, которые изменились, поэтому все последующие шаги также строятся заново.Подводя итог, можно сказать, что использование кэша в значительной степени ограничено: только 2 шага из 15 используют кэш! Чтобы исправить это, нам нужно отправлять образ промежуточного компоновщика в удаленный реестр, чтобы сохранить его слои:
stages:
- build
- test
- deploy
variables:
# отключаем проверку Docker TLS
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: "tcp://localhost:2375"
services:
- docker:stable-dind
docker-build:
image: docker:stable
stage: build
script:
- docker pull my-registry/hello-builder:latest || true
- docker pull my-registry/hello:latest || true
- docker build --cache-from my-registry/hello-builder:latest --target builder -t hello-builder:latest .
- docker build --cache-from my-registry/hello:latest --cache-from my-registry/hello-builder:latest -t hello:latest .
- docker tag hello-builder:latest my-registry/hello-builder:latest
- docker tag hello:latest my-registry/hello:${CI_COMMIT_SHORT_SHA}
- docker tag hello:latest my-registry/hello:latest
- docker push my-registry/hello-builder:latest
- docker push my-registry/hello:${CI_COMMIT_SHORT_SHA}
- docker push my-registry/hello:latest
Мы создаем промежуточный этап нашего сборщика в качестве образ докера, используя опцию target. После этого мы пушим его в удаленный реестр, в конечном итоге извлекая его в качестве кэша для создания нашего финального образа. При запуске конвейера наше время сократилось до 15 секунд!Как видите, сборка постепенно усложняется. Если вы уже начинаете путаться, тогда представьте образ с 3 или 4 промежуточными стадиями! Но это метод работает. Другой недостаток заключается в том, что вам придется каждый раз загружать и выгружать все эти слои, что может быть довольно дорогостоящим с точки зрения затрат на хранение и передачу.Второе решение: внешняя служба dindНам нужно запустить службу dind для выполнения нашей сборки docker. В нашем предыдущем решении dind был встроен в каждую задачу и разделял ее жизненный цикл, что делало невозможным создание надлежащего кэша.Почему бы не сделать Dind гражданином первого класса, создав службу Dind в нашем кластере Kubernetes? Она будет работать с подключенным PersistentVolume для обработки кэшированных данных, и каждая задача может отправлять свои docker команды в эту общую службу.Создать такую службу в Kubernetes достаточно просто:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: docker-dind
name: dind
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: docker-dind
name: dind
spec:
replicas: 1
selector:
matchLabels:
app: docker-dind
template:
metadata:
labels:
app: docker-dind
spec:
containers:
- image: docker:19.03-dind
name: docker-dind
env:
- name: DOCKER_HOST
value: tcp://0.0.0.0:2375
- name: DOCKER_TLS_CERTDIR
value: ""
volumeMounts:
- name: dind-data
mountPath: /var/lib/docker/
ports:
- name: daemon-port
containerPort: 2375
protocol: TCP
securityContext:
privileged: true # Требуется для работы dind контейнера.
volumes:
- name: dind-data
persistentVolumeClaim:
claimName: dind
---
apiVersion: v1
kind: Service
metadata:
labels:
app: docker-dind
name: dind
spec:
ports:
- port: 2375
protocol: TCP
targetPort: 2375
selector:
app: docker-dind
Затем мы немного изменим наш исходный GitLab конвейер, чтобы он указывал на эту новую внешнюю службу, и удалим встроенные dind службы:
stages:
- build
- test
- deploy
variables:
# отключаем проверку Docker TLS
DOCKER_TLS_CERTDIR: ""
# здесь имя хоста dind разрешается как dind служба Kubernetes с помощью kube dns
DOCKER_HOST: "tcp://dind:2375"
docker-build:
image: docker:stable
stage: build
script:
- docker build -t hello .
- docker tag hello:latest my-registry/hello:{CI_COMMIT_SHORT_SHA}
- docker push my-registry/hello:{CI_COMMIT_SHORT_SHA}
Если вы запустите конвейер дважды, второй раз сборка должна занять 10 секунд, что даже лучше, чем в нашем предыдущем решении. Для «большого» образа, для построения которого требуется около 10 минут, эта стратегия также сокращает время построения до нескольких секунд, если слои не изменялись.И последний вариант: использование KanikoПоследним вариантом может быть использование Kaniko. С его помощью вы можете создавать образы Docker без использования демона Docker, делая все, что мы сейчас делали, без особых проблем.Однако обратите внимание, что при этом вы не можете использовать расширенные BuildKitопции, такие как, например, внедрение секретов при создании образа. По этой причине я сконцентрировался на другом решении.ЗаключениеПоскольку при разработке программного обеспечения контейнеры широко используются практически повсюду, их эффективное создание является ключевым моментом в конвейере релиза. Как мы видели, проблема может стать довольно сложной, и каждое решение имеет свои компромиссы. Предлагаемые здесь решения проиллюстрированы с использованием GitLab, но стоит отметить, что они будут работать и в других контейнерных CI средах.А прямо сейчас приглашаем ва ознакомиться с программой супер-интенсива "CI/CD или Непрерывная поставка с Docker и Kubernetes", а таже записаться на день открытых дверей.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Emmanuel Sys
===========Похожие новости:
- [IT-инфраструктура, Облачные сервисы, Социальные сети и сообщества] В сети раскритиковали затратность технической архитектуры Parler
- [Разработка веб-сайтов, DevOps, VueJS, Облачные сервисы] Пишем мессенджер на Vue в облаке Amazon (перевод)
- [Программирование, C, ООП] Трюки с виртуальной памятью (перевод)
- [Программирование, C#, Графический дизайн] Создание Dockers в Corel Draw
- [Тестирование IT-систем, Agile, Управление продуктом, Софт] На ком лежит ответственность за качество программного обеспечения? (перевод)
- [Администрирование доменных имен, Хранение данных, Облачные сервисы, IT-компании] Sci-Hub теперь находится в «нецензурируемой» сети
- [Системное администрирование, IT-инфраструктура, Серверное администрирование, DevOps] Сервер Prometheus и TLS (перевод)
- [Java, Eclipse, NoSQL, Kubernetes] Фирма «1С» приглашает вас принять участие в нашей первой конференции для системных разработчиков
- [Программирование, .NET, Amazon Web Services, C#, DevOps] Nuke: настраиваем сборку и публикацию .NET-проекта
- [CRM-системы, Облачные сервисы] Обзор набора программ Zoho One
Теги для поиска: #_devops, #_oblachnye_servisy (Облачные сервисы), #_kubernetes, #_kubernetes, #_cicd, #_gitlab, #_docker, #_devops, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_devops, #_oblachnye_servisy (
Облачные сервисы
), #_kubernetes
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:23
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Делаем контейнерные CI среды по-настоящему практичными, ускорив сборку образов Docker.Современный цикл разработки программного обеспечения зачастую подразумевает, что ваши приложения регулярно упаковываются в контейнеры. Эта задача может занимать много времени, чем может значительно замедлять ваше тестирование или развертывание. Проблема становится особенно очевидной в контексте процесса непрерывной интеграции и развертывания, когда образы пересобираются при каждом изменении в кодеВ этой статье мы обсудим различные способы ускорения сборки образов Docker в конвейере непрерывной интеграции путем реализации различных стратегий.Локальная упаковка приложенияВ качестве примера мы возьмем достаточно простое приложение Python Flask: from flask import Flask
app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, World!' FROM python:3.7-alpine as builder
# установка зависимостей, необходимых для сборки пакетов python RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip # настройка venv и загрузка или сборка зависимостей ENV VENV="/venv" ENV PATH="${VENV}/bin:${PATH}" COPY requirements.txt . RUN python -m venv ${VENV} \ && pip install --no-cache-dir -r requirements.txt FROM python:3.7-alpine # настройки venv с зависимостями с этапа компоновки ENV VENV="/venv" ENV PATH="${VENV}/bin:$PATH" COPY --from=builder ${VENV} ${VENV} # копирование файлов приложения WORKDIR /app COPY app . # запуск приложения EXPOSE 5000 ENV FLASK_APP="hello.py" CMD [ "flask", "run", "--host=0.0.0.0" ]
docker build -t hello .
docker run -d --rm -p 5000:5000 hello curl localhost:5000 Hello, World! docker build -t hello .
... Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip ---> Using cache ---> 24d044c28dce ... docker tag hello my-registry/hello:1.0
docker push my-registry/hello:1.0 The push refers to repository [my-registry/hello] 8388d558f57d: Pushed 77a59788172c: Pushed 673c6888b7ef: Pushed fdb8581dab88: Pushed 6360407af3e7: Pushed 68aa0de28940: Pushed f04cc38c0ac2: Pushed ace0eda3e3be: Pushed latest: digest: sha256:d815c1694083ffa8cc379f5a52ea69e435290c9d1ae629969e82d705b7f5ea95 size: 1994
stages:
- build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" # адрес localhost используется как контейнером задачи, так и dind контейнером (поскольку они используют один и тот же под) # Таким образом, при выполнении команд Docker эта конфигурация делает службу dind нашим демоном Docker DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker build -t hello . - docker tag my-registry/hello:${CI_COMMIT_SHORT_SHA} - docker push my-registry/hello:${CI_COMMIT_SHORT_SHA} docker build -t hello .
Step 1/15 : FROM python:3.7-alpine as builder ... Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip ---> Running in ca50f59a21f8 fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz ...
stages:
- build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker pull my-registry/hello:latest || true - docker build --cache-from my-registry/hello:latest -t hello:latest . - docker tag hello:latest my-registry/hello:${CI_COMMIT_SHORT_SHA} - docker tag hello:latest my-registry/hello:latest - docker push my-registry/hello:${CI_COMMIT_SHORT_SHA} - docker push my-registry/hello:latest stages:
- build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker pull my-registry/hello-builder:latest || true - docker pull my-registry/hello:latest || true - docker build --cache-from my-registry/hello-builder:latest --target builder -t hello-builder:latest . - docker build --cache-from my-registry/hello:latest --cache-from my-registry/hello-builder:latest -t hello:latest . - docker tag hello-builder:latest my-registry/hello-builder:latest - docker tag hello:latest my-registry/hello:${CI_COMMIT_SHORT_SHA} - docker tag hello:latest my-registry/hello:latest - docker push my-registry/hello-builder:latest - docker push my-registry/hello:${CI_COMMIT_SHORT_SHA} - docker push my-registry/hello:latest apiVersion: v1
kind: PersistentVolumeClaim metadata: labels: app: docker-dind name: dind spec: accessModes: - ReadWriteOnce resources: requests: storage: 500Gi --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: docker-dind name: dind spec: replicas: 1 selector: matchLabels: app: docker-dind template: metadata: labels: app: docker-dind spec: containers: - image: docker:19.03-dind name: docker-dind env: - name: DOCKER_HOST value: tcp://0.0.0.0:2375 - name: DOCKER_TLS_CERTDIR value: "" volumeMounts: - name: dind-data mountPath: /var/lib/docker/ ports: - name: daemon-port containerPort: 2375 protocol: TCP securityContext: privileged: true # Требуется для работы dind контейнера. volumes: - name: dind-data persistentVolumeClaim: claimName: dind --- apiVersion: v1 kind: Service metadata: labels: app: docker-dind name: dind spec: ports: - port: 2375 protocol: TCP targetPort: 2375 selector: app: docker-dind stages:
- build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" # здесь имя хоста dind разрешается как dind служба Kubernetes с помощью kube dns DOCKER_HOST: "tcp://dind:2375" docker-build: image: docker:stable stage: build script: - docker build -t hello . - docker tag hello:latest my-registry/hello:{CI_COMMIT_SHORT_SHA} - docker push my-registry/hello:{CI_COMMIT_SHORT_SHA} =========== Источник: habr.com =========== =========== Автор оригинала: Emmanuel Sys ===========Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_devops, #_oblachnye_servisy ( Облачные сервисы ), #_kubernetes |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:23
Часовой пояс: UTC + 5