Zavolat Další kontakty >
E-mail Kontakty

Capistrano 3: automatizovaný deployment Nette aplikací

Aktualizace produkčních aplikací je vždy kritickou záležitostí. Musíme nahrát nové soubory, vymazat cache aplikace, aktualizovat strukturu databáze, přegenerovat minifikované CSS a JS soubory, ověřit, že vše funguje… Spousta úkolů, na které se snadno zapomene. Podíváme se proto na nástroj Capistrano, kterým je možné vše velmi snadno zautomatizovat.

Capistrano je nástroj psaný v jazyce Ruby. Pokud nejste kamarádi s jeho ekosystémem, určitě si přečtěte článek Ekosystém Ruby: přehled nástrojů. Pokud ještě nemáte, nainstalujte si Ruby a Bundler.

Než se do toho pustíme, tak několik základních předpokladů, které by Vaše aplikace a Váš server měl splňovat:

  • Aplikace je verzovaná ve VCS, ideálně v Gitu.
  • K serveru máme SSH přístup a server má přístup do repository, kde máme aplikaci (tj. na serveru je platný klíč, nebo máme správně nastavený SSH Agent Forwarding).
  • Máme možnost změnit nastavení HTTP serveru, zejména pak document root pro naši aplikaci.
  • V repositáři nejsou uložena hesla, ani jiná část konfigurace specifická pro produkční server.

Dále budu uvažovat, že aplikaci máme v Git repository. Také budeme předpokládat, že jde o Nette aplikaci využívající Composer se standardní adresářovou strukturou, tj.:

.
├── app
├── composer.json
├── composer.lock
├── log
├── temp
├── tests
├── vendor
└── www

Instalace a inicializace aplikace

Capistrano budeme instalovat právě pomocí nástroje Bundler. Zajistíme tak, že budeme v aplikaci později využívat vždy tu verzi, pro kterou máme deployment proces připravený a hlavně vyzkoušený, nikoliv tu, která se nám nainstalovala globálně do systému.

Vytvoříme tedy soubor Gemfile, ve kterém budeme deklarovat verzi 3.2.0 a vyšší:

source 'https://rubygems.org'

gem 'capistrano', '~> 3.2.0'

Nainstalujeme jednoduše příkazem bundle install.

Příprava aplikace

Na začátek bude potřeba vytvořit základní konfiguraci deploymentu. Spustíme následující příkaz:

bundle exec cap install

Příkaz vytvoří soubor Capfile, složku config (pokud neexistuje) a v ní následující soubory a adresáře:

config/
├── deploy/
│   ├── production.rb
│   └── staging.rb
└── deploy.rb

Soubor deploy.rb obsahuje globální nastavení deploymentu. Ve složce deploy jsou pak nastavení pro jednotlivá prostředí, na která můžeme aplikaci deployovat. Můžeme tak snadno použít jeden nástroj pro nasazení aplikace do testovacího i produkčního prostředí.

Globální nastavení

Podívejme se nejprve na soubor deploy.rb. Capistrano nám připravilo okomentovanou šablonu, ze které můžeme vycházet:

# config valid only for Capistrano 3.1
lock '3.2.1'

set :application, 'my_app_name'
set :repo_url, 'git@example.com:me/my_repo.git'

# Default branch is :master
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call

# Default deploy_to directory is /var/www/my_app
# set :deploy_to, '/var/www/my_app'

# Default value for :scm is :git
# set :scm, :git

# Default value for :format is :pretty
# set :format, :pretty

# Default value for :log_level is :debug
# set :log_level, :debug

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
# set :linked_files, %w{config/database.yml}

# Default value for linked_dirs is []
# set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for keep_releases is 5
# set :keep_releases, 5

namespace :deploy do

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      # Your restart mechanism here, for example:
      # execute :touch, release_path.join('tmp/restart.txt')
    end
  end

  after :publishing, :restart

  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end

end

Na začátku bychom měli nastavit název aplikace a cestu ke Git repository:

set :application, 'test-application'
set :repo_url, 'git@git.lynt.cz:testing/test-app.git'

Další nastavení určuje branch, která se bude deployovat. Výchozí je master, a můžeme se na ní buď dotázat uživatele při deploymentu odkomentováním řádku s

ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call

nebo můžeme větev nastavit přímo:

set :branch, :development

Do testovacího prostředí však budeme pravděpodobně chtít deployovat jinou branch, než na produkci, proto toto nastavení necháme do souborů pro jednotlivá prostředí.

Další důležité nastavení je složka, do které se má aplikace deployovat. Můžeme jí opět nastavit globálně, nebo pro každé prostředí zvlášť. Můžeme také nastavit výchozí hodnotu a tu pak jen podle potřeby přepsat:

set :deploy_to, '/data/htdocs/testing/test-application'

V této složce Capistrano vytváří následující adresářovou strukturu:

/data/htdocs/testing/test-application
├── current -> ./releases/YYYYMMDDHHMMSS
├── releases
│   └── YYYYMMDDHHMMSS
├── repo
├── revisions.log
└── shared

Nejdůležitější je složka releases. Pro každý deployment vytvoří Capistrano novou složku s časovým razítkem v názvu. Do ní z Gitu nahraje požadované soubory. Aby se data pořád nemusela stahovat ze vzdáleného repositáře, udržuje jeho mirror ve složce repo. Každý release také zapíše do souboru revisions.log.

Ve složce shared jsou uloženy soubory a složky, které jsou sdílené pro všechny releases. Jedná se zejména o uživatelská data, soubory sessions a další data, o která by nebylo dobré s každým release přijít. Po stažení souborů jsou nasymlinkovány do složky konkrétního release.

A konečně, složka current není složka, ale symlink na poslední úspěšný release. Pokud nedojde k chybě, tak je v posledním kroku deploymentu symlink změněn. Document root aplikace bychom měli nastavit právě do current/www.

Dalším zajímavým nastavením je nastavení sdílených souborů a složek. Do každého release budeme chtít nalinkovat připravený app/config/config.local.neon, který obsahuje nastavení připojení k databázi, a složky log, temp/sessions a www/attachments. Nastavení bude vypadat následovně:

set :linked_files, %w{app/config/config.local.neon}
set :linked_dirs, %w{log temp/sessions www/attachments}

Konec konfiguračního souboru tvoří příklad vlastních úloh, které se mají v rámci deployment procesu spouštět. K těm se dostaneme později.

Nastavení prostředí

Nyní přejdeme k nastavení cílového prostředí. Začněme nastavením prostředí staging, které by mělo sloužit k revizi aplikace před uvedením do produkce. Nastavení produkčního prostředí pak bude analogické.

I v deployment/staging.rb si pro nás Capistrano přichystalo kostru konfigurace (sekce s nastavením SSH pro přehlednost vyjmuta):

# Simple Role Syntax
# ==================
# Supports bulk-adding hosts to roles, the primary server in each group
# is considered to be the first unless any hosts have the primary
# property set.  Don't declare `role :all`, it's a meta role.

role :app, %w{deploy@example.com}
role :web, %w{deploy@example.com}
role :db,  %w{deploy@example.com}


# Extended Server Syntax
# ======================
# This can be used to drop a more detailed server definition into the
# server list. The second argument is a, or duck-types, Hash and is
# used to set extended properties on the server.

server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value

# ...

V kostře najdeme nastavení serverů, na které bude probíhat deployment. Ty mohou být rozděleny do skupin. Na každé skupině pak mohou být aplikovány jiné příkazy. Pro jednoduchost budeme uvažovat jediný server s rolemi web a app. Pro výchozí nastavení stejně role serverů nehrají moc roli, protože výchozí deployment proces probíhá pro všechny role stejně. Více o rolích a jejich filtrování naleznete v kapitole Role filtering oficiální dokumentace.

Konfigurace jednoho vypadá následovně:

server 'deploy.lynt.cz', user: 'deploy', roles: %w{web app}

Capistrano tedy bude přistupovat pomocí SSH na server depoly.lynt.cz jako uživatel deploy. Dále nastavíme branch, která se bude deployovat, a cestu:

set :branch, :staging
set :deploy_to, '/data/htdocs/testing/test-application'

Pokud jsme všechno nastavili správně, měli bychom již nyní být schopni provést deployment:

bundle exec cap staging deploy

Jako první argument uvedeme prostředí, se kterým chceme pracovat, a jako druhý úlohu, kterou chceme provést na jeho serverech. V našem případě deploy.

Příkaz by měl vytvořit na vzdáleném serveru všechny potřebné složky, ale skočit na neexistenci souboru app/config/config.local.neon. Do složky shared/app/config tedy nahrajeme produkční konfiguraci a příkaz spustíme znovu, nyní by již měl deployment proběhnout korektně a měl by být vytvořen symlink current.

Composer

Pokud používáme Composer pro správu závislostí a nemáme závislosti v Git repository, tak se naše aplikace sice nahrála na vzdálený server, ale bez závislostí. Pro capistrano naštěstí existuje plugin, které podporu Composeru doplní.

Do Gemfile stačí přidat řádek gem 'capistrano-composer', soubor tedy bude vypadat následovně:

source 'https://rubygems.org'

gem 'capistrano', '~> 3.2.0'
gem 'capistrano-composer'

Nové závislosti nainstalujeme opět příkazem bundle install. Plugin je ještě nutné povolit v souboru Capfile. Za řádek require 'capistrano/deploy' doplníme require 'capistrano/composer', celý soubor bude vypadat například následovně:

# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# Includes tasks from other gems included in your Gemfile
require 'capistrano/composer'

# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

Nyní by již v rámci deploymentu měla proběhnout i instalace závislostí. Na serveru samozřejmě musí být Composer nainstalovaný a dostupný z proměnné PATH. Tu můžeme upravit v konfiguraci jednotlivých prostředí přidáním nastavení :default_env. Pokud chceme použít složku bin v domovském adresáři uživatele, který provádí deployment, přidáme následující řádku:

set :default_env, { path: "$HOME/bin:$PATH" }

Vlastní úlohy

V rámci deploymentu jistě budeme chtít spouštět vlastní úlohy, například nastavení oprávnění. Úlohy můžeme definovat v souboru config/deploy.rb. Úlohy jsou členěny do namespaces. Úloha pro nastavení oprávnění může vypadat například takto:

namespace :deploy do
  desc 'Fix application permissions.'
  task :fix_permissions do
    on roles(:app) do |host|
      execute "chmod -R u+w,g+wX #{release_path} ; true"
      execute "chmod -R u+w,g+wX #{shared_path} ; true"
      execute "chgrp -R http #{release_path} ; true"
      execute "chgrp -R http #{shared_path} ; true"
    end
  end
end

V úlohy příkazu můžeme určit, s jakými servery budeme pracovat. Uvedením on roles(:app) zajistíme, že se příkaz vykoná jen na aplikačních serverech. V proměnné host pak budeme mít uložené informace o serveru, se kterým právě pracujeme.

Příkaz execute vykoná na vzdáleném serveru zadaný příkaz. Příkaz můžeme zadat jako řetězec, nebo jako seznam řetězců oddělených čárkami – při spouštění se spojí a jednotlivé části správně escapují. Můžeme používat proměnné release_path a shared_path, které obsahují cesty ke složce s aktuálním release a ke sdíleným souborům.

Uvedený příkaz končí vždy příkazem true, protože ve sdílené složce mohou být soubory, ke kterým uživatel pro deployment nemá oprávnění přistupovat – například přístupové logy serveru, nebo soubory vytvořené uživateli aplikace. chmod a chgrp by v takovém případě skončily chybou (návratový kód 1) a celý deployment by se přerušil.

Takto nadefinovanou úlohu však musíme spustit ručně z příkazové řádky:

bundle exec cap staging deploy:fix_permissions

Bylo by dobré ji začlenit do deployment procesu. Pomocí příkazů before a after jej můžeme naplánovat na automatické spuštění před nebo po určitém příkazu. V rámci příkazu deploy se postupně spouští následující úlohy (převzato z Capistrano Flow):

deploy:starting    - start a deployment, make sure everything is ready
deploy:started     - started hook (for custom tasks)
deploy:updating    - update server(s) with a new release
deploy:updated     - updated hook
deploy:publishing  - publish the new release
deploy:published   - published hook
deploy:finishing   - finish the deployment, clean up everything
deploy:finished    - finished hook

Opravu oprávnění naplánujeme před úlohu deploy:publishing, definice příkazu bude vypadat následovně:

namespace :deploy do
  desc 'Fix application permissions.'
  task :fix_permissions do
    on roles(:app) do |host|
      execute "chmod -R u+w,g+wX #{release_path} ; true"
      execute "chmod -R u+w,g+wX #{shared_path} ; true"
      execute "chgrp -R http #{release_path} ; true"
      execute "chgrp -R http #{shared_path} ; true"
    end
  end

  before :publishing, :fix_permissions
end

Pokud spustíme bundle exec cap staging deploy, proběhne před vytvořením symlinku current i nastavení oprávnění.

Závěr

V článku jsme si ukázali nastavení deployment procesu pro jednoduchou aplikaci. Vycházeli jsme z univerzálního deployment procesu, který je nadefinovaný v nástroji Capistrano. Pro složitější scénáře je možné proces snadno rozvinout přidáváním vlastních příkazů.

Capistrano používá knihovnu SSHKit, která umožňuje pokročilé sestavování příkazů, jejich paralelní spouštění na serverech odpovídajících daným kritériím (např. rolím) a mnoho dalšího.

Pro více informací se podívejte do oficiální dokumentace k nástroji Capistrano.

Komentáře (3)

Přidat komentář

napsáno 23. 12. 2014 12:24
Martin Zlámal: ve výsledku je to řešení pomocí SH skriptu velmi podobné. Capistrano dělá v podstatě totéž: nejprve z gitu aplikaci natáhne do vlastní složky, provede vše potřebné (určeno deployment skriptem: instalace závislostí, generování doctrine proxies, nastavení oprávnění apod.), když cokoliv selže, tak se zastaví. Na závěr jen nepřesouvá rsyncem a publikací se rozumí změna symlinku. Celé to také trvá jen pár sekund (nejdelší na celém procesu je instalace závislostí).

Vytváření balíčků (v našem případě RPM - jedeme na CentOSu) jsem také zvažoval, ale nakonec mi to přišlo jako moc velký overhead. Navíc deployovat pomocí Capistrana z libovolného klienta není problém a není nutné dávat vývojářům oprávnění roota, stačí přidat jejich SSH klíč nějakému omezenému uživateli.
napsáno 8. 12. 2014 12:41
Díky! Převzal jsem několik projektů v Nette (na svoje používám Capifony) a bál jsem se, že si budu muset napsat Capistrano script sám.
napsáno 6. 12. 2014 12:07
Já to tedy řeším o dost jinak. U malých aplikací to nemá smysl rozebírat, tak je jen jednoduchý kopírovací skript, ale u takových aplikací se moc nepočítá s dalším updatem. U větších je to buď balíček, tzn "apt-get update", nebo u aplikací které je třeba takto deploynout třeba několikrát za den to řeším přes SH skript.

Ten funguje tak, že se nejdříve zabalí a vyřeší faktury a jiné věci které s tím úplně nesouvisejí, ale chci je mít v bezpečí. Pak se natáhne z produkční větve z GITu obsah do nějaké úplně oddělené složky na serveru a nainstalují se závislosti. Pak se nastaví práva, configy, doctrine proxy, webloader minifikované soubory a takové věci. Vše je napsáno tak, aby když to selže, tak se deploy okamžitě zastavil. Teď tedy když je aplikace připravená, tak se pomocí rsync šoupne na správné umístění, ještě se v tomto umístění dogeneruje cache (dojde tedy ke kompilaci jako takové) a když je vše ok, tak se to publikuje.

Ještě se nestalo, aby aplikace nenastartovala, nebo se provedl deploy s rozbitou aplikací a celé (to co nás zajímá) trvá pár sekund. Navíc je to otázkou jednoho příkazu (nepočítaje instalace gitu a composeru)... (-: