Skip to content

Instantly share code, notes, and snippets.

@blmarket
Created August 30, 2013 11:54
Show Gist options
  • Save blmarket/6389116 to your computer and use it in GitHub Desktop.
Save blmarket/6389116 to your computer and use it in GitHub Desktop.
AWS autoscaling server rolling restart script. useful when you changed AMI.

Replacing API servers

API 서버들을 최신 버전으로 업데이트하는 스크립트.

기존 스크립트와의 차이는 다음과 같다.

  • 서버를 모두 새로 띄운다.
    기존 스크립트는 서버를 한대씩 멈추고 업데이트한 다음, 다시 실행하는 방식으로 작동하였는데, 2~3대의 서버가 있을 때 한대를 내리게 되면 잠시나마 서비스에 상당한 부하가 생기게 된다.
    또한 새로 AMI를 만든 경우에도 동일하게 업데이트를 적용할 수 있다.

  • 새 서버의 작동을 확인한 후에 기존 서버를 닫는다.
    기존 스크립트의 경우 확인 없이 서버를 새로 띄우느라, 서버 코드에 버그가 있을 경우에도 최소한의 검증 없이 최신 버전의 코드로 업데이트가 되던 반면, 새로 짜고있는 코드는 새 서버가 정상적으로 요청을 처리하는 것이 확인된 후에 (이는 ELB의 Health Check를 활용한다) 기존 서버를 닫도록 구현되어 있어 API 서버를 좀 더 안정적으로 작동하도록 한다.

프로그램 작동 환경

  • node.js의 기본 라이브러리인 util이 제공되어야 한다.

    util = require 'util'

  • AWS의 Elastic Load Balancer와 AutoScale을 쓰고 있는 서버군을 대상으로 작동하는 것을 가정한다.

    {AWS} = require './lib/aws'

  • async는 훌륭한 비동기 실행제어 라이브러리이다. async 라이브러리를 잘 쓰면 nesting block을 최소화할 수 있다.

    async = require 'async'

전역 도우미 함수 소개

  • as: AWS에서 제공하는 AutoScaling 라이브러리 instance이다.

    as = new AWS.AutoScaling()

  • elb: AWS에서 제공하는 ELB 라이브러리 instance이다.

    elb = new AWS.ELB()

  • inspect, debug: 함수 결과 디버깅을 쉽게 하기 위해 만든 도우미 함수이다.

    inspect = (obj) -> util.inspect obj, { colors: true, depth: null } debug = (err, res) -> console.log err? && err || inspect(res)

각종 설정 정보

자체적으로 쓰는 모듈이다 보니, 많은 값들이 하드코딩되어있다.

elb_name = 'API'
as_groupname = 'APIServers'

기능 블록 소개

  • getCurrentStatus 현재 몇 대의 API 서버가 작동중인지, 각 서버의 instance Id는 무엇인지 확인하여 저장하는 역할을 한다.

    getCurrentStatus = (cb) -> elb.describeLoadBalancers { LoadBalancerNames: [ elb_name ] }, (err, data) -> return cb(err) if err? api_servers = data.LoadBalancerDescriptions[0] servers = (inst.InstanceId for inst in api_servers.Instances) console.log "Current servers : #{inspect(servers)}" cb(null, servers) return return

  • launchNewServers
    파라미터에 지정된 수만큼의 새 서버를 띄우는 역할을 한다. 처리하는 과정에서 AutoScale Policy를 임시로 하나 만들게 되나 삭제해도 무방하다. 또 AutoScale Group의 MaxSize(가능한 인스턴스의 최대 갯수)의 제한을 받기 때문에, 경우에 따라서는 요청한 수 만큼의 인스턴스가 생성되지 않을 수도 있다.

    launchNewServers = (cnt, cb) -> console.log "Launching #{cnt} servers" as.putScalingPolicy { AutoScalingGroupName: as_groupname PolicyName: 'TempPolicy' ScalingAdjustment: Number(cnt) AdjustmentType: 'ChangeInCapacity' }, (err, res) -> return cb(err) if err? as.executePolicy { AutoScalingGroupName: as_groupname PolicyName: 'TempPolicy' HonorCooldown: false }, (err) -> return cb(err) if err? console.log "Policy Executed" cb(null) return return return

    launchNewServers 4, debug

  • checkHealthyNew
    지정된 서버 목록에 포함되지 않은 서버 중, 상태가 InService(즉, 서비스에 적용된) 로 설정된 서버의 목록을 확인한다.

    checkHealthyNew = (omit_list = [], cb) -> console.log "Getting Health information from ELB..." elb.describeInstanceHealth { LoadBalancerName: elb_name }, (err, res) -> return cb(err) if err? arr = res.InstanceStates console.log arr arr = (item.InstanceId for item in arr when item.State == 'InService') res = (item for item in arr when item not in omit_list) console.log "Running New Servers : #{inspect(res)}" cb null, res return return

  • waitHealthy
    omit_list에 들어있지 않은 서버들이 base_cut 이상으로 작동할 때까지 기다린다.

    waitHealthy = (omit_list = [], base_cut = 1, cb) -> # 주기적 검사용 함수 check = -> console.log "Checking Health..." checkHealthyNew omit_list, (err, res) -> return cb(err) if err? if res.length < base_cut console.log "Wait More..." return setTimeout(check, 5000) # wait 5 sec more. console.log "Continuing..." cb(null) return return setTimeout check, 30000 # wait 30 sec.

  • closeServers
    server_list에 들어있는 서버들을 ELB 및 AutoScale에서 제거한다. 이 프로그램에선 더이상 트래픽이 들어가지 않도록 ELB에서 deregister한 다음, AutoScale에서 제거하도록 구현하였다.

    closeServers = (server_list = []) -> console.log "Terminating... #{inspect(server_list)}" return if server_list.length == 0 # no server to close instances = ( { InstanceId: item } for item in server_list ) elb.deregisterInstancesFromLoadBalancer { LoadBalancerName: elb_name Instances: instances }, (err, res) -> return if err? for server in server_list as.terminateInstanceInAutoScalingGroup { InstanceId: server ShouldDecrementDesiredCapacity: true }, debug

전체 실행 로직 소개

위에 나열된 기능 블록들을 순서대로 실행하는 것이 기본적인 실행 로직이다. 현재는 구현이 다 되어있지 않아 개별 기능 블록을 테스트하고 있는 단계이다.

old_servers = []
async.waterfall [
  getCurrentStatus
  (servers, cb) ->
    old_servers = servers
    launchNewServers(servers.length, cb)
  (cb) -> waitHealthy old_servers, (old_servers.length+1)/2, cb
  (cb) -> closeServers old_servers; cb(null)
], (err) -> throw err if err?

TODO

  • waterfall 블록에 현재 어느 단계가 실행중인지 알려주기 위한 wrapper함수를 만들면 좋을 것 같다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment