きっかけ
1年前くらいにクローラーをEC2からECS + Fargate構成に移行し、運用してきたのですが、少々Fargateがお高いなぁと思い始めてきまして、お盆で時間があるということもあり、Spot Fleetを使って最安なバッチ実行環境の構築に挑戦してみようと思ったのがことの発端です。
以前作ったクローラーは下記記事のやつです。
https://qiita.com/uramotot/items/5f3fe91f9b78ff6ea450
前提
クローラーはPythonで記述されており、以下のライブラリを使っていました。
Architecture
Before
以前はこのような構成で、ECS Schedulerを使ってCrawler用のECS Taskを日時で呼び出し、ECS上にFargateを使って実行されるようになっていました。
Scraping処理の方は、RabbitMQを使って非同期で処理が行えるようになっており、こちらはECS Serviceで常時立っています。
(ECS TaskとECS Serviceについては、以前の記事でも説明しておりますので、割愛します。)
スクレイピング結果は処理が終わり次第、RabbitMQのWorkerがRDSに格納していました。
After
大きな変更が3つあります。
まず、1つ目に、Scraping処理を非同期にしていた部分を取りやめて、Scraping処理をscrapyのitem pipelineで実行することにしました。
理由としては、一年近く運用してみて、Retryすることが一度もなかったことと、要件的にクローリングの失敗した際にリトライが必須ではなく、次の日の実行を待って良いくらい緩いものだったためです。
また、冪等な書き方をしておけば、仮にクローリングに失敗したとしても、あまり問題にならなそうだなと思ったためです。
ということで、Scraping部分がなくなり、シンプルな構成になったことで、必要なリソースをかなり減らす事ができました。
2つ目は、スクレイピングの処理結果を格納する部分もRDSからDynamoDBに移行しました。
こちらの移行理由としては、スクレイピング結果は対象ドメインによってデータ構造が結構変えたいことも多く、ドキュメント型の性質を持っているDynamoDBを選択しました。
また、DynamoDBはReadとWriteの秒間リクエスト数で課金されるため、クローリングの間隔を最適に設定しておけば、かなり安く使うことができます。
(あと、ストレージは最初の25GBは無料で使えるという太っ腹サービスでもあります。)
3つ目は、ECS Taskの起動タイプをFargateからEC2に変更し、ECS Cluster InstanceにEC2 Spot Fleetを用いているところです。
こちらの理由としては、FargateはTaskの実行時にしか課金されないため、バッチ実行時には比較的安く済ませることができるのですが、オンデマンドのEC2インスタンスよりも少しだけ値段が高いです。
また、Spot Instanceのようなものはないので、ある程度vCPUやMemoryを確保するとまぁまぁ高くなってしまいます。
Spot Fleetは必要なvCPUやMemoryを設定するだけで、Spot Instanceを取得してくれる便利サービスです。
Spot Instanceは、AWS Cloud内の使用されていない EC2キャパシティーを活用でき、最大90%の割引を受けられるポテンシャルを持っています。
Spot Instanceを使うことで、vCPUやMemoryをある程度確保したとしても安く済ませることができます。
また、Application Auto Scalingを使うことで、Spot Instanceの数を増減させるScheduleを設定することが可能になります。
これを用いることで、バッチの実行時間にのみSpot Instanceが起動するようになっています。
どれだけ安くなったのか
ということで、3つの大きな変更を加えたわけですが、具体的にどれほど安くなったのでしょうか?
今回はvCPU: 2, Memory: 4GB、その他サービスは最小構成で、月額を計算します。
クローラーの一回の実行時間は3時間とします。
Before
Fargateの料金については現時点では、以下の表のとおりです。
料金 | |
---|---|
1vCPU | 0.05056 [USD/h] |
1GB Memory | 0.00553 [USD/h] |
以下がBefore構成の料金表です。
Service | Capacity | hour | Price per month |
---|---|---|---|
Fargate (Crawler) | vCPU: 1, Memory: 2GB | 90 [h] | 5.5458 [USD] |
Fargate (Scraping) | vCPU: 1, Memory: 2GB | 750 [h] | 46.215 [USD] |
RDS | db.t2.micro | 750 [h] | 21 [USD] |
合計すると、72.7608[USD/month]
なので、日本円で月々8000円くらいになりました。
やはり、Fargateは一時的に建てる場合はとても安いですが、ECS Serviceのように常時起動している場合には高くつきますね。
After
Spot Fleetの割引料は5割にしています。
Spot Instanceは AM 1:00~7:00
の6時間のみ立っているとして計算します。
t3a.mediumの料金は、0.049[USD/h]
です。
t3a.mediumは vCPU: 2, Memory: 4GB
のキャパシティを持っています。
https://aws.amazon.com/ec2/pricing/on-demand/
DynamoDBの料金は以下です。
料金 | |
---|---|
Write Capacity Unit | 0.000742 [USD/month] |
Read Capacity Unit | 0.0001484 [USD/month] |
以下がAfter構成の料金表です。
Service | Capacity | hour | Price per month |
---|---|---|---|
EC2 (Spot) | t3a.medium | 180 [h] | 4.41 [USD] |
DynamoDB | Write: 5, Read: 5 | 750 [h] | 0.004452 [USD] |
驚愕の結果となりました!(わざとらしい)
DynamoDBの料金はProvisionedとオンデマンドの2つの請求オプションがあります。
今回はProvisionedの方式を取りました。理由としては、うまく使えばそちらのほうが安いからです。
EC2 Spot Instanceは6時間のみしか立たない + 5割り引きで、 4.41 [USD/month]
まで落とすことができました!
Before の Fargate (Crawler)
と比較しても、リソースが2倍にも関わらず月々半額以下の値段で利用することができる計算になります。
まとめ
クローラーを ECS + Spot Fleet
の構成に移行してみたら、月々8000円だったものが500円くらいで動かせる様になりました。
Spot Instanceが取得できなかった場合の処理や、厳密なECS Task実行時間にのみSpot Instanceを要求するなどはやっておりませんが、ランニングコストと構築コストを天秤にかけたらちょうど良い感じに収まったと思います。
こういう節約のための施策って、めんどくさかったり、学習コストがかかったり、プログラムを冪等にする必要があるなど、構成が複雑になったりと、色々ハードルがあり、取り掛かるのが億劫だったりします。
しかし、この記事のクローラーは個人利用のため、かなり小さい構成になっていますが、ビジネス利用を考えた場合、この割引率はかなり大きいと思います。
おまけ
ECS + Spot Fleet
でバッチを動かすCloudFormationをおまけとしてつけておきます。
---
AWSTemplateFormatVersion: 2010-09-09
Mappings:
RegionMap:
ap-northeast-1:
ImageId: ami-04a735b489d2a0320 # ECS for AmazonLinux2
Parameters:
InstanceType:
Type: String
DesiredCapacity:
Type: String
PublicSubnet:
Type: AWS::EC2::Subnet::Id
SecurityGroups:
Type: List<AWS::EC2::SecurityGroup::Id>
Cluster:
Type: String
TaskDefinition:
Type: String
StartCron:
Type: String
StopCron:
Type: String
StartTaskCron:
Type: String
Resources:
ClusterInstance:
Type: AWS::EC2::SpotFleet
Properties:
SpotFleetRequestConfigData:
IamFleetRole: !GetAtt SpotFleetRole.Arn
LaunchSpecifications:
- BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
DeleteOnTermination: true
VolumeSize: 30
VolumeType: gp2
EbsOptimized: true
IamInstanceProfile:
Arn: !GetAtt InstanceProfile.Arn
ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', ImageId]
InstanceType: !Ref InstanceType
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeleteOnTermination: true
DeviceIndex: 0
SubnetId: !Ref PublicSubnet
Groups: !Ref SecurityGroups
Monitoring:
Enabled: true
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo ECS_CLUSTER=${Cluster} >> /etc/ecs/ecs.config
echo ECS_BACKEND_HOST >> /etc/ecs/ecs.config
yum install -y https://amazon-ssm-${AWS::Region}.s3.amazonaws.com/latest/linux_amd64/amazon-ssm-agent.rpm
TargetCapacity: 0
TerminateInstancesWithExpiration: true
FleetSchedule:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 0
MinCapacity: 0
ResourceId: !Sub 'spot-fleet-request/${ClusterInstance}'
RoleARN: !GetAtt SpotFleetAutoScaleRole.Arn
ScalableDimension: ec2:spot-fleet-request:TargetCapacity
ScheduledActions:
- Schedule: !Ref StartCron
ScheduledActionName: StartSchedule
ScalableTargetAction:
MaxCapacity: !Ref DesiredCapacity
MinCapacity: !Ref DesiredCapacity
- Schedule: !Ref StopCron
ScheduledActionName: StopSchedule
ScalableTargetAction:
MaxCapacity: 0
MinCapacity: 0
ServiceNamespace: ec2
# === Events ===
Event:
Type: AWS::Events::Rule
Properties:
Name: Crawler
Description: Execute crawling.
ScheduleExpression: !Ref StartTaskCron
State: ENABLED
Targets:
- Id: crawler
Arn: !Ref Cluster
EcsParameters:
TaskDefinitionArn: !Ref TaskDefinition
TaskCount: 1
RoleArn: !GetAtt EventExecutionRole.Arn
# === Role ===
SpotFleetAutoScaleRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- application-autoscaling.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetAutoscaleRole
SpotFleetRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- spotfleet.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetTaggingRole
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: '/'
Roles:
- !Ref InstanceProfileRole
InstanceProfileRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: '/'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
- arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
EventExecutionRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole