【コスト削減】小規模な ECS on Fargate 環境をスモールステップで約76%節約した話

# はじめに

私のチームでは、スクラム開発にRedmine(コンテナ)を利用しています。
ECS on Fargate 環境で Redmine コンテナを利用していますが、当チームはコンテナ経験豊富なメンバーがおらず、当初かなりのコストがかかっていました。  

一般的に推奨されている方法の積み重ねであるため、特に目新しさはありませんが、スモールステップで約76%のコストを削減しましたので、事例を紹介します。

## ECS on Fargate導入・コスト削減の経緯

大まかな経緯は以下の通りです。

1. 当チームのスクラム開発で、管理ツールを検討。
2. メンバーから、ECS on Fargate で組まれた Redmine のテンプレート(CDK)が展開。
3. 当チームのコンテナ運用経験が乏しかったので、学習も兼ねて Redmine のコンテナを導入することに決定。
4. 特に意識せず、ECS on Fargate 環境を構築したところ、AWS 利用料が高額化。
5. スモールステップでコスト削減を実施！

## 環境

Redmine の構築では、以下を参考にさせていただきました。

https://qiita.com/cataiiwai/items/ad0b80cd8024ff14cf87

ほぼそのままですが、コスト削減対応に伴い、以下構成となりました。  
想定の同時利用者数はかなり小規模だったため、Application Auto Scaling は実装していません。

![redmineArchitecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/686571c5-257c-bc58-b902-50d40276b7a8.png)

# スモールステップのコスト削減

2021年11月～2022年4月に、以下のステップでコスト削減を行いました。  
なお、円表示している AWS の利用料は、当時のレートでざっくりドル円変換していますのでご留意ください。

## Step.1 高額な利用料を検知＆目標額を設定

Redmine 環境を初期構成でデプロイしたところ、[AWS Cost Explorer](https://docs.aws.amazon.com/ja_jp/cost-management/latest/userguide/ce-what-is.html)で日次利用料金が以下になっていました。  

![beforeRunningCost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/e10a11c6-1320-79e7-1f78-b0fe62f6c148.jpeg)

2021年11月末の為替レートは約 114 (TTB)でしたので、ドル円変換すると月額41,000円オーバーのランニングコストでした。学習代も兼ねているとはいえ、これでは高すぎるので、コスト削減対応を検討しました。  

どこまでコストを抑えるのかは悩ましいところでしたが、Redmine のホスティングサービス[^1]で提供されているスタンダードプラン(税込8,800円)を目指すことにしました。

[^1]: https://hosting.redmine.jp/service/

## Step.2 スケールダウン

ECSタスクと Auroraインスタンスの CPU・Memory に余裕があったため、以下の通りスケールダウンしました。

- ECSタスク定義(Fargate)
  - CPU   ： 1024 → 256
  - Memory： 2048 → 512
- Auroraインスタンスタイプ
  - db.t3.medium → db.t2.small

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/0fb533e3-1f2b-8668-2be6-e1bcc68e9283.jpeg" width="500">

スケールダウンだけで **32%** ほどのコストを削減できました。  
```月額41,000円ペース→月額28,000円ペース```

## Step.3 スケールイン

Redmine は開発用途であり、1日以上のダウンタイムを許容できるため、Multi AZ から Single AZ 利用の構成に切り替えました。サブネットは料金に影響しないため、Multi AZ 構成のままとしています。

各インスタンスは、以下の通りスケールインしました。（※ECSタスク数は元々1でした。）

- Aurora のインスタンス数:2→1
- NAT Gateway のインスタンス数:2→1

![scaleIn.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/e435243f-6714-92b8-2e59-f14023256009.jpeg)

スケールダウンとスケールインの両アプローチ後も、Redmine を問題なく利用できていたため、次のアプローチに着手しました。

## Step.4 ECSタスクの休日・夜間停止ジョブ追加

Redmine の利用時間を平日の7～22時とするように、チームで決めました。  
利用時間外は、ECSタスク数を0にする Lambda 関数を作成しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/ff9ecc71-df00-9c74-54cd-065ea2635dbd.jpeg" width="450">

この時点で、更に **21%** 程のコストを削減できました。  
```月額28,000円ペース→月額22,000円ペース```

まだまだ高いので、次のアプローチに着手しました。

## Step.5 Auroraクラスターの休日・夜間停止ジョブ追加

ECSタスクと同様に、Aurora クラスターにも休日・夜間停止ジョブを追加しました。  
ECSタスクと違って、SSM ドキュメントが用意されていたため、AWS-StartStopAuroraCluster で実装しました。  

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/dbc268c3-bfb8-5c92-7f50-c26932788364.jpeg" width="250">

やはり、DB の起動時間短縮は大きく、更に **36%** ほどのコストを削減できました。  
```月額22,000円ペース→月額14,000円ペース```

Auroraクラスターの休日・夜間停止ジョブを追加して1年くらい経ちますが、キャパシティ不足で起動できないなどの問題は発生していません。

## Step.6 開発用のNAT Gatewayを削除

Redmine 環境の構築・デプロイをするため、別 VPC で Cloud9 と NAT Gateway を利用していました。Cloud9 の利用ルールを以下の通り定めていたため、別 VPC の NAT Gateway も削除することにしました。

- 開発用途の Cloud9 は直接パブリックサブネットに置いてよい
- Cloud9 の Security Group インバウンドルールは全拒否
- Cloud9 には、Session Manager 経由でアクセス必須

[Step.3](#step3-スケールイン)でも NAT Gateway を削減しましたが、やはりネットワークリソースのコストは大きく、更に **29%** ほどのコストを削減できました。  
```月額14,000円ペース→月額10,000円ペース```

目標としていた8,800円には僅かに届きませんでしたが、約 **76%** の大幅コストカットを実現できたため、一旦コスト削減対応をストップすることにしました。

<img src="https://4.bp.blogspot.com/--X_ESil6eVU/WfVS-DVXgcI/AAAAAAABHyw/zqXzqBYfx9cF9wqYVaWLoQunHzSMlA4rQCLcBGAs/s800/cost_cut_sakugen_samurai.png" width="200">

## Step.7 Fargateのメモリ不足を検知＆検討

[Step.6](#step6-開発用のnat-gatewayを削除) から4ヶ月ほど経過し、チケット駆動開発の意識が高まっていたため、以前よりもRedmine の利用時間が増加しました。  
その結果、Fargate のメモリ不足が顕著になり、画面遷移がかなり低速でした。  

CloudWatch メトリクスを確認すると、メモリ利用率の90%超えが頻発し、CPU も瞬間的に上がっていました。  

![beforeMetrics.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/6b830dad-23c0-b232-c788-53ef07fc3930.jpeg)

最大利用者数は当初予定から変わっていないため、Fargateをスケールアップすることにしました。単純にスケールアップしてしまうと、せっかく削減したコストがまた増加してしまうため、割引率の大きい [Fargate Spot](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/fargate-capacity-providers.html) の導入を検討しました。

元々、[Step.2](#step2-スケールダウン)の段階でも Fargate Spot の利用を考えていましたが、CDK のハイレベルコンストラクト「[ApplicationLoadBalancedFargateService](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService.html)」(L3)で構築しており、当時は Fargate Spot を想定した Props がありませんでした[^2]。CFnテンプレートを[Override](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.CfnService.html#addwbroverridepath-value)することも考えましたが、そこまで費用がかかっていない Fargate のコストを下げる必要がなく、CDK ソースの可読性なども考慮して断念していました。

チームで検討した結果、一旦手動で Fargate Spot を暫定適用し、後追いで CDK ソースを修正することにしました。

[^2]: CDK v2.59.0では、[ApplicationLoadBalancedFargateService](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService.html)に[capacityProviderStrategies](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService.html#capacityproviderstrategies)のpropsが存在するため、簡単に Fargate Spot を適用できます。

## Step.8 スケールアップ＆Fargate Spotの適用

ECSタスク定義を、以下の通りスケールアップし、Fargate Spotを適用しました。

- ECSタスク定義(Fargate)
  - CPU   ： 256 → 512
  - Memory： 512 → 1024

上記対応後、CPU・Memory の使用率が以下の通り50%未満に収まるようになりました。

![afterMetrics.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/f9b4d229-b652-0903-079b-296fff1fbcee.jpeg)
  
CPU・Memory の割当を倍にしましたが、ECS のコストがあがることはなく、むしろ`0.24 $/日 → 0.15 $/日`に削減できました。当初為替レートの114(TTB)で単純計算すると、Step.6から月額約300円の節約になりました[^3]。(初期比、約76%削減)  
```月額10,000円ペース→月額9,700円ペース```

[^3]: Step.6から期間が空いており、本来はデータ量増加などに係るコストや為替レートの変化も考慮すべきです。しかし、ここではFargate Spot適用に伴う変化を明示するため、その他のコスト要因は未考慮にしています。

AWS のリソースに空きがない場合、Fargate Spot を起動できないリスクはあるものの、小規模コンテナのためか特に利用できなかったことはありませんでした。

# おわりに

ECS on Fargate 環境のコスト削減対応について、スモールステップで取り組んだ事例を紹介しました。スクラム開発の管理ツールを変更しようという話もでているので、個人的にはまとめを掲載できて良かったです。  

小規模な事例でしたが、お役に立てれば幸いです。