Capistranoで新規作成したEC2インスタンスの初期設定
本項では、Capistranoで新規作成したEC2インスタンスへSSHで初回ログインした際の、保守用ユーザの作成、初期ユーザに対してのパスワード設定、サーバのホスト名設定など、いわゆるサーバ環境の初期設定を行うタスクを作ってみます。
その前に、ホスト名のつけ方としての色々と試してみての所感なのですが、初回SSHログイン後にそれぞれのインスタンスに対してHOSTS設定する際にEC2側にタグ付けてしていってもできるのですが、まずインスタンス作成する時にあらかじめホスト名の元となるタグを付けておいて、各インスタンスログイン後はそのタグを参照してHOST名を設定する方がスマートだと思いました。特に複数インスタンスを同時に立てる時などにホスト名に連番を振りたいとかいう時は、カウンター変数を使って回しているec2.instances.create()
時にそのカウンターの数値を転用できるので簡単でした。ということで、インスタンス作成時にタグを追加する方法ですが、
# タグ情報
set :host_name, 'deploy-client'
~(中略)~
created_instances = []
cnt = 0
while cnt < fetch(:instance_count) do
i = ec2.instances.create(
~(中略)~
)
sleep 10 while i.status == :panding
i.tags['Name'] = [ fetch(:host_name), format("%02d", cnt+1) ].join('-')
created_instances << i.id
cnt += 1
end
~(省略)~
── と、ec2.instances.create()
の後でタグを付けてやればOKです1。 今回はこのName
タグの値をその後のタスクでインスタンスのホスト名として利用します。
いきなり横道に逸れましたが、本題に戻ります。 初回SSH時のタスクとして、前回作成したinit
タスクを使います。流れとしては、デプロイサーバ側で新たに作成するユーザ用のキーペアを作成しておいて、デフォルトユーザにてSSHログイン後、まず保守用の新規ユーザアカウントを作成ます。その後、そのユーザに公開鍵認証によるSSH設定を行い、デフォルトユーザにはパスワードを設定してsudo権限を剥奪、ホスト名を設定して一旦ログアウトしています。 まず、タスク設定前に各種パラメータを定義します。
# タグ情報
set :host_name, 'deploy-client'
# 保守ユーザアカウント
set :user_account, 'deploy-user'
set :user_password, 'password'
# 初期ユーザパスワード
set :def_password, 'PassWord'
# SSH後にsudoを実行するために必要
set :pty, true
そして、初期設定用のタスクinit
です。
desc 'Check the activation status of new instances'
task :check do
created_instances_list = 'CREATED_INSTANCES'
run_locally do
ec2 = AWS::EC2.new
AWS.memoize do
begin
if test "[ -f ~/#{created_instances_list} ]"
created_instances = capture("cd ~; cat #{created_instances_list}").chomp
ci = created_instances.gsub(/(\[|\s|\])/, '').split(',')
target_instances = ec2.instances.select { |i| i.exists? && i.status == :running && ci.include?(i.id) }.map(&:private_ip_address)
if target_instances.length == 0 then
raise "No created instances"
end
pkfn = fetch(:private_key_file)
target_instances.each { |var|
server var, user: 'ec2-user', roles: %w{web app}, ssh_options: { keys: %W(/home/deploy-user/#{pkfn}), forward_agent: true }
}
end
rescue => e
info e
exit
end
end
end
end
task :init => :check do
run_locally do
# 公開鍵認証用のキーペアを作成する
target_dir = '~/.ssh'
if !test "[ -f #{target_dir}/#{fetch(:user_account)}_rsa ]"
if !test "[ -d #{target_dir} ]"
execute "mkdir -m 700 #{target_dir}"
end
execute "cd #{target_dir}; ssh-keygen -t rsa -N \"\" -f #{target_dir}/#{fetch(:user_account)}_rsa"
end
set :public_key_content, capture("cat #{target_dir}/#{fetch(:user_account)}_rsa.pub").chomp
set :new_private_key_path, "#{target_dir}/#{fetch(:user_account)}_rsa"
end
on roles(:web) do
# 初期設定(ユーザ作成時に.sshコンテナを自動作成する)
if !test "[ -d /etc/skel/.ssh/ ]"
execute :sudo, "mkdir -m 700 /etc/skel/.ssh/; sudo touch /etc/skel/.ssh/authorized_keys; sudo chmod 600 /etc/skel/.ssh/authorized_keys"
end
# 保守用の新規ユーザを作成
is_user = capture(:sudo, "cut -d: -f1 /etc/passwd").chomp.gsub(/(\r\n)/, ',').split(',')
if !is_user.include?(fetch(:user_account)) then
execute :sudo, "useradd -G wheel #{fetch(:user_account)}"
execute :sudo, "echo \"#{fetch(:user_account)}:#{fetch(:user_password)}\" | sudo chpasswd"
end
# 新規ユーザに公開鍵認証によるSSH権限を与える
auth_keys = "/home/#{fetch(:user_account)}/.ssh/authorized_keys"
if capture(:sudo, "cat #{auth_keys}").chomp != fetch(:public_key_content) then
execute :sudo, "echo \"#{fetch(:public_key_content)}\" | sudo tee #{auth_keys}"
end
# 新インスタンスのホスト名を変更
ec2 = AWS::EC2.new
AWS.memoize do
current_private_ip = capture(:sudo, "ifconfig | grep 'inet addr' | cut -d ':' -f 2 | awk 'NR==1 { print $1 }'").chomp
instance_ids = ec2.instances.select { |i| i.exists? && i.status == :running && i.private_ip_address == current_private_ip }.map(&:id)
host_basename = ec2.instances[instance_ids[0]].tags['Name']
if capture(:sudo, "hostname").chomp != host_basename.chomp then
execute :sudo, "echo \"#{host_basename}\" | sudo tee /proc/sys/kernel/hostname"
execute :sudo, "sed -i 's/\\(^HOSTNAME=\\).*/\\1#{host_basename}/' /etc/sysconfig/network"
execute :sudo, "hostname #{host_basename}"
end
end
# デフォルトユーザ「ec2-user」にパスワードを設定し、sudo権限を剥奪
# 保守用の新規ユーザにsudo権限を付与
is_passwd = capture(:sudo, "cut -d: -f1,2 /etc/shadow").chomp.gsub(/(\r\n)/, ',').split(',')
if is_passwd.include?("ec2-user:!!") then
execute :sudo, "echo \"ec2-user:#{fetch(:def_password)}\" | sudo chpasswd"
end
execute :sudo, "sed -i s/ec2-user/#{fetch(:user_account)}/g /etc/sudoers.d/cloud-init"
end
end
いやはや、CapistranoやRuby、DSLの知識が乏しくて結構難航しました。結構ハマったのが、sudo
系のコマンドを複数連携させるような使い方(リダイレクトとパイプ)をする時です。例えばワンライナーでパイプでチェーンさせるコマンドの場合、例えばexecute :sudo, "mkdir ~/new_dir | chmod a+w ~/new_dir"
のような繋げ方をすると、チェーンした後続のchmod
がsudo権限にならず、mkdir
しか実行されなかったので、execute :sudo, "mkdir ~/new_dir | sudo chmod a+w ~/new_dir
のように繋げたら上手くいきました。また、>
とか>>
のリダイレクトを使って標準出力をファイルに追記するような処理はエラーになって動かず、標準出力を拾えるchpasswd
やtee
ようなコマンドでパイプでチェーンさせる必要がありました。 あと、今回check
とinit
のタスクにて、「AWS SDK for Ruby」でAPIを使っているところのレスポンスについてキャッシュがあればそれを利用するようにAWS.memoize
メソッドでメモ化してみました(詳しくはこちらのサイト)。このメモ化を行うことで、check
タスクでAPIレスポンスがキャッシュされるので、後続のinit
タスクのパフォーマンスが抜群に早くなりました。
では、タスクを実行してみます。
$ cap test launch
INFO[a8c8eba0] Running /usr/bin/env echo -n ["i-e66952e0", "i-054a901c"] > ~/CREATED_INSTANCES on localhost
INFO[a8c8eba0] Finished in 0.003 seconds with exit status 0 (successful).
$ cap test init
INFO[d38e8171] Running /usr/bin/env sudo mkdir -m 700 /etc/skel/.ssh/; sudo touch /etc/skel/.ssh/authorized_keys; sudo chmod 600 /etc/skel/.ssh/authorized_keys on 176.34.62.171
INFO[d38e8171] Finished in 0.045 seconds with exit status 0 (successful).
INFO[84cb8ae3] Running /usr/bin/env sudo useradd -G wheel deploy-user on 176.34.62.171
INFO[ef747c5e] Running /usr/bin/env sudo mkdir -m 700 /etc/skel/.ssh/; sudo touch /etc/skel/.ssh/authorized_keys; sudo chmod 600 /etc/skel/.ssh/authorized_keys on 176.34.61.82
INFO[84cb8ae3] Finished in 0.346 seconds with exit status 0 (successful).
INFO[235d5728] Running /usr/bin/env sudo echo "deploy-user:password" | sudo chpasswd on 176.34.62.171
INFO[ef747c5e] Finished in 0.085 seconds with exit status 0 (successful).
INFO[82ead8fe] Running /usr/bin/env sudo useradd -G wheel deploy-user on 176.34.61.82
INFO[82ead8fe] Finished in 0.073 seconds with exit status 0 (successful).
INFO[3fd263c2] Running /usr/bin/env sudo echo "deploy-user:password" | sudo chpasswd on 176.34.61.82
INFO[235d5728] Finished in 0.163 seconds with exit status 0 (successful).
INFO[ad2ae489] Running /usr/bin/env sudo echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgQ31X0qXm/eHXZCIecjv57C66cZ4ikLdprDhHZs+KV5/vK0B+/47cZCXaT7UEHdI+Bm3jNTPJoRE8iPzpkWB9L5Ks13tB7yJ5DGEIbFRe8d3kTa6Uwrj1HpL+i8hSZ7Bzbc4JvF5YULj97NfVAmpNvAqxF1mRWeuzcmevnsVNJ1nF6ePysNjiWmboepWl+MvIJ8xXLYPzrw8mO1kg7WEB0QxqGN5OsVZjjbmEMLliJ+xbOGfxI50FEa+k2445Y3nynBD9krx/1wayurEVn2t8jKWDn6XLSUJ41Ep43QkwFibwtcVBsfDSPIHVm6S3k9RzaAQWpN6qSrUiabk0yAOp deploy-user@devlab-deploy01" | sudo tee /home/deploy-user/.ssh/authorized_keys on 176.34.62.171
INFO[3fd263c2] Finished in 0.117 seconds with exit status 0 (successful).
INFO[ad2ae489] Finished in 0.037 seconds with exit status 0 (successful).
INFO[36d81321] Running /usr/bin/env sudo echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgQ31X0qXm/eHXZCIecjv57C66cZ4ikLdprDhHZs+KV5/vK0B+/47cZCXaT7UEHdI+Bm3jNTPJoRE8iPzpkWB9L5Ks13tB7yJ5DGEIbFRe8d3kTa6Uwrj1HpL+i8hSZ7Bzbc4JvF5YULj97NfVAmpNvAqxF1mRWeuzcmevnsVNJ1nF6ePysNjiWmboepWl+MvIJ8xXLYPzrw8mO1kg7WEB0QxqGN5OsVZjjbmEMLliJ+xbOGfxI50FEa+k2445Y3nynBD9krx/1wayurEVn2t8jKWDn6XLSUJ41Ep43QkwFibwtcVBsfDSPIHVm6S3k9RzaAQWpN6qSrUiabk0yAOp deploy-user@devlab-deploy01" | sudo tee /home/deploy-user/.ssh/authorized_keys on 176.34.61.82
INFO[36d81321] Finished in 0.044 seconds with exit status 0 (successful).
INFO[29ccb02a] Running /usr/bin/env sudo echo "deploy-client-01" | sudo tee /proc/sys/kernel/hostname on 176.34.61.82
INFO[29ccb02a] Finished in 0.021 seconds with exit status 0 (successful).
INFO[5af43785] Running /usr/bin/env sudo sed -i 's/\(^HOSTNAME=\).*/\1deploy-client-01/' /etc/sysconfig/network on 176.34.61.82
INFO[5af43785] Finished in 0.015 seconds with exit status 0 (successful).
INFO[17c7a3cc] Running /usr/bin/env sudo hostname deploy-client-01 on 176.34.61.82
INFO[17c7a3cc] Finished in 0.015 seconds with exit status 0 (successful).
INFO[1a64d176] Running /usr/bin/env sudo echo "ec2-user:PassWord" | sudo chpasswd on 176.34.61.82
INFO[1a64d176] Finished in 0.031 seconds with exit status 0 (successful).
INFO[13f0fbe3] Running /usr/bin/env sudo sed -i s/ec2-user/deploy-user/g /etc/sudoers.d/cloud-init on 176.34.61.82
INFO[13f0fbe3] Finished in 0.015 seconds with exit status 0 (successful).
INFO[23a6c3a5] Running /usr/bin/env sudo echo "deploy-client-02" | sudo tee /proc/sys/kernel/hostname on 176.34.62.171
INFO[23a6c3a5] Finished in 0.043 seconds with exit status 0 (successful).
INFO[d0215821] Running /usr/bin/env sudo sed -i 's/\(^HOSTNAME=\).*/\1deploy-client-02/' /etc/sysconfig/network on 176.34.62.171
INFO[d0215821] Finished in 0.018 seconds with exit status 0 (successful).
INFO[ce6a2993] Running /usr/bin/env sudo hostname deploy-client-02 on 176.34.62.171
INFO[ce6a2993] Finished in 0.017 seconds with exit status 0 (successful).
INFO[071e7b50] Running /usr/bin/env sudo echo "ec2-user:PassWord" | sudo chpasswd on 176.34.62.171
INFO[071e7b50] Finished in 0.085 seconds with exit status 0 (successful).
INFO[28396919] Running /usr/bin/env sudo sed -i s/ec2-user/deploy-user/g /etc/sudoers.d/cloud-init on 176.34.62.171
INFO[28396919] Finished in 0.018 seconds with exit status 0 (successful).
デプロイ成功です。 確認のため、コマンドラインから新しく作られた保守用ユーザでSSHログインしてみます。
$ ssh -i ~/.ssh/deploy-user_rsa deploy-user@176.34.61.82
__| __|_ )
_| ( / Amazon Linux AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-ami/2014.03-release-notes/
8 package(s) needed for security, out of 18 available
Run "sudo yum update" to apply all updates.
[deploy-user@deploy-client-01 ~]$ sudo su -
[root@deploy-client-01 ~]# su ec2-user
[ec2-user@deploy-client-01 root]$ sudo su -
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[sudo] password for ec2-user:
ec2-user is not in the sudoers file. This incident will be reported.
[ec2-user@deploy-client-01 root]$
無事、ログインできました。 さらに、保守用ユーザはパスワードなしでsudoでき、一方、デフォルトユーザのec2-userはsudo権限が剥奪されています。期待通りのデプロイができています。
ただ、このデプロイ設定にはちょっと問題があって、インスタンスラウンチ用のタスク後のインスタンス状態(Instance State)が:running
になって起動済みでも、ステータス確認(Status Checks)が:initializing
状態だと、初回SSHが出来ずにinitタスクが途中で停止してしまいます。現状のcheckタスクではInstance Stateが:runningかどうかしか見ていないための問題ですね。
長くなってきたので、今回はここまでとします。次回は、Status Checksを判定するcheckタスクの改修を行ってみるのと、今回のinitタスクのafter処理として、新たに作成された保守用ユーザのSSH設定をCapistranoの設定ファイルに書き出す処理、そしてようやくパッケージアップデートを行う本来のデプロイ処理の導入までを作ってみようかと。
参考サイト
Name
タグはAWSマネージメントコンソールのインスタンス一覧の名前としても使われるので、このタグを付けておくと、作成したインスタンスが判別し易くなるためオススメします。 ↩