Go言語でWebアプリを作りかけて辞めた話
3行で要約
私のGoスキル
- A Tour of Goを途中まで
- みんなのGo言語を読んだ
- スターティングGo言語を読んだ
- スクレイピングしてDBに保存する簡単なバッチをGoで書いた
背景
とあるtoB向け受発注のWebアプリを作ることにしました。
開発者は私1人。私が目指した基準は以下のものです。
- 少なくとも3年はメンテできること
- 未来の自分(他人)が読んでも理解できること
- このアプリとガッツリ付き合う予定ではなく、出来れば保守は違う人に回したいし機能追加とかも控えめにしたい(重要)
- デプロイとか運用の手間を簡単にしたい
- ついでに、学んでおくことが有益になりそうな言語がいい
これらを踏まえ、以下の理由からGoを選択しました。
- シングルバイナリでデプロイ出来る
- 大体のことは標準ライブラリで出来るイメージ
- フォーマットとか強制されるのが良い
- 静的型付けによる安心感
- Go書いてみたい
選択したフレームワーク
Webフレームワーク
Revelなどのフルスタックフレームワークは選択肢から外しました。
- シンプルな受発注(CRUD)なのでフルスタックの必要性を感じなかった
- これは後に読み間違いと気付く
- Revelがどこまで活発に開発続くか分からない
- 開発が止まるor人気がなくなると使える人が少なくなるのでメンテ性が下がる
- Go使っているのに少なからずDSLを覚えないといけないことに抵抗があった
「なるべくnet/http
の使い方から外れないシンプルなやつ」ということでGinを選びました。
Echoとかも試しましたが好みでGinにしました。
O/Rマッパー
Webフレームワークと同様でDSLを覚えるつもりはありませんでした。
数年後も使われているか分からないライブラリのDSLを覚えるのはきつい。
かつマイグレーションの機能とかも不要で、単純にレコードを構造体にマッピングしてもらえれば十分でした。
gormは色々できるし途中まで使っていたけど辞めました。
gormに限らないが、吐かれるSQLを確認しながら書くのが本末転倒な感じだったので。
これも薄いラッパーということでsqlxを採用しました。
ちなみに以前は「マイグレーション機能っていいな」と思っていましたが、ここ数年で考えが変わりました。
ツールとしてのマイグレーションってそんな必要なのでしょうか。
upについてはalterとかの差分のsqlファイルがあれば十分ですし、downについては今のところ使った記憶がありません。
開発者間/デプロイ環境間でスキーマのversionが異なるような時に便利なのかもしれませんが、そういう開発案件に携わったことがなく。
マイグレーションに限りませんが、昔は「このツール便利!」と歓喜して導入しようとしていたのが今は「便利だけど覚えるコストと負債になり得るリスクを考慮するとどうだろう」と考えるようになってきました。
若手の頃に嫌だった「保守的なエンジニア」になりつつあるのかな…とちょっと心配になります。保守的になると新しい技術を覚える意欲を失いそうで。
ライブラリ管理
glideを採用。
これはちょっと採用理由を忘れました…。
その他
システム上必要なライブラリ(go-sql-driver/mysql
とか)以外は入れていません。
テンプレートも標準のtemplate
で済ませています。凝った画面ではないので十分です。
Goで書き始めた感想
良かった
- 良い
- ライブラリのコードが読みやすい
- 階層がほとんどフラットだし、importや呼び出しの名前空間が明示的なのも良い
- 「この関数どこで定義されているんだ」みたいな余計な手間がない
- 書いていてストレスがほとんどなかった
- go fmtなども含めて標準ライブラリ/ツールで済むのが良かった
- シングルバイナリは良い
- 静的型付けは良い
- テストコードが同階層にあるのは意外と読みやすかった
- ライブラリのコードが読みやすい
- 良くない
- テストコードでassertはやっぱ欲しい
- エラー処理が面倒
私にとってGoは書きやすいし読みやすいものでした。
「書きやすい」 = 「簡単に書ける」ではありません。
「書きやすい」 = 「書き方に迷わない(迷うことが少ない)」です。
コレクション操作などxx言語ならもっと短く簡潔に書けるのに、というケースはありますが不満は感じませんでした。
簡単に書ける言語だと書き方に迷うことがあるし、レビューでも「こう書いた方がシンプル」みたいな横道にそれがちです。
(今回はレビューする/されることはないので関係ありませんが)
同じ名前空間でもファイルが分かれていることに最初は「うっ」となりましたが、慣れたら気にならなくなりました。
不満としては、テストコードでassert系はやはり欲しいと感じました。
私が手を抜いているのか、テストでは基本的に「AとBが一致しませんでした。Aはこの値でBはこの値です」みたいなメッセージが表示できれば十分です。
これを毎回Errorf(...)
で書くのはDRYじゃありませんし単純に面倒です。
また、ここ数年ずっとスクリプト言語で過ごしてきた私にはエラー処理はちょっと面倒でした。慣れだと思います。
そんなわけですごく良い感じにコードを書いていたのですが、ちょっと要件に合わない選択をしたことに気付きました。
GoでWebアプリを作る上で足りていないところ
主にセキュリティ周りの考慮が漏れていました。
CSRFの対策とかid/pass方式の認証とか二重サブミット防止とか。
revelだとcsrfはプラグインでrevel-csrfというのがあるそうですが、ちゃんと調べていません。二重サブミットとかどうなっているんだろう。
システム要件上id/passの認証が必要なのですがginでは対応していません。
ginも他のフレームワークもBasic認証やOAuthは対応しているのですが。
さすがにこれらを自前で実装するのは再発明が過ぎます。
また細かいところでは、例えば「環境変数(production
/develop
/test
とか)ごとに処理を切り替えたい」とかの処理も必要でした。
典型的なところではDBの接続先を切り替えることです。
環境変数を取ってきたりgin.Mode()
から取り出したり出来ることは出来るんですが、自分で書いていくと結局オレオレフレームワークへ近付きます。
ここに至り、最初の基準で設けた「シンプルに」「DSLに依存したくない」というのは諦めました。
システムの機能がシンプルでも開発環境や非機能要件では色々必要だった、という見積が抜けていました。
とはいえ私の調査不足なだけで実は解決案は転がっているのかもしれません。
開発初期のスピード感と対応範囲、情報量および開発者の多さから、選ばれたのはRailsでした。
当初の目論見と真逆です。
GoでWebアプリを作りかけた感想まとめ
- Goは心地よい(not楽しい)
- (今の自分が)Goで何か作るならバックエンドかバッチか認証や更新操作が必要ないWebアプリ
- 新しい言語をやる時はやはりその道の先輩がいないと相談できなくて厳しい
Vim - previmにヘッダーを隠す設定を追加しました
プレビュー時に画面上部に出ていたヘッダー部を非表示に出来ます。
g:previm_show_header *g:previm_show_header* 型:数値 値が1ならば、プレビュー時にヘッダーを表示します。 ヘッダーには編集しているファイル名と更新日時が表示されます。 値が0ならば、プレビュー時にヘッダーを表示しません。 デフォルトでは1に設定されています。 " .vimrc let g:previm_show_header = 0
.vimrc
にlet g:previm_show_header = 0
を追記して頂ければOKです。
Vim - previmでmermaidに対応しました
Shibaで図を書いてTracで共有するを見てmermaidを知りました。
参考: mermaid.jsが素晴らしいけどなかなか使ってる人見かけないので実例晒す(追記あり)
面白そうだし、上記の公式サイトにmarked対応のサンプルもあったのでprevimでも対応しました。
下記のようにコードブロックのタイプをmermaid
としてもらえればOKです。
mermaid自体の具体的な書き方は公式サイトを参考にしてください。
(私も全然分かっていません)
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->E;
```
あとは普通のmarkdownと同様にプレビューされます。
Vim - 文字列連結で再代入する場合はjoinを使う方が早そう
概要
自作プラグインのコードに以下のようなTODOが残っていました。
" TODO リストじゃなくて普通に文字列連結にする(テスト書く) for line in s:do_external_parse(a:lines) let escaped = 適当な処理 call add(converted_lines, escaped) endfor return join(converted_lines, "\\n")
ここでは文字列の配列をぐるぐる回して処理を行い、最終的に改行区切りの文字列としています。
join
で書いたものの文字列連結の方が早いのではと思って上記のようなTODOコメントを残していました。
今日このTODOを消化しようと思い、本当に文字列連結の方が早いのか計測しました。
先に結論
- 10回程度の連結なら大差ない
- 3000回ともなると圧倒的にjoinが早い
- joinの方が実装もシンプル
環境
- Mac OS X 10.9.5
- MacVim Custom Version 7.4 (KaoriYa 20150707)
ベンチマーク
h1mesuke/vim-benchmarkを使用します。
テストデータは1行85文字が3000行です。(3000回連結される)
let s:lines = [] for s:n in range(3000) call add(s:lines, " * 注意:拡張子が`.md`の場合は`markdown`ではなく`modula2`として認識されてしまいます。その場合は以下の設定を.vimrcに記述してください") endfor let s:bm = benchmark#new("concat string") function! s:bm.operator_for() let result = "" let delim = "" for line in s:lines let result = result . delim . line let delim = "\\n" endfor endfunction function! s:bm.operator_while() let result = "" let delim = "" let n = 0 let length = len(s:lines) while n < length let result = result . delim . s:lines[n] let delim = "\\n" let n += 1 endwhile endfunction function! s:bm.join() let tmp = [] for line in s:lines call add(tmp, line) endfor let result = join(tmp, "\\n") endfunction call s:bm.run(3)
結果は以下の通り。
Benchmark: concat string Trial #1 join : 0.008425 operator_for : 0.348224 operator_while : 0.356583 Trial #2 join : 0.010402 operator_for : 0.353079 operator_while : 0.359111 Trial #3 join : 0.008463 operator_while : 0.347276 operator_for : 0.359592
joinが圧倒的に早かった。
Javaで+
連結すると遅いように、Vim Scriptでも気をつけた方がいいのかな。
ちなみに10回程度の連結なら大差ない。
Benchmark: concat string Trial #1 join : 0.000046 operator_for : 0.000063 operator_while : 0.000076 Trial #2 join : 0.000051 operator_for : 0.000080 operator_while : 0.000083 Trial #3 join : 0.000048 operator_for : 0.000066 operator_while : 0.000082
おわりに
「推測するな、計測せよ」の大事さ。
capistrano3のコードを読んで仕組みを理解する
メモです。
はじめに
capistrano3の概要を知るには以下の記事がとても参考になる。
業務での使用も経て何となく分かったつもりだったが、デフォルトで色々やってくれるのが逆に少し気持ち悪かった。
内部を理解しないことには気持ち悪さは解消されそうにないのでコードを読んだ。
だいぶ理解したものの、すぐに忘れそうなのでメモしておく。
対象
- capistrano3について何となく知っている。使ったことがある
- capistranoの内部でどういう風に処理が走っているかは知らない
- capistranoのバージョンは3.4.0
長くなったので、各項目のまとめだけ読むと手っ取り早いと思う。
1. cap install
入り口としてcap install
が何故実行出来るのか、から。
bin/cap
の中を覗く。
require 'capistrano/all' Capistrano::Application.new.run
capistrano/all
を読み込みCapistrano::Application
を実行しているのみ。
1.1. capistrano/all
require 'capistrano/version' require 'capistrano/version_validator' require 'capistrano/i18n' require 'capistrano/dsl' require 'capistrano/application' require 'capistrano/configuration'
ここは特筆することなさそう。
1.2. capistrano/application
Rake::Application
を継承。bin/cap
にあったCapistrano::Application.new.run
のクラス。
クラスがインスタンス化されるときの処理(initialize)が以下のようになっている。
自前のCapfile
に加えて、lib/Capfile
がRakefileとして読み込まれる。
def initialize super @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile end # allows the `cap install` task to load without a capfile def capfile File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile')) end
なおrun
はRakeに処理を委譲しているだけ。
def run Rake.application = self super end
1.3. lib/Capfile
cap install
した時に作られるCapfile
ではなくライブラリ側のCapfile
。
include Capistrano::DSL require 'capistrano/install'
lib/capistrano/install.rb
の定義は1行だけ。
load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake'))
このあと見て行くsetup
やdeploy
でもほとんど同じで、最終的に.rake
ファイルを読み込むようになっている。
`lib/capistrano/tasks/install.rake` desc 'Install Capistrano, cap install STAGES=staging,production' task :install do # ... end
:install
にcap install
時の処理が書かれている。
つまり色々とファイルを自動生成してくれる。
1.4. cap installのまとめ
bin/cap
がcapistrano/all
を読み込み、Cap用のRakeを実行するcapistrano/all
により基本的なDSLが定義される- 中で読まれる
capistrano/application
によりライブラリ側のCapfile
が読まれる - ライブラリの
Capfile
にはinstall
だけが定義されている- なので
cap install
が実行出来る
- なので
- 中で読まれる
cap install
すると生成されるCapfile
は不要なコメントアウト行を除くと以下の通り。
# Load DSL and set up stages require 'capistrano/setup' # Include default deployment tasks require 'capistrano/deploy' # Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
setup
とdeploy
について読み進めていく。
(lib/capistrano/tasks/*.rake
についてはコメントの通りカスタム定義なので読むところはなし)
2. capistrano/setup
include Capistrano::DSL namespace :load do task :defaults do load 'capistrano/defaults.rb' end end stages.each do |stage| Rake::Task.define_task(stage) do set(:stage, stage.to_sym) invoke 'load:defaults' load deploy_config_path load stage_config_path.join("#{stage}.rb") load "capistrano/#{fetch(:scm)}.rb" I18n.locale = fetch(:locale, :en) configure_backend end end require 'capistrano/dotfile'
処理の流れは何となく読み取れる。
- stage(たぶんproductionとかstagingとか)ごとの設定を読み込んでいる
- defaultsっていうのを読み込んでいる。きっとデフォルト設定だろう
- scmの定義を読み込んでいる
- その他なんか設定している
もう少しコードを読んでみる。
2.1. stage(たぶんproductionとかstagingとか)ごとの設定を読み込んでいる
いくつかプロパティが出てくるが、これらはどこで定義されているか。
stages
前述のcapistrano/dsl
にて読み込まれるlib/capistrano/dsl/stages.rb
に定義されている。
def stages Dir[stage_definitions].map { |f| File.basename(f, '.rb') } end def stage_definitions stage_config_path.join('*.rb') end
stage_config_path
はlib/capistrano/dsl/paths.rb
に定義されている。
def stage_config_path Pathname.new fetch(:stage_config_path, 'config/deploy') end
:stage_config_path
が設定されていればそのパス、設定されていなければconfig/deploy
のパスを取得している。
cap install
するとconfig/deploy/{production,staging}.rb
を作成してくれるので、デフォルトではこれらが取得される。
File.basename
なので結果としては['production', 'staging']
という値がstages
の戻り値になっているはず。
deploy_config_path
上記のstage_config_path
と同じくlib/capistrano/dsl/paths.rb
に定義されている。
def deploy_config_path Pathname.new fetch(:deploy_config_path, 'config/deploy.rb') end
こちらもcap install
で作られるconfig/deploy.rb
がデフォルトである。
なおsetup
のコードを再び見ると、loadの順番は次のようになっている。
load deploy_config_path load stage_config_path.join("#{stage}.rb")
config/deploy.rb
を読み込んでからconfig/deploy/*.rb
を読み込む。
なので両者で同じ定義をしているとconfig/deploy/*.rb
が勝ちそうな気がする。試していない。
2.2. defaultsっていうのを読み込んでいる
その名の通り変数のデフォルト値を定義している。
set_if_empty :scm, :git set_if_empty :branch, :master set_if_empty :deploy_to, -> { "/var/www/#{fetch(:application)}" } set_if_empty :tmp_dir, "/tmp" set_if_empty :default_env, {} set_if_empty :keep_releases, 5 set_if_empty :format, :pretty set_if_empty :log_level, :debug set_if_empty :pty, false set_if_empty :local_user, -> { Etc.getlogin }
多くの変数については、自動生成されるdeploy.rb
にコメントアウトされた内容として書かれている。
# Default value for :scm is :git # set :scm, :git ...
2.3. scmの定義を読み込んでいる
前述の通り:scm
はdeploy.rb
などで定義するがデフォルトはgitである。
lib/capistrano/git.rb
が読み込まれ、このあとのdeploy
で必要なコマンドが定義される。
あとでまた詳しく見るとする。
2.4. その他なんか設定している
configure_backendについて。
def configure_backend backend.configure do |sshkit| sshkit.format = fetch(:format) sshkit.output_verbosity = fetch(:log_level) sshkit.default_env = fetch(:default_env) sshkit.backend = fetch(:sshkit_backend, SSHKit::Backend::Netssh) sshkit.backend.configure do |backend| backend.pty = fetch(:pty) backend.connection_timeout = fetch(:connection_timeout) backend.ssh_options = (backend.ssh_options || {}).merge(fetch(:ssh_options,{})) end end end
sshkitの設定ぽい。処理の全体像とは関係が低そうなのでこれ以上は追わない。
2.5. capistrano/setupのまとめ
- SCMやデプロイ先などアプリケーション固有の変数を初期化
config/deploy.rb
の読み込みconfig/deploy/*.rb
の読み込み- SCM固有のコマンド定義を読み込み
- SSH設定を読み込み
3. capistrano/deploy
require 'capistrano/framework' load File.expand_path("../tasks/deploy.rake", __FILE__)
capistrano/framework
については以下のようになっている。
load File.expand_path("../tasks/framework.rake", __FILE__) require 'capistrano/install'
ということで、framework.rake
とdeploy.rake
を読み進める必要がある。
3.1. framework.rake
冒頭の参考記事(2つ目)にもある通り、このファイルはデプロイフローの雛形を定義している。
namespace :deploy do desc 'Start a deployment, make sure server(s) ready.' task :starting do end # ... desc 'Rollback to previous release.' task :rollback do %w{ starting started reverting reverted publishing published finishing_rollback finished }.each do |task| invoke "deploy:#{task}" end end end desc 'Deploy a new release.' task :deploy do set(:deploying, true) %w{ starting started updating updated publishing published finishing finished }.each do |task| invoke "deploy:#{task}" end end task default: :deploy
deploy
/rollback
は共に必要なコマンドを実行するだけ- このあとで詳しく見る
deploy
/rollback
以外のコマンドはデフォルトでは何もしない- 具体的な処理は
deploy.rake
で定義される
- 具体的な処理は
namespace :deploy
のデフォルトは:deploy
- なので
cap deploy
はdeploy
が呼ばれる
- なので
テンプレートメソッドっぽい。
3.2. deploy.rake
:starting
とか:updating
とか、framework.rake
のいくつかのコマンドが再定義されている。
3.2.1. deploy.rake -- deploy
フェーズ毎に簡単に処理を見てみる。
deploy:starting
該当部分だけ抜き出す。
task :starting do invoke 'deploy:check' invoke 'deploy:set_previous_revision' end desc 'Check required files and directories exist' task :check do invoke "#{scm}:check" invoke 'deploy:check:directories' invoke 'deploy:check:linked_dirs' invoke 'deploy:check:make_linked_dirs' invoke 'deploy:check:linked_files' end task :set_previous_revision do on release_roles(:all) do target = release_path.join('REVISION') if test "[ -f #{target} ]" set(:previous_revision, capture(:cat, target, '2>/dev/null')) end end end
- check系のタスクではファイルやディレクトリのチェックや準備を行う
deploy:check:directories
などのタスクは同一ファイルに定義されているexecute :mkdir, '-p', shared_path, releases_path
などをしている
:set_previous_revision
ではリリース前の最新リビジョンを変数に保存しておく- capを使ってデプロイするとデプロイ先にリビジョンが書かれた
REVISION
というファイルが作られる - バージョンによってはないらしい。参考: デプロイしたリビジョンをファイルに書き出す
- capを使ってデプロイするとデプロイ先にリビジョンが書かれた
deploy:started
デフォルトのまま。何もしない。
deploy:updating
:updating
には依存があり、先に:new_release_path
が行われる。
まずは:new_release_path
から読む。
task :new_release_path do set_release_path end
set_release_path
はlib/capistrano/dsl/paths.rb
に定義されている。
def releases_path deploy_path.join('releases') end def set_release_path(timestamp=now) set(:release_timestamp, timestamp) set(:release_path, releases_path.join(timestamp)) end
- リリース先のパスを変数に設定している
releases/20150080072500
みたいなパス- デプロイの構造は公式のStructureを参照
次にupdating
の処理の中身を読む。
task :updating => :new_release_path do invoke "#{scm}:create_release" invoke "deploy:set_current_revision" invoke 'deploy:symlink:shared' end namespace :symlink do # ... desc 'Symlink files and directories from shared to release' task :shared do invoke 'deploy:symlink:linked_files' invoke 'deploy:symlink:linked_dirs' end # ... end desc "Place a REVISION file with the current revision SHA in the current release path" task :set_current_revision do invoke "#{scm}:set_current_revision" on release_roles(:all) do within release_path do execute :echo, "\"#{fetch(:current_revision)}\" >> REVISION" end end end
:set_current_revision
は前述のREVISION
にリビジョンを書き込んでいるだけ:symlink:shared
はsymlinkの必要があるディレクトリやファイルがあれば作っている- 上記のリンク(Structure)の
shared
を参照
- 上記のリンク(Structure)の
:create_release
については少し詳しく読む。
コードを全部載せると長いので概要だけ。
# lib/capistrano/tasks/git.rake desc 'Upload the git wrapper script, this script guarantees that we can script git without getting an interactive prompt' task :wrapper do # ... end desc 'Clone the repo to the cache' task clone: :'git:wrapper' do # ... end desc 'Update the repo mirror to reflect the origin state' task update: :'git:clone' do # ... end desc 'Copy repo to releases' task create_release: :'git:update' do # ... end
- 依存関係を考慮した実行順は wrapper -> clone -> update -> create_release
- wrapper
- 専用のgit-ssh.shを配置する
- clone
- cloneされていなければcloneする
git :clone, '--mirror', repo_url, repo_path
- clone済みなら何もしない
- デプロイ先にrepo/HEADがあるかどうか
- 上記のリンク(Structure)の
repo
を参照
- 上記のリンク(Structure)の
- デプロイ先にrepo/HEADがあるかどうか
- cloneされていなければcloneする
- update
- repoの中で実行される
git :remote, :update
- create_release
- repoの中で実行される
- :repo_tree がある場合
git :archive, fetch(:branch), tree, "| tar -x --strip-components #{components} -f - -C", release_path
- ない場合
git :archive, fetch(:branch), '| tar -x -f - -C', release_path
deploy:updated
デフォルトのまま。何もしない。
deploy:publishing
task :publishing do invoke 'deploy:symlink:release' end namespace :symlink do desc 'Symlink release to current' task :release do on release_roles :all do tmp_current_path = release_path.parent.join(current_path.basename) execute :ln, '-s', release_path, tmp_current_path execute :mv, tmp_current_path, current_path.parent end end end
current -> /var/www/my_app_name/releases/20150120114500/
みたいなsymlinkを作っているだけ。
デプロイ先の構造については上記のリンク(Structure)を参照。
deploy:published
デフォルトのまま。何もしない。
deploy:finishing
task :finishing do invoke 'deploy:cleanup' end desc 'Clean up old releases' task :cleanup do on release_roles :all do |host| releases = capture(:ls, '-xtr', releases_path).split if releases.count >= fetch(:keep_releases) info t(:keeping_releases, host: host.to_s, keep_releases: fetch(:keep_releases), releases: releases.count) directories = (releases - releases.last(fetch(:keep_releases))) if directories.any? directories_str = directories.map do |release| releases_path.join(release) end.join(" ") execute :rm, '-rf', directories_str else info t(:no_old_releases, host: host.to_s, keep_releases: fetch(:keep_releases)) end end end end
descの通り:keep_releases
を超えた古いリリースファイルを消す作業。
capture(:ls, '-xtr', releases_path).split
は、こんなやり方があるのかとちょっと驚いた。
ls -xtr <pat>
とすることで「水平(x)」に「日付(t)」の「降順(r)」で出力させて、splitすることでそのまま配列として扱っている。
あとt(:keeping_releases ...)
のt
って何だと思ったが、i18nのようだった。
# lib/capistrano/dsl.rb def t(key, options={}) I18n.t(key, options.merge(scope: :capistrano)) end
deploy:finished
task :finished do invoke 'deploy:log_revision' end desc 'Log details of the deploy' task :log_revision do on release_roles(:all) do within releases_path do execute %{echo "#{revision_log_message}" >> #{revision_log}} end end end
リリース先のrevisions.log
にメッセージを書き込む。
revisions.log
についても上記のリンク(Structure)を参照。
3.2.2. deploy.rake -- rollback
deploy:starting
deploy:started
deployと同じ。
deploy:reverting
task :reverting do invoke 'deploy:revert_release' end desc 'Revert to previous release timestamp' task :revert_release => :rollback_release_path do on release_roles(:all) do set(:revision_log_message, rollback_log_message) end end task :rollback_release_path do on release_roles(:all) do releases = capture(:ls, '-xt', releases_path).split if releases.count < 2 error t(:cannot_rollback) exit 1 end last_release = releases[1] set_release_path(last_release) set(:rollback_timestamp, last_release) end end
- リリースが1つしかないならロールバック不可
- 一つ前のリリースを
:release_path
に設定
deploy:reverted
デフォルトのまま。何もしない。
deploy:publishing
処理的にはdeployと同じ。
ただし前述の通り:release_path
が直前のリリースパスになっているので、current
がそれにsymlinkされる。
deploy:published
deployと同じ。(何もしない)
deploy:finishing_rollback
task :finishing_rollback do invoke 'deploy:cleanup_rollback' end desc 'Remove and archive rolled-back release.' task :cleanup_rollback do on release_roles(:all) do last_release = capture(:ls, '-xt', releases_path).split.first last_release_path = releases_path.join(last_release) if test "[ `readlink #{current_path}` != #{last_release_path} ]" execute :tar, '-czf', deploy_path.join("rolled-back-release-#{last_release}.tar.gz"), last_release_path execute :rm, '-rf', last_release_path else debug 'Last release is the current release, skip cleanup_rollback.' end end end
- ロールバックしたリリースはいらないよね、っていう作業
rolled-back-release-#{last_release}.tar.gz
という感じのファイル名でアーカイブ- デプロイ先(currentとかと同じ階層)に置かれる
releases
にあるリリースを消す
deploy:finished
deployと同じ。
3.3. capistrano/deploy のまとめ
capistrano/framework
はフローの雛形を提供している- deploy
- デプロイ先のディレクトリなどを準備する
- 初回のみ
git clone
する - 毎回
git remote update
が行われる git archive <branch>
したものがreleases/<timestamp>
の中に置かれる- 上記リリース物と
current
をsymlinkでくっつける releases
が:keep_releases
を超えないように調整する
- rollback
- 直前のリリース物を
current
にする - 直前のリリース物をアーカイブする
- 直前のリリース物を
releases
から削除する
- 直前のリリース物を
framework
とdeploy
のおかげで利用者としては基本的にはフックを定義するだけで良い。ということをちゃんと理解出来た。
基本処理は分かったとして、一応before
/after
のフックについても仕組みを見ておきたい。
4. before/after
capistrano/dsl.rb
で読み込まれるlib/capistrano/dsl/task_enhancements.rb
にて定義されている。
def before(task, prerequisite, *args, &block) prerequisite = Rake::Task.define_task(prerequisite, *args, &block) if block_given? Rake::Task[task].enhance [prerequisite] end def after(task, post_task, *args, &block) Rake::Task.define_task(post_task, *args, &block) if block_given? post_task = Rake::Task[post_task] Rake::Task[task].enhance do post_task.invoke end end
capistranoというよりはRakeの構文の模様。
Rake で任意のタスクの前後に別のタスクを実行する
4.1. before/after のまとめ
- Rakeに委譲しているだけ
おわりに
整理と備忘のために書いたけど、忘れてもまたドキュメントとコードを読んだ方が分かりやすいような気がしてきた。