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のように繋げたら上手くいきました。また、>とか>>のリダイレクトを使って標準出力をファイルに追記するような処理はエラーになって動かず、標準出力を拾えるchpasswdteeようなコマンドでパイプでチェーンさせる必要がありました。 あと、今回checkinitのタスクにて、「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の設定ファイルに書き出す処理、そしてようやくパッケージアップデートを行う本来のデプロイ処理の導入までを作ってみようかと。

参考サイト

  1. NameタグはAWSマネージメントコンソールのインスタンス一覧の名前としても使われるので、このタグを付けておくと、作成したインスタンスが判別し易くなるためオススメします。