KubernetesでRails+Nginxをデプロイする(kubernetes CI/CDまでの道①)
はじめに
以前CI/CDまでの道という記事を連載してDockerを用いたCI/CDに挑戦しました。
ある程度、Dockerに慣れたところで次は以前挫折したKubernetes
を用いたCI/CDに挑戦していきます。その前段階としてRailsをデプロイしていきます。
開発環境
- WSL2 20.04 LTS
- Docker 20.10.12
- docker-compose v2.2.3
- Git 2.25.1
- AWS CLI 2.4.25
上記の環境はすでに用意されている前提で話を進めます。
詳しく知りたい方はCI/CDまでの道を参考にしてみてください
参考書籍
このハンズオンでは以下の書籍を参考にしています。
Kubernetes on AWS ~アプリケーションエンジニア 本番環境へ備える
また、基本的なインフラ環境(VPC, subnetなど)はAWSではじめるインフラ構築入門 安全で堅牢な本番環境のつくり方を参考にしています。
詳しく知りたい方は書籍を読んでいただければと思います。
Kubernetes CI/CDまでの道シリーズ
- kubernetesでRails+Nginxをデプロイする(kubernetes CI/CDまでの道①)
開発環境の準備
eksctl
とkubectl
のインストールをWSL2で行います。
こちらのサイトからLinuxのインストールコマンドをすべて実行します。
eksctl : 0.87.0
kubectl : Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.4", GitCommit:"e6c093d87ea4cbb530a7b2ae91e54c0842d8308a", GitTreeState:"clean", BuildDate:"2022-02-16T12:38:05Z", GoVersion:"go1.17.7", Compiler:"gc", Platform:"linux/amd64"}
を利用しました。
リポジトリ
今回は以下のリポジトリを利用してハンズオンをおこないます。
$ git clone https://github.com/jinwatanabe/Kubernetes_CICD_Road
$ cd ./Kubernetes_CICD_Road/chapter0
基本環境の構築
cloudformation
というフォルダを作成して01_base_resource.yml
を作成します。
Parameters:
ClusterBaseName:
Type: String
Default: eks-work
TargetRegion:
Type: String
Default: ap-northeast-1
AvailabilityZone1:
Type: String
Default: ap-northeast-1a
AvailabilityZone2:
Type: String
Default: ap-northeast-1c
VpcBlock:
Type: String
Default: 10.0.0.0/16
WorkerSubnet1Block:
Type: String
Default: 10.0.0.0/20
WorkerSubnet2Block:
Type: String
Default: 10.0.16.0/20
WorkerSubnet3Block:
Type: String
Default: 10.0.64.0/20
WorkerSubnet4Block:
Type: String
Default: 10.0.80.0/20
Resources:
EksWorkVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcBlock
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: sample-vpc
WorkerSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone1
CidrBlock: !Ref WorkerSubnet1Block
VpcId: !Ref EksWorkVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: sample-subnet-public01
WorkerSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone2
CidrBlock: !Ref WorkerSubnet2Block
VpcId: !Ref EksWorkVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub sample-subnet-public02
WorkerSubnet3:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone1
CidrBlock: !Ref WorkerSubnet3Block
VpcId: !Ref EksWorkVPC
Tags:
- Key: Name
Value: sample-subnet-private01
WorkerSubnet4:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone2
CidrBlock: !Ref WorkerSubnet4Block
VpcId: !Ref EksWorkVPC
Tags:
- Key: Name
Value: sample-subnet-private02
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref EksWorkVPC
WorkerSubnetRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref EksWorkVPC
Tags:
- Key: Name
Value: !Sub sample-rt-public
WorkerSubnetRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref WorkerSubnetRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
WorkerSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref WorkerSubnet1
RouteTableId: !Ref WorkerSubnetRouteTable
WorkerSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref WorkerSubnet2
RouteTableId: !Ref WorkerSubnetRouteTable
SampleSgDb:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for rds
GroupName: sample-sg-db
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: sample-sg-db
VpcId: !Ref EksWorkVPC
Outputs:
VPC:
Value: !Ref EksWorkVPC
WorkerSubnets:
Value: !Join
- ","
- [!Ref WorkerSubnet1, !Ref WorkerSubnet2]
RouteTable:
Value: !Ref WorkerSubnetRouteTable
ここではVPC, subnet, RouteTable, SecurityGroup, インターネットゲートウェイを作成しています。内容は参考書籍と同じです。
次に02_rds.yml
を作成します。
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
EksWorkVPC:
Type: AWS::EC2::VPC::Id
PrivateSubnet01:
Type: AWS::EC2::Subnet::Id
PrivateSubnet02:
Type: AWS::EC2::Subnet::Id
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Resources:
# パラメータグループ
SampleDBPg:
Type: AWS::RDS::DBParameterGroup
Properties:
Family: mysql8.0
Description: sample parameter group
Tags:
- Key: Name
Value: sample-db-pg
# オプショングループ
# 削除の時エラーになる (個別削除が必要)
# SampleDBOg:
# Type: AWS::RDS::OptionGroup
# Properties:
# EngineName: mysql
# MajorEngineVersion: 8.0
# OptionGroupDescription: sample option group
# Tags:
# - Key: Name
# Value: sample-db-og
# DBサブネットグループ
SampleDBSubnet:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupName: sample-db-subnet
DBSubnetGroupDescription: sample db subnet
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
# DB
SampleDB:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage : 20
DBInstanceClass: db.t2.micro
Port: 3306
StorageType: gp2
BackupRetentionPeriod: 7
MasterUsername: admin
MasterUserPassword: password
DBInstanceIdentifier: sample-db
Engine: mysql
EngineVersion: 8.0.23
DBSubnetGroupName: !Ref SampleDBSubnet
DBParameterGroupName: !Ref SampleDBPg
# OptionGroupName: !Ref SampleDBOg
MultiAZ: true
VPCSecurityGroups:
- !Ref DBSecurityGroup
Outputs:
RDSEndpoint:
Value: !GetAtt SampleDB.Endpoint.Address
RDSを作成しています。設定内容は参考書籍と同じです。
AWSマネジメントコンソールからCloudFormation
を開きます。
「スタックの作成」をクリックします。
「テンプレートファイルのアップロード」を選択して、
「ファイルの選択」をクリックして01_base_resouce.yml
を選択します。
「次へ」をクリックします。
「スタックの名前」を「eks-work-base」として「次へ」をクリック
「次へ」をクリック
「スタックの作成」をクリック
「CREATE_COMPLETE」となれば成功です。
次に「RDS」も同じ流れで作成します。
「ファイルの選択」で「02_rds.yml」を選択します。
項目名 | 値 |
---|---|
スタックの名前 | eks-work-rds |
DBSecurityGroup | sample-db-sg |
EksWorkVPC | sample-vpc |
PrivateSubnet01 | sample-subnet-private01 |
PrivateSubnet02 | sample-subnet-private02 |
あとはすべて同じになります。「CREATE_COMPLETE」になれば成功です。
EKSクラスターの作成
以下のコマンドを実行します。
eksctl create cluster --vpc-public-subnets [WorkerSubnetsの値] --name eks-work-cluster --region ap-northeast-1 --version 1.18 --nodegroup-name eks-work-nodegroup --node-type t2.small --nodes 2 --nodes-min 2 --nodes-max 5
WorkerSubnetの値には、eks-work-base
の出力を参考に入力します。
私の場合は
eksctl create cluster --vpc-public-subnets subnet-0cb56e22f90be9f90,subnet-0bdc0562ca0e8bf86 --name eks-work-cluster --region ap-northeast-1 --version 1.18 --nodegroup-name eks-work-nodegroup --node-type t2.small --nodes 2 --nodes-min 2 --nodes-max 5
となります。
コマンドを実行するとCloudFormationでクラスター作成が始まります。
20分程度で2つのスタックが作成されます。
master.keyの作成
/config
にmaster.key
とcredentials.yml
を用意します。
Rails(Docker)をProductionモードで起動してみる (CI/CDまでの道⑤)で詳しく説明をしています。
MySQLの作成
まずはローカルでMySQLのポッドを作成してデプロイすることにします。
kubernetes
というフォルダを作成して、mysql_pod.yml
を作成します。
apiVersion: v1
kind: Service
metadata:
name: mysql-server
spec:
type: ClusterIP
ports:
- name: mysql
port: 3306
targetPort: 3306
protocol: TCP
selector:
app: mysql-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-server
spec:
selector:
matchLabels:
app: mysql-server
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql-server
spec:
containers:
- image: mysql:5.7
name: mysql
resources:
env:
- name: MYSQL_PASSWORD
value: password
- name: MYSQL_USER
value: admin
- name: MYSQL_ROOT_PASSWORD
value: password
- name: LANG
value: C.UTF-8
ports:
- containerPort: 3306
name: mysql
ここではサービスとデプロイメントを作成しています。ClusterIPを設定することで他のポッドからMySQLに接続できるようにしています。
以下のコマンドでMySQLを起動します。
$ kubectl apply -f ./kubernetes/mysql_pod.yml
# 起動確認
$ kubectl get all
以下のように表示されれば大丈夫です。
NAME READY STATUS RESTARTS AGE
pod/mysql-server-74d59686b8-k62vn 1/1 Running 0 19s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 172.20.0.1 <none> 443/TCP 12m
service/mysql-server ClusterIP 172.20.160.156 <none> 3306/TCP 19s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mysql-server 1/1 1 1 19s
NAME DESIRED CURRENT READY AGE
replicaset.apps/mysql-server-74d59686b8 1 1 1 19s
Dockerfileの修正
コンテナ間でのファイルマウントの関係でProduction
で最後に行っていた起動コマンドをコメントにします。
init_containerを利用するのですが、そこでコンテナが落ちるようにするためです。
(省略)
# production
FROM compile as production
ENV RAILS_ENV=production
VOLUME /myapp/public
VOLUME /myapp/tmp
# CMD /bin/sh -c "bundle exec puma -C config/puma.rb"
またそれに伴い、docker-compose.production.yml
も修正します。
version: "3.7"
services:
rails:
build:
context: .
target: production
container_name: rails
volumes:
- .:/myapp
- public-data:/myapp/public
- tmp-data:/myapp/tmp/sockets
- log-data:/myapp/log
- /myapp/node_modules
command: ['/bin/sh', '-c', 'bundle exec puma -C config/puma.rb']
env_file:
- .env
depends_on:
- db
environment:
DB_HOST: db
DB_USERNAME: root
db:
image: mysql:8.0.27
container_name: db
environment:
TZ: Asia/Tokyo
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- db:/var/lib/mysql
web:
build:
context: containers/nginx
container_name: nginx
volumes:
- public-data:/myapp/public
- tmp-data:/myapp/tmp/sockets
ports:
- 80:80
depends_on:
- rails
volumes:
db:
driver: local
public-data:
tmp-data:
log-data:
起動コマンドを実行するように変更しました。
イメージの作成
ECRにsample-rails
、sample-nginx
リポジトリを作成します。
RailsとNginxのイメージをPushします。
基本的にはプッシュコマンド通りに実行しますが、2つ目を以下に変更します。
# Railsの2つ目のコマンド
$ docker build --target production --no-cache -t sample-rails .
# Nginxの2つ目のコマンド
$ docker build -f ./containers/nginx/Dockerfile -t sample-nginx .
RailsとNginxの起動
kubernetes/
にdeployment_rails.yml
を作成します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: rails
labels:
app: rails
spec:
replicas: 2
selector:
matchLabels:
app: rails
template:
metadata:
labels:
app: rails
spec:
volumes:
- name: public-data
emptyDir: {}
- name: tmp-data
emptyDir: {}
initContainers:
- name: pre-rails
image: [ECRのRailsイメージURL]
command: ['/bin/sh', '-c', 'cp -a /myapp/public/* /mnt/empty-dir-content/']
volumeMounts:
- name: public-data
mountPath: "/mnt/empty-dir-content/"
containers:
- name: rails
image: [ECRのRailsイメージURL]
ports:
- containerPort: 3000
command: ['/bin/sh', '-c', 'bundle exec puma -C config/puma.rb']
env:
- name: DB_USERNAME
value: root
# value: admin
- name: DB_PASSWORD
value: password
- name: DB_DATABASE
value: myapp
- name: DB_HOST
# value: [RDSのエンドポイント]
value: mysql-server
volumeMounts:
- name: tmp-data
mountPath: /myapp/tmp/sockets
- name: nginx
image: [ECRのNginxイメージURL]
ports:
- containerPort: 80
volumeMounts:
- name: public-data
mountPath: /myapp/public
- name: tmp-data
mountPath: /myapp/tmp/sockets
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: 250m
memory: 769Mi
RailsとNginxのイメージURLはECRから参照してください。
RDSのエンドポイントは、CloudFormationのeks-work-rds
の出力から確認ができます。
起動をしてみます。
$ kubectl apply -f ./kubernetes/deployment_rails.yml
# 確認
$ kubectl get all
pod/mysql-server-74d59686b8-k62vn 1/1 Running 0 14m
pod/rails-668f498ffb-574s4 2/2 Running 0 50s
pod/rails-668f498ffb-rg78j 2/2 Running 0 50s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 172.20.0.1 <none> 443/TCP 26m
service/mysql-server ClusterIP 172.20.160.156 <none> 3306/TCP 14m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mysql-server 1/1 1 1 14m
deployment.apps/rails 2/2 2 2 50s
NAME DESIRED CURRENT READY AGE
replicaset.apps/mysql-server-74d59686b8 1 1 1 14m
replicaset.apps/rails-668f498ffb 2 2 2 50s
デプロイすることができました。
RailsとNginxを起動する上でのポイントはファイルマウントにinit_container
を利用していることです。
Kubernetesでファイル共有をしたがコンテナにあったファイルが空になってしまう
ローカルでの検証
以下のコマンドで試しにアクセスしてみます。
# コンテナに入る
$ kubectl exec -it [ポッドの名前] sh
$ rails db:migrate
$ kubectl port-forward [ポッドの名前] 8080:80
ポッドの名前はkubectl get all
したときにポッド名にrails
が含まれているもの2つのうちどちらかになります。(本当は1つのポッドに1つのコンテナがふさわしいです)
コンテナに入ってls
で確認してからmigrate
します。
localhost:8080/test
にアクセスします。表示されれば成功です。
終わったらCtrl+Cでport-forwardを消します。
RDSにDBを変更
deployment_rails.yml
の
DB_USERNAME
をadmin
DB_HOST
のValueをRDSのエンドポイント
に変更します。
(省略)
env:
- name: DB_USERNAME
# value: root
value: admin
- name: DB_PASSWORD
value: password
- name: DB_DATABASE
value: myapp
- name: DB_HOST
value: sample-db.cmb2t0anuznk.ap-northeast-1.rds.amazonaws.com
# value: [RDSのエンドポイント]
# value: mysql-server
(省略)
以下のコマンドでデプロイの更新を行います。
$ kubectl apply -f ./kubernetes/deployment_rails.yml
$ kubectl get all
$ kubectl exec -it [ポッドの名前] sh
$ rails db:migrate
ポッドの名前は先ほど変わっているため注意が必要です。
アクセスして確認します。
$ kubectl port-forward [ポッド名] 8080:80
localhost:8080/test
にアクセスできれば成功です。
終わったらCtrl+Cでport-forwardを消します。
ロードバランサーの設定
/kuberntest
にservice.yml
を作成します。
apiVersion: v1
kind: Service
metadata:
name: rails-server
spec:
type: LoadBalancer
selector:
app: rails
ports:
- protocol: TCP
port: 80
targetPort: 80
以下のコマンドでロードバランサーを作成します。
$ kubectl apply -f ./kubernetes/service.yml
マネジメントコンソールで「EC2」→「ロードバランサー」→「インスタンス」から状況を確認します。
「ステータス」が「InService」となっていれば準備ができています。
以下のコマンドでエンドポイントを確認します。
$ kubectl get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 172.20.0.1 <none> 443/TCP 52m
service/mysql-server ClusterIP 172.20.160.156 <none> 3306/TCP 39m
service/rails-server LoadBalancer 172.20.139.103 ab3e3414309ad4427b7b7f92c27b7f7b-1395408648.ap-northeast-1.elb.amazonaws.com 80:32538/TCP 3m47s
サービスの「EXTERNAL-IP」をコピーしてブラウザで
EXTERNAL-IP/test
にアクセスします。
ロードバランサーを介してアクセスすることができました。
お片付け
以下のコマンドでEKS関連を削除します。
$ kubectl delete deployment rails
$ kubectl delete service rails-server
$ kubectl delete deployment mysql-server
$ kubectl delete service mysql-server
$ kubectl delete service kubernetes
$ eksctl delete cluster --name eks-work-cluster
次にCloudFormationで立ち上げたものをすべて削除します。
最後にECRで作成した2つのリポジトリの削除します。
おまけ
起動ができない場合は以下のコマンドでログを確認してください。
$ kubectl logs [ポッドの名前] --container [コンテナ名(ここではrails or nginx)]
MySQLコンテナには以下では入れます。
$ kubectl exec -it $(kubectl get pod | grep mysql | cut -d " " -f 1) sh
おわりに
本日作成したものは以下のリポジトリのchapter1
にあります。
今回はKubernetesの基本の基本を学ぶことができました。
次回はAWSのCodeサービスを用いたCI/CDにチャレンジしたいと思います。
Discussion