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}&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 #{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
이제 서버를 시작합니다.
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.rb의 nginx_node
변수에 할당되었는지 확인합니다.
root# grep 'nginx_node =' /usr/bin/autoscale_nginx.rb
nginx_node = "172.31.38.163"
3-1. NGINX Plus 대시보드
NGINX Plus 대시보드에 액세스하면 다음과 같이 표시됩니다.

포트 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 대신 다음 데모 페이지가 표시됩니다.

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

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의 최신 소식들을 발 빠르게 전달 받아보세요.
댓글을 달려면 로그인해야 합니다.