【初心者向け】EKSを用いてReactAppを公開してみた

# はじめに
### 「おぼろげながら浮かんできたんです。EKSという単語が。」

なんとなくですが、EKSを使ってみたくなったので使ってみました。EKSというのはAWSで使用できるkubernetesのサービスです。AWSにはEKSの他にもECSというAWSが独自に用意したコンテナのオーケストレーションツールも存在します。
学習コストの低さやメンテナンスの運用などを考えると、自分は試すならECSからかな、と今になっては思うんですがしょうがありません。なぜなら、おぼろげながら浮かんじゃったから。。。

# Kubernetesとは何？

この記事を読んでいる聡明な皆様なら既に知っていると思いますが、念のためKubernetesについても説明します。（「クバネティス」、「クーバネティス」と読みます。私はどや顔で「くー↑ばねいつ↓」と読んでいました）

> Kubernetes (K8s)は、デプロイやスケーリングを自動化したり、コンテナ化されたアプリケーションを管理したりするための、オープンソースのシステムです。
引用：[kubernetes公式ドキュメント](https://kubernetes.io/ja/)

一見するとよくわかりませんが、三回ぐらい読んでもよくわかりません。
皆さんはアプリをコンテナ化したことはあるでしょうか？
では、複数のコンテナをアプリとしてデプロイした際のコンテナ同士の連携、リソースの管理、デプロイなどを経験したことはあるでしょうか？
これらの工程にはかなりの工数がかかり、経験豊富な方でないと難しい部分があります。
（実はそういった経験がないので何がそんなにめんどくさいのかピンと来ていません）

そいつをバシッと解決してくれるのがKubernetesになります。
Kubernetesは、複数コンテナの運用を考えたときに出てくるそういっためんどくさい部分をなんかうまいぐわいにやってくれる機能、という理解で大丈夫です。
そのような機能を相称して「オーケストレーションツール」と呼びます。

# EKSとは

EKSはAWSでKubernetesを実行できるサービスになります。
といっても、AWSのコンソール上でバチバチに開発するというわけではなく、適当なサーバーからアクセスしてEKSを操作することになります。
kubectlというKubernetesをコマンドで操作するためのツールがあるのですが、こちらを開発に用います。
Kubernetesに慣れている人にとっては、新たに勉強する必要がなく、スムーズにEKSに移行できるためこちらの仕様は大変ありがたいものになります。

今まで通りコマンドで開発、見通しの良いコンソール上で作成したリソース等の確認って使い方が一番いいんでしょうか。

### コントロールプレーンとワーカー
kubernetesはコントロールプレーンと複数のワーカーで構成されます。
コントロールプレーンとワーカーとまとめてクラスタと呼びます。
ワーカーは、コンテナを入れるところ、コントロールプレーンはワーカーとコンテナを管理するところです。

例えば、複数のワーカーにコンテナとして同一のアプリをデプロイしたとします。
複数の人からアプリにアクセスがあったときに常に同じワーカー（サーバー）のコンテナを動かしていたらそのサーバーのリソースがすぐに枯渇してしまいそうですよね。
そんな時、コントロールプレーンが、それらしくリソースを分配してくれます。

また、そのようなアプリをアップデートする際には、ローリングアップデートという技術を使うこともできます。
複数ワーカーのコンテナを一度にアップデートするのではなく、一つ一つ順番にアップデートすることで、アプリを停止させることなくアップデートが可能になります。その制御もコントロールプレーンが担っています。

### 料金
皆さん気になりだしてるのがこちらだと思います。料金体系。
比較のためにAWSにある同じオーケストレーションツールであるECSの値段も載せておきます。

| 機能名 | クラスタ | ワーカー |
| --- | ----------- | ------- |
| EKS | 月 : 74$ | EC2 or Fargate の料金 |
| ECS | 月 :  0$ | EC2 or Fargate の料金 |

EKSはクラスタを作成すると一時間当たり0.10$、大体月に74$ かかります。
ところがどっこい、ECSは0$と中々太っ腹です。

ワーカーは結局サーバーなのでAWSにあるサーバーの機能を利用することになります。どのサーバーを利用するかによってその料金は変わります。

### メンテナンス
これからEKSの導入を考えている人には絶対知っておいてほしい事項です。
EKSはKubernetesを利用しているため、その機能はKubernetesのバージョンに依存しています。

そのバージョン管理が問題でして、EKSでは約三か月に一回マイナーバージョンがリリースされます。
しかし、各バージョンのサポート期限はリリースされてから一年間であり、その期限が来る前にクラスタ及びワーカーのEKSバージョンの更新を行う必要があるんです。

仮に今使っている機能の仕様が急に変わったり使えなくなったりなどの可能性を考えると、こちらはかなりのメンテナンスコストが要求されると考えられます。

ところがどっこい、ECSにはバージョンという概念が存在しません。
ECSでは、アップデートによって使えなくなる機能もなければ、定期的な更新作業も必要ないです。
なんかいつの間にか便利機能が追加されてるから、使ってみよーってノリで新しい機能を試すことができます。

### ネットワークの設定
ここまで聞くと、「EKSとECSだったらECSの方が良さそう。。。」といった意見が出そうですがそちらの考えは早計です。

ワーカーにコンテナをデプロイする、コンテナ同士を連携するといったときに、適宜ネットワークを引く必要があります。
こちらの設定をEKSは全て自動でやってくれるのに対し、ECSでは適宜自分で設定する必要があります。
クラスタの規模が大きくなり、サービスが複雑になるにつれ、そのネットワークの管理工数というのはどんどん増えていきます。

以上を踏まえると、サービスが複雑になるのであればEKS、シンプルになるのであればECSを採用するのが無難だと思います。

# この記事の目的
何となく浮かんできたEKSという単語ですが、とりあえず複数ノードにまたがるアプリを実装してみようと思いました。
で、アプリに関しては普段使い慣れてるReactアプリをとりあえずデプロイしてみようと思います。

最終的な形としては、複数ノードにコンテナ化したReact App + Nginxをぶち込むということで実現しようと思います。

# 手順

## 事前準備
　EKSでは、適当なサーバーからアクセスして操作する必要があるので、まずはそのサーバーの準備を行います。
私は操作する端末として、とりあえずローカルに入っていたWSL Ubuntu20.04を用いました。

aws-cliのインストール
```
$ pip install awscli
```

aws-cliの設定
```
aws configure
 AWS Access Key ID [None]: ＜アクセスキー＞
 AWS Secret Access Key [None]: ＜シークレットキー＞
 Default region name [None]: ap-northeast-1
 Default output format [None]: json
```

eksctlのインストール（AWS EKSと接続したりするための独自のもの）
```
$ 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バージョン確認
```
$ eksctl version
  0.31.0
```

kubectlのインストール
 ※2023年9月現在、EKSの最新バージョンが1.27だったため、それに合わせます。
　EKSのバージョンとかけ離れている、それより高いバージョンのkubectlは使用できません。
```
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"
sudo apt update
sudo apt install -y kubectl=1.27.5-00
```

## ネットワーク・ロールの構築
KESを使用するために入れ物としてネットワークを準備しておく必要があります。
また、クラスタ、ワーカーを動かす権限としてロールの作成も行う必要があります。

以下のyaml, jsonを用いてcloudformationのコンソールからスタックを作成し、ネットワーク・ロールを作成いたします。

スタック名 EKS-Vpc
※パラメータはIPアドレスはVPCを確認し余ってるところで適宜実装してください
```yaml
---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Amazon EKS Sample VPC - Public subnets only'

Parameters:
  VpcBlock:
    Type: String
    Default: 10.10.0.0/16
    Description: The CIDR range for the VPC. This should be a valid private (RFC 1918) CIDR range.
  Subnet01Block:
    Type: String
    Default: 10.10.0.0/24
    Description: CidrBlock for subnet 01 within the VPC
  Subnet02Block:
    Type: String
    Default: 10.10.1.0/24
    Description: CidrBlock for subnet 02 within the VPC
  Subnet03Block:
    Type: String
    Default: 10.10.2.0/24
    Description: CidrBlock for subnet 03 within the VPC. This is used only if the region has more than 2 AZs.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: "Worker Network Configuration"
        Parameters:
          - VpcBlock
          - Subnet01Block
          - Subnet02Block
          - Subnet03Block

Conditions:
  Has2Azs:
    Fn::Or:
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - ap-south-1
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - ap-northeast-2
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - ca-central-1
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - cn-north-1
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - sa-east-1
      - Fn::Equals:
        - {Ref: 'AWS::Region'}
        - us-west-1

  HasMoreThan2Azs:
    Fn::Not:
      - Condition: Has2Azs

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock:  !Ref VpcBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
      - Key: Name
        Value: !Sub '${AWS::StackName}-VPC'
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
  VPCGatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: Public Subnets
      - Key: Network
        Value: Public
  Route:
    DependsOn: VPCGatewayAttachment
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  Subnet01:
    Type: AWS::EC2::Subnet
    Metadata:
      Comment: Subnet 01
    Properties:
      MapPublicIpOnLaunch: true
      AvailabilityZone:
        Fn::Select:
        - '0'
        - Fn::GetAZs:
            Ref: AWS::Region
      CidrBlock:
        Ref: Subnet01Block
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: !Sub "${AWS::StackName}-Subnet01"
      - Key: kubernetes.io/role/elb
        Value: 1
  Subnet02:
    Type: AWS::EC2::Subnet
    Metadata:
      Comment: Subnet 02
    Properties:
      MapPublicIpOnLaunch: true
      AvailabilityZone:
        Fn::Select:
        - '1'
        - Fn::GetAZs:
            Ref: AWS::Region
      CidrBlock:
        Ref: Subnet02Block
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: !Sub "${AWS::StackName}-Subnet02"
      - Key: kubernetes.io/role/elb
        Value: 1
  Subnet03:
    Condition: HasMoreThan2Azs
    Type: AWS::EC2::Subnet
    Metadata:
      Comment: Subnet 03
    Properties:
      MapPublicIpOnLaunch: true
      AvailabilityZone:
        Fn::Select:
        - '2'
        - Fn::GetAZs:
            Ref: AWS::Region
      CidrBlock:
        Ref: Subnet03Block
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: !Sub "${AWS::StackName}-Subnet03"
      - Key: kubernetes.io/role/elb
        Value: 1
  Subnet01RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref Subnet01
      RouteTableId: !Ref RouteTable
  Subnet02RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref Subnet02
      RouteTableId: !Ref RouteTable
  Subnet03RouteTableAssociation:
    Condition: HasMoreThan2Azs
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref Subnet03
      RouteTableId: !Ref RouteTable
  ControlPlaneSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Cluster communication with worker nodes
      VpcId: !Ref VPC

Outputs:
  SubnetIds:
    Description: All subnets in the VPC
    Value:
      Fn::If:
      - HasMoreThan2Azs
      - !Join [ ",", [ !Ref Subnet01, !Ref Subnet02, !Ref Subnet03 ] ]
      - !Join [ ",", [ !Ref Subnet01, !Ref Subnet02 ] ]
  SecurityGroups:
    Description: Security group for the cluster control plane communication with worker nodes
    Value: !Join [ ",", [ !Ref ControlPlaneSecurityGroup ] ]
  VpcId:
    Description: The VPC Id
    Value: !Ref VPC
```

スタック名 EKS-Service-Role
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Amazon EKS Cluster Role'

Resources:
  eksClusterRole:
    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/AmazonEKSClusterPolicy

Outputs:

  RoleArn:
    Description: The role that Amazon EKS will use to create AWS resources for Kubernetes clusters
    Value: !GetAtt eksClusterRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}-RoleArn"
```
スタック名 EKS-Nodegroup-Role
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Amazon EKS - Node Group Role

Mappings:
  ServicePrincipals:
    aws-cn:
      ec2: ec2.amazonaws.com.cn
    aws-us-gov:
      ec2: ec2.amazonaws.com
    aws:
      ec2: ec2.amazonaws.com

Resources:
  NodeInstanceRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !FindInMap [ServicePrincipals, !Ref "AWS::Partition", ec2]
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy"
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonEKS_CNI_Policy"
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
      Path: /

Outputs:
  NodeInstanceRole:
    Description: The node instance role
    Value: !GetAtt NodeInstanceRole.Arn
```

## クラスターの作成
コンソールでクラスターを作成を行いました。（10分ぐらいかかります）
設定は以下の設定で行いました。

>名前：EKS-cluster
バージョン：1.27
クラスターサービスロール　EKS-Service-Roleで作成したロール
VPC、サブネット　EKS-Vpcで作成したVPC、サブネット
セキュリティグループ　EKS-Vpcで作成したセキュリティグループ
アドオン　→　デフォルト（VPC CNI, kube-proxy, CoreDNS）

kubeconfigファイルの作成
クラスターにアクセスするためのkubekonfigファイルを作成する必要があります。
クラスター作成完了後に実行します。
```
aws eks --region ap-northeast-1 update-kubeconfig --name EKS-cluster
```
接続テスト
```
kubectl get svc
```

## ワーカーノードの作成
コンソール上でワーカーノードを作成します。（5分ぐらいかかります）
コンピューティングでノードグループにあるノードグループの追加を選択し作成します。
設定は以下の設定で行いました。

>名前：EKS-worker-nodes
ノードIAMロール　EKS-Nodegroup-Roleで作成したロール
EC2の設定は適宜
　AMIタイプ「Amazon Linux (AL2_x86_64)
　インスタンスタイプ「t3.small」
　ディスクサイズ「10」
　最小サイズ「2」
　最大サイズ「2」
　希望サイズ「2」
サブネット　EKS-Vpcで作成したサブネット
ノードへのリモートアクセスを許可する　ON
SSHキーを選択
次からのリモートアクセスを許可　全て

これにて、KESの基本的な準備が完了いたしました。
ここから先はreact appとnginxをコンテナ化しデプロイしてロードバランサーで公開する手順になります。

## React App 作成

```
nvm use 18.16.1
npx create-react-app test-app
```

appディレクトリ直下にDockerfile格納します。

Dockerfile
```dockerfile
FROM node:18.16.1 as builder

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY ["package.json", "package-lock.json", "./"]
RUN npm install
COPY [".", "./"]
RUN npm run build

FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY --from=builder ["/usr/src/app/build", "./"]
COPY ["nginx/default.conf", "/etc/nginx/conf.d"]
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```

appディレクトリ直下の/nginx にdefault.conf格納します。

default.conf
```conf
server {
  listen 80;
  location / {
    root /usr/share/nginx/html;
    index index.html index.htm
    try_files $uri /index.html;
  }
}
```

## Dockerイメージ作成
※kubernetesのネームスペース、ECRのリポジトリ名をとりあえずeks-testで作成します。

Dockerビルド
```
docker build -t eks-test .
```

以下コマンドを実行し localhost:3000でアクセスできればOKです。
```
docker run -p 3000:80 eks-test 
```

## ECRにコンテナ格納
まずは、ECRにコンテナのリポジトリを作成します。
コンソール上でECR（Elastic Container Registry）を開き、リポジトリの作成をクリックします。
名前を「eks-test」として入力し作成します。
eks-testの詳細にある「プッシュコマンドの表示」をクリックし、そこにある「認証・イメージタグ付け・プッシュコマンド」をローカルで実行し作成したリポジトリに格納します。


□コンテナイメージのデプロイ・サービス公開

コンテナイメージのデプロイ・サービスを一元管理するために、namespaceの作成を行います。
```
kubectl create namespace eks-test
```

以下のデプロイ・サービス公開するための設定ファイルを適当なディレクトリに格納してください。

EKS-Deployment.yaml（ECRのURIを適宜書き換えること）
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: eks-test
spec:
  selector:
    matchLabels:
      app: eks-test
  replicas: 2
  template:
    metadata:
      labels:
        app: eks-test
    spec:
      containers:
        - name: eks-test
          image: <ECRのURI>
          imagePullPolicy: Always
          ports:
            - containerPort: 80
```

EKS-Service.yaml
```yaml
apiVersion: v1
kind: Service
metadata:
  name: eks-test
spec:
  type: LoadBalancer
  selector:
    app: eks-test
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

格納したディレクトレイで以下コマンドを実行します。
アプリデプロイ
```
kubectl apply -f EKS-Deployment.yaml -n eks-test
```

サービス作成
```
kubectl apply -f EKS-Service.yaml -n eks-test
```

以下で取得できるEXTERNAL-IPでアプリにアクセスすることができれば成功です。（なんだかんだ公開に二分ぐらいまちます。）
```
kubectl get service -n eks-test
```

以上で、EKSを用いてReactAppを公開する手順を終了します。

# 全削除
作成したリソースの削除方法です。お片付けはちゃんとしないとね。

ネームスペース削除（紐づいたノード、サービスも消える）
```
kubectl delete namespace eks-test
```

ECRリポジトリの削除
```
aws ecr delete-repository --repository-name eks-test --force --region ap-northeast-1
```

ノードグループの削除
```
aws eks delete-nodegroup --nodegroup-name EKS-worker-nodes --cluster-name EKS-cluster --region ap-northeast-1
```

クラスター削除
```
aws eks delete-cluster --name EKS-cluster --region ap-northeast-1
```
クラウドフォーメーション削除
```
aws cloudformation delete-stack --stack-name EKS-Vpc
aws cloudformation delete-stack --stack-name EKS-Service-Role
aws cloudformation delete-stack --stack-name EKS-Nodegroup-Role
```

# 終わりに
ここまで読んでいただきありがとうございました。
記事を書くに当たり、以下サイトを参考にさせていただきました。
また、執筆する気持ちになりましたらその時お会いしましょう。

- [コンテナサービス「Amazon EKS」とは？実際に使用してみた](https://business.ntt-east.co.jp/content/cloudsolution/column-try-40.html)
- [Kubernetes超入門](https://qiita.com/dsagnlaiweudlbfna/items/55b162197afa74306067)
- [Dockerで起動できるReactアプリをEKSにデプロイしてみた](https://zenn.dev/jojojo/articles/14d5995c00d0b6)