Cloudsail.io
Published in

Cloudsail.io

Oszczędne architektury z EC2 — automatyzacja oszczędzania z instancjami Spot i Cloudformation

W trakcie jednego z ostatnich treningów zacząłem się zastanawiać nad sposobem wykorzystania instancji EC2 w trybie cenowym Spot w celu zminimalizowania kosztów infrastruktury za pomocą przerzutu (failover) do OnDemand w przypadku nagłego wzrostu zapytań.

Okazuje się, ze istnieje bardzo prosty sposób na osiągniecie sporych oszczędności za pomocą jednego wzorca Cloudformation, lecz wymaga to tworzenia aplikacji, które mogą działać bez problemów, jeżeli pojedyńcze instancje przestaną działać.

Tryby cenowe EC2

Przy zakupie instancji EC2 musimy zdecydować w jakim trybie cenowym chcemy je nabyć. Istnieją trzy tryby:

  • On demand — najdroższy z trybów, przy którym płacimy więcej za możliwość użycia instancji kiedy chcemy.
  • Reserved — rezerwujemy dane instancje na pewien okres czasu i dzięki temu płacimy mniej, lecz wymaga to od nas oszacowania ich ilości. Tryb ten zapewnia oszczędności aż do 75% w porównaniu do on demand, ale musimy je zarezerwować na conajmniej rok.
  • Spot — najtańszy tryb cenowy, ale również najbardziej ryzykowny. Polega on na tym, że instancje, które nie są używane, są wystawiane na aukcji. W momencie, w którym nasza oferta zostanie przebita, instancja zostanie wyłączona w ciągu kilku minut, wiec nie nadają się one do krytycznych prac. Tryb ten zapewnia zniżkę aż do 90% w porównaniu do on demand.

Jeżeli korzystalibyśmy jedynie z instancji w trybie on demand, to mielibyśmy problem wysokich kosztów. Przy reserved musielibyśmy je zarezerwować na rok i nie moglibyśmy dowolnie skalować.

Jeżeli chcielibyśmy stworzyć flotę instancji, które zapewnia nam jak najwyższe oszczędności, a jednocześnie pozwalała na skalowanie, przy zmieniającym się zapotrzebowaniu, to moglibyśmy stworzyć hybrydę z instancji spot i on demand.

Flota hybryda

Stwórzmy najpierw nasza flotę w oparciu o instancje Spot.
Rozpocznijmy od stworzenia nowej templatki Cloudformation o dowolnej nazwie, ale z końcówka .yaml.

AWSTemplateFormatVersion: ‘2010–09–09’Metadata: 
License: Apache-2.0
Description: ‘Failover spot to on demand.’

Następnie tworzymy Resources, czyli części infrastruktury. W ramach tego PoC będziemy używać już stworzonego VPC, a nasza infrastruktura zostanie stworzona w Irlandii, czyli regionie eu-west-1.


Parameters:
VPCName:
Type: String
Default: ‘vpc-xxxxxx’

Następnie tworzymy Security Group dla grupy instancji w trybie spot.

Resources:
SpotSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'SpotSG'
GroupDescription: 'Security group for spot instances.'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
VpcId: !Ref 'VPCName'

I dodajemy Application Load Balancer oraz Launch Configuration

  ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: 'ipv4'
Name: 'ALB'
Scheme: 'internet-facing'
Type: "application"

SpotLC:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
LaunchConfigurationName: 'SpotLC'
ImageId: ami-07683a44e80cd32c5
SpotPrice: "0.0040"
SecurityGroups:
- !Ref SpotSG
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: "/dev/sdc"
VirtualName: ephemeral0

Następnie dodajemy Auto Scaling Group. Autoscaling będzie się troszczyć, aby w ramach możliwości liczba instancji Spot wynosiła conajmniej 2, ale nigdy więcej niż 5.

  SpotASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: 'SpotASG'
AvailabilityZones:
- 'eu-west-1a'
Cooldown: "120"
MetricsCollection:
- Granularity: '1Minute'
Metrics:
- GroupInServiceInstances
DesiredCapacity: "3"
HealthCheckGracePeriod: 30
HealthCheckType: "EC2"
LaunchConfigurationName: 'SpotLC'
MaxSize: "5"
MinSize: "2"
DependsOn:
- SpotLC
- ALB

Kolejnym krokiem jest dodanie tych samych komponentów dla instancji w trybie on demand.

Największą różnicą jest oczywiście konfiguracja AutoScalingGroup, która tym razem pozwala na conajmniej 0 instancji (w razie, gdy instancje spot działają), a maksymalnie 3:

OnDemandSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'OnDemandSG'
GroupDescription: 'SG for on demand failover instances.'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
VpcId: !Ref 'VPCName'

OnDemandLC:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
LaunchConfigurationName: 'OnDemandLC'
ImageId: ami-07683a44e80cd32c5
SecurityGroups:
- !Ref OnDemandrSG
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: "/dev/sdc"
VirtualName: ephemeral0

OnDemandASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: 'OnDemandASG'
AvailabilityZones:
- 'eu-west-1a'
Cooldown: "120"
MetricsCollection:
- Granularity: '1Minute'
Metrics:
- GroupInServiceInstances
DesiredCapacity: "0"
HealthCheckGracePeriod: 30
HealthCheckType: "EC2"
LaunchConfigurationName: 'OnDemandLC'
MaxSize: "3"
MinSize: "0"
DependsOn:
- OnDemandLC
- ALB

Dzieki temu otrzymamy infrastrukturę z luźnymi komponentami, które nie mają ze sobą żadnego powiązania. Następnym krokiem będzie powiązanie ich ze sobą.

Skalowanie dzięki Cloudwatch Alarms

Następnie stwórzmy dwa alarmy Cloudwatch. Pierwszy zostanie aktywowany, jeżeli liczba instancji Spot będzie niższa niż 2. W tym wypadku będziemy chcieli aktywować skalowanie w górę grupy instancji on demand.

# Cloudwatch alarms
ScaleUpOnDemandAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
EvaluationPeriods: '1'
Statistic: SampleCount
Threshold: '2'
AlarmDescription: Alarm if less than 2 spot instances
AlarmActions:
- Ref: ScaleUpPolicy
Namespace: AWS/AutoScaling
Period: '60'
Dimensions:
- Name: AutoScalingGroupName
Value: 'SpotASG'
ComparisonOperator: LessThanThreshold
MetricName: 'GroupInServiceInstances'

Tworzymy więc Scaling Policy, które stopniowo zwiększy liczbę instancji w grupie skalowania instancji on demand. Będzie to robić tak długo, aż liczba instancji w grupie skalowania osiągnie narzucony limit.

ScaleUpPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref OnDemandASG
AdjustmentType: 'ChangeInCapacity'
Cooldown: '30'
PolicyType: 'SimpleScaling'
ScalingAdjustment: 1

Mamy więc failover/przerzut do on demand, ale to zapewniłoby nam jednorazowe zmniejszenie kosztów, jedynie przy deploymencie. Dzięki drugiemu alarmowi i drugiej Scaling Policy możemy zmniejszyć liczbę instancji w tej grupie.

ScaleDownOnDemandAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
EvaluationPeriods: '1'
Statistic: SampleCount
Threshold: '2'
AlarmDescription: Alarm if # spot instances more than 2
AlarmActions:
- Ref: ScaleDownPolicy
Namespace: AWS/AutoScaling
Period: '60'
Dimensions:
- Name: AutoScalingGroupName
Value: 'SpotASG'
ComparisonOperator: GreaterThanThreshold
MetricName: 'GroupInServiceInstances'

ScaleDownPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref OnDemandASG
AdjustmentType: 'ChangeInCapacity'
Cooldown: '30'
PolicyType: 'SimpleScaling'
ScalingAdjustment: -1

Dlaczego to działa?

Instancje spot są zazwyczaj włączane przez Cloudformation jednorazowo. AutoScalingGroup zapewnia automatyzację procesu kupowania tego typu instancji, dzięki czemu w momencie, w którym będziemy mieli dostęp do mniejszej ilości instancji niż minimum ustawione w ASG, to ASG będzie aktywnie próbować zakupić dodatkowe instancje trybie spot za ustaloną cenę.

Cały kod możesz znaleźć na moim Githubie:

https://github.com/spejss/Cloudformation-Templates/blob/master/Spot2OnDemandFailover.yaml

oraz tutaj:

AWSTemplateFormatVersion: '2010-09-09'

Metadata:
License: Apache-2.0
Description: 'Failover spot to on demand.'

Parameters:
VPCName:
Type: String
Default: 'vpc-xxxxxx'

Resources:
SpotSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'SpotSG'
GroupDescription: 'Security group for spot instances.'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
VpcId: !Ref 'VPCName'

ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: 'ipv4'
Name: 'ALB'
Scheme: 'internet-facing'
Type: "application"

SpotLC:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
LaunchConfigurationName: 'SpotLC'
ImageId: ami-07683a44e80cd32c5
SpotPrice: "0.0040"
SecurityGroups:
- !Ref SpotSG
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: "/dev/sdc"
VirtualName: ephemeral0

SpotASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: 'SpotASG'
AvailabilityZones:
- 'eu-west-1a'
Cooldown: "120"
MetricsCollection:
- Granularity: '1Minute'
Metrics:
- GroupInServiceInstances
DesiredCapacity: "3"
HealthCheckGracePeriod: 30
HealthCheckType: "EC2"
LaunchConfigurationName: 'SpotLC'
MaxSize: "5"
MinSize: "2"
DependsOn:
- SpotLC
- ALB

OnDemandSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: 'OnDemandSG'
GroupDescription: 'SG for on demand failover instances.'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
VpcId: !Ref 'VPCName'

OnDemandLC:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
LaunchConfigurationName: 'OnDemandLC'
ImageId: ami-07683a44e80cd32c5
SecurityGroups:
- !Ref OnDemandrSG
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: "/dev/sdc"
VirtualName: ephemeral0

OnDemandASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: 'OnDemandASG'
AvailabilityZones:
- 'eu-west-1a'
Cooldown: "120"
MetricsCollection:
- Granularity: '1Minute'
Metrics:
- GroupInServiceInstances
DesiredCapacity: "0"
HealthCheckGracePeriod: 30
HealthCheckType: "EC2"
LaunchConfigurationName: 'OnDemandLC'
MaxSize: "3"
MinSize: "0"
DependsOn:
- OnDemandLC
- ALB


ScaleUpOnDemandAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
EvaluationPeriods: '1'
Statistic: SampleCount
Threshold: '2'
AlarmDescription: Alarm if less than 2 spot instances
AlarmActions:
- Ref: ScaleUpPolicy
Namespace: AWS/AutoScaling
Period: '60'
Dimensions:
- Name: AutoScalingGroupName
Value: 'SpotASG'
ComparisonOperator: LessThanThreshold
MetricName: 'GroupInServiceInstances'

ScaleUpPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref OnDemandASG
AdjustmentType: 'ChangeInCapacity'
Cooldown: '30'
PolicyType: 'SimpleScaling'
ScalingAdjustment: 1

ScaleDownOnDemandAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
EvaluationPeriods: '1'
Statistic: SampleCount
Threshold: '2'
AlarmDescription: Alarm if # spot instances more than 2
AlarmActions:
- Ref: ScaleDownPolicy
Namespace: AWS/AutoScaling
Period: '60'
Dimensions:
- Name: AutoScalingGroupName
Value: 'SpotASG'
ComparisonOperator: GreaterThanThreshold
MetricName: 'GroupInServiceInstances'

ScaleDownPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref OnDemandASG
AdjustmentType: 'ChangeInCapacity'
Cooldown: '30'
PolicyType: 'SimpleScaling'
ScalingAdjustment: -1

--

--

We develop AR & Serverless with React.js and Vue.js | Fullstack AWS / AWS Sumerian AR. Contact us miki@cloudsail.io

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Miki Graf

Miki Graf

Serverless Ninja | CEO @Cloudsailio 👨🏽‍💻 | TS, React Native, CDK/Amplify🚀 | 📚 Teaching programming on @Udemy