Dockeryzacja aplikacji Rails pod development i CI
Docker dojrzał z ciekawostki w domyślny sposób na spójne środowisko. Przewodnik po dockeryzacji aplikacji Rails pod development i CI — docker-compose dla całego stosu, dyscyplina cache'u warstw i gdzie Docker pomaga, a gdzie dodaje tarcie.
Kilka lat temu Docker był obiecującą ciekawostką; dziś jest dojrzały, a docker-compose uczynił
uruchamianie całego stosu aplikacji — aplikacji, bazy, cache, workerów w tle — jedną komendą. Dla
zespołu Rails największym zyskiem nie jest już „wysyłaj kontenery na produkcję” (choć możesz); to
spójność: każdy programista i twoje CI uruchamiają dokładnie to samo środowisko, a „działa
na mojej maszynie” przestaje być zdaniem, które ktokolwiek wypowiada. Oto jak rozsądnie
zdockeryzować aplikację Rails pod development i CI — i uczciwa uwaga o tym, gdzie zarabia na swoje,
a gdzie dodaje tarcie.
Dockerfile, z dyscypliną cache’u
Obraz zaczyna się od bazy Ruby i instaluje biblioteki systemowe oraz gemy. Najważniejszą rzeczą — tą samą lekcją z wczesnych dni Dockera, wciąż tą, którą ludzie mylą — jest skopiowanie Gemfile’i i bundle przed skopiowaniem reszty kodu, by edycja kodu aplikacji nie unieważniała (wolnej) warstwy instalacji gemów:
FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# warstwa bundle — przebudowywana tylko, gdy Gemfile się zmieni
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4
# teraz kod
COPY . .
EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Ułóż to na odwrót, a każda zmiana kodu reinstaluje każdy gem — dwuminutowe czekanie, które sprawia, że ludzie porzucają Dockera. Z poprawnym warstwowaniem gemy reinstalują się tylko, gdy Gemfile faktycznie się zmieni.
docker-compose: cały stos w jednej komendzie
To czyni Dockera naprawdę przyjemnym w 2018. Aplikacja Rails to nie jeden proces — to aplikacja,
baza, zwykle Redis, często worker w tle. docker-compose deklaruje je wszystkie, połączone, w
jednym pliku:
# docker-compose.yml
version: "3"
services:
web:
build: .
command: bundle exec rails server -b 0.0.0.0
volumes:
- .:/app # żywy kod, bez przebudowy przy edycji
- bundle:/usr/local/bundle
ports:
- "3000:3000"
depends_on: [db, redis]
environment:
DATABASE_URL: postgres://postgres@db/app_development
REDIS_URL: redis://redis:6379
db:
image: postgres:10
redis:
image: redis:4
worker:
build: .
command: bundle exec sidekiq
depends_on: [db, redis]
volumes:
bundle:
docker-compose up # cały stos — web, db, redis, worker — startuje razem
Nowy programista klonuje repo i uruchamia jedną komendę, by dostać dokładnie ten stos, który
mają wszyscy — właściwe Ruby, właściwą wersję Postgresa, właściwy Redis, bez README „najpierw
zainstaluj te czternaście rzeczy”. Dwa praktyczne dotknięcia czynią doświadczenie deweloperskie
dobrym: zamontuj kod jako wolumen (.:/app), by edycje były żywe bez przebudowy obrazu, i
zamontuj nazwany wolumen dla bundle, by gemy przetrwały restarty kontenera zamiast się
reinstalować. Usługi dosięgają się po nazwie (aplikacja łączy się z db i redis jako
hostnamami), co jest siecią compose obsługującą okablowanie za ciebie.
Docker w CI: to samo środowisko, od końca do końca
Ten sam obraz i ustawienie compose wypłacają drugą dywidendę w CI. Zamiast szytego na miarę środowiska twojego dostawcy CI — konkretnego Ruby, konkretnego Postgresa, skonfigurowanego po ich myśli — uruchamiasz testy wewnątrz tych samych kontenerów, w których rozwijasz. Config CI buduje obraz (albo pobiera cache’owany) i uruchamia zestaw wobec tego samego Postgresa i Redisa:
# CI: zbuduj, potem uruchom zestaw w kontenerze wobec prawdziwych usług db/redis
test:
- docker-compose build
- docker-compose run web bundle exec rails db:create db:schema:load
- docker-compose run web bundle exec rspec
Wygraną jest to, że „przechodzi w CI” i „przechodzi na mojej maszynie” wreszcie znaczą to samo, bo są tym samym środowiskiem — eliminując całą klasę zagadek „zielone lokalnie, czerwone w CI” spowodowanych dryfem środowiska. (Paruje się to naturalnie z pracą nad testowaniem równoległym: uruchom zestaw przez kontenery/węzły w ten sam sposób.) Dla szybkości buildu cache’uj warstwy obrazu między przebiegami CI i rozważ build wieloetapowy, by finalny obraz niósł tylko to, czego potrzebuje.
Gdzie Docker pomaga, a gdzie dodaje tarcie
Uczciwa ocena, bo Docker nie jest darmowy:
- Naprawdę pomaga: onboarding (jedna komenda do działającego stosu), spójność środowiska (dev = CI, opcjonalnie = produkcja), uruchamianie wielu projektów z konfliktowymi wersjami zależności obok siebie i dopasowanie OS oraz wersji bibliotek produkcji dokładnie. Dla zespołu większego niż jeden albo projektu z kilkoma usługami to realne, codzienne wygrane.
- Dodaje tarcie: jest krzywa uczenia, buildy i zużycie dysku są cięższe, a na macOS w 2018
wydajność systemu plików zamontowanych wolumenów jest zauważalnie wolna (Docker działa tam w
VM), co może sprawić, że aplikacja Rails z zamontowanym wolumenem czuje się ospała — realny,
dobrze znany punkt bólu. Dla solowego programisty na prostej aplikacji natywne ustawienie
rbenv+ lokalny Postgres może wciąż być szybsze i prostsze na co dzień.
Osąd: wartość Dockera rośnie z rozmiarem zespołu, liczbą usług i tym, jak bardzo dryf środowiska cię ugryzł. Dla aplikacji wielousługowej i zespołu jest wart tarcia; dla jednoosobowego, jednousługowego projektu zważ to uczciwie, zamiast przyjmować dla mody.
Werdykt
Docker dojrzał w domyślny sposób na spójne, odtwarzalne środowisko, a dla zespołu Rails jego
największym zyskiem jest spójność, nie deployment: docker-compose up daje każdemu programiście i
twojemu CI ten sam pełny stos — aplikację, bazę, cache, workery — z jednego pliku i jednej
komendy, kończąc „działa na mojej maszynie” na dobre. Zrób cache’owanie warstw Dockerfile dobrze
(Gemfile przed kodem), zamontuj kod i bundle jako wolumeny dla szybkiej pętli dev i uruchamiaj CI
w tych samych kontenerach, by zielone znaczyło zielone wszędzie. Zważ realne tarcie — krzywą
uczenia i wydajność wolumenów na macOS — wobec rozmiaru twojego zespołu i stosu. Dla większości
zespołów w 2018 handel jest wyraźnie wart, a odtwarzalność, którą kupuje, to fundament, na którym
zbudowane jest wszystko inne — równoległe CI, dopasowanie produkcji, bezbolesny onboarding.