Amazon EKSでRails API アプリケーションを動かす


# 概要
巷で話題のAmazon EKSを使ってRailsアプリを動かしてみました。
[チュートリアル](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started.html)に毛が生えたくらいの内容です。


# EKS とは
AWSでやってるkubernetesのマネージドサービス。
以下のリージョンで利用可能。（2018.06.18現在）

- 米国西部 (オレゴン) (us-west-2)
- 米国東部 (バージニア北部) (us-east-1)

# 載せるアプリ

- Rails : 5.2.0
- Ruby : 2.5.1
- DB : PostgreSQL

今回、DBにはRDSを使用した。

しかし、[ストレージクラス](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/storage-classes.html)というものを使うとkubernetes上でストレージを用意できるっぽい。
これを使うとDBのような永続化が必要なサービスも動かすことができる。


# 必要なもの
- バージョン1.15.32以降のAWSCLI
    - Amazon EKSを触るのに必要
    - brew版は `1.15.32以降` の要件が満たせず、pipで再度インストールした（2018.06.18現在）
- [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/)
    - Kubernetesクラスタに対してコマンドを実行するためのCLI
    - Homebrewでインストールした
- [heptio-authenticator-aws](https://github.com/heptio/authenticator)
    - IAMの情報を使ってKubernetesクラスタへの認証を行うためのツール
    - `go get` でgithubからインストールした









# リソースの作成

## ベースにした構成

こちらの[cfn.yml](https://github.com/y13i/aws-eks-example/blob/master/cfn.yml)を ~~パクった~~ 参考にした。
https://github.com/y13i/aws-eks-example

[cfn.yml](https://github.com/y13i/aws-eks-example/blob/master/cfn.yml)を拝借し、カスタマイズして使用した。
その節はお世話になりました。ありがとうございました。

このファイルは、EKSの利用に必要なリソースすべてをひとつのCloudFormationスタックで作成するようにしていて、さらにネットワーク構成をあるあるな形にしたものになっている。

ざっくりこんな感じ。

- VPC
    - パブリックサブネット*2
    - プライベートサブネット*2
- プライベートサブネット内にアプリ用のインスタンス設置
- EKSクラスタの作成


## カスタマイズ的な部分

Railsアプリを動かすのに、さらに以下のものが必要だったためyamlに追記した

- RDS
    - プライベートサブネット内に設置
- ECRのリポジトリ作成
- S3の設定

Parameters: の追加分

```yaml
  DBUsername:
    Type: String
  DBPassword:
    Type: String
    NoEcho: true
```

Resources: の追加分

```yaml

  VPCEndpointForS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
      - Ref: PublicRouteTable
      - Ref: PrivateRouteTable0
      - Ref: PrivateRouteTable1
      VpcId:
        Ref: Vpc
      ServiceName:
        Fn::Join:
        - "."
        - - com
          - amazonaws
          - Ref: AWS::Region
          - s3
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription:
        Ref: AWS::StackName
      SubnetIds:
      - Ref: PrivateSubnet0
      - Ref: PrivateSubnet1
      Tags:
      - Key: Name
        Value:
          Ref: AWS::StackName
  DBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Family: postgres9.6
      Description:
        Ref: AWS::StackName
      Parameters:
        client_encoding: UTF8
        timezone: Asia/Tokyo
  DBInstance:
    Type: "AWS::RDS::DBInstance"
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      DBName:
        Ref: AWS::StackName
      DBParameterGroupName:
        Ref: DBParameterGroup
      DBSubnetGroupName:
        Ref: DBSubnetGroup
      Engine: postgres
      EngineVersion: "9.6.6"
      MasterUsername:
        Ref: DBUsername
      MasterUserPassword:
        Ref: DBPassword
      MultiAZ: true
      PubliclyAccessible: false
      StorageType: gp2
      Tags:
      - Key: Name
        Value:
          Ref: AWS::StackName
      VPCSecurityGroups:
      - Fn::GetAtt:
        - DBSecurityGroup
        - GroupId
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for rds
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId:
            Fn::GetAtt:
            - NodeSecurityGroup
            - GroupId
      VpcId:
        Ref: Vpc
      Tags:
      - Key: Name
        Value:
          Ref: AWS::StackName
  ContainerRepository:
    Type: AWS::ECR::Repository
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 365
      LogGroupName:
        Ref: AWS::StackName
  StorageBucket:
    Type: AWS::S3::Bucket
  StorageUser:
    Type: AWS::IAM::User
    Properties:
      ManagedPolicyArns:
      - Ref: StorageUserPolicy
  StorageAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName:
        Ref: StorageUser
  StorageUserPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action: s3:*
          Resource:
          - Fn::GetAtt:
            - StorageBucket
            - Arn
          - Fn::Join:
            - ""
            - - Fn::GetAtt:
                - StorageBucket
                - Arn
              - /*
              - 
```

Outputs: の追加分

```yaml


  ContainerRepository:
    Value:
      Ref: ContainerRepository
  DBHostName:
    Value:
      Fn::GetAtt:
        - DBInstance
        - Endpoint.Address
  DBPort:
    Value:
      Fn::GetAtt:
        - DBInstance
        - Endpoint.Port
```



# 準備ができたのでデプロイする

ディレクトリ構造はこんな感じ。

api/
　├ app/
　├ config/
　│　.
　│　.
　│　.
　├ infra/
　│　└ cfn.yml
　└ Dockerfile


apiディレクトリにてコマンド実行
※完了まで15〜20分くらい

```
$ aws cloudformation deploy --template-file infra/cfn.yml --capabilities CAPABILITY_IAM --stack-name development --parameter-overrides DBUsername=app DBPassword=hogehoge
```

デプロイが完了したら、マネジメントコンソールから
CloudFormation > スタックを選択 > [出力]タブを選択 し、出力を確認する。

<img width="685" alt="ss1.png" src="https://qiita-image-store.s3.amazonaws.com/0/213486/541b3ad4-5a7b-b5c9-c452-d145bcea1db9.png">



# EKSクラスタにアクセスするため、認証まわりの設定

## .kube/ 作成

```
$ mkdir -p .kube/
```

## kubeconfigファイル 作成

```
$ touch ./.kube/k8s-config.yml
```

```yaml:.kube/k8s-config.yml
apiVersion: v1
clusters:
- cluster:
    server: <CFnで出力された EKSClusterEndpoint>
    certificate-authority-data: <CFnで出力された EKSClusterCertificateAuthorityData>
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: aws
  name: aws
current-context: aws
kind: Config
preferences: {}
users:
- name: aws
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      command: heptio-authenticator-aws
      args:
        - "token"
        - "-i"
        - <CFnで出力された EKSClusterName>
```

## 環境変数KUBECONFIGの設定

kubectl コマンド実行時にk8s-config.ymlを参照するようパスを追加する。

direnv なり .bash_profile なりに追記。

```
export KUBECONFIG=<k8s-config.ymlへのパス>
```



# クラスタにアクセスしてみる

```
$ kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   xxx.xx.x.x   <none>        443/TCP   8m
```


※ `command: heptio-authenticator-aws` 関連でエラーが出る場合は、 `k8s-config.yml` からheptio-authenticator-awsまでの相対パスに書き換えてください

例） `../../../go/bin/heptio-authenticator-aws`



# ワーカノードの設定

## kubernetesの設定ファイル置き場を用意

```
$ mkdir ./kubernetes/
```

## 設定ファイルの作成

```
$ touch kubernetes/aws-auth-configmap.yml
```

```yaml:kubernetes/aws-auth-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: <CFnで出力された NodeInstanceRole>
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes
```



## クラスタにnodeを参加させる

```
$ kubectl apply -f kubernetes/aws-auth-configmap.yml
configmap "aws-auth" created
```


## node一覧を取得し、確認してみる

```
$ kubectl get nodes
NAME                          STATUS    ROLES     AGE       VERSION
ip-XX-X-XX-XXX.ec2.internal   Ready     <none>    1m        v1.10.3
ip-XX-X-XX-XX.ec2.internal    Ready     <none>    51s       v1.10.3
```

### STATUSがReadyになっていなかったらReadyになるまで温かく見守る

```
$ kubectl get nodes --watch
```



## 環境変数設定
direnv でも .bash_profile でも好きなのにどうぞ。

```
export AWS_ACCOUNT_ID=xxxxx
export AWS_DEFAULT_REGION=us-east-1
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=xxxxx
export AWS_SECRET_ACCESS_KEY=xxxxxx
export CONTAINER_REPOSITORY=<CFnで出力された ContainerRepository>
export CONTAINER_IMAGE_TAG=<適当なタグ>
```



## ECRへログイン、イメージのbuild、タグ付け、プッシュ

```
$ $(aws ecr get-login --no-include-email)
$ docker build -t "$CONTAINER_REPOSITORY:$CONTAINER_IMAGE_TAG" .
$ docker tag "$CONTAINER_REPOSITORY:$CONTAINER_IMAGE_TAG" "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$CONTAINER_REPOSITORY:$CONTAINER_IMAGE_TAG"
$ docker push "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$CONTAINER_REPOSITORY:$CONTAINER_IMAGE_TAG"
```


## kubernetesのマニフェスト作成

### secretの作成
Podで環境変数を使うために用意してる。

```
$ touch kubernetes/eks-secret.yml
```


```yaml:kubernetes/eks-secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: eks-secret
type: Opaque
data:
  HOGE: aG9nZQ==
```

`data` は `KEY : VALUE` のペアで、VALUEはbase64エンコードした値。

以下のようにコマンドで生成したものをコピペしてる。

```
$ echo -n "hoge" | base64
aG9nZQ==
```

この場合、Podでは値が `hoge` である環境変数 `HOGE` が使える状態になる。


作成したマニフェストをクラスタに適用する。

```
$ kubectl create -f kubernetes/eks-secret.yml
```


### deploymentの作成

Pods と ReplicaSets の設定がまとめられたもの。

```
$ touch kubernetes/eks-deployment.yml
```

ECRのイメージ情報を取得しておく

```
$ echo "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$CONTAINER_REPOSITORY:$CONTAINER_IMAGE_TAG"
xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx
```

取得したイメージ情報を `image: ` につっこむ

URLからタグまでフルで指定する必要がある。（[Using AWS EC2 Container Registry](https://kubernetes.io/docs/concepts/containers/images/)）

```yaml:kubernetes/eks-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: eks-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: rails-api
  template:
    metadata:
      labels:
        app: rails-api
    spec:
      containers:
      - name: rails-api
        image: xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx
        ports:
        - containerPort: 3000
        envFrom:
        - secretRef:
            name: eks-secret
        args:
          - rails
          - server
          - -p
          - '3000'
          - -b
          - '0.0.0.0'
```



これは以下のような意味を持つ。

- `xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx` イメージをもとにしたコンテナが入ったPodを作る。
- そのPodにラベル `app=rails-api` を付与する。
- ラベル `app=rails-api` がついたPodが3つになるようレプリケーションする。
- Podは外部通信用に3000ポートを開ける。
- コンテナ作成時に `rails server -p '3000' -b '0.0.0.0'` を実行する。




作成したマニフェストをクラスタに適用する。

```
$ kubectl create -f kubernetes/eks-deployment.yml
```


### serviceの作成

Podを置いてアプリが動くのは良いとして、そのアプリにどうやってアクセスするの？ を解決するのがservice。

ユーザーからのリクエストを処理するのにこいつが必要。

```
$ touch kubernetes/eks-service.yml
```

```yaml:kubernetes/eks-service.yml
kind: Service
apiVersion: v1
metadata:
  name: eks-service
spec:
  type: LoadBalancer
  selector:
    app: rails-api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
```

これは以下のような意味をもつ。

- 80ポートへアクセスがあったとき、ラベルが `app=rails-api` のPodの3000ポートにつなぐ



作成したマニフェストをクラスタに適用する。

```
$ kubectl create -f kubernetes/eks-service.yml
```


### jobの作成

アプリのように常に動かしたいものではなく、なにかを実行したら消えて欲しいものにはjobを使う。

例えばRailsの場合、 `rails db:create` とか `rails db:migrate` みたいなのは一度だけ実行したいものだと思う。

そのためにjobを用意した。


```
$ touch kubernetes/eks-create-job.yml
```


```yaml:kubernetes/eks-create-job.yml
apiVersion: batch/v1
kind: Job
metadata:
  name: eks-create-job
spec:
  backoffLimit: 3
  parallelism: 1
  completions: 1
  template:
    spec:
      containers:
      - name: rails-create
        image: xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx
        ports:
        - containerPort: 3000
        envFrom:
        - secretRef:
            name: eks-secret
        args:
          - rails
          - db:create
      restartPolicy: Never
```

Podの部分はdeploymentと同じで、arg(=実行するコマンド)が違う。

意味としては

- `xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx` イメージをもとにしたコンテナが入ったPodを作る。
- Podは外部通信用に3000ポートを開ける。
- コンテナ作成時に `rails db:create` を実行する。
- 実行が失敗したら3回までリトライする。
- Podは最大1つ実行する。
- 1つのPodの処理が成功したらjobの成功とする。
- コンテナの再起動は行わない。



作成したマニフェストをクラスタに適用する。

```
$ kubectl create -f kubernetes/eks-create-job.yml
```


migrateのほうも同様です。

```
$ touch kubernetes/eks-migrate-job.yml
```


```yaml:kubernetes/eks-migrate-job.yml
apiVersion: batch/v1
kind: Job
metadata:
  name: eks-migrate-job
spec:
  backoffLimit: 3
  parallelism: 1
  completions: 1
  template:
    spec:
      containers:
      - name: rails-migrate
        image: xxxxx.dkr.ecr.us-east-1.amazonaws.com/xxxxx:xxxxx
        ports:
        - containerPort: 3000
        envFrom:
        - secretRef:
            name: eks-secret
        args:
          - rails
          - db:migrate
      restartPolicy: Never
```


作成したマニフェストをクラスタに適用する。

```
$ kubectl create -f kubernetes/eks-migrate-job.yml
```

# アプリにアクセスする

serviceを作成すると、EKSによって自動でロードバランサが作成される。

そこにアクセスしたいので、確認するために以下のコマンドを実行する。

```
$ kubectl describe services
```

その中から以下の項目を見つける。

```
LoadBalancer Ingress: xxxxxxxxxx.us-east-1.elb.amazonaws.com
```

`xxxxxxxxxx.us-east-1.elb.amazonaws.com` がエンドポイントとなる。


ブラウザやPostmanなんかでアクセス可能になる。


# 参考URL

[Deploy a Kubernetes Application with Amazon Elastic Container Service for Kubernetes](https://aws.amazon.com/jp/getting-started/projects/deploy-kubernetes-app-amazon-eks/)