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