Automatische Skalierung von AWS Fargate Containern

Bei einem internen Projekt, das mit AWS Fargate läuft, haben wir uns mit automatischer Skalierung beschäftigt.

In diesem Blogbeitrag möchte ich zeigen, wie wir eine Rails-Webanwendung und deren Resque-Worker anhand ihrer Auslastung automatisch skalieren lassen.

Skalierung der Webanwendung

Um den Fargate-Service für die Webanwendung zu skalieren ist ein ApplicationLoadBalancer nötig.

Wir nutzen CloudFormation, um die AWS-Ressourcen aufzusetzen. Das Template um einen ApplicationLoadBalancer anzulegen und mit dem Web-Service zu verknüpfen sieht wie folgt aus:

Wird der ECS-Service dann skaliert, kümmert sich der ApplicationLoadBalancer darum, eingehende Anfragen an die neuen ECS-Tasks zu verteilen.

Für unsere Webanwendung wollen wir den Service je nach CPU-Last skalieren lassen. Bei AWS passiert das über CloudWatch-Alarme. Die Alarme bekommen dann als Alarm-Aktion eine ScalingPolicy zugewiesen, in der dann beschrieben ist, wie skaliert werden soll.

Hier eine Beispielkonfiguration, die den Web-Service jeweils um einen Task bis auf maximal fünf Tasks hoch skaliert, wenn die letzten drei Minuten die CPU-Last über 90 % war. Runter skaliert wird, wenn die letzten drei Minuten die CPU-Last unter 30 % war:

Über die Eigenschaft Cooldown bei der ScalingPolicy kann man außerdem angeben, wie lange gewartet werden soll, bis die nächste Skalierung ausgeführt wird.

Skalierung der Resque-Worker

Zur Skalierung unserer Resque-Worker haben wir die Anzahl der wartenden Jobs genommen. Anders als die CPU-Auslastung wird das aber natürlich nicht automatisch in CloudWatch erfasst. Also müssen wir das selbst als eine Custom-Metric an CloudWatch senden.

Wir haben das durch einen eigenen Resque-Job gelöst, der mit resque-scheduler jede Minute ausgeführt wird und die aktuelle Anzahl der Jobs in allen Queues an CloudWatch schickt.

Wir haben immer mindestens einen Worker laufen, daher wird die Anzahl der Jobs auch zuverlässig erfasst.

require 'aws-sdk-cloudwatch'
class CloudwatchJob
  @queue = :critical
  def self.perform
    queue_size = Resque.queues.sum { |q| Resque.size(q) }
    if Rails.env.production?
      Aws::CloudWatch::Client.new.put_metric_data(
        namespace: 'NWW/Resque',
        metric_data: [
          {
            metric_name: 'QueueSize',
            dimensions: [{ name: 'Stack', value: ENV['STACK'] }],
            timestamp: Time.zone.now.iso8601,
            value: queue_size,
            unit: 'Count'
          }
        ]
      )
    else
      Rails.logger.info "Resque-Queue-Size is #{queue_size}"
    end
  end
end

Vielen Dank außerdem an XDEV (@XDEVSoftware), für die umfangreiche Beratung zu AWS ECS.