NGINX Plus 및 Chef 를 통한 Autoscaling

클라우드 환경에서 Autoscaling 을 처리하기 위한 해결책은 많지만, 일반적으로 특정 클라우드 제공업체의 특정 인프라에 의존합니다. NGINX Plus 의 유연성과 Chef 의 기능을 활용하여 대부분의 클라우드 제공업체에서 작동하는 Autoscaling 시스템을 구축할 수 있습니다.

Chef 에는 쿡북, 노드, 데이터 백 등과 같은 객체를 조작하기 위한 커맨드 라인 도구인 knife가 있습니다. knife을 확장하는 플러그인이 있어서 한 클라우드에 특정한 기능을 추상화하여 knife 명령이 다른 클라우드에서도 동일한 방식으로 작동하도록 할 수 있습니다.

목차

1. 요구사항
2. 기본 설정
 2-1. NGINX Plus
 2-2. NGINX Plus 업스트림 애플리케이션 서버
 2-3. Auto Scaling 서버
 2-4. Auto Scaling 스크립트
3. Auto Scaling 스택 배포
 3-1. NGINX Plus 대시보드
 3-2. 부하 생성
4. 결론

1. 요구사항

이 설정에서는 GitHub Repository에서 NGINX Chef 쿡북을 활용하고 있습니다. 또한, 클라우드 간의 전환을 더 간편하게 하기 위해 Hosted Chef를 사용하고 있습니다.

이 설정은 현재 AWS, Azure, 그리고 OpenStack과 함께 작동하도록 구성되어 있습니다. knife 클라우드 플러그인을 모두 포함하도록 확장하는 것도 가능하지만, 테스트는 진행되지 않았습니다.

2. 기본 설정

이 구성은 클러스터의 일부인 다른 노드에 대한 정보를 조회하기 위해 역할 구성원에 크게 의존합니다.
NGINX Plus 서버, 업스트림 애플리케이션 서버 및 Autoscaling 서버의 세 가지 기본 역할이 있습니다. 마지막은 NGINX Plus 페이지를 모니터링하고 통계를 기반으로 서버를 확장 또는 축소하기 위해 API 호출을 수행하는 노드입니다.

2-1. NGINX Plus

name "nginx_plus_autoscale"
description "Sample role to install NGINX Plus"
run_list "recipe[nginx]","recipe[nginx::autoscale]"
default_attributes "nginx" => { "install_source" => "plus",
                                "plus_status_enable" => true,
                                "enable_upstream_conf" => true,
                                "plus_status_allowed_ips" => ['104.245.19.144', '172.31.0.0/16', '127.0.0.1'],
                                "server_name" => "test.local",
                                "upstream" => "test",
                                "nginx_repo_key" => "-----BEGIN PRIVATE KEY-----nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbYwum24BwEYDf4P4x0/KZjkKN7/EE/gg0qAU3ebG5kY8gWb8NpQ2itj/DfmwPAEnvI6In86c6YFokAZxeo6HbkKkeQKBgQDGQEHp2lCON9FLgRNMjtcp4S2VYjxdAMVinDLkIgVb9qgh6BvTDt5hRY/Vhcx8xV70+BCnoMSzbvLWhZbpSrdmD9nOj1KkPcWn4ArSv6prlYItUwWbNtFLw/E=n-----END PRIVATE KEY-----",
  "nginx_repo_crt" => "-----BEGIN CERTIFICATE-----nMIIDrDCCApSgAwIBAgICBs8wDQYJKoZIhvcNAQEFBQAwXjELMAkGA1UI2pLoSbonYiEvivb4Cg7POn+cQBwurcYUH/jB9zLPPSwlqcUiG2hScuEeaBiEoK/ixHIRuMV9nyp3xTi3b0ZKvOFjEZpBHB8WIdQVneTNRvaFLbiwznhiAe7D4uMaAEYqF96GTgX2XnbovinLlYPfdi7BhlXTI9u78+tqbo0YVsSBiDV49hcIA=n-----END CERTIFICATE-----" }

2-2. NGINX Plus 업스트림 애플리케이션 서버

name "test-upstream"
description "Sample role to install the NGINX Plus hello demo"
run_list "recipe[nginx::hello-demo]"
default_attributes "nginx" => { "application_port" => "80"}

2-3. Auto Scaling 서버

name "autoscaler"
description "Sample role to install autoscaler script"
run_list "recipe[nginx::autoscale_script]"
default_attributes "nginx" => { "server_name" => "test.local",
				"upstream" => "test",
				"cloud_provider" => "ec2" }

다음은 역할에서 활용되는 다양한 속성에 대한 간략한 분석입니다.

  • install_source – 오픈 소스 대신 NGINX Plus를 설치하도록 NGINX 쿡북에 알립니다.
  • plus_status_enable – NGINX Plus 상태 페이지를 활성화합니다.
  • enable_upstream_conf – 동적 재구성 API 활성화
  • plus_status_allowed_ips – 상태 페이지 및 재구성 API에 액세스할 수 있는 IP 주소 또는 범위 목록
  • server_name – server – NGINX Plus 구성에서 지시문을 정의합니다.
  • upstreamserver_name – 앞서 언급한 구성  과 함께 사용할 업스트림 그룹을 정의합니다.
  • nginx_repo_key – 리포지토리에 액세스하기 위한 인증서 키를 정의합니다.
  • nginx_repo_crt – 리포지토리에 액세스하기 위한 인증서를 정의합니다.
  • application_port – 업스트림 애플리케이션 서버가 청취하는 포트를 정의합니다.
  • cloud_providerautoscale_nginx– 스크립트에 사용할 클라우드 공급자(AWS/Azure/Google/OpenStack)를 정의합니다.

또한 활용 중인 다양한 클라우드 공급자에 액세스하기 위한 자격 증명으로 knife.rb 파일을 구성해야 합니다. 다음은 지원되는 클라우드 Provider에 대한 세부 정보가 포함된 knife.rb 샘플입니다.

current_dir = File.dirname(__FILE__)
log_level                :info
log_location             STDOUT
node_name                "damiancurry"
client_key               "#{current_dir}/damiancurry.pem"
chef_server_url          "https://api.chef.io/organizations/nginx"
cookbook_path            ["#{current_dir}/../cookbooks"]
#AWS variables
knife[:aws_access_key_id] = 
knife[:aws_secret_access_key] = 
#Azure variables
knife[:azure_tenant_id] = 
knife[:azure_subscription_id] = 
knife[:azure_client_id] = 
knife[:azure_client_secret] = 
#OpenStack variables
knife[:openstack_auth_url] = 
knife[:openstack_username] = 
knife[:openstack_password] = 
knife[:openstack_tenant] = 
knife[:openstack_image] = 
knife[:openstack_ssh_key_id] = "demo_key"

2-4. Auto Scaling 스크립트

이제 Autoscaling 을 가능하게 하는 몇 가지 스크립트를 살펴보겠습니다. 첫 번째는 온라인인 노드의 변경 사항을 감지하기 위해 NGINX Plus 노드에서 실행되는 스크립트입니다. 이 스크립트는 아직 Chef 템플릿 형식으로 작성되어 가독성이 떨어집니다. 스크립트는 확장된 상태 페이지의 실행 중인 구성을 Chef 로 관리되는 업스트림 구성 파일과 비교합니다.

#!/bin/bash
NGINX_NODES="$(mktemp)"
/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?upstream=<%= node['nginx']['upstream'] %>"| /usr/bin/awk '{print $2}' | /bin/sed -r 's/;//g' | /usr/bin/sort > $NGINX_NODES
CONFIG_NODES="$(mktemp)"
/bin/grep -E '[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}' /etc/nginx/conf.d/<%= node['nginx']['upstream'] %>-upstream.conf | /usr/bin/awk '{print $2}' | /bin/sed -r 's/;//g' | /usr/bin/sort > $CONFIG_NODES
DIFF_OUT="$(mktemp)"
/usr/bin/diff $CONFIG_NODES $NGINX_NODES > $DIFF_OUT
ADD_NODE=`/usr/bin/diff ${CONFIG_NODES} ${NGINX_NODES} | /bin/grep "<" | /usr/bin/awk '{print $2}'`
DEL_NODE=`/usr/bin/diff ${CONFIG_NODES} ${NGINX_NODES} | /bin/grep ">" | /usr/bin/awk '{print $2}'`

for i in $ADD_NODE; do
    echo "adding node ${i}";
    /usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?add=&upstream=<%= node['nginx']['upstream'] %>&server=${i}&max_fails=0"
done
for i in $DEL_NODE; do
    echo "removing node ${i}";
    #NODE_ID=`/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?upstream=<%= node['nginx']['upstream'] %>" | /bin/grep ${i} | /usr/bin/awk '{print $4}' | /bin/sed -r 's/id=//g'`
    NODE_ID=`/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?upstream=<%= node['nginx']['upstream'] %>" | /bin/grep ${i} | /bin/grep -oP 'id=Kd+'`
    NODE_COUNT=`/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?upstream=<%= node['nginx']['upstream'] %>" | /bin/grep -n ${i} | /bin/grep -oP 'd+:server' | /bin/sed -r 's/:server//g'`
    JSON_NODE_NUM=$(expr $NODE_COUNT - 1)
    NODE_CONNS=`/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/status" | /usr/bin/jq ".upstreams.<%= node['nginx']['upstream'] %>.peers[${JSON_NODE_NUM}].active"`
    NODE_STATE=`/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/status" | /usr/bin/jq ".upstreams.<%= node['nginx']['upstream'] %>.peers[${JSON_NODE_NUM}].state"`
    if [[ ${NODE_STATE} == '"up"' ]] && [[ ${NODE_CONNS} == 0 ]]; then
	echo "nodes is up with no active connections, removing ${i}"
	/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?remove=&upstream=<%= node['nginx']['upstream'] %>&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"draining"' ]] && [[ ${NODE_CONNS} == 0 ]]; then
    echo "nodes is draining with no active connections, removing ${i}"
    /usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?remove=&upstream=<%= node['nginx']['upstream'] %>&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"down"' ]]; then
	echo "node state is down, removing ${i}":
	/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?remove=&upstream=<%= node['nginx']['upstream'] %>&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"unhealthy"' ]]; then
	echo "node state is down, removing ${i}":
	/usr/bin/curl -s "http://localhost:<%= node['nginx']['plus_status_port'] %>/upstream_conf?remove=&upstream=<%= node['nginx']['upstream'] %>&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"up"' ]] && [[ ${NODE_CONNS} != 0 ]]; then
	echo "node has active connections, draining connections on ${i}"
    fi
done

rm $NGINX_NODES $CONFIG_NODES $DIFF_OUT

다음은 업스트림 구성을 생성하는 데 사용되는 논리입니다.

upstream_node_ips = []
upstream_role = (node[:nginx][:upstream]).to_s
search(:node, "role:#{node[:nginx][:upstream]}-upstream") do |nodes|
  host_ip = nodes['ipaddress']
  unless host_ip.to_s.strip.empty?
    host_port = nodes['nginx']['application_port']
    upstream_node_ips << "#{host_ip}:#{host_port}" # if value.has_key?("broadcast")
  end
end

template "/etc/nginx/conf.d/#{node[:nginx][:upstream]}-upstream.conf" do
  source 'upstreams.conf.erb'
  owner 'root'
  group node['root_group']
  mode 0644
  variables(
    hosts: upstream_node_ips
  )
  # notifies :reload, 'service[nginx]', :delayed
  notifies :run, 'execute[run_api_update_script]', :delayed
end

이 애플리케이션에 대해 정의한 업스트림 역할에 현재 할당된 노드를 찾기 위해 Chef 검색 기능을 사용하고 있음을 알 수 있습니다. 그런 다음 노드의 IP 주소와 애플리케이션 포트를 추출하고 해당 정보를 배열로 템플릿에 전달합니다. 업스트림 구성의 템플릿 버전은 다음과 같습니다.

upstream <%= node['nginx']['upstream'] %> {
       zone <%= node['nginx']['upstream'] %> 64k;
       <% @hosts.each do |node| -%>
       server <%= node %>;
       <% end %>
   }

마지막으로 Autoscaling 을 처리하는 실제 스크립트는 다음과 같습니다.

require 'chef/api_client'
require 'chef/config'
require 'chef/knife'
require 'chef/node'
require 'chef/search/query'
require 'net/http'
require 'json'
class MyCLI
  include Mixlib::CLI
end

Chef::Config.from_file(File.expand_path("~/.chef/knife.rb"))
nginx_node = "<%= @nginx_host %>"
cloud_provider = "<%= node['nginx']['cloud_provider'] %>"
nginx_upstream = "<%= node['nginx']['upstream'] %>"
nginx_server_zone = "<%= node['nginx']['server_name'] %>"
if cloud_provider == "ec2"
  create_args = ["#{cloud_provider}", 'server', 'create', '-r', "role[#{nginx_upstream}-upstream]", '-S', 'chef-demo', '-I', 'ami-93d80ff3', '--region', 'us-west-2', '-f', 'm1.medium', '-g', 'chef-demo', '--ssh-user', 'ubuntu', '-i', '~/.ssh/chef-demo.pem']
elsif cloud_provider == "openstack"
  create_args = ["#{cloud_provider}", 'server', 'create', '-i', '~/.ssh/demo_key.pem', '--ssh-user', 'ubuntu', '-f', 'demo_flavor', '--openstack-private-network', '-Z', 'nova', '-r', "role[#{nginx_upstream}-upstream]"]
else
  puts "Please specify a valid cloud provider"
  exit
end
sleep_interval_in_seconds = 10
min_server_count = 1
max_server_count = 10
min_conns = 10
max_conns = 20
nginx_status_url = "http://#{nginx_node}:8080/status"

def get_nginx_active_servers(nginx_status_data, nginx_upstream)
  active_nodes = Array.new
  peers = nginx_status_data["upstreams"]["#{nginx_upstream}"]["peers"]
  peers.each do |node|
    if node["state"] == "up"
      active_nodes.push node["server"]
    end
  end
  return active_nodes
end

def get_nginx_server_conns(nginx_status_data, nginx_server_zone)
  return nginx_status_data["server_zones"]["#{nginx_server_zone}"]["processing"]
end

def add_backend_node(create_args)
  #search for existing hostnames to pick a new one
  query = Chef::Search::Query.new
  #nodes = query.search('node', 'role:#{nginx_upstream}-upstream').first rescue []
  nodes = query.search('node', 'role:<%= node['nginx']['upstream'] %>-upstream').first rescue []
  hosts = Array.new
  used_num = Array.new
  nodes.each do |node|
    node_name = node.name
    hosts.push node_name
    num = node_name.scan(/d+/)
    used_num.push num
  end
  used_num.sort!
  fixed1 = used_num.flatten.collect do |num| num.to_i end
  fixed_num = fixed1.sort!
  firstnum = fixed_num.first
  lastnum = fixed_num.last
  firsthost = hosts.sort[0].to_i
  lasthost = hosts.sort[-1].to_i

  unless firstnum.nil? && lastnum.nil?
    total = (1..lastnum).to_a
    missingnum = total-fixed_num
  end
  newhostname = ""
  if missingnum.nil?
    puts "No existing hosts"
    fixnum = "1"
    newnum = fixnum.to_i
    newhostname = "<%= node['nginx']['upstream'] %>-app-#{newnum}"
  elsif missingnum.any?
    puts "Missing numbers are #{missingnum}"
    newnum = missingnum.first
    newhostname = "<%= node['nginx']['upstream'] %>-app-#{newnum}"
  else
    newnum = lastnum + 1
    puts "new number is n"
    newhostname = "<%= node['nginx']['upstream'] %>-app-#{newnum}"
  end
  new_create_args = create_args + ['--node-name', newhostname]
  knife = Chef::Knife.new
  knife.options=MyCLI.options
  Chef::Knife.run(new_create_args, MyCLI.options)
  #sleep to wait for chef run
  1.upto(10) do |n|
    puts "."
    sleep 1 # second
  end
end

def del_backend_node(nginx_status_data, nginx_node, active_nodes, cloud_provider, nginx_upstream)
  #lookup hostnames/ips and pick a backend at random
  query = Chef::Search::Query.new
  #nodes = query.search('node', 'role:#{nginx_upstream}-upstream').first rescue []
  nodes = query.search('node', 'role:<%= node['nginx']['upstream'] %>-upstream').first rescue []
  hosts = Array.new
  nodes.each do |node|
    node_name = node.name
    node_ip = node['ipaddress']
    if active_nodes.any? { |val| /#{node_ip}/ =~ val }
      hosts.push "#{node_name}:#{node_ip}"
    end
  end
  del_node = hosts.sample
  node_name = del_node.rpartition(":").first
  node_ip = del_node.rpartition(":").last
  puts "Removing #{node_name}"
  nginx_url = "http://#{nginx_node}:8080/upstream_conf?upstream=#{nginx_upstream}"
  response = Net::HTTP.get(URI(nginx_url))
  node_id = response.lines.grep(/#{node_ip}/).first.split('id=').last.chomp
  drain_url = "http://#{nginx_node}:8080/upstream_conf?upstream=#{nginx_upstream}&id=#{node_id}&drain=1"
  Net::HTTP.get(URI(drain_url))
  sleep(5)
  knife = Chef::Knife.new
  knife.options=MyCLI.options
  #delete_args = ["#{cloud_provider}", 'server', 'delete', "#{node_name}", '--purge', '-y']
  #Chef::Knife.run(delete_args, MyCLI.options)
  delete_args = "#{cloud_provider} server delete -N #{node_name} -P -y"
  `knife #{delete_args}`
end


last_conns_count = -1

while true
  response = Net::HTTP.get(URI(nginx_status_url))
  nginx_status_data = JSON.parse(response)

  active_nodes = get_nginx_active_servers(nginx_status_data, nginx_upstream)
  server_count = active_nodes.length
  current_conns = get_nginx_server_conns(nginx_status_data, nginx_server_zone)

  conns_per_server = current_conns / server_count.to_f

  puts "Current connections = #{current_conns}"
  puts "connections per server = #{conns_per_server}"

  if server_count < min_server_count
    puts "Creating new #{cloud_provider} Instance"
    add_backend_node(create_args)
  elsif conns_per_server > max_conns
    if server_count < max_server_count
      puts "Creating new #{cloud_provider} Instance"
      add_backend_node(create_args)
    end
  elsif conns_per_server < min_conns
    if server_count > min_server_count
      del_backend_node(nginx_status_data, nginx_node, active_nodes, cloud_provider, nginx_upstream)
    end

  end

  last_conns_count = current_conns
  sleep(sleep_interval_in_seconds)
end


이 스크립트의 주요 기능은 서버의 상태 페이지를 모니터링하고 통계에 따라 노드를 NGINX Plus 노드에 추가하거나 제거하는 것입니다. 현재 상태에서 이 스크립트는 활성 연결 수를 부하 분산 풀의 활성 서버 수로 나눈 결과에 기반하여 결정을 내립니다. NGINX Plus 상태 페이지에서 사용 가능한 다른 통계 중 하나를 사용하도록 이를 쉽게 수정할 수 있습니다.

3. Auto Scaling 스택 배포

먼저 knife-ec2 플러그인을 사용하여 자동 확장기 인스턴스를 시작합니다.

chef-repo$ knife ec2 server create -r "role[autoscaler]" -g sg-1f285866 -I ami-93d80ff3 -f m1.medium -S chef-demo --region us-west-2  --node-name autoscaler-test --ssh-user ubuntu -i ~/.ssh/chef-demo.pem
Instance ID: i-0c359f3a443d18d64
Flavor: m1.medium
Image: ami-93d80ff3
Region: us-west-2
Availability Zone: us-west-2a
Security Group Ids: sg-1f285866
Tags: Name: autoscaler-test
SSH Key: chef-demo

Waiting for EC2 to create the instance......
Public DNS Name: ec2-35-164-35-19.us-west-2.compute.amazonaws.com
Public IP Address: 35.164.35.19
Private DNS Name: ip-172-31-27-162.us-west-2.compute.internal
Private IP Address: 172.31.27.162

Waiting for sshd access to become available
SSH Target Address: ec2-35-164-35-19.us-west-2.compute.amazonaws.com(dns_name)
done

SSH Target Address: ec2-35-164-35-19.us-west-2.compute.amazonaws.com()
Creating new client for autoscaler-test
Creating new node for autoscaler-test
Connecting to ec2-35-164-35-19.us-west-2.compute.amazonaws.com
ec2-35-164-35-19.us-west-2.compute.amazonaws.com -----> Installing Chef Omnibus (-v 12)
…
ec2-35-164-35-19.us-west-2.compute.amazonaws.com Chef Client finished, 6/6 resources updated in 13 seconds

다음은 이 노드의 오토스케일링을 실제로 처리하는 스크립트 /usr/bin/autoscale_nginx.rb 입니다 . 이 시점에서 변수에 할당된 IP 주소가 없다는 점에 유의하십시오 nginx_node(두 번째 stanza 의 두 번째 줄). 이는 아직 서버를 생성하지 않았기 때문입니다. 서버가 생성되면 Chef가 해당 정보로 스크립트를 업데이트합니다.

require 'chef/config'
require 'chef/knife'
require 'chef/node'
require 'chef/search/query'
require 'net/http'
require 'json'
class MyCLI
  include Mixlib::CLI
end

Chef::Config.from_file(File.expand_path("~/.chef/knife.rb"))
<strong>nginx_node = "[]"</strong>
cloud_provider = "ec2"
nginx_upstream = "test"
nginx_server_zone = "test.local"
if cloud_provider == "ec2"
  create_args = ["#{cloud_provider}", 'server', 'create', '-r', "role[#{nginx_upstream}-upstream]", '-S', 'damiancurry', '-I', 'ami-93d80ff3', '--region', 'us-west-2', '-f', 'm1.medium', '--ssh-user', 'ubuntu', '-i', '~/.ssh/damiancurry.pem']
elsif cloud_provider == "openstack"
  create_args = ["#{cloud_provider}", 'server', 'create', '-i', '~/.ssh/demo_key.pem', '--ssh-user', 'ubuntu', '-f', 'demo_flavor', '--openstack-private-network', '-Z', 'nova', '-r', "role[#{nginx_upstream}-upstream]"]
else
  puts "Please specify a valid cloud provider"
  exit
end
sleep_interval_in_seconds = 10
min_server_count = 1
max_server_count = 10
min_conns = 10
max_conns = 20
nginx_status_url = "http://#{nginx_node}:8080/status"

def get_nginx_active_servers(nginx_status_data, nginx_upstream)
  active_nodes = Array.new
  peers = nginx_status_data["upstreams"]["#{nginx_upstream}"]["peers"]
  peers.each do |node|
    if node["state"] == "up"
      active_nodes.push node["server"]
    end
  end
  return active_nodes
end

def get_nginx_server_conns(nginx_status_data, nginx_server_zone)
  return nginx_status_data["server_zones"]["#{nginx_server_zone}"]["processing"]
end

def add_backend_node(create_args)
  knife = Chef::Knife.new
  knife.options=MyCLI.options
  Chef::Knife.run(create_args, MyCLI.options)
  #sleep to wait for chef run
  1.upto(10) do |n|
    puts "."
    sleep 1 # second
  end
end

def del_backend_node(nginx_status_data, nginx_node, active_nodes, cloud_provider, nginx_upstream)
  #lookup hostnames/ips and pick a backend at random
  query = Chef::Search::Query.new
  #nodes = query.search('node', 'role:#{nginx_upstream}-upstream').first rescue []
  nodes = query.search('node', 'role:test-upstream').first rescue []
  hosts = Array.new
  nodes.each do |node|
    node_name = node.name
    node_ip = node['ipaddress']
    if active_nodes.any? { |val| /#{node_ip}/ =~ val }
      hosts.push "#{node_name}:#{node_ip}"
    end
  end
  del_node = hosts.sample
  node_name = del_node.rpartition(":").first
  node_ip = del_node.rpartition(":").last
  puts "Removing #{node_name}"
  nginx_url = "http://#{nginx_node}:8080/upstream_conf?upstream=#{nginx_upstream}"
  response = Net::HTTP.get(URI(nginx_url))
  node_id = response.lines.grep(/#{node_ip}/).first.split('id=').last.chomp
  drain_url = "http://#{nginx_node}:8080/upstream_conf?upstream=#{nginx_upstream}&amp;id=#{node_id}&amp;drain=1"
  Net::HTTP.get(URI(drain_url))
  sleep(5)
  knife = Chef::Knife.new
  knife.options=MyCLI.options
  #delete_args = ["#{cloud_provider}", 'server', 'delete', "#{node_name}", '--purge', '-y']
  #Chef::Knife.run(delete_args, MyCLI.options)
  delete_args = "#{cloud_provider} server delete #{node_name} -P -y"
  `knife #{delete_args}`
end


last_conns_count = -1

while true
  response = Net::HTTP.get(URI(nginx_status_url))
  nginx_status_data = JSON.parse(response)

  active_nodes = get_nginx_active_servers(nginx_status_data, nginx_upstream)
  server_count = active_nodes.length
  current_conns = get_nginx_server_conns(nginx_status_data, nginx_server_zone)

  conns_per_server = current_conns / server_count.to_f

  puts "Current connections = #{current_conns}"
  puts "connections per server = #{conns_per_server}"

  if server_count &lt; min_server_count
    puts "Creating new #{cloud_provider} Instance"
    add_backend_node(create_args)
  elsif conns_per_server &gt; max_conns
    if server_count &lt; max_server_count
      puts "Creating new #{cloud_provider} Instance"
      add_backend_node(create_args)
    end

이제 서버를 시작합니다.

default$ knife ec2 server create -r "role[nginx_plus_autoscale]" -g sg-1f285866 -I ami-93d80ff3 -f m1.medium -S chef-demo --region us-west-2 --ssh-user ubuntu -i ~/.ssh/chef-demo.pem --node-name nginx-autoscale
Instance ID: i-0856ee80f54c8f3e6
Flavor: m1.medium
Image: ami-93d80ff3
Region: us-west-2
Availability Zone: us-west-2b
Security Group Ids: sg-1f285866
Tags: Name: nginx-autoscale
SSH Key: chef-demo

Waiting for EC2 to create the instance.......
Public DNS Name: ec2-35-165-171-46.us-west-2.compute.amazonaws.com
Public IP Address: 35.165.171.46
Private DNS Name: ip-172-31-38-163.us-west-2.compute.internal
Private IP Address: 172.31.38.163

Waiting for sshd access to become available
SSH Target Address: ec2-35-165-171-46.us-west-2.compute.amazonaws.com(dns_name)
done

SSH Target Address: ec2-35-165-171-46.us-west-2.compute.amazonaws.com()
Creating new client for nginx-autoscale
Creating new node for nginx-autoscale
Connecting to ec2-35-165-171-46.us-west-2.compute.amazonaws.com
ec2-35-165-171-46.us-west-2.compute.amazonaws.com -----> Installing Chef Omnibus (-v 12)
…
ec2-35-165-171-46.us-west-2.compute.amazonaws.com Chef Client finished, 24/34 resources updated in 43 seconds

그런 다음 Autoscaler 인스턴스에서 새 노드의 IP 주소가 autoscale_nginx.rbnginx_node 변수에 할당되었는지 확인합니다.

root# grep 'nginx_node =' /usr/bin/autoscale_nginx.rb
nginx_node = "172.31.38.163"

3-1. NGINX Plus 대시보드

NGINX Plus 대시보드에 액세스하면 다음과 같이 표시됩니다.

NGINX Plus 및 Chef 를 통한 Autoscaling - Dashboard 1

포트 80에서 NGINX Plus 서버를 누르면 백엔드 애플리케이션 서버를 아직 시작하지 않았기 때문에 502 Bad Gateway 오류 페이지가 표시됩니다.

오토스케일러 스크립트를 시작하고 애플리케이션 노드를 시작하기 전에 이러한 새 노드를 실행 중인 NGINX Plus 구성인 /tmp/api_update.sh에 추가하는 스크립트를 살펴보겠습니다.

#!/bin/bash
NGINX_NODES="$(mktemp)"
/usr/bin/curl -s "http://localhost:8080/upstream_conf?upstream=test"| /usr/bin/awk '{print $2}' | /bin/sed -r 's/;//g' | /usr/bin/sort > $NGINX_NODES
CONFIG_NODES="$(mktemp)"
/bin/grep -E '[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}' /etc/nginx/conf.d/test-upstream.conf | /usr/bin/awk '{print $2}' | /bin/sed -r 's/;//g' | /usr/bin/sort > $CONFIG_NODES
DIFF_OUT="$(mktemp)"
/usr/bin/diff $CONFIG_NODES $NGINX_NODES > $DIFF_OUT
ADD_NODE=`/usr/bin/diff ${CONFIG_NODES} ${NGINX_NODES} | /bin/grep "<" | /usr/bin/awk '{print $2}'`
DEL_NODE=`/usr/bin/diff ${CONFIG_NODES} ${NGINX_NODES} | /bin/grep ">" | /usr/bin/awk '{print $2}'`

for i in $ADD_NODE; do
    echo "adding node ${i}";
    /usr/bin/curl -s "http://localhost:8080/upstream_conf?add=&upstream=test&server=${i}&max_fails=0"
done
for i in $DEL_NODE; do
    echo "removing node ${i}";
    #NODE_ID=`/usr/bin/curl -s "http://localhost:8080/upstream_conf?upstream=test" | /bin/grep ${i} | /usr/bin/awk '{print $4}' | /bin/sed -r 's/id=//g'`
    NODE_ID=`/usr/bin/curl -s "http://localhost:8080/upstream_conf?upstream=test" | /bin/grep ${i} | /bin/grep -oP 'id=Kd+'`
    NODE_COUNT=`/usr/bin/curl -s "http://localhost:8080/upstream_conf?upstream=test" | /bin/grep -n ${i} | /bin/grep -oP 'd+:server' | /bin/sed -r 's/:server//g'`
    JSON_NODE_NUM=$(expr $NODE_COUNT - 1)
    NODE_CONNS=`/usr/bin/curl -s "http://localhost:8080/status" | /usr/bin/jq ".upstreams.test.peers[${JSON_NODE_NUM}].active"`
    NODE_STATE=`/usr/bin/curl -s "http://localhost:8080/status" | /usr/bin/jq ".upstreams.test.peers[${JSON_NODE_NUM}].state"`
    if [[ ${NODE_STATE} == '"up"' ]] && [[ ${NODE_CONNS} == 0 ]]; then
	echo "nodes is up with no active connections, removing ${i}"
	/usr/bin/curl -s "http://localhost:8080/upstream_conf?remove=&upstream=test&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"draining"' ]] && [[ ${NODE_CONNS} == 0 ]]; then
    echo "nodes is draining with no active connections, removing ${i}"
    /usr/bin/curl -s "http://localhost:8080/upstream_conf?remove=&upstream=test&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"down"' ]]; then
	echo "node state is down, removing ${i}":
	/usr/bin/curl -s "http://localhost:8080/upstream_conf?remove=&upstream=test&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"unhealthy"' ]]; then
	echo "node state is down, removing ${i}":
	/usr/bin/curl -s "http://localhost:8080/upstream_conf?remove=&upstream=test&id=${NODE_ID}"
    elif [[ ${NODE_STATE} == '"up"' ]] && [[ ${NODE_CONNS} != 0 ]]; then
	echo "node has active connections, draining connections on ${i}"
    fi
done

rm $NGINX_NODES $CONFIG_NODES $DIFF_OUT

이 스크립트는 Chef가 실행될 때마다 호출되며 기존 실행 구성을 자동 확장 그룹에 대해 정의된 업스트림 구성 파일과 비교합니다. 아래 레시피 스니펫에서 볼 수 있듯이 Chef는 구성 파일을 관리하지만 NGINX Plus가 업데이트될 때 다시 로드하지 않습니다. 대신 apt_update 스크립트를 호출합니다.

template "/etc/nginx/conf.d/#{node[:nginx][:upstream]}-upstream.conf" do
  source 'upstreams.conf.erb'
  owner 'root'
  group node['root_group']
  mode 0644
  variables(
    hosts: upstream_node_ips
  )
  # notifies :reload, 'service[nginx]', :delayed
  notifies :run, 'execute[run_api_update_script]', :delayed
end

이제 autoscaler 스크립트를 시작하고 일부 애플리케이션 서버를 온라인 상태로 만듭니다. Chef 클라이언트와 함께 제공된 경로를 사용해야 하므로 Ruby 바이너리에 대한 정규화된 경로를 사용합니다.

ubuntu$ /opt/chef/embedded/bin/ruby /usr/bin/autoscale_nginx.rb
Current connections = 0
connections per server = NaN
Creating new ec2 Instance
No existing hosts
test-app-1
Instance ID: i-0c671d851a1c5e6d0
Flavor: m1.medium
Image: ami-93d80ff3
Region: us-west-2
Availability Zone: us-west-2b
Security Group Ids: chef-demo
Tags: Name: test-app-1
SSH Key: chef-demo

Waiting for EC2 to create the instance...
…
ec2-35-165-4-158.us-west-2.compute.amazonaws.com Chef Client finished, 16/26 resources updated in 34 seconds
…
Private IP Address: 172.31.40.186
Environment: _default
Run List: role[test-upstream]
.
.
.
Current connections = 0
connections per server = 0.0
Current connections = 0
connections per server = 0.0

이제 애플리케이션 노드 하나가 실행되었으므로 NGINX Plus 노드로 돌아가면 502 Bad Gateway 대신 다음 데모 페이지가 표시됩니다.

NGINX Plus Demo page

이제 대시보드에 정의된 업스트림이 있습니다.

NGINX Plus Upstream Dashodrd

3-2. 부하 생성

다음으로 wrk와 같은 도구를 사용하여 사이트에 대한 부하를 생성합니다.

wrk$ ./wrk -c 25 -t 2 -d 10m http://ec2-35-165-171-46.us-west-2.compute.amazonaws.com/
Running 10m test @ http://ec2-35-165-171-46.us-west-2.compute.amazonaws.com/
  2 threads and 25 connections

Autoscaler 노드에서 스크립트가 연결 증가를 포착하고 새 인스턴스를 시작하는 것을 볼 수 있습니다.

Current connections = 0
connections per server = 0.0
Current connections = 24
connections per server = 24.0
Creating new ec2 Instance
new number is
2
test-app-2
Instance ID: i-07186f5451c7d9e77
Flavor: m1.medium
Image: ami-93d80ff3
Region: us-west-2
Availability Zone: us-west-2b
Security Group Ids: chef-demo
Tags: Name: test-app-2
SSH Key: chef-demo

Waiting for EC2 to create the instance......
...
ec2-35-166-214-136.us-west-2.compute.amazonaws.com Chef Client finished, 16/26 resources updated in 35 seconds
Current connections = 24
connections per server = 12.0
Current connections = 24
connections per server = 12.0

이제 대시보드에 두 개의 업스트림 노드가 있습니다. 노드에 평균 20개 이상의 활성 연결이 있을 때 확장하도록 구성되어 있기 때문에 스크립트는 이 시점에 남아 있습니다. NGINX Plus 서버의 포트 80을 가리키는 브라우저를 새로고침하면 서로 다른 백엔드 노드 사이를 전환하면서 데이터가 변경되는 것을 볼 수 있습니다. 트래픽 생성을 중지하면 스크립트가 항상 하나 이상의 서버를 실행하도록 구성되어 있으므로 노드 중 하나를 오프라인 상태로 만드는 것을 볼 수 있습니다.

Current connections = 24
connections per server = 12.0
Current connections = 0
connections per server = 0.0
Removing test-app-2
no instance id is specific, trying to retrieve it from node name
WARNING: Deleted server i-0dcf4740c1b34417f
WARNING: Deleted node test-app-2
WARNING: Deleted client test-app-2
Current connections = 0
connections per server = 0.0

4. 결론

이는 사용자 환경에 맞는 맞춤형 자동 크기 조정 솔루션을 구축하기 위한 출발점으로 의도된 다소 기본적인 설정입니다. 그리고 다른 클라우드 공급자로 마이그레이션하려는 경우 Chef 구성에서 ['nginx']['cloud_provider'] 속성 하나를 변경하는 것만큼 간단합니다.

NGINX Plus로 Autoscaling 을 직접 사용해 보십시오. 오늘 무료 30일 평가판을 시작하거나 당사에 문의하여 사용 사례에 대해 논의하십시오.

아래 뉴스레터를 구독하여 NGINX의 최신 소식들을 발 빠르게 전달 받아보세요.