AWS CloudFront経由で静的コンテンツを配信する
画像、音声、css、jsなど静的コンテンツをAWS CloudFront経由で配信してwebサーバの負荷を軽減するようにします。 CloudFrontはリバースプロキシ型のキャッシュサーバのように動作します。
CloudFrontを試していたところ一つ問題を確認しています。クライアント端末がgoogleのパブリックDNSを使っている場合だと、CloudFrontからのリソース読み込みが遅くなる可能性があります。(参考になる記事) 今のところサーバサイドで解決する方法はわかっていません。2014-4-14:AWSのほうで対応したようです
構成
動的コンテンツと静的コンテンツのURLのドメインをあらかじめ分けておいて、静的コンテンツのリクエストはCloudFrontのほうに届くようにしておきます。静的コンテンツ配信のドメインをimage.staging.example.comとします。
端末 | リクエスト http://image.staging.example.com/assets/xxxx | CloudFront [image.staging.example.com] | キャッシュがCloudFrontになければ、下のoriginからリソースを取得してきて端末に配信する | (キャッシュできるものであればキャッシュする) | web server(origin) [origin.staging.example.com] | CloudFrontからのリクエストに応じてリソースを配信する | app server
アプリがRails製なのでasset_hostで指定します。
- config/environments/staging.rb
config.action_controller.asset_host = "image.staging.example.com"
CloudFrontディストリビューションの作成
スクリプトで作成できるようにしておきます。
- CloudFront配信のドメイン - image.staging.example.com
- オリジンサーバのドメイン - origin.staging.example.com
stage = ARGV.shift stage ||= "staging" asset_host = "image.#{stage}.example.com" origin_host = "origin.#{stage}.example.com" cf = AWS::CloudFront.new cf.client.create_distribution({ :distribution_config => { :caller_reference => Time.now.to_i.to_s, :aliases => {:items => [asset_host], :quantity => 1}, :default_root_object => "index", :origins => { :items => [{ :id=>"Custom-#{origin_host}", :domain_name => origin_host, :custom_origin_config => { :http_port => 80, :https_port => 443, :origin_protocol_policy => "http-only" } }], :quantity => 1 }, :default_cache_behavior => { :target_origin_id => "Custom-#{origin_host}", :forwarded_values => { :query_string => false, :cookies=>{:forward => "none"} }, :trusted_signers => { :items => [], :enabled => false, :quantity => 0 }, :viewer_protocol_policy => "allow-all", :min_ttl => 0, :allowed_methods => {:items => ["GET", "HEAD"], :quantity => 2} }, :cache_behaviors => {:items => [], :quantity => 0}, :comment => "#{stage}", :logging => { :enabled => false, :include_cookies => false, :bucket => "", :prefix => "", }, :price_class => "PriceClass_All", :enabled => true, :viewer_certificate => {:cloud_front_default_certificate => true}, :restrictions => { :geo_restriction => {:items => [], :restriction_type => "none", :quantity => 0} }, } }) resp = cf.client.list_distributions resp[:items].each do |distribution| pp distribution end
DNSの設定
CloudFrontでディストリビューションを作成した後、image.staging.example.comのDNS AレコードをCloudFrontで作成したディストリビューションのエイリアスに設定します。
AWSのScheduled Scalingを使って日時に合わせてインスタンス数を調整する
時間帯やイベント開催中かどうかによってアクセス数が変動すると思われるので、appsサーバのインスタンス増減をスケジューリングできるようにします。以下の想定でスケジュールを設定してみます。
スケジュールの例
イベント非開催時の通常時
- 深夜過疎(1:00-6:00)
- ベースn=1台とする(下記ソースのSIZE_SMALL)
- 通常時
- n+1台(下記ソースのSIZE_MEDIUM)
- 夜間ピーク時(18:00-25:00)
- n+2台(下記ソースのSIZE_LARGE)
イベント開催時
- 一週間継続して行われるイベント(例: 1月14日〜20日の全日)
- ベースのnを+3する。(下記ソースのEXTRA1)
- 短時間型イベント(例: 毎月29日の12:00-13:00)
- n+5台(下記ソースのEXTRA2)
スケジュールの設定方法
一旦wheneverを使ってスケジュール定義をする
AWS Scheduled Scalingではcronフォーマットで日時指定できます。
Wheneverを使ってスケジュールの定義とcron形式への出力を行い、改めてas.scheduled_actions.create
を叩いてスケジュールを登録するようにします。
wheneverでそのままcrontabを作成して、cronでのオートスケーリングのスケジュール管理をすることも考えたのですが、cronは指定の時間にコマンド叩くだけでちゃんとスケジュールされているかどうかまで事前に確認できないので、AWSのスケジューラを使うようにします。
台数、発動時間の指定
- config/auto_scaling_schedule_staging.rb
運用スケジュールやサーバ負荷に応じて随時このファイルを編集します。
# -*- coding: utf-8 -*- # 日付、時間帯によるインスタンス数の調整を行うタイミングをcronで管理する # (指定の時間になったらaws autoscaling put-scheduled-update-group-actionを実行するだけ) # 日時はUTC指定 EXTRA1 = 3 #イベント1時のインスタンス追加分 EXTRA2 = 5 #イベント2の時のインスタンス追加分 SIZE_SMALL = 1 # 過疎時間帯のインスタンス数 SIZE_MEDIUM = 2 # 通常時のインスタンス数 SIZE_LARGE = 3 # 夜間のピークタイムのインスタンス数 # イベント1の開催日(1週間程度開催されるキャンペーンなど) event1_days = [17,18,19,20,21,22,23] # イベント2の開催日(12:00-13:00に開催するタイムセールスのような短時間のイベント) event2_days = [29] # event1 (1..31).each{|day| if event1_days.include?(day) extra = EXTRA1 event = "_event" else extra = 0 event = "" end # 6:00-18:00 every "00 21 #{day} * *" do command "daytime#{event} #{SIZE_MEDIUM + extra}" end # 18:00-25:00 every "00 09 #{day} * *" do command "night#{event} #{SIZE_LARGE + extra}" end # 25:00-6:00 every "00 16 #{day} * *" do command "midnight#{event} #{SIZE_SMALL + extra}" end } # event2 event2_days.each{|day| event1_days.include?(day)?extra = EXTRA1 : extra = 0 # 10:30-13:20 every "30 01 #{day} * *" do command "event2_scale_out #{SIZE_MEDIUM + EXTRA2}" end every "20 04 #{day} * *" do command "event2_scale_in #{SIZE_MEDIUM + extra}" end }
AWS Auto Scaling scheduled actionsの更新
- auto_scaling_schedule_update.rb
wheneverでcronフォーマットを出力してそれを強引に整形してscheduled_actions.create
に渡しています。
cap staging update_auto_scaling_schedule
でこのスクリプトが実行されるようにします。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- $VERBOSE=nil require "pp" require "aws-sdk" require "whenever" require "active_support/core_ext/kernel/reporting" stage = ARGV.shift stage ||= "staging" group = "#{stage}-app-scaling-group" AWS.config(YAML.load(File.read('/etc/scripts/aws/config.yml'))) as = AWS::AutoScaling.new as.scheduled_actions.filter(:group => group).each{|schedule_action| schedule_action.delete } cron = Whenever.cron({:file => "auto_scaling_schedule_#{stage}.rb"}).split("\n") cron.each{|line| if md = line.match(/^(.+)\/bin\/bash -l -c \'(.+) (.+)\'$/) pp line as.scheduled_actions.create(md[2], :group => group, :desired_capacity => md[3].to_i, :recurrence => md[1], :min_size => md[3].to_i, :max_size => md[3].to_i ) end } #確認 system("aws autoscaling describe-scheduled-actions --auto-scaling-group-name #{group} --output json | jq '.ScheduledUpdateGroupActions []| {ScheduledActionName, Time, StartTime, MinSize, MaxSize, DesiredCapacity, Recurrence }'") exit(0)
- 実行結果
"00 21 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * * /bin/bash -l -c 'daytime 2'" "00 09 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * * /bin/bash -l -c 'night 3'" "00 16 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * * /bin/bash -l -c 'midnight 1'" "00 21 17,18,19,20,21,22,23 * * /bin/bash -l -c 'daytime_event 5'" "00 09 17,18,19,20,21,22,23 * * /bin/bash -l -c 'night_event 6'" "00 16 17,18,19,20,21,22,23 * * /bin/bash -l -c 'midnight_event 4'" "30 01 29 * * /bin/bash -l -c 'event2_scale_out 7'" "20 04 29 * * /bin/bash -l -c 'event2_scale_in 2'" { "Recurrence": "00 16 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * *", "DesiredCapacity": 1, "MaxSize": 1, "MinSize": 1, "StartTime": "2014-02-12T16:00:00Z", "Time": "2014-02-12T16:00:00Z", "ScheduledActionName": "midnight" } { "Recurrence": "00 21 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * *", "DesiredCapacity": 2, "MaxSize": 2, "MinSize": 2, "StartTime": "2014-02-12T21:00:00Z", "Time": "2014-02-12T21:00:00Z", "ScheduledActionName": "daytime" } { "Recurrence": "00 09 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,24,25,26,27,28,29,30,31 * *", "DesiredCapacity": 3, "MaxSize": 3, "MinSize": 3, "StartTime": "2014-02-13T09:00:00Z", "Time": "2014-02-13T09:00:00Z", "ScheduledActionName": "night" } { "Recurrence": "00 09 17,18,19,20,21,22,23 * *", "DesiredCapacity": 6, "MaxSize": 6, "MinSize": 6, "StartTime": "2014-02-17T09:00:00Z", "Time": "2014-02-17T09:00:00Z", "ScheduledActionName": "night_event" } { "Recurrence": "00 16 17,18,19,20,21,22,23 * *", "DesiredCapacity": 4, "MaxSize": 4, "MinSize": 4, "StartTime": "2014-02-17T16:00:00Z", "Time": "2014-02-17T16:00:00Z", "ScheduledActionName": "midnight_event" } { "Recurrence": "00 21 17,18,19,20,21,22,23 * *", "DesiredCapacity": 5, "MaxSize": 5, "MinSize": 5, "StartTime": "2014-02-17T21:00:00Z", "Time": "2014-02-17T21:00:00Z", "ScheduledActionName": "daytime_event" } { "Recurrence": "30 01 29 * *", "DesiredCapacity": 7, "MaxSize": 7, "MinSize": 7, "StartTime": "2014-03-29T01:30:00Z", "Time": "2014-03-29T01:30:00Z", "ScheduledActionName": "event2_scale_out" } { "Recurrence": "20 04 29 * *", "DesiredCapacity": 2, "MaxSize": 2, "MinSize": 2, "StartTime": "2014-03-29T04:20:00Z", "Time": "2014-03-29T04:20:00Z", "ScheduledActionName": "event2_scale_in" }
AWS Auto Scalingでunicornサーバとresqueサーバの台数を調節する
ピークタイムやイベント開催中かどうかによってWEBサイトの負荷が大きく変動するのが予想されるので、オートスケーリングを使用してインスタンスを必要な時に必要な台数だけ確保するようにしてサーバ費用を抑えるようにします。
どのサーバにオートスケーリングを利用するか
以下2つのサーバ群に対してオートスケーリングを利用します。
- unicornが動くアプリケーションサーバ
- resque-workerが動くバックグラウンドジョブサーバ
費用けちってサービスレベルがどのくらい下がるか
オンデマンドのEC2を1台だけ固定で用意して、残り必要な台数はオートスケーリングで起動することにします。
サーバ費用をけちるためにスポットインスタンスでオートスケーリングすることにしたので、一時的にスポットインスタンスの料金が急騰した場合にスポットインスタンスがターミネートされて固定分の1台しか稼働していないという状況になることが想定されます。
1台では当然捌けずメンテナンス行きとなるので、メンテ中にスポットの料金上限を引き上げたり、別のインスタンスタイプに変更するなどしてインスタンスを確保する必要があります。
スポットインスタンスの料金が頻繁に急騰しやすい、オートスケール発動時にインスタンスリソースを確保できないなど運用が安定しないようであれば、固定サーバ分を増やして、オートスケールを使わないようにしようと思います(費用も考慮しますが)
スクリプトで新規作成する
AWS Management Consoleから設定可能ですが、いつものようにスクリプトで新規作成できるようにしておきます。 appサーバ(unicorn)のオートスケーリングとbackground(resque-work)の2つそれぞれ作成します。
以下の設定を一気に作成します。
- launch config
- オートスケーリング発動時にどういうインスタンスを起動するか
- auto scaling group
- どのlaunchconfigを使うか・最大、最小インスタンス数、どのELBに所属するかなど
- policy(インスタンス増設時、縮小時)
- インスタンス増設、縮小する際の一度に増減する台数
- cloudwatch metric alarm(CPU高負荷、低負荷)
- Cloudwatchでの観測値をAutoScalingの発動条件にする設定
インスタンス起動後にELBにつなぐかどうか
appサーバはELBにつなぐため、as.create_auto_scaling_group
内で:load_balancer_names => [elb],
としてますが、resque-workサーバはELBにつなぐ必要がないため、この記述はしません。
スポットインスタンスにするかオンデマンドにするか
as.create_launch_configuration
内に:spot_price => 料金
を記述した場合はスポットインスタンス、記述していない場合はオンデマンドインスタンスで立ち上がります。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- require 'aws-sdk' require 'base64' stage = ARGV.shift stage ||= "staging" AWS.config(YAML.load(File.read("config.yml"))) as = AWS::AutoScaling.new.client cw = AWS::CloudWatch.new.client template_name="#{stage}-app" launch_config = "#{stage}-launch-config" auto_scaling_group = "#{stage}-app-scaling-group" policy_add = "#{stage}-scaling-policy-add" policy_remove = "#{stage}-scaling-policy-remove" ssh_key = "sshkey" security_group = stage instance_type = stage == "production" ? "c1.xlarge" : "m1.small" spot_price = stage == "production" ? "0.76" : "0.05" min_size = stage == "production" ? 1 : 0 max_size = stage == "production" ? 1 : 1 elb = "#{stage}-lb-1" cooldown_time = 300 cooldown_time_add = 300 cooldown_time_remove = 3000 adjustment_add = stage == "production" ? 5 : 1 adjustment_remove = stage == "production" ? 5 : 1 az = ["ap-northeast-1a", "ap-northeast-1c"] user_data = Base64.encode64("RAILS_ENV=#{stage}\nSERVER_ROLES=#{stage}-app\nSTAGE=#{stage}\nEC2_NAME=#{stage}-app-autoscale") interval = 1200 scaleout_policy = "#{stage}-scaleout-policy" scalein_policy = "#{stage}-scalein-policy" scaleout_alarm = "#{stage}-scaling-cpu-high" scalein_alarm = "#{stage}-scaling-cpu-low" #テンプレートとなるインスタンスののAMI-IDを取得 (ec2についているタグ「template=staging-app」というものをテンプレートとみなす) ami_id = AWS::EC2.new.images.tagged("template").tagged_values(template_name).map(&:id)[0] #launch-configの作成 as.create_launch_configuration( :launch_configuration_name => launch_config, :image_id => ami_id, #どのAMIを使うか :key_name => ssh_key, # ec2-userに登録するssh公開鍵の設定 :security_groups => [security_group], #どのセキュリティグループに属するか :instance_type => instance_type, # インスタンスタイプ :spot_price => spot_price, # スポットプライスの許容料金 :user_data => user_data, #インスタンス起動時に設定するuserdata ) #auto scaling groupの作成 as.create_auto_scaling_group( :auto_scaling_group_name => auto_scaling_group, :launch_configuration_name => launch_config, #どのlaunchconfigを用いてオートスケーリングするか :availability_zones => [az[0]], # どのアベイラビリティゾーンに配置するか :desired_capacity => min_size, # 維持したい台数 :min_size => min_size, #縮小できる最少台数 :max_size => max_size, #増設できる最大台数 :default_cooldown => cooldown_time, # 次のオートスケーリングが発動可能になるまでの時間間隔 :load_balancer_names => [elb], # そのelbに所属するか :tags => [ {:key => "#{stage}:role:app" , :value => "1"}, {:key => "#{stage}:role:web" , :value => "1"} ], ) #policy(インスタンス増設時)作成 as.put_scaling_policy( :policy_name => policy_add, :auto_scaling_group_name => auto_scaling_group, :adjustment_type => "ChangeInCapacity", # 指定した値だけ台数を増減させる :scaling_adjustment => adjustment_add, # 1回のオートスケーリングアクティビティで減らす台数 :cooldown => cooldown_time_add, # 次のオートスケーリングが発動可能になるまでの時間間隔 ) #policy(インスタンス縮小時)作成 as.put_scaling_policy( :policy_name => policy_remove, :auto_scaling_group_name => auto_scaling_group, :adjustment_type => "ChangeInCapacity", # 指定した値だけ台数を増減させる :scaling_adjustment => adjustment_remove, # 1回のオートスケーリングアクティビティで減らす台数 :cooldown => cooldown_time_remove, # 次のオートスケーリングが発動可能になるまでの時間 ) scaleout_arn = as.describe_policies(:policy_names => [policy_add])[:scaling_policies][0][:policy_arn] scalein_arn = as.describe_policies(:policy_names => [policy_remove])[:scaling_policies][0][:policy_arn] # Cloudwatchの観測値をAutoScalingの発動条件にする設定(CPU高負荷) cw.put_metric_alarm( :alarm_name => scaleout_alarm, :alarm_description => "", :actions_enabled => true, :alarm_actions => [scaleout_arn], #しきい値超えと判定した際に行うアクション :metric_name => 'CPUUtilization', #測定項目にCPU使用率 :namespace => 'AWS/EC2', :statistic => 'Average', 'Average', #平均を測定値とする :dimensions => [{:name => 'AutoScalingGroupName', :value => auto_scaling_group}], # 測定対象 :period => 60, :evaluation_periods => 15, # 60秒x15回=15分間観測値が超えていたら、しきい値超えと判定 :threshold => 95, # 95% :comparison_operator => 'GreaterThanThreshold',# 以上だったら ) # Cloudwatchの観測値をAutoScalingの発動条件にする設定(CPU低負荷) cw.put_metric_alarm( :alarm_name => scalein_alarm, :alarm_description => "", :actions_enabled => true, :alarm_actions => [scalein_arn], #しきい値超えと判定した際に行うアクション :metric_name => 'CPUUtilization', #測定項目にCPU使用率 :namespace => 'AWS/EC2', :statistic => 'Average', #平均を測定値とする :dimensions => [{:name => 'AutoScalingGroupName', :value => auto_scaling_group}], # 測定対象 :period => 60, # 60秒に1回測定 :evaluation_periods => 15, # 60秒x15回=15分間観測値が超えていたら、しきい値超えと判定 :threshold => 10, # しきい値=10% :comparison_operator => 'LessThanThreshold',# 以下だったら ) #launch configの表示 p as.describe_launch_configurations(:launch_configuration_names => [launch_config])[:launch_configurations] # auto scaling groupの表示 p as.describe_auto_scaling_groups(:auto_scaling_group_names => [auto_scaling_group])[:auto_scaling_groups] # インスタンス増設時のpolicy表示 p as.describe_policies(:policy_names => [policy_add]) # インスタンス縮小時のpolicy表示 p as.describe_policies(:policy_names => [policy_remove]) # cluodwatchのしきい値超え時の設定表示(CPU高負荷) p cw.describe_alarms(:alarm_names => [scaleout_alarm])[:metric_alarms] # cluodwatchのしきい値超え時の設定表示(CPU低負荷) p cw.describe_alarms(:alarm_names => [scalein_alarm])[:metric_alarms]
今回は新規作成の手順でした
オートスケールを機能させるにはいくつかのconfig設定が必要で、今回まとめて新規作成を行いました。 アプリがデプロイされたら、LaunchConfigを新しいAMIで作りなおしてAutoScalingGroupも更新してと日々アップデート作業が必要になります。
また、今回はオートスケール発動条件に・cpu90%以上で増設・cpu10%以下縮小という設定を行いましたが、他にも深夜1:00-5:00はインスタンスを縮小するなどスケージュール設定を行うこともできます。実はこのスケジュール機能が使えそうと思っていて、このためにオートスケーリングを設定していたりもします。 スケジュール設定は次の記事に書き残しておきます。
AWS SNSでメール通知を行う
AWS Cloudwatchで監視を行っていてしきい値超えを検知した場合に、AWS SNSで設定されている通知を行う事ができます。 SNSでの通知としては・メールを送信・指定のURLにアクセスする・APNsにPush通知を行うなどがありますが、今回は指定のメールアドレスにメールを送信します。
下記のように3つのトピックを用意して、アラートの重要度に合わせて通知先を振り分けるようにします。
- systemalert
- ただちにサービスレベルに影響しない(ディスク残量低下、オートスケール発動の通知)
- systemcritical
- サービスレベルに影響がある(ELBのレスポンスタイムが2秒以上、RDSのレプリケーション遅延を10秒以上検知)
- systemservicedown
- サービスダウンしている状態(サイトヘルスチェックでNG)
トピックアクションとして、以下のように指定のメールアドレスにメール送信を行うようにします。
- systemalert-{stage|production}@watch.example.com
- systemcritical-{stage|production}@watch.example.com
- systemservicedown-{stage|production}@watch.example.com
- 上記メールアドレスにメールが送られてきたらskepeのアラート部屋にも発信したいので、watchサーバ上の/etc/aliasesで設定をしておきます。 設定はこんなかんじ
上記トピックをスクリプトで新規作成できるようにしておきます。 なおトピックのサブスクリプション作成時に、指定したメールアドレスに確認用のメールが送信されるので、受信してメールの中の確認用URLにアクセスする必要があります。
stage = ARGV.shift stage ||= "staging" az = ARGV.shift az ||="main" domain = "@watch.example.com" topics = ["systemalert-#{stage}", "systemcritical-#{stage}", "systemservicedown-#{stage}"] AWS.config(YAML.load(File.read('/etc/scripts/aws/config.yml'))) client = AWS::SNS::Client.new topics.each{|topic| # create topic response = client.create_topic( :name => topic ) # subscript client.subscribe( :topic_arn => response[:topic_arn], :protocol => "email", :endpoint => topic + domain ) }
AWS RDSでMySQLサーバを立ち上げる
RDSを使ってmysqlをmaster,slaveのレプリケーション構成で立ち上げます。 redisと同じようにスクリプトでセットアップできるようにします。パラメータは初期パラメータとしてとりあえずの設定をしています。 MultiAZでマスタを立ち上げるとactiveがap-northeast-1cになる場合があるので、その時はfailover付きでリブートし直します。
AWS RDSでMySQLサーバを立ち上げる RDSを使ってmysqlをmaster,slaveのレプリケーション構成で立ち上げます。 redisと同じようにスクリプトでセットアップできるようにします。パラメータは初期パラメータとしてとりあえずの設定をしています。 MultiAZでマスタを立ち上げるとactiveがap-northeast-1cになる場合があるので、その時はfailover付きでリブートし直します。
stage = "staging" master = "#{stage}-master-1" slave = "#{stage}-slave-1" subnet_group = "#{stage}-rds-subnet" param_group = "#{stage}-mysql56" param_group_family = "mysql5.6" option_group = "#{stage}-mysql56" subnets = ["subnet-xxx", "subnet-xxx"] availability_zones = ["ap-northeast-1a", "ap-northeast-1c"] instance_class = "db.m1.small" storage = 10 db_name = "dbname_#{stage}" username = "xxx" password = "xxx" engine_version = "5.6.13" stage == "production" ? sg = "sg-xxx" : sg="sg-xxx" stage == "production" ? arn = "xxx" : arn = "xxx" event_categories = ["availability", "configuration change", "creation", "deletion", "failover", "failure", "low storage", "maintenance", "notification", "recovery", "restoration"]
client = AWS::RDS::Client.new
オプショングループ作成
client.create_db_parameter_group( :option_group_name => option_group_name, :engine_name => :mysql, :major_engine_version => "5.6", :option_group_description => "#{stage} param group" )
パラメータグループ作成
client.create_db_parameter_group( :db_parameter_group_name => param_group, :db_parameter_group_family => param_group_family, :description => "#{stage} param group" )
パラメータ変更
client.modify_db_parameter_group( :db_parameter_group_name => param_group, :parameters => [ {:parameter_name => "character_set_client", :parameter_value => "utf8mb4", :apply_method => "immediate"}, {:parameter_name => "character_set_connection", :parameter_value => "utf8mb4", :apply_method => "immediate"}, {:parameter_name => "character_set_database", :parameter_value => "utf8mb4", :apply_method => "immediate"}, {:parameter_name => "character_set_server", :parameter_value => "utf8mb4", :apply_method => "immediate"}, {:parameter_name => "character_set_results", :parameter_value => "utf8mb4", :apply_method => "immediate"}, {:parameter_name => "collation_connection", :parameter_value => "utf8mb4_general_ci", :apply_method => "immediate"}, {:parameter_name => "init_connect", :parameter_value => "SET NAMES utf8mb4;", :apply_method => "immediate"}, {:parameter_name => "max_connect_errors", :parameter_value => "999999999", :apply_method => "immediate"}, {:parameter_name => "max_allowed_packet", :parameter_value => "67108864", :apply_method => "immediate"}, {:parameter_name => "slow_query_log", :parameter_value => "ON", :apply_method => "immediate"}, {:parameter_name => "long_query_time", :parameter_value => "0.1", :apply_method => "immediate"}, {:parameter_name => "min_examined_row_limit", :parameter_value => "1000", :apply_method => "immediate"}, {:parameter_name => "query_cache_type", :parameter_value => "1", :apply_method => "pending-reboot"}, {:parameter_name => "general_log", :parameter_value => "OFF", :apply_method => "immediate"}, {:parameter_name => "wait_timeout", :parameter_value => "60", :apply_method => "immediate"}, {:parameter_name => "lock_wait_timeout", :parameter_value => "3", :apply_method => "immediate"}, {:parameter_name => "skip_name_resolve", :parameter_value => "ON", :apply_method => "pending-reboot"}, {:parameter_name => "log_output", :parameter_value => "FILE", :apply_method => "immediate"},
] )
サブネットグループ作成
client.create_db_subnet_group( :db_subnet_group_name => subnet_group, :db_subnet_group_description => "#{stage} subnet group" , :subnet_ids => subnets )
マスタDB作成
client.create_db_instance( :db_name => db_name, :db_instance_identifier => master, :allocated_storage => storage, :db_instance_class => instance_class, :engine => "mysql", :master_username => username, :master_user_password => password, :vpc_security_group_ids => [sg], :db_subnet_group_name => subnet_group, #:availability_zone => availability_zones[0], :db_parameter_group_name => param_group, :option_group_name => option_group, :preferred_maintenance_window => "mon:05:30-mon:06:30", :backup_retention_period => 30, :preferred_backup_window => "19:00-20:00", :port => 3306, :multi_az => true, :engine_version => engine_version, :auto_minor_version_upgrade => false, )
完成を待つ
while "available" != client.describe_db_instances(:db_instance_identifier => master)[:db_instances][0][:db_instance_status] puts "waiting for creating master" sleep 30 end
スレーブDB作成
client.create_db_instance_read_replica( :db_instance_identifier => slave, :source_db_instance_identifier => master, :db_instance_class => instance_class, :availability_zone => availability_zones[0], :port => 3306, :auto_minor_version_upgrade => false, )
完成を待つ
while "available" != client.describe_db_instances(:db_instance_identifier => slave)[:db_instances][0][:db_instance_status] puts "waiting for creating slave" sleep 30 end
イベント通知の設定を行う
client.create_event_subscription( :subscription_name => "#{stage}-event", :sns_topic_arn => arn, :source_type => "db-instance", :event_categories => event_categories, :source_ids => [master, slave], :enabled => true )
AWS ElasticacheでRedisサーバを立ち上げる
Redisを使用するのでelasticacheのredisでmaster-slaveのレプリケーション構成を作ります。 手順は以下のようになります。
- パラメータグループを新規作成、一部のパラメータを変更する(aof,timeout)
- このパラメータグループを使うノードを作成する(マスターノードとなる)
- このノードをマスターとするレプリケーショングループを作成する
- このレプリケーショングループに所属するノードをつくる(スレーブノードとなる)
AWS Mamagement Consoleからでもさくさくと作れますが、複製を作るかもしれないのでスクリプトにしておきます。
なお、アプリからredisに接続する時のエンドポイントはレプリケーショングループID.xxx.0001.apne1.cache.amazonaws.com:6379
といた具合になります。
master = "#{stage}-master-1" slave = "#{stage}-slave-1" repl_group = "#{stage}-redis-1" subnet_group = "#{stage}-redis-subnet" param_group = "#{stage}-redis26" family_param_group = "redis2.6" availability_zones = ["ap-northeast-1a", "ap-northeast-1c"] node_type = "cache.m1.small" arn = "xxx" security_group = "xxx" subnets = ["subnet-xxx", "subnet-xxx"] redis_version = "2.6.13" client = AWS::ElastiCache::Client.new #パラメータグループの作成 client.create_cache_parameter_group( :cache_parameter_group_name => param_group, :cache_parameter_group_family => family_param_group, :description => "#{param_group}", ) #パラメータ変更 client.modify_cache_parameter_group( :cache_parameter_group_name => param_group, :parameter_name_values => [ {:parameter_name => "timeout", :parameter_value => "300"}, {:parameter_name => "appendonly", :parameter_value => "yes"} ] ) #サブネットグループの作成 client.create_cache_subnet_group( :cache_subnet_group_name => subnet_group, :cache_subnet_group_description => "#{stage} redis subnet group", :subnet_ids => [subnets[0]] ) #マスターノードの作成 client.create_cache_cluster( :cache_cluster_id => master, :num_cache_nodes => 1, :cache_node_type => node_type, :engine => "redis", :engine_version => redis_version, :cache_parameter_group_name => param_group, :cache_subnet_group_name => subnet_group, :security_group_ids => [security_group], :preferred_availability_zone => availability_zones[0], :preferred_maintenance_window => "mon:19:00-mon:20:00", :notification_topic_arn => arn, :auto_minor_version_upgrade => false ) #マスターの作成を待つ while "available" != client.describe_cache_clusters(:cache_cluster_id => master)[:cache_clusters][0][:cache_cluster_status] sleep 30 end #レプリケーショングループの作成 client.create_replication_group( :replication_group_id => repl_group, :primary_cluster_id => master, :replication_group_description => "#{stage} replication group" ) #レプリケーショングループの作成を待つ while "available" != client.describe_replication_groups(:replication_group_id => repl_group)[:replication_groups][0][:status] sleep 30 end #スレーブノードの作成 client.create_cache_cluster( :cache_cluster_id => slave, :replication_group_id => repl_group, )
AWS ELB(Elastic Load Balancing)作成
インターネットからのHTTPリクエストを受けるproxyサーバとRailsが動くアプリケーションサーバの間にロードバランサであるELBを挟んで複数のアプリケーションサーバにリクエストを振るようにします。ちなみにELBはZeus製と聞いたことがあります。
AWS Mamagement Consle上のブラウザ操作で簡単作成できるのですが、環境の複製を作るといった場合に同じ操作をするのはしんどいので、スクリプトで作成できるようにします。
こんな感じ
stage = "staging" subnet = "xxxx" sg = "xxx" elb_name = "#{stage}-lb-1" #ロードバランサの作成 # subnets指定することでそのsubnetsにいるインスタンスだけにフォワードする # scheme : internalでsubnet内に配置 # http(tcp:80)をlistenしてhttp(tcp:80)にフォワードする lb = AWS::ELB::LoadBalancerCollection.new.create(elb_name, :subnets => [subnet], :security_groups => [sg], :scheme => "internal", :listeners => [{ :port => 80, :protocol => :http, :instance_port => 80, :instance_protocol => :http, }] ) #ヘルスチェックの設定 # unhealthy_threshold # ヘルスチェックの結果、連続okでない回数がこの指定値以上になれば # インスタンスにリクエストを送らない(out of service) # healthy_threshold # out of serviceのインスタンスにヘルスチェックを行った際、この指定値以上に連続okとなれば # このインスタンへのリクエスト配信を再開する(inservice) # interval - 6秒に1回ELBが配下のインスタンスにヘルスチェック用リクエストを飛ばす # timeout - 配下のインスタンスからの応答待ち時間(超えたらunhealth+1) # target - ヘルスチェックのリクエストにtcp:80でGET/healthを使用する lb.configure_health_check( :healthy_threshold => 2, :unhealthy_threshold => 10, :interval => 6, :timeout => 5, :target => "HTTP:80/health" ) #インスタンスをELB配下に登録する(web=stagingのタグを持つインスタンスが対象) lb.instances.register(AWS::EC2.new.instances.tagged("web").tagged_values(stage).select{|ins| ins }) # クッキーでのスティッキーポリシーの作成 # cookie_name - リクエスト振り分けをスティッキーにする際にキーとして使用する名前 AWS::ELB::Client.new.create_app_cookie_stickiness_policy( :load_balancer_name => elb_name, :policy_name => "lb-cookie-session", :cookie_name => "_session_id" ) # ELBリスナーにポリシー適用 # tcp80ポートリスナーに上記のcookie sticknessポリシーを適用する AWS::ELB::Client.new.set_load_balancer_policies_of_listener( :load_balancer_name => elb_name, :load_balancer_port => 80, :policy_names => ["lb-cookie-session"] )
redis-commanderでredisサーバへの操作を行えるようにする
node.jsで作られたredisマネジメントツールredis-commanderをインストールします。 GUIでredisの操作できます。
手順
$sudo yum install nodejs $sudo yum install npm $sudo npm install -g redis-commander
haproxy
redis-commanderからはローカルのhaproxyを経由してredisサーバに接続するようにします。
listen redis bind :6379 mode tcp balance source server redis1 staging-redis-1.example.internal:6379 inter 5s fall 2 rise 2 check port 6379
monitでプロセス監視を行う
プロセスの起動はmonit経由で行います。
- /etc/monit.d/staging
check process redis-commander matching "redis-commander" start program = "/usr/bin/redis-commander" as uid ec2-user and gid ec2-user stop program = "/usr/bin/killall -9 redis-commander" if 5 restarts within 5 cycles then timeout
動作確認
AWSセキュリティグループのポリシー追加
会社のIP、プロキシIPからセキュリティグループappへのtcp:8081接続を許可します。
画面を開く
http://redis-commanderをインストールしたサーバ:8081
で画面が開きます。
今回はredidのindex1〜4まで使う予定なのでmore->Add Server
より指定のindex分、接続を行っておきます。
unicorn/resqueのプロセス管理にeyeを使う
unicorn,resqueのプロセス管理に今回はeyeを使うことにします。 はじめBluepillで設定していたのですが、Ruby2.0環境だとBluepillのプロセスがゾンビで溜まりまくるという問題があり、代替をさがしていたところeyeというのがありました。God、Bluepillを参考に作られているということで設定も似た感じかな?と思い採用しています。今のところRuby2.x環境でも問題は起きていません。
EyeとMonitで監視対象のプロセスをわける
デプロイの際に再起動するプロセスをeyeで、それ以外をmonitで監視するようにします。
インストール
bundleでは無くgemで入れてます。
$gem install eye
eyeの起動方法はeye load 設定ファイル
となります。
設定ファイルはサーバの役割毎に用意しておきます。
各サーバに環境変数SERVER_ROLE
というのを設定していて、この環境変数から読み込む設定ファイルが決まるようします。
これでappsサーバはunicornを起動する設定を読み込み、resqueサーバはresque関連を起動する設定を読み込むことになります。
eyeの設定ファイル
Railsプロジェクトのconfig/eye/以下に設定ファイルをそれぞれ配置しておきました。 以下はunicornとresque-workerプロセスを管理する設定例になります。 各プロセスの起動停止コマンドや、監視内容を設定しています(プロセスのメモリが300m超えたら再起動するとか)
- config/eye/staging-app.eye
# -*- coding: utf-8 -*- ENV['RAILS_ROOT'] ||= File.expand_path(File.join(File.dirname(__FILE__), "../..")) rails_root = ENV['RAILS_ROOT'] rails_env = ENV['RAILS_ENV'] stage = ENV['STAGE'] current_path = "/home/ec2-user/example/#{stage}/current" shared_path = "/home/ec2-user/example/#{stage}/shared/" # load submodules, here just for example Eye.load("./eye/*.rb") # Eye self-configuration section Eye.config do logger "/var/log/eye/eye.log" end Eye.application "#{stage}-app" do working_dir current_path process("unicorn") do pid_file "tmp/pids/unicorn.pid" start_command "bundle exec unicorn -Dc config/unicorn/#{stage}.rb -E #{rails_env}" stdall "log/unicorn.log" # stop signals: # http://unicorn.bogomips.org/SIGNALS.html stop_signals [:TERM, 10.seconds] # soft restart restart_command "kill -USR2 {PID}" check :cpu, :every => 30, :below => 99, :times => 3 check :memory, :every => 30, :below => 300.megabytes, :times => [3,5] start_timeout 120.seconds restart_grace 120.seconds monitor_children do stop_command "kill -QUIT {PID}" check :cpu, :every => 30, :below => 99, :times => 3 check :memory, :every => 30, :below => 300.megabytes, :times => [3,5] end end process ("resque_worker") do pid_file "tmp/pids/resque_worker.pid" start_command "bundle exec rake -t -f Rakefile environment resque:work" stop_signals [:QUIT, 5.seconds,:TERM, 5.seconds,:KILL] start_timeout 120.seconds restart_grace 120.seconds daemonize false env "RAILS_ENV" => rails_env, "QUEUE" => "*", "INTERVAL" => 1, "BACKGROUND" => "yes", "PIDFILE" => "tmp/pids/resque_worker.pid" stdall "log/resque_worker.log" monitor_children do stop_command "kill -QUIT {PID}" check :cpu, :every => 30, :below => 99, :times => 3 check :memory, :every => 30, :below => 300.megabytes, :times => [3,5] end end end
デプロイタスクも用意する
デプロイ時にunicorn,resqueの再起動が必要になるので、eyeを通して再起動を行えるようにタスクを書いておきます。 なお、unicornの再起動にはUSR2でのリスタートとプロセスを落として起動するの2つの方法で再起動できるようにそれぞれタスクを用意しました。 (ファイルアップロードのデプロイ時はUSR2リロードで良く、currentシンボリックリンクが切り替わる場合は停止、起動を行います)
- cap staging deploy:reload - eye restartが実行される(unicornのUSR2リロード)
- cap staging deploy:restart - eye stop,startが実行される)
lib/capistrano/tasks/eye.cap
namespace :eye do desc "eye start" task :start do on roles(:app), in: :parallel do execute "sudo /etc/init.d/eye start || true" end end desc "eye stop" task :stop do on roles(:app), in: :parallel do execute "sudo /etc/init.d/eye stop || true" end end desc "eye restart" task :restart do on roles(:app), in: :sequence, wait: 10 do execute "sudo /etc/init.d/eye restart || true" end end desc "eye stop and start" task :stop_start do on roles(:app), in: :sequence, wait: 20 do |host| puts host if host.roles.include?(:web) execute "ssh gateway elb_remove.rb #{fetch(:stage)} #{host} || true" end execute "sudo /etc/init.d/eye stop || true" execute "sudo /etc/init.d/eye start || true" puts "sleep 30" sleep 30 if host.roles.include?(:web) execute "ssh gateway elb_add.rb #{fetch(:stage)} #{host} || true" end end end desc "eye info" task :info do on roles(:app), in: :parallel do count = 0 while capture("sudo /etc/init.d/eye info || true").match(/starting/) && count < 18 puts "loop" count = count + 1 sleep 10 end end end after :restart, "eye:info" after :stop_start, "eye:info" end
nginxでソーリーページを返す - メンテ以外の場合
以下の状況の場合はnginxでエラーハンドリングを行います。
nginxが40X,50Xのステータスコードでレスポンスを返すと、プラットフォーム側で用意しているエラー画面が表示される場合があります。 これを見た時プラットフォーム側のエラーなのか、こちらのサーバ側のエラーなのか判断つきません。 40x,50xのエラー時でもプラットフォームには200を伝えてこちらの用意したソーリーページを表示するようにします。
ステータスコードを書き換えたうえで事前に用意した専用のコンテンツを返すには下記のように設定します。 appsからのレスポンス待ちを5000msに設定しています。超えた場合は504となります。
server { ... proxy_read_timeout 5000ms; proxy_connect_timeout 5000ms; proxy_intercept_errors on; error_page 400 =200 /system/400.html; error_page 403 =200 /system/403.html; error_page 404 =200 /system/404.html; error_page 500 =200 /system/500.html; error_page 502 =200 /system/502.html; error_page 503 =200 /system/503.html; error_page 504 =200 /system/504.html; ... }
書き換わったステータスコードがnginxのログにも出力されるので、アクセスログからエラー状況の調査ができなくなります。
対策としてログフォーマットに$upstream_status
を追加して、こちらのステータスコードでエラー状況を判断するようにします。