Amazon EKSでEC2料金を節約する

この記事は [NTTコムウェア Advent Calender 2021](https://qiita.com/advent-calendar/2021/nttcomware) 12日目の記事です。

## はじめに
NTTコムウェアのtonomuraです。
本記事では、AWSのマネージドサービスであるAmazon EKSにおけるリソース使用料の節約を試みた内容を紹介します。

## 想定読者
・Amazon EKSをステージング環境で利用していて、利用料金に悩んでいる方
・平日日勤帯以外などの未使用時間にサーバを止めておきたい方
・コスト削減したいけど[AWS Fargate](https://aws.amazon.com/jp/fargate/)への移行に踏み切れない方

## Amazon EKSとは

[Amazon Elastic Kubernetes Service](https://aws.amazon.com/jp/eks/) の略であり、AWS上でkubernetes環境を簡易に構築・運用できるマネージド・サービスです。

似たようなサービスにAmazon ECSやAmazon EKS on Fargateなどがあり、これらはサーバを意識することなくコンテナをデプロイできるサービスになります。

対して、Amazon EKSはワーカーノードと呼ばれる、コンテナがデプロイされるサーバ(EC2インスタンス)は自分たちで管理する必要があり、従量課金で使った分だけ課金されてしまいます。


## 本記事の内容

そこで、主にステージング環境用途で使用する場合において、少しでも利用料金を節約するために、使っていない間にEC2インスタンスを落としておく方法を紹介します。

また、EC2インスタンスの復旧までにどのくらい時間がかかるのか、kubernetesリソースの状態はどのように遷移するのか、コマンド実行結果と合わせて紹介します。


## 前提条件
・本記事で扱うアプリケーションは、コンテナ化されたWeb三層構造のサンプルアプリです
・複数のサービスを組み合わせている場合、Podの起動順序等の制約により、正しく動作しない場合があります
・EKSクラスター([eksctl](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eksctl.html)を利用して構築)や[Auto Scalingグループ](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/cluster-autoscaler.html)は予め設定済みの状態です

・使用するツールとワーカーノードの構成は以下のとおりです

```
$ aws --version
aws-cli/2.2.12 Python/3.8.8 Linux/4.14.77-70.59.amzn1.x86_64 exe/x86_64.amzn.2018 prompt/off
```
```
$ kubectl get nodes
NAME                                       STATUS   ROLES    AGE     VERSION
ip-10-**-**-**.region.compute.internal     Ready    <none>   38h   v1.20.7-eks-*****
ip-10-**-**-**.region.compute.internal     Ready    <none>   38h   v1.20.7-eks-*****
ip-10-**-**-**.region.compute.internal     Ready    <none>   38h   v1.20.7-eks-*****
ip-10-**-**-**.region.compute.internal     Ready    <none>   38h   v1.20.7-eks-*****
```
ノード名は伏せていますが、2つのノードグループで構成されており、各ノードグループに対してデプロイされるPodを振り分けています。

## 初期状態
まずはどのようなPodが立ち上がっているかを確認します。
今回の環境では、サンプルアプリ以外にもPodのログを収集する[Container Insights](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights.html) のリソースが動いています。
 
```
$ kubectl get pods -A
NAMESPACE           NAME                                      READY   STATUS    RESTARTS   AGE
amazon-cloudwatch   cloudwatch-agent-fjfdz                    1/1     Running   0          20m
amazon-cloudwatch   cloudwatch-agent-jz4wf                    1/1     Running   0          36m
amazon-cloudwatch   cloudwatch-agent-x28qt                    1/1     Running   0          27m
amazon-cloudwatch   cloudwatch-agent-z5c5j                    1/1     Running   0          27m
amazon-cloudwatch   fluentd-cloudwatch-dgt4x                  1/1     Running   0          20m
amazon-cloudwatch   fluentd-cloudwatch-gj9lf                  1/1     Running   0          27m
amazon-cloudwatch   fluentd-cloudwatch-jqtcz                  1/1     Running   0          36m
amazon-cloudwatch   fluentd-cloudwatch-xqdnk                  1/1     Running   0          27m
test-ap             test-ap-5ffc6fc4cc-29cmv                  1/1     Running   0          44d
test-ap             test-ap-5ffc6fc4cc-c4h9f                  1/1     Running   0          44d
test-ap             test-ap-5ffc6fc4cc-msrtp                  1/1     Running   0          44d
test-ap             test-ap-5ffc6fc4cc-v7cdx                  1/1     Running   0          44d
test-ap             nginx-8c489f6cf-968ps                     1/1     Running   0          29m
test-ap             nginx-8c489f6cf-cb26n                     1/1     Running   0          29m
test-ap             nginx-8c489f6cf-fzxc9                     1/1     Running   0          29m
test-ap             nginx-8c489f6cf-nsdph                     1/1     Running   0          29m
kube-system         alb-ingress-controller-68f5cb8b4b-rklj2   1/1     Running   0          29m
kube-system         aws-node-hj5gf                            1/1     Running   0          27m
kube-system         aws-node-jfqps                            1/1     Running   0          37m
kube-system         aws-node-w2sbs                            1/1     Running   0          27m
kube-system         aws-node-zxctr                            1/1     Running   0          21m
kube-system         coredns-5fd8748bdd-5jhct                  1/1     Running   0          29m
kube-system         coredns-5fd8748bdd-kkr2x                  1/1     Running   0          29m
kube-system         efs-csi-node-6kq4b                        3/3     Running   0          37m
kube-system         efs-csi-node-g9lss                        3/3     Running   0          27m
kube-system         efs-csi-node-jzbpf                        3/3     Running   0          27m
kube-system         efs-csi-node-v4p7w                        3/3     Running   0          21m
kube-system         kube-proxy-46xxl                          1/1     Running   0          21m
kube-system         kube-proxy-gbktt                          1/1     Running   0          27m
kube-system         kube-proxy-hbnff                          1/1     Running   0          37m
kube-system         kube-proxy-xq275                          1/1     Running   0          27m
```

※補足
マネージドサービスのため、`etcd`や`apiserver`などのリソースは参照できないため登場しません。


## ノードを落としてみる
早速、この状態からワーカーノードを落としてみます。
落とす方法としては、Auto Scalingグループの設定の内、「希望する容量」を変更し、ノードが強制的に0台になるようにします。

コマンドは以下のとおりです。

```
#ノードグループ1 の希望する容量を「0」にする
$ aws autoscaling set-desired-capacity --auto-scaling-group-name eksctl-test-ap-nodegroup-z1-nodeg-NodeGroup-1PXBUVII5GDBS \
  --desired-capacity 0
```
```
#ノードグループ2 の希望する容量を「0」にする
$ aws autoscaling set-desired-capacity --auto-scaling-group-name eksctl-test-ap-nodegroup-z2-nodeg-NodeGroup-1IHPM2OH5CEW9 \
  --desired-capacity 0
```

ノードの状態を確認してみます。

```
$ kubectl get nodes
No resources found in default namespace.
```
リソースが見つからない状態になりました。

この時点で、EC2インスタンスは停止状態のため、EC2料金は課金されなくなりました。

続いて、PodのSTATUS(状態)を確認してみます。

```
$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS    RESTARTS   AGE
test-ap       test-ap-5ffc6fc4cc-4lc8c                  0/1     Pending   0          2s
test-ap       test-ap-5ffc6fc4cc-d756p                  0/1     Pending   0          2s
test-ap       test-ap-5ffc6fc4cc-gjkt9                  0/1     Pending   0          2s
test-ap       test-ap-5ffc6fc4cc-ljmp7                  0/1     Pending   0          2s
test-ap       nginx-8c489f6cf-dthxl                     0/1     Pending   0          22s
test-ap       nginx-8c489f6cf-lsb8q                     0/1     Pending   0          23s
test-ap       nginx-8c489f6cf-p878k                     0/1     Pending   0          2s
test-ap       nginx-8c489f6cf-vdcg9                     0/1     Pending   0          2s
kube-system   alb-ingress-controller-68f5cb8b4b-mshkq   0/1     Pending   0          2s
kube-system   coredns-5fd8748bdd-cf584                  0/1     Pending   0          2s
kube-system   coredns-5fd8748bdd-zdzkm                  0/1     Pending   0          2s
```

一部のPodが見えなくなり、STATUS(状態)が`Pending`(保留)に変わりました。

この状態で試しにアプリにアクセスしてみます。

```
$  curl -k https://xxxx/test-ap/login/ -o /dev/null -w '%{http_code}\n' -s
503
```
503エラーとなりアクセスできなくなりました。

## ノードを復旧させる
復旧させる方法は、Auto Scalingグループの設定を同じコマンドでもとに戻すだけです。

```
#ノードグループ1 の希望する容量を「2」にする
$ aws autoscaling set-desired-capacity --auto-scaling-group-name eksctl-test-ap-nodegroup-z1-nodeg-NodeGroup-1PXBUVII5GDBS \
  --desired-capacity 2
```
```
#ノードグループ2 の希望する容量を「2」にする
$ aws autoscaling set-desired-capacity --auto-scaling-group-name eksctl-test-ap-nodegroup-z2-nodeg-NodeGroup-1IHPM2OH5CEW9 \
  --desired-capacity 2
```

ノードのSTATUS(状態)を確認します。

```
$ kubectl get nodes
No resources found in default namespace.
```
実行直後はまだノードの情報は出力されません。
しばらく`-w`オプションで待機すると、およそ2分ほどでノードの状態が確認できるようになりました。

```
$ kubectl get nodes -w
NAME                                     STATUS     ROLES    AGE   VERSION
ip-xx-xx-xx-xx.region.compute.internal   NotReady   <none>   0s    v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal   NotReady   <none>   0s    v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal   NotReady   <none>   0s    v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal   NotReady   <none>   0s    v1.20.7-eks-xxxxxx
```

この時点ではSTATUS(状態)は`NotReady`ですが、しばらくすると、

```
$ kubectl get nodes
NAME                                       STATUS   ROLES    AGE     VERSION
ip-xx-xx-xx-xx.region.compute.internal     Ready    <none>   45s   v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal     Ready    <none>   45s   v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal     Ready    <none>   45s   v1.20.7-eks-xxxxxx
ip-xx-xx-xx-xx.region.compute.internal     Ready    <none>   45s   v1.20.7-eks-xxxxxx
```
STATUS(状態)が`Ready`になりました。

その後、Podの状態を確認します。

```
$ kubectl get pods -A
NAMESPACE           NAME                                      READY   STATUS    RESTARTS   AGE
amazon-cloudwatch   cloudwatch-agent-79tfh                    1/1     Running   0          3m41s
amazon-cloudwatch   cloudwatch-agent-fqgzd                    1/1     Running   0          3m51s
amazon-cloudwatch   cloudwatch-agent-gbvbk                    1/1     Running   0          4m1s
amazon-cloudwatch   cloudwatch-agent-nprzt                    1/1     Running   0          4m11s
amazon-cloudwatch   fluentd-cloudwatch-4gfqf                  1/1     Running   0          3m51s
amazon-cloudwatch   fluentd-cloudwatch-5rxrk                  1/1     Running   0          4m11s
amazon-cloudwatch   fluentd-cloudwatch-cd52v                  1/1     Running   0          3m41s
amazon-cloudwatch   fluentd-cloudwatch-p7tx4                  1/1     Running   0          4m1s
test-ap             test-ap-5ffc6fc4cc-4lc8c                  1/1     Running   0          7m31s
test-ap             test-ap-5ffc6fc4cc-d756p                  1/1     Running   0          7m31s
test-ap             test-ap-5ffc6fc4cc-gjkt9                  1/1     Running   0          7m31s
test-ap             test-ap-5ffc6fc4cc-ljmp7                  1/1     Running   0          7m31s
test-ap             nginx-8c489f6cf-dthxl                     1/1     Running   0          7m51s
test-ap             nginx-8c489f6cf-lsb8q                     1/1     Running   0          7m52s
test-ap             nginx-8c489f6cf-p878k                     1/1     Running   0          7m31s
test-ap             nginx-8c489f6cf-vdcg9                     1/1     Running   0          7m31s
kube-system         alb-ingress-controller-68f5cb8b4b-mshkq   1/1     Running   0          7m31s
kube-system         aws-node-54975                            1/1     Running   0          4m31s
kube-system         aws-node-54gjt                            1/1     Running   0          4m31s
kube-system         aws-node-m4lwc                            1/1     Running   0          4m32s
kube-system         aws-node-xw6zn                            1/1     Running   0          4m31s
kube-system         coredns-5fd8748bdd-cf584                  1/1     Running   0          7m31s
kube-system         coredns-5fd8748bdd-zdzkm                  1/1     Running   0          7m31s
kube-system         efs-csi-node-4gvj4                        3/3     Running   0          4m32s
kube-system         efs-csi-node-5v556                        3/3     Running   0          4m31s
kube-system         efs-csi-node-8fln9                        3/3     Running   0          4m31s
kube-system         efs-csi-node-dzfqt                        3/3     Running   0          4m31s
kube-system         kube-proxy-4wdrm                          1/1     Running   0          4m31s
kube-system         kube-proxy-4xtr8                          1/1     Running   0          4m31s
kube-system         kube-proxy-zndfc                          1/1     Running   0          4m31s
kube-system         kube-proxy-zvvxs                          1/1     Running   0          4m31s
```

すべてのPodが元の`Running`に戻りました。

再びアプリにアクセスしてみます。

```
$  curl -k https://xxxx/test-ap/login/ -o /dev/null -w '%{http_code}\n' -s
200
```
無事に元どおりアクセス可能な状態に戻りました。

復旧までにかかる時間はトータルで10分程度ではありますが、すべてのノードをコマンド一つで停止し、復旧することができました。

## まとめ

Auto Scalingグループの設定を変更するだけで、Amazon EKS 上のEC2インスタンスを停止し、リソース使用料を節約できました。
一時的にPodは`Pending`になるものの、ノード復旧後も正常にアプリにアクセスすることができました。

## その他の使い方

コマンドも簡易であるため、[AWS Lambda](https://aws.amazon.com/jp/lambda/)などと組み合わせ、任意のイベントをトリガーとして活用できます。
例えば、チャットに「業務終了」と発言したタイミングでステージング環境を落としたり、定期的に自動実行させることも可能かと思います。