EKSでRayクラスター作成

# 概要
AWS EKS上でRayクラスターを作成してJupyter notebookを利用する手順を記載する。

EKSの作成には、AWSコンソールを利用する方法とeksctlを使う方法があるが、eksctlだとEKSを利用するのに必要なAWSリソースも同時に作成してくれるので今回はeksctlを使う。

EKSはEKS ClusterとEKS Nodegroupからなる。EKS ClusterがKubernetesクラスターのうち**コントロールプレーン**を担い、EKS Nodegroupが**データプレーン**（＝実際にコンテナ稼働するワーカーノード群）を担う。

# 参考

- **EKS作成**

https://dev.classmethod.jp/articles/getting-started-amazon-eks-with-eksctl/

- **Helm**

EKS作成後にRayクラスターをKubernetes上にデプロイする。Helmは、Kubernetesリソースのデプロイを支援するツール。

https://atmarkit.itmedia.co.jp/ait/articles/2104/15/news009.html

- **Helm Chart**

Ray公式が紹介しているものを利用

https://docs.ray.io/en/latest/cluster/kubernetes.html

# 前提
- AWSアカウントを持っていること
- 踏み台サーバ(EC2)が作成済み

# 手順

1. eksctlでEKS Cluster＆EKS Nodegroup作成
1. Rayクラスター作成
1. Jupyter notebookへアクセス

## 0. Kubectlのインストール

```bash
curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl
curl -o kubectl.sha256 https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl.sha256
openssl sha1 -sha256 kubectl
chmod +x ./kubectl
mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin
echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc
kubectl version --short --client
```

## 1. EKS Cluster、EKS Nodegroupを作成

注意：踏み台サーバ(EC2)上でeksctlを実行する、すなわち踏み台サーバがAWSリソースやEKS Cluster/Nodegroupを作成するので、踏み台サーバのIAMロールに権限を付与しておいてあげる必要がある。最小限の権限がわからなかったので（調べたら出てきたが試してないので省く）`AdministratorAccess`ポリシーを踏み台サーバのIAMロールに割り当てた。

まずは踏み台サーバにeksctlをインストールする。

```bash
$ curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
$ sudo mv /tmp/eksctl /usr/local/bin
$ eksctl version
```

踏み台サーバからeksctl createでEKS Cluster, Nodegroupを作成する。`sudo`を抜くとエラーになるので注意。

```bash
$ sudo eksctl create cluster \
    --name ray-eks-cluster \
    --region us-west-2 \
    --nodegroup-name ray-nodegroup \
    --node-type t3.large \
    --nodes 3 \
    --nodes-min 2 \
    --nodes-max 4 \
    --managed
```

これで一気に各種AWSリソースとEKS Cluster/Nodegroupを作成してくれる。eksctlはCloud Formationを利用してこれらを作成するので、リソースの作成状況はCloud Formationで確認できる。

リソースの作成が完了したら、kubectlでEKS Clusterを操作するために、`kubeconfig`を作成する。

```bash
$ aws eks update-kubeconfig --region us-west-2 --name ray-eks-cluster
```

次のコマンドで以下の出力が得られれば`kubeconfig`が正しく設定されている。

```bash
$ kubectl get svc
NAME             TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
svc/kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   1m
```

以下のコマンドで接続しているクラスタを確認できる。

```bash
$ kubectl config get-contexts
```

## 2. Rayクラスター作成

踏み台サーバ上にHelmをインストールする。`helm version`でバージョンが確認できれば正常にインストールされている。

```bash
$ curl -O https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
$ bash ./get-helm-3
$ helm version
```

次にRay用のHelm Chartをダウンロードし、Kubernetesクラスター上にKuberntesリソース(Ray cluster)をデプロイする。

```bash
$ sudo apt-get install git
$ git clone https://github.com/ray-project/ray.git
$ cd ray/deploy/charts
$ sudo helm -n ray install example-cluster --create-namespace ./ray
```

Kubernetes NodeやPodが確認できる。

```bash
$ sudo kubectl get nodes
$ sudo kubectl get pods
$ sudo kubectl get pods -n ray
```

# 3. Jupyter notebookへアクセス
Jupyter notebookをHead node上で起動する予定。なので、Head nodeへアクセスする必要がある。アクセスの方法はいろいろあると思うが、3通りの方法を記載する。

1. 同じVPC内に踏み台サーバ設置
1. 既存のServiceをLoadBalancerに変更して外部公開
1. 新たにLoadBalancer Serviceを作成して外部公開

1.については、同じVPC内にあるAWSリソースからEKSのPodにアクセスできることを利用している。

## 3-1. 同じVPC内に踏み台サーバ(EC2)を設置
以下に概要のみ記す。まずEKSクラスタとして起動されたEC2がどのVPCに入っているかAWSコンソールから確認する。次に、同じVPC内のパブリックサブネット内に踏み台サーバとしてEC2を起動する。（※eksctlにより各アベイラビリティゾーン毎にパブリックサブネットとプライベートサブネットが１つずつ作成されているはず）。手順概要は以上。起動したEC2上でブラウザを開いてJupyter notebookを触るので、踏み台サーバはGUI化しておく必要がある（手順省略）

## 3-2. 既存のServiceをLoadBalancerに変更
デフォルトでは以下のClusterIP Serviceが作成される。これをLoadBalancerに変更して外部からアクセス可能なURLを生成する。

```bash
$ kubectl get svc -n ray
NAME                           TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)                                         AGE
example-cluster-ray-head       ClusterIP      10.100.231.189   <none>                                                                    10001/TCP,8265/TCP,8000/TCP                     74m
```

下記のコマンドで`spec`配下の`type`の値を`LoadBalancer`に変更する。

```bash
kubectl -n ray patch svc example-cluster-ray-head -p '{"spec": {"type": "LoadBalancer"}}'
```

再度Serviceを確認すると、EXTERNAL-IPが生成されていることがわかる。

```bash
$ kubectl get svc -n ray
NAME                           TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)                                         AGE
example-cluster-ray-head       LoadBalancer   10.100.231.189   a94bd10a573874295a1d41aa77f99957-1031258648.us-west-2.elb.amazonaws.com   10001:32634/TCP,8265:31708/TCP,8000:31710/TCP   76m
```

**【!! 注意 !!】**
EXTERNAL-IPが生成されても実際にルーティング処理できるようには結構時間がかかる気がする（全然数えてないけど10分くらい？）。


次に、example-cluster-ray-headのbashに入って(Head nodeに転送されるので実際にはHead nodeに入っている)、Jupyter notebookを起動する。

```bash
$ sudo kubectl -n ray exec -it service/example-cluster-ray-head -- bash
$ pip3 install notebook
$ jupyter notebook --ip=* --no-browser --port 8000
```

ブラウザで`http://<EXTERNAL-IP>:8000`にアクセスする。Jupyter notebook起動時に表示されたtokenを入力してJupyter notebookにログインする。あとはJupyter notebookを使ってRayを試す。

```
http://a94bd10a573874295a1d41aa77f99957-1031258648.us-west-2.elb.amazonaws.com:8000/
```

**【!! 注意 !!】**
`kubectl get svc -n ray`の結果では`8000:31710/TCP `となっており8000番ポートを31710番に転送しているように見えるが、Jupyterを31710番ポートで起動してもなぜかアクセスできない（なにが原因なんだろ、、、）。8000番ポートで起動すればアクセスできる。


## 3-3. 新たにLoadBalancerを作成して外部公開
Jupyter notebookに外部からアクセスするためLoadBalancer Serviceを作成する。まずはLoadBalancer用のyamlを作成する。

既存のServiceを参考にするため既存のServiceをyamlで出力する。

```bash
$ sudo kubectl get service -o yaml -n ray
```

上記コマンドの出力結果を元に`lb.yaml`を作成する。重要な点は
`selector:
    cluster.ray.io/component: example-cluster-ray-head`
として、LoadBalancerへのトラヒックがHead nodeにルーティングされるように設定しているところ。

```yaml:lb.yaml
apiVersion: v1
kind: Service
metadata:
  name: example-cluster-ray-head-lb
  namespace: ray
spec:
  clusterIP: 10.100.231.167
  clusterIPs:
  - 10.100.231.167
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: client
    port: 10001
    protocol: TCP
    targetPort: 10001
  - name: dashboard
    port: 8265
    protocol: TCP
    targetPort: 8265
  - name: ray-serve
    port: 8000
    protocol: TCP
    targetPort: 8000
  selector:
    cluster.ray.io/component: example-cluster-ray-head
  sessionAffinity: None
  type: LoadBalancer
```

作成した`lb.yaml`をもとにLoad Balancerを作成する。

```bash
$ sudo kubectl create -f lb.yaml -n ray
```

Load Balancer serviceが作成されたことを確認する。

```bash
$ sudo kubectl get service -n ray
NAME                          TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)                                         AGE
example-cluster-ray-head      ClusterIP      10.100.150.175   <none>                                                                    10001/TCP,8265/TCP,8000/TCP                     142m
example-cluster-ray-head-lb   LoadBalancer   10.100.150.177   a932049cbef824477bfc553edba1d781-1349688053.us-west-2.elb.amazonaws.com   10001:31563/TCP,8265:31733/TCP,8000:30620/TCP   56s
```

**【!! 注意 !!】**
EXTERNAL-IPが生成されても実際にルーティング処理できるようには結構時間がかかる気がする（全然数えてないけど10分くらい？）。

example-cluster-ray-head (ClusterIP)のbashに入って、Jupyter notebookを起動する。指定するポートはexample-cluster-ray-head-lbの宛先ポート。

```bash
$ sudo kubectl -n ray exec -it service/example-cluster-ray-head -- bash
$ pip3 install notebook
$ jupyter notebook --ip=* --no-browser --port 8000
```

ブラウザで`http://<EXTERNAL-IP>:8000`にアクセスする。Jupyter notebook起動時に表示されたtokenを入力してJupyter notebookにログインする。あとはJupyter notebookを使ってRayを試す。

```
http://a932049cbef824477bfc553edba1d781-1349688053.us-west-2.elb.amazonaws.com:8000/
```

**【!! 注意 !!】**
`kubectl get svc -n ray`の結果では`8000:31710/TCP `となっており8000番ポートを31710番に転送しているように見えるが、Jupyterを31710番ポートで起動してもなぜかアクセスできない（なにが原因なんだろ、、、）。8000番ポートで起動すればアクセスできる。


## 4. 削除

使い終わったら削除を忘れずに。

まずK8sリソースを削除。

```bash
# まずカスタムリソースrayClusterの「example-cluster」を削除
$ kubectl -n ray delete raycluster example-cluster

# 次に「example-cluster」をアンインストール
$ helm -n ray uninstall example-cluster
release "example-cluster" uninstalled

# 最後にネームスペースrayを削除
$ kubectl delete namespace ray
namespace "ray" deleted
```

さらにEKS clusterを削除。

```bash
$ sudo eksctl delete cluster \
    --name ray-eks-cluster \
    --wait
```

以上。