Capistrano3でJenkinsサーバーからS3経由でWebサーバーにソースコードを配布してみた話

## 初めに
こんにちは。[CYBIRDエンジニア Advent Calendar](http://qiita.com/advent-calendar/2015/cybird)最終日担当の[@jin_k](http://qiita.com/jin_k)です。
CYBIRD2015年の新卒入社でゲームインフラ部という部署に所属しております。

24日目は[@gotyoooo](http://qiita.com/gotyoooo)さんの[Amazon VPC NAT Gatewayがあるとき。ないとき。](http://qiita.com/gotyoooo/items/a5e9dc4e19f6463ad081)でした。
Amazon VPC NATGatewayがあるときとないときが分かりやすく整理されていて、NAT Gatewayを使うと何か良いか書いていますので、ぜひご覧ください！

##内容について
今回はJenkinsサーバーからS3経由でWebサーバーにソースコードを配布してみた話をしたいと思います。
(Jenkinsサーバーを立てる過程の説明とかはありません。)

従って今回の話はちょっと変わったCapistranoの話をするので、Capistranoのデプロイ流れや説明・使い方についての話はしません。
とはいえ、自分もまだ初心者なので何か間違ったことが書いてある場合、ご指摘お願いします。
##今回のCapistranoの動き
###このような動きをした理由
- Capistrano3系でGithubとの連携がHTTPSでアクセスするため、ソースコードにgitのアカウント情報が載せているのでセキュリティー的に良くないです。
- 各自WebサーバーがダウンロードすることでJenkinsのサーバーの負担を減らす。
- いずれにせよ100%信頼はできないが、それでもgitよりS3の方が信頼できる。

###仕組み(pull)
<img width="1014" alt="스크린샷 2015-12-22 15.41.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/101553/c6e5a253-469b-37f2-dfac-71fa1e63ba6e.png">
1.gitとJenkinsを連携し、最新のfileをJenkinsに返却します。
　(今回は１番に関する内容はありません。)
2.cap実行し、tarでソースコードを固めます。
3.固めたtarをS3にアップロードします。
 (現在デプロイするソースコードと同じものがS3にある場合、3番のことはしません。)
4.各自S3からダウンロードするようWebサーバーに命令します。
5.各自Webサーバーがs3からtarをダウンロードします。
6.ダウンロードしたtarを展開します。
###仕組み(push)
<img width="990" alt="스크린샷 2015-12-22 19.19.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/101553/5c6f06dc-03e7-277b-cbd2-fe51a9e44907.png">

1.gitとJenkinsを連携し、最新のfileをJenkinsに返却します。
　(今回は１番に関する内容はありません。)
2.cap実行し、tarでソースコードを固めます。
3.JenkinsサーバーからWebサーバーにtarを送りつけます。
4.tarを展開します。

※pull以外はすべてpushの処理をします。
##インストール
- archive/tar/minitar 0.5.2

```
$ gem install archive-tar-minitar -v 0.5.2
```
- Ruby 2.1.0p0

```
$ rvm install ruby-2.1.0
$ rvm use ruby-2.1.0
$ ruby --version
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-linux]
```

- Capistrano 3.2.1

```
$ gem install capistrano
$ cap --version
Capistrano Version: 3.2.1 (Rake Version: 10.1.0)
```

S3へのアップロードするためにRubyのAWS SDKを利用します。

- aws-sdk-core 2.1.16

```
$ gem install aws-sdk-core -v 2.1.16
```

- aws-sdk-resources 2.1.16

```
$ gem install aws-sdk-resources -v 2.1.16
```

S3経由でデプロイするためAWS CLIが必要となりますのでWebサーバーにinstallします。
CredentialFileはJenkinsサーバー、Webサーバー両方必要です。

```
# yum -y install python-setuptools
# easy_install pip
# pip install awscli
# vi /etc/aws/credential-file
====
[default]
output = json
region = ap-northeast-1
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
====
```
## デプロイ方法
###ソースコード


```rb:Capfile
$:.unshift File.expand_path(File.dirname(__FILE__) + '/lib')

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

# Includes default deployment tasks
require 'capistrano/deploy'

Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
```

```rb:config/deploy.rb
# config valid only for Capistrano 3.1
set :pty, true
lock '3.2.1'
set :log_level, SSHKit::Logger::INFO
set :application, 'JkTest'
set :scm, :tarball_deploy

set :tmp_dir, '/tmp'
set :copy_exclude, ['docs']

set :project_release_id, `git log --pretty=format:'%h' -n 1 HEAD`

#(省略)
```
```rb:config/deploy/production_pull.rb
set :deploy_to, ENV['DEPLOY_TO']#Webサーバーにソースコードを置くところです。
set :resource_path, ENV['RESOURCE_PATH']#以下のJenkinsサーバーにあるものをtarで固めます。

set :tmp_tarball, '/tmp/production'
set :aws_credential_file, ENV['AWS_CREDENTIAL_FILE']#awsにアクセスするためにcredential-fileが書いているところです。

servers_str = ENV['SERVERS'].to_s
servers_str = servers_str.gsub(/\s/, '')

servers_str.split(',').each do |target_server|
    if target_server.empty?
        next
    end
    server target_server,
        user: ENV['WEB_SERVER_USER'],#Webサーバーのユーザー
        roles: ['app'],
        ssh_options: {
            keys: [ENV['WEB_SERVER_SSH']],#Webサーバーにアクセスするためのsshが置いてある場所
            forward_agent: false,
            auth_methods: ['publickey']
        }
end
```

```rb:config/deploy/production_push.rb
set :deploy_to, ENV['DEPLOY_TO']
set :resource_path, ENV['RESOURCE_PATH']

set :tmp_tarball, '/tmp/production'
servers_str = ENV['SERVERS'].to_s
servers_str = servers_str.gsub(/\s/, '')

servers_str.split(',').each do |target_server|
    if target_server.empty?
        next
    end
    server target_server,
        user: ENV['WEB_SERVER_USER'],
        roles: ['app'],
        ssh_options: {
            keys: [ENV['WEB_SERVER_SSH']],
            forward_agent: false,
            auth_methods: ['publickey']
        }
end
```

```rb:lib/capistrano/tarball_deploy.rb
require 'digest/md5'
require 'zlib'
require 'archive/tar/minitar'
require 'aws-sdk-core'

namespace :tarball_deploy do

    s3_samefile_check = false#今デプロイするソースコードがS3に同じものがあるかcheckする変数

    if !ENV['DEPLOY_TYPE'].nil?
        # ENVで渡ってきていれば値を上書きする
        set :deploy_type, ENV['DEPLOY_TYPE']
    end

    desc 'check ENV value and configuration'
    task :check do
        if fetch(:tmp_tarball).nil?
            set :tmp_tarball, ENV['TMP_TARBALL'] ? ENV['TMP_TARBALL'] : '/tmp'
        end

        run_locally do
            execute "mkdir -p #{fetch(:tmp_tarball).gsub(/\/$/, '')}"
        end
        on roles(:app) do
            unless test 'which md5sum'
                error 'command `md5sum\' is not found.'
                abort
            end
        end

        if fetch(:deploy_type) == 'pull'
            # check AWS configuration
            #(省略)
        end
    end

```
ENVで渡している環境変数をcheckします。


```rb:lib/capistrano/tarball_deploy.rb

#(省略)
            # tarをアップロード
            if fetch(:deploy_type) == 'pull'
                unless (s3_samefile_check)
                    run_locally do
                        upload_tarball_to_s3(tarball)
                    end
                end
            else
                # pull以外はすべてpush型
                on roles(:app) do
                    upload! tarball, fetch(:tmp_dir)
                end
            end
```
tarをS3にアップロードする関数実行

```rb:lib/capistrano/tarball_deploy.rb
        on roles(:app) do
            if fetch(:deploy_type) == 'pull'
                # S3からダウンロードする処理
                execute "export AWS_CONFIG_FILE=#{fetch(:aws_credential_file)} && aws s3 cp s3://#{fetch(:bucket_name)}/#{fetch(:s3_path) + File.basename(tarball)} #{fetch(:tmp_dir)}/#{File.basename(tarball)}"
            end

            # check tarball hash
            #(省略)

```
S3からダウンロードします。

```rb:lib/capistrano/tarball_deploy.rb
    desc 'create tarball'
    file 'create_resource_tarball' do
        # create tarball on Capistrano installed server
        run_locally do
            if fetch(:deploy_type) == 'pull'
                Aws.config[:credentials] = Aws::SharedCredentials.new(
                path: fetch(:aws_credential_file),
                profile_name: fetch(:aws_profile_name)
                )
                Aws.config[:region] = fetch(:region)
                s3 = Aws::S3::Client.new()
                resp = s3.list_objects(bucket: fetch(:bucket_name),prefix: fetch(:s3_path))
                resp.contents.each do |object|
                    if ("#{fetch(:s3_path)}release-#{fetch(:project_release_id)}.tar.gz" == "#{object.key}")
                        s3_samefile_check = true
                    end
                end
            end

            unless (s3_samefile_check)
                info 'start to create tarball...'
                Zlib::GzipWriter.open("#{fetch(:tmp_tarball)}/release-#{fetch(:project_release_id)}.tar.gz", Zlib::BEST_COMPRESSION) do |gz|
                    out = Archive::Tar::Minitar::Output.new(gz)
                    Dir.chdir("#{fetch(:resource_path)}")

                    files = FileList['**/*']
                    exclude_list = fetch(:copy_exclude) + ['.git']

                    files.exclude(exclude_list).each do |file|
                        Archive::Tar::Minitar::pack_file(file, out)
                    end
                    out.close
                end
                info 'Finished'
                set :tarball_hash_value, Digest::MD5.hexdigest(File.open("#{fetch(:tmp_tarball)}/release-#{fetch(:project_release_id)}.tar.gz").read)
                info "created tarball's hash: #{fetch(:tarball_hash_value)}"
            else
                info 'Alrady updated.'
            end
        end
    end
```
同じものがS3にない場合、tarを固めます。

```rb:lib/capistrano/tarball_deploy.rb
#(省略)
    def upload_tarball_to_s3(upload_file)
        Aws.config[:credentials] = Aws::SharedCredentials.new(
            path: fetch(:aws_credential_file),
            profile_name: fetch(:aws_profile_name)
        )
        Aws.config[:region] = fetch(:region)
        s3 = Aws::S3::Client.new()
        s3.put_object(
            bucket: fetch(:bucket_name),
            body: File.open(upload_file),
            key: fetch(:s3_path) + File.basename(upload_file)
        )
    end

    def check_aws_configuration()
    #(省略)
end
```

##Jenkins設定
###パラメーター設定
以下のようにすべて環境変数で渡したものをパラメーター化します。
<img width="631" alt="스크린샷 2015-12-22 19.51.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/101553/1dc42d7f-4b7c-01a2-0ee6-7d28d2862d79.png">

###シェルスクリプト
```
cd ${WORKSPACE}/capistrano
SERVERS=${SERVER} /usr/local/bin/cap production_pull deploy DEPLOY_TYPE=${DEPLOY_TYPE} WEB_SERVER_SSH=${WEB_SERVER_SSH} WEB_SERVER_USER=${WEB_SERVER_USER} S3_PATH=${S3_PATH} REGION=${REGION} BUCKET_NAME=${BUCKET_NAME} RESOURCE_PATH=${RESOURCE_PATH} AWS_PROFILE_NAME=${AWS_PROFILE_NAME} AWS_CREDENTIAL_FILE=${AWS_CREDENTIAL_FILE} DEPLOY_TO=${DEPLOY_TO}
```


##起きたエラー
```
エラー部分：sudo: sorry,you must have a tty to run sudo.
```
参考：http://qiita.com/e2_adachi/items/7abf974d81067bd2b746
set :pty, trueを書くことで解決

```
The deploy has failed with an error: Exception while executing as xx@xxxxx: sudo /etc/init.d/httpd
　　　　　　graceful　　　 exit status: 1
　　　　　　sudo /etc/init.d/httpd graceful stdout: [sudo] password for xx:
　　　　　　sudo /etc/init.d/httpd graceful stderr: Nothing written
```
参考：http://blog.suz-lab.com/2011/01/sudoetcinitdhttpd.html
http://yut.hatenablog.com/entry/20111013/1318436872


```
/tmp/hudson4186181752264859077.sh: line 3: cap: command not found
```
参考：http://itpro.nikkeibp.co.jp/article/COLUMN/20060228/230996/
which capで確認したところ、capではなく、/usr/local/bin/capでした。

```
The deploy has failed with an error: Exception while executing as xx@xxxxxx: getaddrinfo: nodename nor servname provided, or not known
```
参考:http://www.linux-beginner.com/linux_setei1.html
IPアドレスとそれに対応するホスト名の設定ができてない。


## 最後に
今回Capistranoのことを書きましたが、いかがでしょうか。誰かの参考になれば幸せです。

Capistranoを知ったきっかけは先輩から社内で使っているCapistranoの改善のことで実行テスト・ソースコード修正をお願いされたことでした。Capistranoを理解やエラーが出ると詰まったり、先輩からお願いされたソースコードの内容が理解できなくって色々大変でしたが、いい経験になりました！

残念ながら[CYBIRDエンジニア Advent Calendar](http://qiita.com/advent-calendar/2015/cybird)は以上となります。
来年がお楽しみです！

Merry Christmas!!!