Zgodnie z obietnicą z poprzedniego wpisu tej serii, dziś przedstawię drugą część na temat migracji mojego bloga z Wordpressa do Jekylla. Jej tematem będzie deployment do Heroku czyli hostowanie bloga w chmurze. Ogólnie nie jest to nic specjalnie skomplikowanego - zresztą sam się zaraz przekonasz. Zapraszam do lektury!

Co to jest Heroku?

Zgodnie z tym co napisano na stronie “About” serwisu Heroku, jest to oparta o kontenery platforma do hostowania aplikacji w chmurze (ang. Platform as a Service czyli PaaS). Usługa ta umożliwia łatwy deployment, zarządzanie oraz skalowanie nowoczesnych aplikacji internetowych. Dzięki temu twórcy aplikacji mogą skupić się na tworzeniu swoich produktów zamiast martwić się o infrastrukturę, serwery i inne tego typu “pierdoły”.

W dzisiejszym wpisie skupię się tylko na deploymencie, który w Heroku oparty jest o repozytorium Git. Uzyskujemy do niego dostęp po założeniu konta w Heroku i utworzeniu aplikacji (można to zrobić w serwisie lub z konsoli). Następnie wystarczy zainstalować Heroku CLI i wywołać polecenie w konsoli:

heroku login

Po wprowadzeniu loginu i hasła jesteśmy zalogowani w Heroku. Repozytorium Git w Heroku traktuje się raczej jako sposób na deployment niż jako główne miejsce do przechowywania plików projektu. Te zwykle trzyma się w osobnym, zewnętrznym repozytorium, na przykład w GitHubie lub BitBucket. W takim przypadku, mając już utworzone repozytorium Git, wystarczy dodać do niego dodatkowe remote, które wskaże na repo Heroku:

heroku git:remote -a nafrontendzie-blog

Od tej pory, możemy “pushować” nasze zmiany do Heroku:

git push heroku master

Po każdym “pushu” Heroku może dla nas wykonać określone operacje (na przykład dla aplikacji Node.js może to być jakaś komenda npm scripts itp.). Za chwilę wykorzystamy tę możliwość przy deploymencie bloga.

Blog oparty na Jekyllu - deployment do Heroku

Po tym krótkim wprowadzeniu czas teraz przejść do części właściwej czyli konfiguracji deploymentu bloga opartego na Jekyllu. Cały proces rozpoczniemy od instalacji odpowiednich pakietów Ruby. Robimy to poprzez dodanie odpowiednich wpisów do pliku Gemfile. Plik ten wygląda u mnie tak (pokazywałem go już ostatnio):

source "https://rubygems.org"

ruby '2.4.1'

gem "jekyll", "3.6.0.pre.beta1"
gem 'json'

# plugins
gem 'jekyll-sitemap'
gem 'jekyll-twitter-plugin'

# for deployment
gem 'rack-contrib'
gem 'rack-rewrite'
gem 'rake'
gem 'puma'

group :jekyll_plugins do
   gem "jekyll-feed", "~> 0.6"
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Wymagane pakiety

Najważniejsze, na co musisz zwrócić uwagę, to cztery linie oznaczone komentarzem # for deployment. Są to pakiety, które będą nam potrzebne do deploymentu.

Pierwszy z nich - rack-contrib (link) to zestaw rozszerzeń dla intefejsu web-servera Rack dostępnego w Ruby. Wykorzystamy go m.in. do rozwiązywania adresów URL, tak aby nie było potrzeby wyświetlania rozszerzenia *.html dla podstron (i do kilku innych rzeczy).

Pakiet rack-rewrite (link) pozwoli skonfigurować przekierowania 301 - struktura bloga została niech zmieniona podczas migracji i zaistniała potrzeba odpowiedniego przekierowania niektórych zasobów na nowe adresy.

Gem rake to program podobny do znanego chyba każdemu programu make - pozwala on na uruchamianie skryptów pisanych za pomocą składni Ruby. Skrypt taki pozwoli nam na budowanie Jekylla po stronie Heroku, dzięki czemu nie będzie potrzeby “commitowania” katalogu _site do repozytorium.

Na koniec pakiet puma. Jest to lekki web-server kompatybilny z Rack. Będzie on odpalany przez Heroku i w ten sposób będą serwowane statyczne pliki bloga.

Mając w powyższy sposób zdefiniowany plik Gemfile wystarczy uruchomić bundler aby zainstalować wszystkie pakiety i zaktualizować plik Gemfile.lock:

bundle

Definicja platform buildowania

Heroku jest takie “mądre”, że umie rozpoznać język w jakim napisana jest aplikacja. W naszym przypadku rozpozna więc, że jest to Ruby. Po rozpoznaniu języka projektu, Heroku będzie próbować wykonywać niezbędne instalacje pakietów dla danej platformy po każdym “pushu” - w przypadku Ruby będzie to więc uruchomienie “bundlera” i zainstalowanie na serwerze pakietów wymienionych w pliku Gemfile!

Jest to super opcja, ja jednak potrzebowałem zrobić coś jeszcze… Otóż skrypty JavaScript i CSS dla mojego bloga ogarniane są za pomocą webpacka. Potrzebowałem więc, aby po “pushu”, Heroku uruchomiło polecenie npm install oraz wykonało produkcyjny “build” webpacka. Aby to zrobić, musiałem poinformować Heroku, że mój blog to aplikacja nie tylko Ruby ale też Node.js!

W Heroku, za deployment dla poszczególnych platform odpowiadają tzw. “buildpacki”. Możemy sprawdzić jakie “buildpacki” są zdefiniowane dla nasze aplikacji wywołując poniższą komendę:

heroku buildpacks

Oczekiwany przeze mnie efekt tej komendy to:

=== nafrontendzie-blog Buildpack URLs
1. heroku/nodejs
2. heroku/ruby

Jeśli jest inny to najlepiej najpierw wyczyścić wszystkie “buildpacki”:

heroku buildpacks:clear

Następnie możemy dodać obsługę Node.js:

heroku buildpacks:add --index 1 heroku/nodejs

Zwróć uwagę na parametr --index 1 - spowoduje on, że operacje dla Node.js będą wykonywane jako pierwsze. Dodajmy teraz jeszcze obsługę Ruby:

heroku buildpacks:add --index 2 heroku/ruby

Powyższe polecenie doda obsługę Ruby na drugim miejscu.

Od teraz, każdy “push” do repozytorium Heroku, spowoduje najpierw uruchomienie kroków deploymentu Node.js (czyli de facto npm install/yarn), a dopiero po nich uruchomione zostaną kroki niezbędne do zbudowania aplikacji Ruby (tutaj do gry wchodzi polecenie bundle i plik Gemfile). Oczywiście można konfigurować co się dokładnie dzieje ale to temat na osobny wpis - w moim przypadku nie trzeba było nic dodatkowo robić.

Build webpacka na Heroku

Mając zdefiniowaną odpowiednią kolejność “buildpacków” mamy pewność, że Heroku w pierwszej kolejności zainstaluje wszystkie pakiety znajdujące się w pliku package.json. Kiedy to się stanie, chciałbym aby wykonany został produkcyjny “build” webpacka. Aby to zrobić, wystarczy do sekcji scripts pliku package.json dodać skrypt o nazwie heroku-postbuild - jeśli Heroku napotka ten skrypt, wykona go zaraz po instalacji pakietów. Spójrz jakie skrypty zdefiniowałem u siebie:

"scripts": {
    "start:js": "NODE_ENV=dev webpack",
    "start:www": "jekyll serve --watch --future",
    "build:js": "NODE_ENV=prod webpack",
    "build:www": "JEKYLL_ENV=production jekyll build",
    "critical": "node ./tools/critical-css.js",
    "crop": "node ./tools/crop-images.js",
    "deploy": "yarn build:js && yarn build:www && yarn critical || true && git add . || true && git commit -m \"deployment\" || true && git push || true && git push heroku",
    "heroku-postbuild": "yarn build:js"
  }

Jak widzisz, na końcu zdefiniowałem skrypt heroku-postbuild, który uruchamia skrypt build:js, a ten z kolei buduje produkcyjną wersję webpackowych “bundli”. Dzięki temu to wszystko zadzieje się na serwerach Heroku!

Build Jekylla na Heroku

Podobny efekt jak w przypadku webpacka chciałem uzyskać dla Jekylla. Idealnie by było gdyby polecenie…

JEKYLL_ENV=production jekyll build

…dało się wykonać automatycznie na serwerze.

Aby to osiągnąć wykorzystamy dodany wcześniej do pliku Gemfile pakiet rake i zdefiniujemy skrypt, który wywoła odpowiednią komendę po instalacji niezbędnych pakietów Ruby. Do tego celu, w głównym katalogu projektu bloga utworzyłem plik Rakefile z taką oto zawartością:

namespace :assets do
  task :precompile do
    puts `JEKYLL_ENV=production bundle exec jekyll build`
  end
end

Sama obecność pliku Rakefile spowoduje, że Heroku odczyta go automatycznie i wykona zawarte w nim operacje. Powyższy skrypt dodaje polecenie budowania Jekylla do zadania rake assets:precompile wykonywanego przez Heroku.

W tym momencie możemy dodać katalog _site do pliku .gitignore!

Tutaj jeszcze mała uwaga: Heroku, podczas procesu deploymentu dodaje katalog vendor do głównego katalogu projektu. Trzeba go dodać do atrybutu exclude w pliku _config.yml aby wszystko działało!

Konfiguracja web-serwera

Na koniec pozostaje nam tylko konfiguracja web-serwera, który będzie serwować pliki bloga odwiedzającym. Do tego celu wykorzystamy wspomniany wcześniej interfejs webservera rack, zestaw rozszerzeń rack-contrib oraz bibliotekę rack-rewrite. Cała niezbędna konfiguracja znajduje się w pliku config.ru w głównym katalogu projektu i u mnie wygląda następująco:

require 'rack'
require 'rack/contrib/try_static'

gem 'rack-rewrite', '~> 1.5.0'
require 'rack/rewrite'

use Rack::Rewrite do
  r301 %r{.*}, 'https://www.nafrontendzie.pl$&', :scheme => 'http'
  r301 %r{.*}, 'https://www.nafrontendzie.pl$&', :if => Proc.new {|rack_env|
    rack_env['SERVER_NAME'] != 'www.nafrontendzie.pl' and rack_env['rack.url_scheme'] == 'https'
  }
  r301 %r{^/category/programowanie/(.*)$}, '/kategoria/programowanie'
  r301 %r{^/category/przemyslenia/(.*)$}, '/kategoria/opinie'
  r301 %r{^/(.*)/$}, '/$1'
  r301 %r{^/(.*)/(\?.*)$}, '/$1$2'
  r301 '/category/programowanie', '/kategoria/programowanie'
  r301 '/category/przemyslenia', '/kategoria/opinie'
end

# enable compression
use Rack::Deflater

# static configuration (file path matches reuest path)
use Rack::TryStatic,
      :root => "_site",  # static files root dir
      :urls => %w[/],    # match all requests
      :try => ['.html', 'index.html', '/index.html'], # try these postfixes sequentially
      :gzip => true,     # enable compressed files
      :header_rules => [
        [['css', 'js', 'jpg', 'png', 'jpeg', 'gif'], {'Cache-Control' => 'public, max-age=604800'}]
      ]

# otherwise 404 NotFound
notFoundPage = File.open('_site/404.html').read
run lambda { |_| [404, {'Content-Type' => 'text/html'}, [notFoundPage]]}

Z interesujących rzeczy w powyższym kodzie: w sekcji Rack::Rewrite mamy definicję przekierowań 301; dalej, w sekcji Rack::TryStatic definiujemy translację adresów w stylu /jakis-adres na adres /jakis-adres.html; na koniec dodajemy jeszcze obsługę strony 404.

Pozostała nam już ostatnia rzecz do wykonania: poinformowanie Heroku, co ma zrobić po zainstalowaniu wszystkich pakietów i uruchomieniu wszystkich zdefiniowanych powyżej komend. Do tego celu służy plik Procfile, który u mnie wygląda następująco:

web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-production}
console: echo console
rake: echo rake

Powyższe spowoduje uruchomienie web-servera puma na Heroku.

Skrypt deploymentu

Powyżej pokazałem sekcję scripts pliku package.json. Znajduje się tam skrypt o nazwie deploy, który wygląda tak:

"deploy": "yarn build:js && yarn build:www && yarn critical || true && git add . || true && git commit -m \"deployment\" || true && git push || true && git push heroku"

Wywołuje on kilka innych skryptów npm, takich jak build:js, build:www czy critical. Dwa pierwsze budują webpacka i Jekylla lokalnie - robię to głównie w celu upewnienia się, że wszystko jest OK. Trzeci z nich uruchamia generowanie krytycznego kodu CSS, który potem wklejam “inline” do sekcji head pliku HTML (w celu optymalizacji szybkości wczytywania strony) - to temat na osobny wpis.

Następnie, w powyższym skrypcie odpalam komendy “commitujące” i “pushujące” ewentualne zmiany do Gita (GitHub). Najważniejsze jest ostanie polecenie: git push heroku - powoduje ono “pushnięcie” zmian do repozytorium Heroku, co będzie skutkowało uruchomieniem na serwerach Heroku, po kolei:

  • instalacji wymaganych pakietów Node.js
  • produkcyjnego builda webpacka
  • instalacji pakietów Ruby
  • produkcyjnego builda Jekylla
  • odpaleniem web-servera puma

I to by było na tyle - cały deployment chwilę trwa ale wszystko dzieje się automatycznie, a cały proces uruchamiany jest jedną komendą.

Podsumowanie

Jak widzisz, było trochę konfiguracji ale nie była ona jakoś mocno skomplikowana. Ostatecznie jednak wszystko sprowadza się do uruchomienia komendy yarn deploy, tak więc na codzień korzystanie z tego rozwiązania jest całkiem wygodne.

Mam nadzieję, że powyższy opis, jak i ten z poprzedniej części na temat migracji bloga będzie dla kogoś przydatny. Tym wpisem kończę temat migracji bloga - czas wrócić do normalności!