[Перевод] Запуск R-скриптов в Docker

Это любительский перевод статьи «Running your R Script in Docker», опубликованной на сайте STATWORX. Оригинал статьи объясняет, как настроить и запустить R-скрипт внутри Docker-контейнера, делая код воспроизводимым, изолированным и готовым к автоматическому выполнению.

Перевод предназначен для тех, кто интересуется интеграцией R и Docker. В статье освещаются основные этапы создания Docker-образа с R-скриптом, его настройка и запуск.

Мой канал — https://t.me/tidy_mng

С момента выхода в 2014 году Docker стал важнейшим инструментом для развертывания приложений. В компании STATWORX язык R занимает центральное место в ежедневной работе. Поэтому появление проекта Rocker от RStudio, который значительно упростил контейнеризацию R-кода, вызвало у нас настоящий восторг.

Контейнеризация полезна в самых разных ситуациях. На мой взгляд, она особенно удобна при развертывании R-кода в облачной среде, где рабочие процессы должны выполняться по заданному расписанию. Docker идеально подходит для таких задач по двум причинам: во-первых, вы можете легко настроить запуск контейнера с нужной периодичностью, а во-вторых, благодаря статичности контейнеров вы всегда знаете, какого поведения и какого результата ожидать.

Если вам нужно развернуть модель машинного обучения, которая регулярно делает предсказания, рассмотрите использование Docker. В этом блоге я шаг за шагом покажу, как настроить выполнение R-скрипта в Docker-контейнере. Для упрощения мы будем работать с локальным набором данных.

Сразу хочу отметить, что эта статья не является общим руководством по работе с Docker. Если вы не знаете, что такое образы и контейнеры, рекомендую сначала изучить Docker Curriculum. Если же вас интересует запуск RStudio внутри контейнера Docker, то обратите внимание на руководство OpenSciLabs.

Данная статья посвящена контейнеризации R-скрипта, который будет автоматически выполняться при каждом запуске контейнера, без необходимости какого-либо взаимодействия с пользователем. Это устраняет потребность в использовании RStudio IDE. Основы работы с Dockerfile и командной строкой будут рассмотрены лишь поверхностно, поэтому перед чтением статьи лучше ознакомиться с базовыми принципами работы с Docker.

Что нам понадобится

Для выполнения всей процедуры нам понадобятся следующие элементы:

R-скрипт, который мы встроим в Docker-образ.

Базовый образ, на основе которого мы создадим наш новый образ.

Dockerfile, содержащий инструкции для создания нового образа.

Вы можете склонировать все необходимые файлы и структуру папок из GitHub-репозитория STATWORX.

d968ee346f2180f642b82c55476c70c2.png

R-скрипт

Мы работаем с очень простым R-скриптом, который выполняет следующие шаги:

1. Импортирует датафрейм.

2. Обрабатывает данные.

3. Создает график на основе обработанных данных.

4. Экспортирует как график, так и исходные данные.


Для примера используется набор данных US 500 Records, предоставленный Брайаном Даннингом. Если вы хотите следовать примеру, скопируйте этот набор данных в папку 01_data.

library(readr)
library(dplyr)
library(ggplot2)
library(forcats)

# import dataframe
df <- read_csv("01_data/us-500.csv")

# manipulate data
plot_data <- df %>%
  group_by(state) %>%
  count()

# save manipulated data to output folder
write_csv(plot_data, "03_output/plot_data.csv")

# create plot based on manipulated data
plot <- plot_data %>% 
  ggplot()+
  geom_col(aes(fct_reorder(state, n), 
               n, 
               fill = n))+
  coord_flip()+
  labs(
    title = "Number of people by state",
    subtitle = "From US-500 dataset",
    x = "State",
    y = "Number of people"
  )+ 
  theme_bw()

# save plot to output folder
ggsave("03_output/myplot.png", width = 10, height = 8, dpi = 100)

Это создаёт простой столбчатый график на основе нашего набора данных:

e028ff54e7067db04e98e21ceeeaf6c3.png

Мы используем этот скрипт не только для выполнения R-кода внутри Docker-контейнера, но также для работы с данными, находящимися за пределами контейнера, с последующим сохранением результатов.

Базовый образ

В качестве базового образа для нашего проекта мы используем один из репозиториев проекта Rocker, доступных на DockerHub.

Так как в нашем скрипте используются пакеты из Tidyverse, очевидным выбором является образ rocker/tidyverse. Однако этот образ включает RStudio, что не требуется для текущего проекта. Поэтому вместо него мы будем использовать образ rocker/r-base и построим наш собственный образ с поддержкой Tidyverse.

Для загрузки базового образа выполните следующую команду в терминале:

docker pull rocker/r-base

После загрузки мы можем запустить контейнер на основе этого образа:

docker run -it --rm rocker/r-base

Поздравляем! Теперь вы запустили R внутри Docker-контейнера! Терминал превратился в R-консоль, с которой вы можете взаимодействовать благодаря аргументу -it. Аргумент --rm гарантирует, что контейнер автоматически удалится после его остановки.

Вы можете поэкспериментировать с сессией R внутри контейнера. Чтобы выйти, выполните функцию q() из R-консоли. Например, вы можете начать установку необходимых для вашего рабочего процесса пакетов с помощью команды install.packages(). Однако этот процесс часто бывает утомительным и занимает много времени.

Гораздо эффективнее заранее включить необходимые пакеты в образ, чтобы каждый раз не устанавливать их вручную. Для этого нам понадобится Dockerfile.

Dockerfile

С помощью Dockerfile мы указываем Docker, как создать наш новый образ. Dockerfile — это текстовый файл, который должен называться Dockerfile.txt и по умолчанию располагаться в корневой директории контекста сборки (в нашем случае это папка R-Script in Docker).

Во-первых, необходимо указать базовый образ, на основе которого будет создаваться новый. Далее, в зависимости от наших требований к образу, мы добавляем список инструкций, чтобы работа контейнеров была максимально простой и эффективной. В данном случае в качестве базового образа мы используем ранее упомянутый rocker/r-base.

Затем мы воспроизводим локальную структуру папок, чтобы указать нужные директории в Dockerfile. После этого копируем файлы, которые должны быть доступны из образа, в эти директории — так мы добавляем наш R-скрипт в Docker-образ.

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

Просто скопировать R-скрипт недостаточно — нужно также настроить автоматическое его выполнение при запуске контейнера. Таким образом, мы получаем наш первый Dockerfile!

# Base image https://hub.docker.com/u/rocker/
FROM rocker/r-base:latest

## create directories
RUN mkdir -p /01_data
RUN mkdir -p /02_code
RUN mkdir -p /03_output

## copy files
COPY /02_code/install_packages.R /02_code/install_packages.R
COPY /02_code/myScript.R /02_code/myScript.R

## install R-packages
RUN Rscript /02_code/install_packages.R

Не забудьте подготовить и сохранить скрипт install_packages.R, в котором будут указаны R-пакеты, необходимые для предварительной установки в вашем образе.

В нашем случае файл будет выглядеть примерно так:

install.packages("readr")
install.packages("dplyr")
install.packages("ggplot2")
install.packages("forcats")

Сборка и запуск образа

Теперь у нас собраны все необходимые компоненты для создания нового Docker-образа. Используйте терминал, чтобы перейти в папку, где находится ваш Dockerfile, и выполните команду для сборки образа:

docker build -t myname/myimage .

-t myname/myimage: задает имя вашего образа (в данном случае myname/myimage).

.: указывает текущую директорию как контекст сборки.

Процесс сборки может занять некоторое время из-за установки пакетов. После завершения вы можете протестировать новый образ, запустив контейнер с помощью команды:

docker run -it --rm -v ~/"R-Script in Docker"/01_data:/01_data -v ~/"R-Script in Docker"/03_output:/03_output myname/myimage

Использование аргумента -v указывает Docker, какие локальные папки привязать к созданным папкам внутри контейнера. Это важно, так как мы хотим, чтобы контейнер мог получить доступ к нашему датафрейму, а также сохранить результаты работы локально, чтобы они не терялись после остановки контейнера.

Теперь этот контейнер может взаимодействовать с нашим датафреймом, который находится в папке 01_data, и имеет копию нашего рабочего скрипта в своей внутренней папке 02_code.

Выполнение команды source("02_code/myScript.R") в R запустит скрипт и сохранит результаты в папку 03_output внутри контейнера. Эти результаты также будут автоматически скопированы в локальную папку 03_output.

286e9e46bfc3fd0d6108500837c2aded.png

Улучшение существующей настройки

После того как мы протестировали и убедились, что наш R-скрипт успешно выполняется в контейнере, осталось исправить несколько недочетов.

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

Для этого мы добавим в Dockerfile последнюю команду, которая будет указывать на автоматическое выполнение скрипта. Например:

## run the script
CMD Rscript /02_code/myScript.R

Эта команда указывает на местоположение нашего скрипта внутри структуры папок контейнера, помечает его как R-скрипт и настраивает его автоматическое выполнение при каждом запуске контейнера.

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

Поэтому я предлагаю следующий подход:

  • Создать промежуточный образ, в котором будут предустановлены все важные пакеты и зависимости.

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

Этот метод позволяет ускорить процесс сборки, так как установка пакетов выполняется только один раз, а последующие сборки происходят значительно быстрее.

Создание промежуточного образа

Dockerfile для нашего промежуточного образа выглядит очень похоже на предыдущий пример. Поскольку я решил изменить свой скрипт install_packages() для включения полного набора пакетов tidyverse на будущее, мне также понадобилось установить несколько пакетов debian, от которых зависит tidyverse.

Не все эти пакеты абсолютно необходимы, но каждый из них может быть полезен так или иначе.

# Base image https://hub.docker.com/u/rocker/
FROM rocker/r-base:latest

# system libraries of general use
## install debian packages
RUN apt-get update -qq && apt-get -y --no-install-recommends install \
    libxml2-dev \
    libcairo2-dev \
    libsqlite3-dev \
    libmariadbd-dev \
    libpq-dev \
    libssh2-1-dev \
    unixodbc-dev \
    libcurl4-openssl-dev \
    libssl-dev

## update system libraries
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get clean

## create directories
RUN mkdir -p /02_code

## copy files
COPY /02_code/install_packages.R /02_code/install_packages.R

## install R-packages
RUN Rscript /02_code/install_packages.R

Я создаю образ, переходя в папку, где находится мой Dockerfile, и выполняю команду сборки Docker:

docker build -t oliverstatworx/base-r-tidyverse .

Я также загрузил этот образ в свой репозиторий на DockerHub. Если вам понадобится базовый R-образ с предустановленным tidyverse, вы можете просто создать свой образ на его основе, избегая сложностей с самостоятельной сборкой.

Теперь, когда промежуточный образ создан, мы можем изменить наш исходный Dockerfile, чтобы он базировался на этом образе, а не на rocker/r-base. Установка пакетов больше не требуется, так как этим уже занимается промежуточный образ.

Кроме того, мы добавляем последнюю строку, которая автоматически запускает наш скрипт при старте контейнера. Финальный Dockerfile может выглядеть следующим образом:

# Base image https://hub.docker.com/u/oliverstatworx/
FROM oliverstatworx/base-r-tidyverse:latest

## create directories
RUN mkdir -p /01_data
RUN mkdir -p /02_code
RUN mkdir -p /03_output

## copy files
COPY /02_code/myScript.R /02_code/myScript.R

## run the script
CMD Rscript /02_code/myScript.R

Последние штрихи

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

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

  • Удаление предупреждений и сообщений, которые больше не имеют значения (так как образ уже протестирован).

  • Добавление информативных сообщений, чтобы пользователь знал, какая часть скрипта выполняется в данный момент контейнером.

Пример модификации R-скрипта:

suppressPackageStartupMessages(library(readr))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(forcats))

options(scipen = 999,
        readr.num_columns = 0)

print("Starting Workflow")

# import dataframe
print("Importing Dataframe")
df <- read_csv("01_data/us-500.csv")

# manipulate data
print("Manipulating Data")
plot_data <- df %>%
  group_by(state) %>%
  count()

# save manipulated data to output folder
print("Writing manipulated Data to .csv")
write_csv(plot_data, "03_output/plot_data.csv")

# create plot based on manipulated data
print("Creating Plot")
plot <- plot_data %>% 
  ggplot()+
  geom_col(aes(fct_reorder(state, n), 
               n, 
               fill = n))+
  coord_flip()+
  labs(
    title = "Number of people by state",
    subtitle = "From US-500 dataset",
    x = "State",
    y = "Number of people"
  )+ 
  theme_bw()

# save plot to output folder
print("Saving Plot")
ggsave("03_output/myplot.png", width = 10, height = 8, dpi = 100)
print("Worflow Finished")

После перехода в папку, где находится наш Dockerfile, мы снова пересобираем образ с помощью команды:

docker build -t myname/myimage .

Затем запускаем контейнер на основе этого образа и связываем папки 01_data и 03_output с локальными директориями. Это позволяет импортировать данные из локальной папки и сохранять результаты работы контейнера локально. Пример команды:

docker run -it --rm -v ~/"R-Script in Docker"/01_data:/01_data -v ~/"R-Script in Docker"/03_output:/03_output myname/myimage

Поздравляем! Теперь у вас есть чистый Docker-образ, который не только автоматически выполняет ваш R-скрипт при каждом запуске контейнера, но и информирует в консоли о том, какая часть кода выполняется в данный момент. Удачной работы с Docker!

© Habrahabr.ru