Edited at

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

More than 1 year has passed since last update.


概要

巷で話題のAmazon EKSを使ってRailsアプリを動かしてみました。

チュートリアルに毛が生えたくらいの内容です。


EKS とは

AWSでやってるkubernetesのマネージドサービス。

以下のリージョンで利用可能。(2018.06.18現在)


  • 米国西部 (オレゴン) (us-west-2)

  • 米国東部 (バージニア北部) (us-east-1)


載せるアプリ


  • Rails : 5.2.0

  • Ruby : 2.5.1

  • DB : PostgreSQL

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

しかし、ストレージクラスというものを使うとkubernetes上でストレージを用意できるっぽい。

これを使うとDBのような永続化が必要なサービスも動かすことができる。


必要なもの


  • バージョン1.15.32以降のAWSCLI


    • Amazon EKSを触るのに必要

    • brew版は 1.15.32以降 の要件が満たせず、pipで再度インストールした(2018.06.18現在)




  • kubectl


    • Kubernetesクラスタに対してコマンドを実行するためのCLI

    • Homebrewでインストールした




  • heptio-authenticator-aws


    • IAMの情報を使ってKubernetesクラスタへの認証を行うためのツール


    • go get でgithubからインストールした




リソースの作成


ベースにした構成

こちらのcfn.ymlパクった 参考にした。

https://github.com/y13i/aws-eks-example

cfn.ymlを拝借し、カスタマイズして使用した。

その節はお世話になりました。ありがとうございました。

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

ざっくりこんな感じ。


  • VPC


    • パブリックサブネット*2

    • プライベートサブネット*2



  • プライベートサブネット内にアプリ用のインスタンス設置

  • EKSクラスタの作成


カスタマイズ的な部分

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


  • RDS


    • プライベートサブネット内に設置



  • ECRのリポジトリ作成

  • S3の設定

Parameters: の追加分

  DBUsername:

Type: String
DBPassword:
Type: String
NoEcho: true

Resources: の追加分


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: の追加分

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 > スタックを選択 > [出力]タブを選択 し、出力を確認する。


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


.kube/ 作成

$ mkdir -p .kube/


kubeconfigファイル 作成

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


.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


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


kubernetes/eks-secret.yml

apiVersion: v1

kind: Secret
metadata:
name: eks-secret
type: Opaque
data:
HOGE: aG9nZQ==

dataKEY : 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


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


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


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


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