はじめに
2023年の6月から新卒・文系・未経験の状態からエンジニアになったエイスケです。(もうこの肩書を使えなくなる時期が近付いている。。)
エンジニア研修の中でAWSサービスに興味を持ち、そこからCLFやSAAを取ったりして業務でもAWSを主に触るようになりました。
合格体験記をどちらとも書いているのぜひ!
SAAを取ったときにコスト削減のアーキテクトについての問題を解くことも多く、実際これをいつか業務の中でやってみたいなと思っていた矢先、新人教育の一環で業務改善かコスト削減をやってみようという話になり、コスト削減を選び取り組んでいきました。
この記事では新卒エンジニアが少ない知識の中でコスト削減に向き合っていく様が見られると思います。ぜひ最後まで読んでください!
時間がない人向け
APN1-NatGateway-Hours(NAT Gatewayの時間当たり料金)のコストを64%減、1環境当たり695ドル削減させました!
方法としては、CloudformationのCondition属性を使い、開発環境の停止時に削除し、起動時に作成する流れを自動化することで必要な時にだけNAT Gatewayを配置することで実現しました。
Cloudformationの管理下から外さないことや排他制御の考慮、本番環境への影響等については、本編を読んでいただけると嬉しいです!
最初の方は方法論ではなく今回実装した方法に行きつくまでの過程が書かれているので、そこには興味ないよという方は、
【ここからでOK】どのようにNAT Gatewayを必要な時だけ配置することを実現したのか
から読んでいただくといいかなと思います!
何のコストを削減したのか
タイトルの通りNAT Gatewayの時間当たりにかかる開発環境のコストです。
NAT Gatewayに目をつけるまで
TrustedAdviser
何のコスト削減をしていくか探していく中で、私が最初に見たものはTrustedAdviserです。
TrustedAdviserはAWS側が自動的にコスト削減やセキュリティ強化の提案をしてくれるものです。
これを見る限りでは、使用率の低いEC2インスタンスなどを環境停止するだとか、インスタンスタイプを見直そうだとかをアドバイスしてくれます。
もちろんここに手を付けるのも考えましたが、私のチームでは開発環境のEC2等は夜に環境停止するようになっています。そのため、そのイベントが何らかの理由で実行できなかった場合に通知するなどしかなく、あれば助かるもの止まりでした。
CostExploler
2つ目に見たものは、CostExplolerです。
これはどのサービスにどれくらいのお金がかかっているのかを見ることができます。
(AWS公式サイトより)
この画像のようにコストがかかっている日も、どのサービスのどの料金形態にコストがかかっているかもすぐに分かります。
わたしたちのチームで、1番コストがかかっていたサービスは、
EC2その他
その他ってなになに~?という感じでしたが、調べてみると
「グループ化の条件」の「ディメンション」を「使用タイプ」にすることで確認できるそうです。
早速中身を見てみると、EC2その他の中でも1番コストがかかっていたのが、、、
APN1-NatGateway-Hours
NAT Gatewayでした。え、あのSAAでもめちゃくちゃ出てきたNAT Gatewayって高いの?
調べてみると、1時間当たりにかかっている金額は0.062USD。日本円になおすと9.3円(1ドル=150円)。
めちゃくちゃ安くないか?さすがAWS!規模の経済万歳!
でもこれ、1時間当たりですからね。。。
冗長性のためにNATGatewayを2つのパブリックサブネットにそれぞれ1台ずつ置いているので、24時間になれば446.4円。1か月になれば13392円。1年になれば160704円です。
これが1環境当たりにかかってくるお金になります。私のチームでは開発で使っている環境数は8つでしたからNAT Gatewayに毎月107136円、毎年128万ほどNAT Gatewayの配置に使っていることになります。
塵も積もれば山となる。たとえ1時間9円だとしてもそれが24時間365日積もれば、16万円になるんです。びっくりですよね。
この1番コストの割合を占めるNATGatewayという山を削っていっちゃおうというのが私の考えです。
労力を割くなら労力に対して得られるメリットが大きいものに取り掛かったほうがいいですからね!
どうやってコストを削減したのか
結論:既存のEC2等の環境停止ジョブ内でNAT Gatewayを削除し、環境起動ジョブ内でNAT Gatewayを作成するようにした
そもそもなぜNAT Gatewayのコストは高くなるのか
これは上でも述べた、塵も積もれば山になる。ですね。
まず、NAT Gatewayの課金形態は2つあります。
NAT ゲートウェイの時間単位料金とNAT ゲートウェイのデータ処理料金です。
時間単位は1時間当たりにかかっている金額は0.062USD
データ処理料金は1 GB のデータが NAT ゲートウェイを経由すると0.062USD
という料金構成になっています。データ処理の料金は処理量が多くなかったため、今回は対策をする必要はなかった。もしデータ処理量が多い場合は下のリンクのようにNAT Gatewayを通すべきデータなのかを検討してみるといいと思います。
で、
問題は時間単位でかかってくるコストです。
開発環境ではコスト削減のためにEC2やECSタスクを時間になれば自動的に停止するみたいなことをやっている人たちは多いと思います。
しかし、これを実装したときにNAT Gatewayも停止(削除)できるようになっていたら問題は起こらないのですが、その機能はないです。
つまり、環境は停止している状態で全く使っていないのにもかかわらず、NAT Gatewayは存在しているだけでお金がかかり続けるのです。
深夜も土日祝も使ってないのに課金されるなんて厄介!どうしようか、、、
そうだ。使ってないときは削除し使うときだけ配置しよう。
非常に単純な考えですが、使ってないときは消してしまって使うときに配置してしまおう。と考えました。
実際、このNAT Gatewayのコスト削減に取り組んできた偉人たちは調べてみると出てきました。
大体2つの記事で書いてあることはNATGatewayを削除するLambda関数を作成し、EventBridgeで時間になったらLambda関数を実行するといったものでした。
じゃあこの記事サンプルコードもあるしこれ上手く使えば簡単にできるのでは?!
と思いましたが、ちゃんと問題がありました。
直面した問題その①:Cloudformationの管理外になる。
まいった。
私のチームではCloudformationでリソースを管理しています。IaCというやつですね。
もしLambdaで削除して作成したらCloudformationの管理から外れてしまい、非常に管理のコストがかかってしまいます。せっかく金銭的なコストを減らしたのにそれ以上の運用コストをかけてしまっては元も子もないですよね。
では、Cloudformationの管理から外さずにリソースの削除作成ができるものはないのだろうか?
そこで私は思いつきました。
NAT Gatewayを作成している既存のスタックから作成する部分だけを切り出して、
そのスタックを削除したり作成したりすればCloudformation管理下のまま削除作成ができるのでは!!!
Cloudformationのスタックを削除したらそのスタックで作成されたものは削除されるという機能を利用してしまおう。という魂胆です。
この一連の流れを環境停止ジョブと環境起動ジョブと同じタイミングで実行されるように実装できれば、無事にコスト削減できそうですね。
しかしこの方法でも直面する問題がありました。
直面した問題その②: 本番環境へのデプロイがすごく面倒。
Cloudformationで管理されているとはいえ、作成されるスタックが異なってしまうので、本番環境でも1度削除して作成しなければなりませんでした。
NAT GatewayはECSタスク等で使われていたので本番環境起動中に無くしてしまうことはダウンタイムに繋がってしまいます。
なので、ブルーグリーンデプロイを行うことも視野に入れましたが、その時がちょうどたまたま1年で1番使われる時期でデプロイの時期を遅らせるか他の方法を考えるかしかありませんでした。
他の方法で言うと、一時的にNAT Gatewayを手動で1台増やし、そのNAT Gatewayにルーティングを切り替えてその間にデプロイを行いルーティングを新しいNAT Gatewayに戻すという方法もあり、ブルーグリーンデプロイよりかはリスクが小さめでデプロイできそうだと思いました。
こういう本番環境へのデプロイ方法とそのあとの運用方法まで考えるように先輩方からアドバイスをいただいていたので、たくさん考えて相談しながら徐々に形が見えてきました。
形が見えてくれば見えてくるほど、心の中で
難しそ~~~、、、
という感情が湧いてきました。スタック切り出さずに作成したり削除したりする方法はないのだろうか、、、そんなことを思いながらQiitaを見ていると、
お?Cloudformation管理下にあるリソースをたまに削除したり復活したりさせる方法?めっちゃそれっぽい!
記事の中に書いてあることは、
NATゲートウェイとそのセキュリティグループ、NATゲートウェイへのルートを作成するかどうかをCreateNatGatewayのパラメータで指定し、その値を各リソースのConditionに与えることで作成要否を判断する。
という、CloudformationのConditionテンプレート内でリソースの作成の要否を判断できるCondition属性を使うことで、自由に作成削除を行うことができる。というものでした。
このとき私の頭の中では、
こんな都合のいいものある??いやこれCloudformationで管理されてるリソースすべてにConditionをつけて管理しないといけないんだ!大変!無理!
と逆の自己都合解釈をしてしまい、Qiitaの徘徊に戻ってしまいました。
その後、先輩に相談していたときこの記事の話をしたところ、
『お、これ使えそうだね。』 と。
あれそういえばこれ、
確かにすごい初めの方に相談したときにConditionがどうとか言ってたような言ってなかったような。。
というかすかな記憶が掘り起こされました。
先輩のひとこと一言にはヒントがちゃんと隠されてるので、皆さん先輩の言うことは0から100まで聞き逃さず理解しておきましょう。
よし。Conditionを使う方法を考えてみよう。
【ここからでOK】どのようにNAT Gatewayを必要な時だけ配置することを実現したのか
ここまでをまとめると、
今回のNAT Gatewayを必要な時だけ配置することを実現するには主に以下の課題があります。
1. Cloudformationの管理下から外すことなく、NAT Gatewayを削除したり作成したりしないといけない
2. 本番環境にはデプロイ時を含め影響を与えないようにする
3. 新卒未経験の自分でも実装できないといけない
この課題に対してCondition属性を扱うことはベストな選択でした。
改めて、Conditionについて解説すると、
条件を作成し、それをリソースまたは出力に関連付けることで、条件が true の場合にのみ AWS CloudFormation がリソースまたは出力を作成するようにできます
この条件がtrueの場合のみリソースを作成することができるというものですね。
つまり条件がfalseの場合にリソースは作成されない状態にする。つまり削除されるということです。
Conditionを使うと何がいいのか
Conditionを使うことで、上で記載した課題が解消されました。
-
Cloudformationの管理下から外すことなく、NAT Gatewayを削除したり作成したりしないといけない
→スタックを切り出すこともLambdaで削除作成することがないので、Cloudformation管理下から外れることはありません。 -
本番環境にはデプロイ時を含め影響を与えないようにする
→Conditionをtrue以外にしない限りは既存のNAT Gatewayを削除することはなく、デプロイも通常時の適用で成り立つようになります。 -
新卒未経験の自分でも実装できないといけない
→実際に実装してみてCloudformationテンプレートの書き方が厄介なくらいでした。この方法に行きつき、設計書を書くまでの方が難しかったなと思います。
これに加え、既存のCloudformationテンプレート内の修正になるので、パラメーターの受け渡しなどをほとんど考えなくてよくなったことは大きな利点としてあると思います。
アーキテクチャ図
設計書を作成する際に作成したものを修正したものになっているので、若干分かりにくいかと思いますが、
アーキテクチャの内容として、
①NATゲートウェイとNATゲートウェイへのルートを作成するかどうかをCreateNatGatewayのパラメータで指定し、その値を各リソースのConditionに与えることで作成要否を判断する。
②Codebuildの環境起動ジョブの中で環境を起動する前に、CreateNatGatewayにtrueを入れるようにCloudformationのスタックを更新する。
③Codebuildの環境起動ジョブの中で環境を停止した後に、CreateNatGatewayにfalseを入れるようにCloudformationのスタックを更新する。
といった構成です。
こうすることで、今回の実装のほとんどの部分が完成しました。
実際に実装してみる
Conditionの実装に関しては、こちらの記事を参考にNAT Gatewayを作成しているスタックを修正してください。
ざっくりと書き出すとこんな感じ↓
AWSTemplateFormatVersion: "2010-09-09"
Description: "Temporary Resource Test"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Configuration"
Parameters:
- CreateNatGateway
- ClientKeyName
Parameters:
CreateNatGateway:
Type: String
Default: "true"
AllowedValues:
- "true"
- "false"
Conditions:
CreateNatGatewayCondition:
!Equals [!Ref CreateNatGateway, "true"]
Resources:
NatGatewayElasticIp:
Type: "AWS::EC2::EIP"
Condition: CreateNatGatewayCondition
Properties:
Domain: "vpc"
次に
環境起動前にスタックの更新する部分をコードで表すとこんな感じ↓
aws cloudformation update-stack --stack-name xxxxxxxxxx \
--use-previous-template \
--parameters \
ParameterKey=Stage,UsePreviousValue=true \
ParameterKey=Environment,UsePreviousValue=true \
ParameterKey=CreateNatGateway,ParameterValue=true \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
aws cloudformation wait stack-update-complete --stack-name xxxxxxxxxx
aws cloudformation update-stack
を使うことでスタック名で指定したものを更新することができます。
use-previous-template
で現在のテンプレートの仕様ができ、
psrameters
で各パラメータを設定することができます。
ここでCreateNatGateway
をtrue
にしています。
他のパラメーターは必須で設定されているものがあったりするので、
その時はUsePreviousValue=true
を使うことで前回入力された値を使うといった設定をしておきましょう。
また、aws cloudformation wait stack-update-complete
をつけておくことで更新が終わるまで待っといてくれます。こうすることでNAT Gatewayができるまで環境が起動されることはないです。
排他制御
NAT Gatewayを作成削除するスタックを更新するのは、環境起動と環境停止に加えて環境をデプロイするCodebuildジョブもあります。
開発をする中では、同時に実行されてしまい、あとから更新した方が失敗してしまう可能性も考慮しないといけないので、そこへの対策も挙げていきます。
更新前にスタックの更新を待つ
スタックの更新をする前に、
aws cloudformation wait stack-update-complete
を実行することで更新が被る前にその更新が終わるまで待つことで、スタックの更新の被りを避けることが可能になりました。
しかし、これには1つ難点がありました。
スタックの状態がUPDATE_COMPLETE以外の場合に待ち続けてしまい、更新が行われなくなってしまいます。。。
特に新しい開発環境を構築した場合スタックの状態はCREATE_COMPLETEとなり、一度手動で更新しないと環境停止ジョブで更新がされずいつまでたってもNAT Gatewayが削除されないみたいなことが起こります。
更新が失敗したときに少し待ってリトライすることを繰り返す
こちらを最終的には採用しました。
- |
max_retry_count=10
retry_interval=30
retry_count=0
while true; do
aws cloudformation update-stack --stack-name xxxxxxxxxx \
--use-previous-template \
--parameters \
ParameterKey=Stage,UsePreviousValue=true \
ParameterKey=Environment,UsePreviousValue=true \
ParameterKey=CreateNatGateway,ParameterValue=true \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
if [ $? -eq 0 ]; then
aws cloudformation wait stack-update-complete --stack-name xxxxxxxxxx
break
fi
retry_count=$((retry_count + 1))
if [ $retry_count -eq $max_retry_count ]; then
echo "max attempt exceeded."
exit 1
fi
sleep $retry_interval
done
スタックを更新するときに失敗した場合30秒待って、もう一度更新する。といった処理を最大10回するようにします。
このようにすることで、スタック更新が被ったとしてもNAT Gatewayを作成削除ができる確率がぐんと高まります。
これを既存の環境起動ジョブと環境停止ジョブに導入することで、必要な時にNAT Gatewayが作成され、必要でなくなったら削除されるといった流れができました。
テストも行い、ちゃんとNAT Gatewayが削除され、変更してはいけないパラメータを保持したままNAT Gatewayの削除を環境停止時に、作成を起動時に行うことができるようになりました。
どのくらい削減されたのか
結局色々やってコスト削減としてはそんなに、、みたいな感じだとやった意味がないですね。
結論どれくらい削減されたかというと
64%以上
つまり最低64%の削減ができたわけです。
1環境当たり年で104279円(695ドル)削減です。これが私のチームでは8環境あったので83万円(5560ドル)ほどコスト削減が実現できたわけですね。
ヤッタ!
なぜ64%以上といっているのかというと、
今回の場合平日の朝10時から夜10時まで起動した場合を計算したので、
実際は環境起動しない日もあるし、午後の3時間だけ起動しているみたいな日もあるし、祝日もあります。
となるとこの64%は最低値となり実際は70%ほど削減できていると思います。
このコスト削減に取り組んだ際に、8環境のうちいらない環境を消す機会にもなったので、
正確な数字は分からないままとなってしまいましたが、より大きいコスト削減を実現できているのでこういった感じのまとめ方になってしまいました。(なのでタイトルは推定です笑)
是非皆さんもNAT Gateway見直してみてはいかかでしょうか!!
新卒エンジニアがコストに向き合って感じたこと
最後に1か月強の間、コストに向き合ってみて感じたことをつづっていこうかなと思います。
① 1ドル増やすのは簡単だが、1ドル下げるのは難しい。
AWSのサービスは従量課金制のものが多く、どれも気軽に始められるのは素晴らしいですが、それが積もればそこそこの額になるというのを感じました。
その額になったときにどう下げるかを考えると、CostExploerを見たり、TrustedAdviserをみたり、使ってないリソースはないか見たり、、、
それでこのリソースは消せそう!と思ってもチームに確認を取ったり、本当に要らないか調べたり、それでいて減らせるコストに見合ってなかったらやる意味あるのか?と思ってしまいます。
そういったことにならないように日ごろから一時的に使ったリソースは削除するところまでがセットにする意識を持ち続けることがコスト削減の1歩目なんだろうなと感じました。
②新人だからこそお金に向き合うことで成果が見えやすい。
正直、未経験でエンジニアになった新卒は勉強することばっかりで、何の役にも立てていないんじゃないかと思うことは少しはあると思います。
そのようなときに、コスト削減に取り組むことで自分の働きの数値が削減された金額で見れるようになると、単純にうれしい気持ちになります。
今回私の場合は695ドル×開発環境数分の削減ができ、月にもらえる金額くらいは働けたかなと満足できました。
③コストを減らす手段にコストをかけてみる。
今回、NAT Gatewayのコストを削減するために、Lambdaを使うか、スタックを切り出してそのスタックを削除作成するか、みたいな感じでいろいろな手段を考える時間を取りました。
もちろんそこまで考えないと、チームとしても任せるのは不安というのはあったと思うんですが、この考える期間を多くとることで様々な視点からの課題点は見えてこなかったし、それを解決できる最適な方法も見つけられなかったなと思います。
コスト削減はどの会社もエンジニアも抱えている問題で検索してみればいろいろな方法が出てくるので、一つ一つ吟味する時間を取ることで新人の私でも実装することができました。
おわりに
今回はNAT Gatewayのコスト削減に取り組んだ新卒エンジニアの備忘録を記しました。
長ったらしくて読みにくかったと思いますが、
1年前まではエンジニアとして働くことなんて1ミリも考えていなかった大学生だった私がこういった考え方や方法でコスト削減を実現したことを残すべきだと思い長々と書いてみました。
数年後の自分がこの記事を読んだときにもっといい方法でコスト削減を実現できるようにこれからも日々精進していきたいと思います!