詳細検索

Initial setup of a newly created EC2 instance in Capistrano

Avatar
by maeno

Initial setup of a newly created EC2 instance in Capistrano
Translated from 日本語 • View original

In this section, we will create tasks to initialize the server environment, such as creating a maintenance user, setting a password for the initial user, and setting the hostname of the server when logging in to a newly created EC2 instance in Capistrano for the first time via SSH.

Before that, I tried various ways to name the host, but you can also tag the EC2 side when setting HOSTS for each instance after the first SSH login, but first of all, when creating an instance, attach a tag that will be the source of the hostname in advance. After logging in each instance, I thought it would be smarter to refer to that tag and set the HOST name. Especially when you want to put a sequential number on the hostname when setting up multiple instances at the same time, it was easy because you can divert the number of the counter when you are using the counter variable 'ec2.instances.create()'. So, how to add tags when creating an instance,

# Tag Information
set :host_name, 'deploy-client'

~(Omitted)~

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。 In this case, we will use the value of this 'Name' tag as the hostname of the instance in subsequent tasks.

I suddenly strayed from the sidewalk, but I will return to the main topic. For the first SSH task, use the 'init' task you created last time. The flow is to create a key pair for the newly created user on the deployment server, and after SSH login with the default user, create a new user account for maintenance first. After that, SSH is set for the user with public key authentication, a password is set for the default user to strip sudo privileges, and a hostname is set to log out once. First, define various parameters before setting up the task.

# Tag Information
set :host_name, 'deploy-client'

# Maintenance User Account
set :user_account, 'deploy-user'
set :user_password, 'password'

# Initial User Password
set :def_password, 'PassWord'

# Required to run sudo after SSH
set :pty, true

And then there is the task 'init' for the initial setup.

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
    # Create a key pair for public key authentication
    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
    # Initial setup (automatically create .ssh containers when creating users)
    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

# Create a new user for maintenance
    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

# Give new users SSH privileges with public key authentication
    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

# Change the hostname of the new instance
    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

# Set a password for the default user "ec2-user" and remove sudo privileges
    # Grant sudo privileges to new users for maintenance
    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

No, I had a hard time because I had little knowledge of Capistrano, Ruby, and DSL. I was quite addicted to using 'sudo' commands (redirects and pipes) in a way that linked multiple 'sudo' commands. For example, for a one-liner pipe-chaining command, e.g. 'execute :sudo, "mkdir ~/new_dir | chmod a+w ~/new_dir"', the subsequent 'chmod' in the chain was not sudo privileged, only 'mkdir' was executed, so 'execute :sudo, "mkdir ~/new_dir | sudo chmod a+w ~/new_dir' and it worked. Also, the process of adding standard output to a file using '>' or '>>' redirects became errors and did not work, and I had to pipe it with commands like 'chpasswd' or 'tee' that could pick up the standard output. Also, in the 'check' and 'init' tasks this time, I made a note using the 'AWS.memoize' method so that if there is a cache for the response where the API is used in the "AWS SDK for Ruby", I will use it (see this site for details). )。 This memoization caches the API response in the 'check' task, which makes the subsequent 'init' task perform much faster.

Now, let's try to run the task.

$ 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).

The deployment was successful. To confirm, SSH in with the newly created maintenance user from the command line.

$ 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]$

I was able to log in safely. In addition, the maintenance user can sudo without a password, while the default user, ec2-user, has been stripped of sudo privileges. The deployment is as expected.

However, there is a bit of a problem with this deployment configuration, even if the Instance State after the task for the instance launch is ':running' and it has been started, but the status checks are in the ':initializing' state, the init task will stop in the middle of the first SSH without being able to SSH for the first time. This is a problem because the current check task only looks at whether the Instance State is :running.

It's been a long time, so I'll stop here for this time. Next time, I will try to modify the check task that determines the Status Checks, and as an aftermath of this init task, I will write the SSH settings of the newly created maintenance user to the Capistrano configuration file, and finally introduce the original deployment process to update the package.

Reference site

  1. The 'Name' tag is also used as the name of the instance list in the AWS Management Console, so we recommend adding this tag to make it easier to identify the instance you created. 

Related Articles