Amazon EKSのチュートリアルをやってみて、Kubernetesによるコンテナ管理に触れてみる

Kubernetesをローカル環境で使用したことはありましたが、複数ホストをまたいだコンテナ管理に触れてみたいということで、[Amazon EKSのチュートリアル](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started.html) をやってみました。ハマりポイントもあったので、勘所をまとめています。

#前提
- クライアント側の作業はmacOSで実施
- macOSにはAWS CLIとkubectlをインストールしていること
 - AWS CLI：1.18.10以上
 - kubectl：クラスターのマイナーバージョンとの差分が1つ以内のバージョン（[kubectlのインストールおよびセットアップ](https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/) 参照）
 
>たとえば、クライアントがv1.2であれば、v1.1、v1.2、v1.3のマスターで動作するはずです。最新バージョンのkubectlを使うことで、不測の事態を避けることができるでしょう。

# 今回のゴール
今回構築するシステムの構成図はこちらです。
![eks_schedule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/4fee13eb-f8cd-4589-4e8c-c7148ab0372d.png)
AWSリソースとKubernetesリソースを一緒に表現しているため、分かりにくいかもしれませんが、3つのAZ（サブネット）にまたがるWeb+Redisアプリケーションを構築します。EKSクラスターとは、Kubernetesの様々なリソースを管理する集合体のことを指します。

**Kubernetesリソースの基本説明**

| リソース名 | 用途                                  |
|:---------|:-------------------------------------|
| Node     | コンテナを配置するためのホスト（EC2に相当） |
| Pod      | 複数のコンテナの集合体（単体のケースもある） |
| Service  | Podの集合体への通信経路を定義          |

# EKSの利用方法
2通りの方法が存在しますが、今回はAWSマネジメントコンソールで実施しています。

- eksctlを使用
- AWSマネジメントコンソールを使用

# 作業手順
以下の作業を順番に実施していきます。
※ 詳細は [AWSマネジメントコンソールの開始方法] (https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started-console.html) を参照

## EKSサービスロールを作成する
EKSクラスターが各種AWSリソースを参照・作成するために必要なIAMロールです。
下記のテンプレートを使用して、AWS CloudFormationコンソールでスタックを作成します。

```eks-service-role.yml
---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Amazon EKS Service Role'


Resources:

  eksServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - eks.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSServicePolicy
        - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy

Outputs:

  RoleArn:
    Description: The role that Amazon EKS will use to create AWS resources for Kubernetes clusters
    Value: !GetAtt eksServiceRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}-RoleArn"
```

## EKSワーカーノードロールを作成する
EKSワーカーノードが各種AWSリソースを参照・作成するために必要なIAMロールです。
下記のS3テンプレートURLを指定して、AWS CloudFormationコンソールでスタックを作成します。

https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2019-11-15/amazon-eks-nodegroup-role.yaml

## EKSクラスターVPCを作成する
下記のS3テンプレートURLを指定して、AWS CloudFormationコンソールでスタックを作成します。

https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2019-11-15/amazon-eks-vpc-sample.yaml

今回、AZ (`ap-northeast-1a`、`ap-northeast-1c`、`ap-northeast-1d`) にまたがる3つのパブリックサブネットを持つVPCを構築します。


**スタック作成中に `Template error: Fn::Select cannot select nonexistent value at index 2` エラーが発生した場合**
使用しているAWSアカウントでは、default subnetにap-northeast-1dが含まれていない可能性が高いので、以下のコマンドを実行してから、再度スタックの作成をしてみてください。

```
$ aws ec2 create-default-subnet --availability-zone ap-northeast-1d
```

## EKSクラスターを作成する
任意のクラスター名を入力して、事前に作成したEKSサービスロール、VPC、サブネット、セキュリティーグループを指定します。
<img width="574" alt="スクリーンショット 2020-03-29 22.27.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/58c88bb9-b31c-2929-8192-063526959754.png">
<img width="571" alt="スクリーンショット 2020-03-29 22.28.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/93fb3fa9-1f7f-fdda-8447-50c509cafa12.png">

## kubeconfigファイルを作成する
```
$ aws eks --region ap-northeast-1 update-kubeconfig --name <cluster name>
Added new context arn:aws:eks:ap-northeast-1:************:cluster/<cluster name> to /Users/********/.kube/config
```
上記コマンドでkubectlのカレントの接続先にEKSクラスターが上書き設定されます。
※ kubeconfigについては、[kubectlの接続設定ファイル（kubeconfig）の概要](https://qiita.com/shoichiimamura/items/91208a9b30e701d1e7f2) を参照

## EKSノードグループを起動する
EKSクラスター上に、ノードグループを起動します。
<img width="547" alt="スクリーンショット 2020-03-29 23.08.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/5730fefb-dbd9-d060-79eb-f4a59bf25c53.png">
任意のノードグループ名を入力して、事前に作成したEKSワーカーノードロール、サブネットを指定します。

<img width="550" alt="スクリーンショット 2020-03-29 23.10.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/b71cf169-1f8b-d522-8b3e-32692f796b5e.png">
ここではインスタンスタイプを指定しますが、重要なポイントとして、KubernetesはPod単位でプライベートIPアドレスが割り振られるため、ノード(=ホスト)に配置するPod数を計算して、適切なインスタンスタイプを選ぶ必要があります。

**例）T3系の最大IP数**

| インスタンスタイプ | 最大ENI数(1) | ENIあたりIP数(2) | 最大IP数(1)×(2) |
|:---------------|:-------------|:---------------|:----------------|
| t3.micro       | 2            | 2              | 4               |
| t3.small       | 3            | 4              | 12              |
| t3.medium      | 3            | 6              | 18              |
| t3.large       | 3            | 12             | 36              |
※ 詳細は[Elastic Network Interface](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html) を参照

なお、kube-systemと呼ばれるシステム用のNamespace上でもIPアドレスは利用されているため、それらを引いた数がPodで使用できるIP数となります。

```
$ kubectl get pod --namespace kube-system -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP                NODE                                                 NOMINATED NODE   READINESS GATES
aws-node-d5krq             1/1     Running   0          62m   192.168.220.97    ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
aws-node-flh75             1/1     Running   0          62m   192.168.152.228   ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>
aws-node-mnpxl             1/1     Running   0          62m   192.168.113.251   ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>
coredns-58986cd576-kc8xv   1/1     Running   0          95m   192.168.171.225   ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>
coredns-58986cd576-rqwqs   1/1     Running   0          95m   192.168.225.139   ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
kube-proxy-j9lk5           1/1     Running   0          62m   192.168.152.228   ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>
kube-proxy-jgmfb           1/1     Running   0          62m   192.168.220.97    ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
kube-proxy-rfjtd           1/1     Running   0          62m   192.168.113.251   ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>
```

<img width="547" alt="スクリーンショット 2020-03-29 23.10.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/c13574eb-bf21-5167-552f-bc1cd52662e1.png">
最後にノード数ですが、こちらも事前に作成したサブネットや配置するリソースに応じて、適切な数を指定する必要があります。

## サンプルアプリケーションを起動する
ここからサンプルのゲストブックアプリケーションを作成します。
※ 詳細は [ゲストブックアプリケーションを起動する] (https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/eks-guestbook.html) を参照

### Redisマスターレプリケーションコントローラーを作成 (Pod作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-master-controller.json
```
### Redisマスターサービスを作成 (Service作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-master-service.json
```
RedisマスターPodがノードグループのうち1ノードに配置され、そのPodへの通信経路であるServiceも作成されています。

```
$ kubectl get pod,svc -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP               NODE                                                 NOMINATED NODE   READINESS GATES
pod/redis-master-5l8nr   1/1     Running   0          9m25s   192.168.74.238   ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>

NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE     SELECTOR
service/kubernetes     ClusterIP   10.100.0.1     <none>        443/TCP    11h     <none>
service/redis-master   ClusterIP   10.100.59.64   <none>        6379/TCP   4m31s   app=redis,role=master
```
PodとServiceの関係性について、もう少し触れておくと、実行した下記の定義ファイルにあるように、PodのラベルとServiceのセレクタが合致した場合、対象のPodはそのServiceのターゲットとなり、Serviceを経由してトラフィックが流れる仕組みになっています。

```redis-master-controller.json
"template":{
   "metadata":{
      "labels":{
         "app":"redis",
         "role":"master"
      }
   },
```
```redis-master-service.json
"selector":{
   "app":"redis",
   "role":"master"
}
```

### Redisスレーブレプリケーションコントローラーを作成 (Pod作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-slave-controller.json
```
### Redisスレーブサービスを作成 (Service作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-slave-service.json
```
RedisスレーブPod（レプリカ数2）がノードグループのうち2ノードに配置され、そのPodへの通信経路であるServiceも作成されています。

```
$ kubectl get pod,svc -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP                NODE                                                 NOMINATED NODE   READINESS GATES
pod/redis-master-5l8nr   1/1     Running   0          28m     192.168.74.238    ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>
pod/redis-slave-hqpd2    1/1     Running   0          4m48s   192.168.192.184   ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
pod/redis-slave-nrnwk    1/1     Running   0          4m48s   192.168.133.49    ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>

NAME                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE     SELECTOR
service/kubernetes     ClusterIP   10.100.0.1       <none>        443/TCP    11h     <none>
service/redis-master   ClusterIP   10.100.59.64     <none>        6379/TCP   23m     app=redis,role=master
service/redis-slave    ClusterIP   10.100.106.145   <none>        6379/TCP   4m34s   app=redis,role=slave
```
### ゲストブックレプリケーションコントローラーを作成 (Pod作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/guestbook-controller.json
```
### ゲストブックサービスを作成 (Service作成)
```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/guestbook-service.json
```
ゲストブックPod（レプリカ数3）がノードグループのうち3ノードに均等に配置され、そのPodへの通信経路であるServiceも作成されています。

```
$ kubectl get pod,svc -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP                NODE                                                 NOMINATED NODE   READINESS GATES
pod/guestbook-2wcm6      1/1     Running   0          4m20s   192.168.222.236   ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
pod/guestbook-cnl46      1/1     Running   0          4m20s   192.168.124.194   ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>
pod/guestbook-smjjm      1/1     Running   0          4m20s   192.168.160.43    ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>
pod/redis-master-5l8nr   1/1     Running   0          38m     192.168.74.238    ip-192-168-113-251.ap-northeast-1.compute.internal   <none>           <none>
pod/redis-slave-hqpd2    1/1     Running   0          14m     192.168.192.184   ip-192-168-220-97.ap-northeast-1.compute.internal    <none>           <none>
pod/redis-slave-nrnwk    1/1     Running   0          14m     192.168.133.49    ip-192-168-152-228.ap-northeast-1.compute.internal   <none>           <none>

NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)          AGE     SELECTOR
service/guestbook      LoadBalancer   10.100.186.33    ac867331c722611ea8bc70e5c3799917-1903484356.ap-northeast-1.elb.amazonaws.com   3000:32317/TCP   4m13s   app=guestbook
service/kubernetes     ClusterIP      10.100.0.1       <none>                                                                         443/TCP          12h     <none>
service/redis-master   ClusterIP      10.100.59.64     <none>                                                                         6379/TCP         33m     app=redis,role=master
service/redis-slave    ClusterIP      10.100.106.145   <none>                                                                         6379/TCP         14m     app=redis,role=slave
```
システム構成も冒頭に説明した [今回のゴール](#今回のゴール) 通りに作られているようです。

### ゲストブックアプリケーションにブラウザでアクセス
http://ac867331c722611ea8bc70e5c3799917-1903484356.ap-northeast-1.elb.amazonaws.com:3000
<img width="744" alt="スクリーンショット 2020-03-30 10.44.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35420/a554f570-b37a-75ea-93f8-fbd2d790f588.png">

無事、ゲストブックアプリケーションが表示されました。

# さいごに
今回のチュートリアルでは、作成したRedisおよびゲストブックPodが、均等に各ノードに配置されています。これにはタネも仕掛けも分からず、どこか釈然としない気持ちになってしまいましたが、Kubernetesの公式ドキュメントに以下の一節を見つけました。
[Node上へのPodのスケジューリング](https://kubernetes.io/ja/docs/concepts/configuration/assign-pod-node/)
> Podが稼働するNodeを特定のものに指定したり、優先条件を指定して制限することができます。 これを実現するためにはいくつかの方法がありますが、推奨されている方法はラベルでの選択です。 スケジューラーが最適な配置を選択するため、一般的にはこのような制限は不要です。

柔軟な指定もできるようですが、スケジューラー(Podのノードへの割り当てをつかさどるマスターコンポーネント)が頑張ってくれるようです。流石。