woshidan's blog

そんなことよりコードにダイブ。

Capistranoでぺらいち未満のWebサイトをデプロイしてみます

今日はRubyで書かれたビルド・デプロイ作業の自動化フレームワークCapistranoにさわってみます。

Capistranoでできること

最初に、入門 Capistrano 3 ~ 全ての手作業を生まれる前に消し去りたいを読めばいいという話ではあるんですが、Capistranoでできることを手短にまとめておきます。

  • ビルド作業で共通で使う設定を定義する
    • set :xxx, value_of_xxx と書いておけば、setした後続のtaskの中で fetch :xxx と書いてその値を参照できる
  • ビルドの際に行う作業をtaskとして定義する
  • after_task => :before_task などの記法でタスク間の依存関係が指定できる
    • after_task => :before_task:after_task を行う前に :before_task を行う必要がある、依存関係の指定
    • before task_a, task_b, after task_b, task_atask_b の前に task_a を行う指定(task_bの実行にtask_aが必要といったタスク間に依存関係はない)

今日やること

  • nginxで ホスト名/ にアクセスしたらHTMLのページを返す単純なWebサイトのデプロイ
  • デプロイ先はEC2
  • (たぶん必要ないかもですが)勉強用にデプロイ前後でnginxの再起動を行う

という内容でCapistranoの設定ファイルを書いて動かしてみます。

設定ファイルを動かしているCapistranoのバージョンは 3.11.0 で、書く内容の設定ファイルのイメージはざっくりと以下です。

# config/deploy.rb
# ローカルホストで行う設定を書く

# HTMLのソースコードを取得
# ソースコードをアップロードのためにzipに固める
# config/deploy/production.rb
# デプロイ先のサーバでおこなう設定を書く

# EC2サーバの設定
# htmlファイルのzipのアップロード & 解凍
# nginxの停止・起動

TL;DR

下にも書いたんですが、今日やったことのまとめです。

  • GitHubリポジトリからソースコードを取ってくる
    • ブランチの指定はまだ
  • config/deploy/#{environment}.rbserver を書いてデプロイ先のサーバを指定
    • ssh_optionssshログインするときの秘密鍵etcを指定
  • deploy_to でデプロイ先のサーバ上のどこのディレクトリにアップロードするかを指定
  • on roles(:role) でリモートのサーバで実施するコマンド, run_locally でローカルサーバで実行するタスクを書く
    • execute "command" でタスクの中で実行するコマンドを書く
    • upload! では、 deploy_toディレクトリ内にある git リポジトリ直下から他のディレクトリへコピーするファイルを指定する
      • cap install したそのままの状態で git pluginを使っている場合、 upload! は実態がリモートホスト上のコピー*1なので、upload!on roles(:role) に書く
  • task :a => :b:a を実行するときはその前に :b を実行するという指定ができる

実際手を動かしたメモ

Capistranoのインストールと設定ファイルの生成

$ gem install capistrano
$ cap --version
Capistrano Version: 3.11.0 (Rake Version: 12.3.1)

$ cap install
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile
Capified

$ tree .
.
├── Capfile
├── config
│   ├── deploy
│   │   ├── production.rb
│   │   └── staging.rb
│   └── deploy.rb
├── html
│   └── index.html# デプロイ対象のHTML
└── lib
    └── capistrano
        └── tasks

HTMLのソースコードを取得

デプロイ対象のHTML含めて先ほど表示したファイルをGitHubのプライベートリポジトリで管理することにして、デプロイ時はプライベートリポジトリからソースコードを取ってくることにします。

# config/deploy.rb
# config valid for current version and patch releases of Capistrano
lock "~> 3.11.0"

set :application, "capistrano_experiment"
set :repo_url, "git@github.com:woshidan/capistranotest.git"

task :download do
  run_locally do # run_locally do; ... end でローカルマシン(今回は自分のPC)上で実行するコマンドを書く. run_locally ブロックと呼ぶ
    info "downloading master branch source from GitHub." # info, warn... などでログレベルに応じたログを出力できる
    execute "git checkout master && git pull origin master" # タスク内でコマンドを実行するときは、execute "コマンド" で書くkaku
  end
end

話を単純にするため、作業用のディレクトリではすでにgitのリポジトリをcloneしてきていて、masterブランチのみをdeploy対象とするものとします*2

この時点ではタスクを定義しているだけで、deployの際にdownloadタスクを実行すると設定していないので cap production deploy しても何も起こりませんし、 downloading master branch source from GitHub. のログも log/capistrano.log に出力されません。

ソースコードをアップロードのためにzipに固める

サーバにアップロードする必要のあるファイル、今回は量はそこまでするほど量はないですが、index.htmlをzipに固めるタスクです。

# config/deploy.rb

task :archive => :download do
  run_locally do
    info "archive html directory to zip"
    execute "zip -r html.zip html"
  end
end

:archive => :download の部分は :archive タスクは :download タスクをやった後にしてください、ということですね。

EC2のインスタンスを立ち上げて、nginxを入れる

nginxで静的ファイルを表示するサイトを作る予定と書いたので、適当なEC2インスタンスを立ち上げて以下のコマンドでnginxを入れておく。 セキュリティグループは雑にマイIP*3からのみSSH, HTTP, HTTPSアクセスを許可。

# 参考: http://d.hatena.ne.jp/january/20130819
$ sudo yum install nginx
$ which nginx # インストール確認
/usr/sbin/nginx
$ sudo nginx # nginx起動
$ ps -ef | grep nginx # masterプロセスとworkerプロセスの起動を確認
root      2759     1  0 09:27 ?        00:00:00 nginx: master process nginx
nginx     2760  2759  0 09:27 ?        00:00:00 nginx: worker process
ec2-user  2762  2686  0 09:27 pts/0    00:00:00 grep --color=auto nginx

ホストのアドレスにアクセスすると

f:id:woshidan:20180913184629p:plain

のようなページが出てnginxが起動していることが確認できる。

/etc/nginx/nginx.conf をみると、

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    index   index.html index.htm;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /usr/share/nginx/html;

とあり、

$ less /usr/share/nginx/html/index.html | grep Welcome
        <h1>Welcome to <strong>nginx</strong> on the Amazon Linux AMI!</h1>

と確かめてみると、と表示されたページのソースコードの所在が確認できたので、今日のところは server ディレクティブの root/var/www に変更し、 /var/www 以下に index.html をアップロードすることを目標にする。

# 変更後のconfファイル
    index   index.html index.htm;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /var/www; # ここを変えた
        # root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        # redirect server error pages to the static page /40x.html
        #
        # error_page 404 /404.html; # 40xなどを/var/wwwに用意するのが面倒なので一旦コメントアウト
        #     location = /40x.html {
        # }

        # redirect server error pages to the static page /50x.html
        #
        # error_page 500 502 503 504 /50x.html;
        #     location = /50x.html {
        # }

また、デプロイが完了するまで仮に表示するページを用意しておき

[ec2-user@ip-172-31-27-79 ~]$ sudo mkdir /var/www
[ec2-user@ip-172-31-27-79 ~]$ sudo touch /var/www/index.html
[ec2-user@ip-172-31-27-79 ~]$ sudo vim /var/www/index.html

再起動して設定が変更されたことを確認しておく。

[ec2-user@ip-172-31-27-79 ~]$ sudo service nginx stop
Stopping nginx:                                            [  OK  ]
[ec2-user@ip-172-31-27-79 ~]$ sudo service nginx start
Starting nginx:                                            [  OK  ]

f:id:woshidan:20180913184810p:plain

EC2サーバの設定を書く

さて、デプロイ先のサーバができたところでCapistranoの設定ファイルにデプロイ先のサーバの設定を書いていく。

# config/deploy/production.rb

# server ホスト名(アクセスできるものだったらよいのでIPアドレス)
# user ログインユーザ
# roles あとでリモートホストで行うタスクを設定するとき on roles(:web) のように使う、デプロイ作業用グループ
server "5x.xxx.xxx.x0", user: "ec2-user", roles: %w{web}

そういえば、 config/deploy/production.rb のようなデプロイ先のステージの設定ファイルを用意すると

$ cap production download
00:00 download
      downloading master branch source from GitHub.
      01 git checkout master && git pull origin master
      01 Already on 'master'
      01 From https://github.com/woshidan/capistranotest
      01  * branch            master     -> FETCH_HEAD
      01 Already up-to-date.
    ✔ 01 woshidan@localhost 1.083s

# task :archive => :download do
$ cap production archive
00:00 download
      downloading master branch source from GitHub.
      01 git checkout master && git pull origin master
      01 Already on 'master'
      01 From https://github.com/woshidan/capistranotest
      01  * branch            master     -> FETCH_HEAD
      01 Already up-to-date.
    ✔ 01 woshidan@localhost 1.097s
00:01 archive
      archive html directory to zip
      01 zip -r html.zip html
      01 updating: html/ (stored 0%)
      01 updating: html/index.html (deflated 20%)01 woshidan@localhost 0.042s

のように、設定したタスクが実行できるようになる。

htmlファイルのzipのアップロード & 解凍

今回のWebサイトについて、具体的なデプロイ作業とは、nginxで表示するhtmlファイルのzipのアップロードと、アップロードしたファイルの解凍・配置となる。その作業を行うタスクを書いていく。

Capistranoでファイルをアップロードする際は upload! ローカルファイルへのパス, アップロード先サーバ上のパス を利用するが、その設定は

# config/deploy.rb

task :deploy => :archive do
  on roles(:web) do
    upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード
  end
end

でありますが、そのままだと

00:01 git:wrapper
      01 mkdir -p /tmp
(Backtrace restricted to imported tasks)
cap aborted!
SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@5x.xxx.xxx.x0: Authentication failed for user ec2-user@5x.xxx.xxx.x0


Caused by:
Net::SSH::AuthenticationFailed: Authentication failed for user ec2-user@5x.xxx.xxx.x0

Tasks: TOP => deploy:check => git:check => git:wrapper
(See full trace by running task with --trace)
The deploy has failed with an error: Exception while executing as ec2-user@5x.xxx.xxx.x0: Authentication failed for user ec2-user@5x.xxx.xxx.x0


** DEPLOY FAILED
** Refer to log/capistrano.log for details. Here are the last 20 lines:

のようにエラーになってしまうので、sshのオプションを config/deploy/production.rb (デプロイ先の設定を書くファイル) に追加します。

# config/deploy/production.rb

server "5x.xxx.xxx.x0", user: "ec2-user", roles: %w{web}
set :ssh_options, keys: %{./capistrano_test.pem}, auth_methods: %w{publickey}

すると、今度は

 DEBUG [0691849d] Command: ( export GIT_ASKPASS="/bin/echo" GIT_SSH="/tmp/git-ssh-capistrano_experiment-production-woshidan.sh" ; /usr/bin/env git ls-remote git@github.com:woshidan/capistranotest.git HEAD )

 DEBUG [0691849d]   /usr/bin/env: 

 DEBUG [0691849d]   git

 DEBUG [0691849d]   : No such file or directory

のようなメッセージで失敗しますが、これはデプロイ先のサーバにgitがない、という意味なのでデプロイ先のサーバに

$ sudo yum install git

のようにgitをインストールします*4

実は、Capfileを cap install から何も変更せずに進めた場合、

task :deploy => :archive do
  on roles(:web) do
    upload! "./html.zip", "/var/www" # リモートホスト上の /var/www へ ローカルホストの ./html.zip をアップロード
  end
end

deploy のタスクを開始した時点で :deploy_to に指定したディレクトリ、または /var/www/#{application}ディレクトリで git clone を行なっているようです。

task :deploy => :archive do

end

のように :deploy タスクの中身を空にしても

00:00 archive
      archive html directory to zip
      01 zip -r html.zip html
      01 updating: html/ (stored 0%)
      01 updating: html/index.html (deflated 20%)
    ✔ 01 woshidan@localhost 0.042s
    ✔ 02 ec2-user@5x.xxx.xxx.x0 0.421s
00:01 git:wrapper
      01 mkdir -p /tmp
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s
      Uploading /tmp/git-ssh-capistrano_experiment-production-woshidan.sh 100.0%
      02 chmod 700 /tmp/git-ssh-capistrano_experiment-production-woshidan.sh
    ✔ 02 ec2-user@5x.xxx.xxx.x0 0.092s
00:01 git:check
      01 git ls-remote git@github.com:woshidan/capistranotest.git HEAD
      01 b132c31d1fc37f08848d8b860bf57f20ad4ef635   HEAD
    ✔ 01 ec2-user@5x.xxx.xxx.x0 1.774s
00:03 deploy:check:directories
      01 mkdir -p /home/ec2-user/upload_prepare_dir/shared /home/ec2-user/upload_prepare_dir/releases
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.132s
00:04 git:clone
      The repository mirror is at /home/ec2-user/upload_prepare_dir/repo
00:04 git:update
      01 git remote set-url origin git@github.com:woshidan/capistranotest.git
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.093s
      02 git remote update --prune
      02 Fetching origin
      02 From github.com:woshidan/capistranotest
      02    f4e5e6b..b132c31  master     -> master
    ✔ 02 ec2-user@5x.xxx.xxx.x0 2.188s
00:06 git:create_release
      01 mkdir -p /home/ec2-user/upload_prepare_dir/releases/20180913134338
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.100s
      02 git archive master | /usr/bin/env tar -x -f - -C /home/ec2-user/upload_prepare_dir/releases/20180913134338
    ✔ 02 ec2-user@5x.xxx.xxx.x0 0.096s
00:07 deploy:set_current_revision
      01 echo "b132c31d1fc37f08848d8b860bf57f20ad4ef635" > REVISION
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s
00:07 deploy:symlink:release
      01 ln -s /home/ec2-user/upload_prepare_dir/releases/20180913134338 /home/ec2-user/upload_prepare_dir/releases/current
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.092s
      02 mv /home/ec2-user/upload_prepare_dir/releases/current /home/ec2-user/upload_prepare_dir
    ✔ 02 ec2-user@5x.xxx.xxx.x0 0.090s
00:07 deploy:cleanup
      Keeping 5 of 6 deployed releases on 5x.xxx.xxx.x0
      01 rm -rf /home/ec2-user/upload_prepare_dir/releases/20180913112304
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.118s
00:07 deploy:log_revision
      01 echo "Branch master (at b132c31d1fc37f08848d8b860bf57f20ad4ef635) deployed as release 20180913134338 by woshidan" >> /home/ec2-user/upload_prepare_dir/r…
    ✔ 01 ec2-user@5x.xxx.xxx.x0 0.098s

git clone が行われています。

# config/deploy.rb
set :deploy_to, "/home/ec2-user/upload_prepare_dir"

task :deploy => :archive do
  on roles(:web) do
    upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード
  end
end

とした場合、 /home/ec2-user/upload_prepare_dir にてgit cloneやgit cloneしてきたコードのバージョン管理用のディレクトリを作成して、 upload! では、そのディレクトリから2つめに指定したディレクトリへファイルをコピーしているようです*5

[ec2-user@ip-172-31-27-79 ~]$ pwd
/home/ec2-user
$ tree .
.
├── deploy_target_dir
│   └── html.zip
├── html
│   └── index.html
└── upload_prepare_dir
    ├── current -> /home/ec2-user/upload_prepare_dir/releases/20180913134913
    ├── releases
    │   └── 20180913134913
    │       ├── Capfile
    │       ├── config
    │       │   ├── deploy
    │       │   │   ├── production.rb
    │       │   │   └── staging.rb
    │       │   └── deploy.rb
    │       ├── html
    │       │   └── index.html
    │       └── REVISION
    ├── repo
    │   ├── branches
    │   ├── config
    │   ├── description
    │   ├── FETCH_HEAD
    │   ├── HEAD
    │   ├── hooks
    │   │   ├── applypatch-msg.sample
    │   │   ├── commit-msg.sample
    │   │   ├── post-update.sample
    │   │   ├── pre-applypatch.sample
    │   │   ├── pre-commit.sample
    │   │   ├── prepare-commit-msg.sample
    │   │   ├── pre-push.sample
    │   │   ├── pre-rebase.sample
    │   │   ├── pre-receive.sample
    │   │   └── update.sample
    │   ├── info
    │   │   └── exclude
    │   ├── objects
    │   │   ├── info
    │   │   └── pack
    │   │       ├── pack-455700f6724c559c3d0264e92c2888bf6b191610.idx
    │   │       └── pack-455700f6724c559c3d0264e92c2888bf6b191610.pack
    │   ├── packed-refs
    │   └── refs
    │       ├── heads
    │       └── tags
    ├── revisions.log
    └── shared

20 directories, 27 files

上は実際にデプロイ先のサーバで tree . してみたところ。

アップロードした zip ファイルを解凍して /var/www/index.html に配置するところまで追加*6

# config/deploy.rb

task :deploy => :archive do
  on roles(:web) do
    execute "mkdir -p /home/ec2-user/deploy_target_dir"
    upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード
    execute "cd /home/ec2-user/deploy_target_dir && unzip -o /home/ec2-user/deploy_target_dir/html.zip"
    execute "sudo cp /home/ec2-user/deploy_target_dir/html/index.html /var/www/index.html"
  end
end

nginxの停止・再起動

今回のデプロイには変更ないのだけどデプロイ時にアプリケーションを再起動する、というのはよくあることだから、素振りとしてnginxを再起動しておく。

# config/deploy.rb

task :deploy => :archive do
  on roles(:web) do
    execute "mkdir -p /home/ec2-user/deploy_target_dir"
    upload! "./html.zip", "/home/ec2-user/deploy_target_dir" # リモートホスト上の /home/ec2-user/deploy_target_dir へ ローカルホストの ./html.zip をアップロード
    execute "cd /home/ec2-user/deploy_target_dir && unzip -o /home/ec2-user/deploy_target_dir/html.zip"
    execute "sudo cp /home/ec2-user/deploy_target_dir/html/index.html /var/www/index.html"

    # 以下を追加
    execute "sudo service nginx stop"
    execute "sudo service nginx start"
  end
end

デプロイした結果を確認できたのでよさそう。

今日はcapistranoを使って

  • GitHubリポジトリからソースコードを取ってくる
    • ブランチの指定はまだ
  • config/deploy/#{environment}.rbserver を書いてデプロイ先のサーバを指定
    • ssh_optionssshログインするときの秘密鍵etcを指定
  • deploy_to でデプロイ先のサーバ上のどこのディレクトリにアップロードするかを指定
  • on roles(:role) でリモートのサーバで実施するコマンド, run_locally でローカルサーバで実行するタスクを書く
    • execute "command" でタスクの中で実行するコマンドを書く
    • upload! では、 deploy_toディレクトリ内にある git リポジトリ直下から他のディレクトリへコピーするファイルを指定する
      • cap install したそのままの状態で git pluginを使っている場合、 upload! は実態がリモートホスト上のコピー*7なので、upload!on roles(:role) に書く
  • task :a => :b:a を実行するときはその前に :b を実行するという指定ができる

あたりの復習をして、さぼったなぁと思うことは

  • git pushしていれば、いまいるブランチをデプロイできるようにする
  • deploy ユーザーを用意して直接デプロイ対象のディレクトリへファイルをアップロードする
  • 静的ファイルのアップロードではなくもう少し動作するアプリをdeployする
  • ビルドサーバを用意して、ビルドサーバからだけアップロードできるようにする
  • プラグインの詳細

ですが、また今度でいいかなと思います。

とりあえず、現場からは以上です。

*1:デプロイプロセス全体としてはアップロードに見える?

*2:ビルドサーバとかで作業を行うならこういうわけにもいきませんが、今日はcapistranoの勉強ということにして、その辺はまた別途やろうと思います

*3:AWSはセキュリティグループのIPアドレスの設定でマイIPを設定するとAWSに接続しているISPが割り振る範囲のIPを入力してくれるみたい

*4:参考: https://qiita.com/himatani/items/87d54752021879e1ec89

*5:Capfileを何もいじっていなかったので https://github.com/capistrano/capistrano/blob/220db8fabab15b9d5cd5c9ab1f2744e0aa346eb0/lib/capistrano/scm/tasks/git.rake#L1-L2 や Capfile 中の install_plugin Capistrano::SCM::Git あたりが原因と思われます...

*6:実際はnginxで公開するディレクトリ /var/www/ にアップロドして、 /var/www/current/index.html あたりをnginxで公開するパスとしたほうがよさそう。そのためのdeployユーザの設定などがあるが今日は時間がないのでこういう感じで

*7:デプロイプロセス全体としてはアップロードに見える?