Edited at

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

More than 3 years have passed since last update.


初めに

こんにちは。CYBIRDエンジニア Advent Calendar最終日担当の@jin_kです。

CYBIRD2015年の新卒入社でゲームインフラ部という部署に所属しております。

24日目は@gotyooooさんのAmazon VPC NAT Gatewayがあるとき。ないとき。でした。

Amazon VPC NATGatewayがあるときとないときが分かりやすく整理されていて、NAT Gatewayを使うと何か良いか書いていますので、ぜひご覧ください!


内容について

今回はJenkinsサーバーからS3経由でWebサーバーにソースコードを配布してみた話をしたいと思います。

(Jenkinsサーバーを立てる過程の説明とかはありません。)

従って今回の話はちょっと変わったCapistranoの話をするので、Capistranoのデプロイ流れや説明・使い方についての話はしません。

とはいえ、自分もまだ初心者なので何か間違ったことが書いてある場合、ご指摘お願いします。


今回のCapistranoの動き


このような動きをした理由


  • Capistrano3系でGithubとの連携がHTTPSでアクセスするため、ソースコードにgitのアカウント情報が載せているのでセキュリティー的に良くないです。

  • 各自WebサーバーがダウンロードすることでJenkinsのサーバーの負担を減らす。

  • いずれにせよ100%信頼はできないが、それでもgitよりS3の方が信頼できる。


仕組み(pull)

스크린샷 2015-12-22 15.41.43.png

1.gitとJenkinsを連携し、最新のfileをJenkinsに返却します。

 (今回は1番に関する内容はありません。)

2.cap実行し、tarでソースコードを固めます。

3.固めたtarをS3にアップロードします。

(現在デプロイするソースコードと同じものがS3にある場合、3番のことはしません。)

4.各自S3からダウンロードするようWebサーバーに命令します。

5.各自Webサーバーがs3からtarをダウンロードします。

6.ダウンロードしたtarを展開します。


仕組み(push)

스크린샷 2015-12-22 19.19.00.png

1.gitとJenkinsを連携し、最新のfileをJenkinsに返却します。

 (今回は1番に関する内容はありません。)

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
====


デプロイ方法


ソースコード


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 }



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`

#(省略)



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



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



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します。


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にアップロードする関数実行


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からダウンロードします。


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を固めます。


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設定


パラメーター設定

以下のようにすべて環境変数で渡したものをパラメーター化します。

스크린샷 2015-12-22 19.51.46.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は以上となります。

来年がお楽しみです!

Merry Christmas!!!