なになれ

IT系のことを記録していきます

MEAN StackをKubernetesで動かす(その1)

前回はMEAN StackをDocker Composeで動かしてみました。
hi1280.hatenablog.com

今回はMEAN StackをKubernetesで動かします。
Dockerに対応したので、Kubernetesでも動くようになっているはずです。試してみます。

なお、今回のKubernetesの環境はGKE(Google Kubernetes Engine)を使用します。
手軽にKubernetes環境が用意できるので、試しに使ってみるには良いと思います。
無料のトライアル期間もあります。

MEAN Stackのプログラム一式はこちら
github.com

Docker Composeの時と似たような構成でAngularとExpressとMongoDBを動かします。
実運用に耐えうることを想定して、以下の内容を含めます。

  • MongoDBは可用性を高めるためにReplica Setで構成する
  • 外部公開を考慮して、HTTPS対応を行う

HTTPS対応の話は次回に行います。今回はHTTPでアクセスするところまでです。

事前準備

GKEを利用するために、GCP(Google Cloud Platform)用のコマンドラインツール(gcloud)をインストールします。
Quickstarts  |  Cloud SDK  |  Google Cloud

kubectlコマンドをインストールします。
Quickstart  |  Kubernetes Engine Documentation  |  Google Cloud

gcloudコマンドでKubernetesクラスタを構築します。

$ gcloud container clusters create mean-example

MongoDBのセットアップ

MongoDBを作成する前にデータを保存するディスクを準備する必要があります。

ディスクの設定

何のディスクを利用するのかを決めるためにStorageClassを作成します。
ここでは、GCE(Google Compute Engine)のディスクを利用するように設定します。

gce-storageclass.yml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
$ kubectl apply -f gce-storageclass.yml

MongoDBの作成

Replica Setで認証を行うためにkeyfileを用意します。
KubernetesのSecretに登録します。

$ TMPFILE=$(mktemp)
$ /usr/bin/openssl rand -base64 741 > $TMPFILE
$ kubectl create secret generic shared-bootstrap-data --from-file=internal-auth-mongodb-keyfile=$TMPFILE
$ rm $TMPFILE

Replica Setにおける認証の参考情報
Internal Authentication — MongoDB Manual

MongoDB用のStatefulSetとServiceを作成します。

mongodb-service.yml

apiVersion: v1
kind: Service
metadata:
  name: mongodb-service
  labels:
    name: mongo
spec:
  ports:
  - port: 27017
    targetPort: 27017
  clusterIP: None
  selector:
    role: mongo
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongod
spec:
  selector:
    matchLabels:
      role: mongo
  serviceName: mongodb-service
  replicas: 3
  template:
    metadata:
      labels:
        role: mongo
        environment: test
        replicaset: MainRepSet
    spec:
      terminationGracePeriodSeconds: 10
      volumes:
        - name: secrets-volume
          secret:
            secretName: shared-bootstrap-data
            defaultMode: 256
      containers:
        - name: mongod-container
          image: mongo:3.4.16-jessie
          command:
            - "mongod"
            - "--bind_ip"
            - "0.0.0.0"
            - "--replSet"
            - "MainRepSet"
            - "--auth"
            - "--clusterAuthMode"
            - "keyFile"
            - "--keyFile"
            - "/etc/secrets-volume/internal-auth-mongodb-keyfile"
            - "--setParameter"
            - "authenticationMechanisms=SCRAM-SHA-1"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: secrets-volume
              readOnly: true
              mountPath: /etc/secrets-volume
            - name: mongodb-persistent-storage-claim
              mountPath: /data/db
  volumeClaimTemplates:
  - metadata:
      name: mongodb-persistent-storage-claim
      annotations:
        volume.beta.kubernetes.io/storage-class: "slow"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

ServiceはHeadless Serviceで作成します。
この後にReplica Setの設定を行う際に、PodのIPアドレスを知る必要があるためです。

StatefulSetではMongoDBコンテナが起動する3つのPodを作成します。
3つのPodがReplica Setで利用されます。
mongodコマンドで、事前に作成したSecretを利用して認証を有効にします。 volumeClaimTemplatesで先ほどのStorageClassを指定しています。
Pod毎に10Gのディスクが設定されます。

$ kubectl apply -f mongodb-service.yml

MongoDBの設定

PrimaryになるMongoDBに接続します。

$ kubectl exec -it mongod-0 -c mongod-container bash
$ mongo

Replica Setを設定します。

> rs.initiate({_id: "MainRepSet", version: 1, members: [
       { _id: 0, host : "mongod-0.mongodb-service:27017" },
       { _id: 1, host : "mongod-1.mongodb-service:27017" },
       { _id: 2, host : "mongod-2.mongodb-service:27017" }
  ]})

ユーザを作成します。

> db.getSiblingDB("admin").createUser({
      user : "xxx",
      pwd  : "xxx",
      roles: [ { role: "root", db: "admin" } ]
  })
> use admin
> db.auth("xxx","xxx")
> db.getSiblingDB("my-heroes").createUser({
      user : "xxx",
      pwd : "xxx",
      roles: [{role:"readWrite", db: "my-heroes"}]
  })

ユーザ、パスワードは適宜設定する。

Angular+Expressの作成

Expressを作成します。

webapp-backend-service.yml

apiVersion: v1
kind: Service
metadata:
  name: webapp-backend-service
  labels:
    name: webapp-backend
spec:
  type: ClusterIP
  ports:
    - protocol: 'TCP'
      port: 3000
      targetPort: 3000
  selector:
    app: webapp-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp-backend
  template:
    metadata:
      labels:
        app: webapp-backend
    spec:
      containers:
      - name: webapp-backend
        image: asia.gcr.io/arched-photon-204013/mean-example_backend:latest
        ports:
        - containerPort: 3000
        env:
        - name: PORT
          value: '3000'
        - name: MONGODB_URI
          value: 'mongodb://xxx:xxx@mongod-0.mongodb-service:27017,mongod-1.mongodb-service:27017,mongod-2.mongodb-service:27017/my-heroes?replicaSet=MainRepSet'

コンテナレジストリにはGCR(Google Container Registry)を利用しています。
前回のExpress実行用のDockerfileから作成したイメージです。

Replica SetのMongoDBに接続するため、3つのMongoDBをホスト名で指定しています。

$ kubectl apply -f webapp-backend-service.yml

Angularを作成します。

webapp-frontend-service.yml

apiVersion: v1
kind: Service
metadata:
  name: webapp-frontend-service
  labels:
    app: webapp-frontend
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 80
      targetPort: 80
  selector:
    app: webapp-frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp-frontend
  template:
    metadata:
      labels:
        app: webapp-frontend
    spec:
      containers:
      - name: webapp-frontend
        image: asia.gcr.io/arched-photon-204013/mean-example_frontend:latest
        ports:
        - containerPort: 80
        env:
        - name: APP_HOST
          value: 'webapp-backend-service'
        - name: APP_PORT
          value: '3000'
        command: ['/bin/sh']
        args: ['-c', "envsubst '$$APP_HOST$$APP_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]

HTTPSに対応するためにIngressを利用することになります。Ingressを利用するためには、事前にNodePortのServiceを作成しておく必要があります。

コンテナイメージに関しては、前回のAngularアプリ実行用のDockerfileから作成したイメージです。

$ kubectl apply -f webapp-frontend-service.yml

外部からの疎通が可能なネットワークの設定

Ingressを作成します。

ingress.yml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: webapp-service
  labels:
    app: webapp
spec:
  backend:
    serviceName: webapp-frontend-service
    servicePort: 80

backendでは先ほど作成したNodePortのServiceを指定しています。

$ kubectl apply -f ingress.yml

Ingressを作成することで、GCLB(Google Cloud Load Balancer)が設定されます。

$ kubectl get ingress

NAME             HOSTS     ADDRESS           PORTS     AGE
webapp-service   *         xxx.xxx.xxx.xxx   80        5m

5分ほどで静的IPアドレスが割り当てられて、アクセスできるようになります。

HTTPS対応の話は次回に。

参考にした資料

Kubernetesの基礎情報 thinkit.co.jp

MongoDBのKubernetes対応 pauldone.blogspot.com

MEAN StackをDocker Composeで動かす

前回はMEAN StackをHeroku上で動かしてみました。
hi1280.hatenablog.com

今回はMEAN Stackアプリに手を加えて、Dockerに対応させます。
Dockerにすれば、他の環境への移行も楽になるのではと思います。
Docker Composeを使って、nginx+Node.js+MongoDBの環境でMEAN Stackアプリを動かします。

プログラム一式はこちら
github.com

Docker Composeを使って、以下の3つのコンテナで構成します。

  • nginxでAngularを動かす(フロントエンドアプリ)
  • Node.jsでExpressを動かす(バックエンドアプリ)
  • MongoDBを動かす(DB)

Angularビルド環境用のDockerfile

AngularビルドのためにAngular CLI環境のDockerfileを作ります。

Dockerfile

FROM node:8.11.3-alpine
LABEL authors="hi1280"
RUN npm install -g @angular/cli@6.0.8
RUN npm cache clean --force

Node.jsの環境にAngular CLIをインストールしているだけというシンプルなモノです。

Angularアプリ実行用のDockerfile

Angularをビルドして、それをそのままDockerコンテナに含めると容量がかなり大きくなってしまいます。
そのため、Angularビルドとnginx上でのAngularアプリの実行を分けます。
これはDockerのマルチステージビルドの機能を使えば、実現できます。

Use multi-stage builds | Docker Documentation

マルチステージビルドを用いたDockerfileを作ります。

Dockerfile

# Angular Build
FROM hi1280/angular-cli:latest as build-stage
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm install --production --silent
COPY . .
RUN npm run build
# nginx
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build-stage /usr/src/app/dist/mean-example  /usr/share/nginx/html
COPY ./nginx /etc/nginx/conf.d
CMD ["nginx", "-g", "daemon off;"]

COPY --from=build-stageでビルド後の成果物のみをコピーしています。
これにより、最小限のファイルサイズになります。
AngularでビルドしたHTMLリソースをnginxで動かすDockerコンテナになります。

Express実行用のDockerfile

Expressを動かすDockerfileを作ります。

Dockerfile

FROM node:8.11.3-alpine
WORKDIR /usr/src/app
COPY ["server.js", "/src/server/package.json", "/src/server/package-lock.json", "./"]
COPY ["/src/server", "./src/server"]
RUN npm install --production --silent
EXPOSE 3000
ENV NODE_ENV production
CMD [ "node", "server.js" ]

Node.jsの環境を公式のイメージから取得して、Expressを動かすだけのDockerコンテナになります。

Docker Composeで各コンテナを連携

これまでに用意した各Dockerコンテナを連携します。

docker-compose.yml

version: '2.1'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile-frontend
    ports:
      - 80:80
    environment:
      - APP_HOST=backend
      - APP_PORT=3000
    command: /bin/sh -c "envsubst '$$APP_HOST$$APP_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
    depends_on:
      - backend
  backend:
    build:
      context: .
      dockerfile: Dockerfile-backend
    ports:
      - 3000:3000
    depends_on:
      - db
    environment: 
      - MONGODB_URI=mongodb://db:27017/my-heroes
  db:
    image: mongo:3.4.16-jessie
    restart: always
    volumes: 
      - /data/db:/data/db

URLにサービス名を指定することで、各サービスにアクセスすることができます。
mongodb://db:27017/my-heroesといった形です。
depends_onを指定することで、サービスを立ち上げるために事前に起動が必要なサービスを指定できます。
これにより、サービスを起動する順番を制御することができます。

Docker Composeにおけるnginxの設定変更

環境変数を用いて動的にnginxの設定を変更しています。
nginxの公式Dockerイメージにそのやり方が記載されています。(Using environment variables in nginx configurationという箇所です)
https://hub.docker.com/_/nginx/
こちらも参考にしました。
Docker上のNginxのconfに環境変数(env)を渡すたったひとつの全く優れてない方法(修正:+優れている方法)

default.conf.template

server {
    listen       80;
    server_name  localhost;

    location /api/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_pass http://${APP_HOST}:${APP_PORT}/api/;
    }

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

}

${APP_HOST}と${APP_PORT}が環境変数で定義した値に変わります。
Expressのサービス名にアクセスする設定になります。

まとめ

公式のDockerイメージを活用すれば、プログラムをコピーして、起動コマンドを実行するといった形で、Dockerコンテナ化できるので難しいことはあまりないかなと思います。
ハマったら、コンテナのシェルに入ってデバッグするといったことができると良いです。

MEAN StackをHeroku上で動かす

MEAN Stackを運用するサービスを検討していましたが、Herokuが良いだろうという結論になり、実際に使ってみました。
Herokuを選んだ決め手は以下のとおりです。

  • Node.jsに対応している
  • MongoDBがアドオンで簡単に使える

Herokuのドキュメントを参考にして、自分なりにやってみました。
Create a Web App and RESTful API Server Using the MEAN Stack | Heroku Dev Center

MEAN Stackの構成

MEAN Stackのアプリを用意しました。
github.com Node.jsでExpressのAPIサーバとAngularで作成したフロントのコンテンツの両方を動かす形です。
これをHerokuでそのまま動かそうという算段です。

プログラムの参考元はこちら github.com

Herokuへのデプロイ

Heroku CLIをインストールします。
The Heroku CLI | Heroku Dev Center

Herokuのアプリ環境を作成します。

$ heroku create
...
Creating app... done, ⬢ secret-scrubland-78144
https://secret-scrubland-78144.herokuapp.com/ | https://git.heroku.com/secret-scrubland-78144.git

gitの設定にherokuというリポジトリが追加されています。

$ git config --list
...
remote.heroku.url=https://git.heroku.com/secret-scrubland-78144.git
remote.heroku.fetch=+refs/heads/*:refs/remotes/heroku/*

デプロイを行うには、このリポジトリにPushします。

$ git push heroku master

MongoDBの追加

HerokuにMongoDBを作成するには、mLabのアドオンを追加します。

$ heroku addons:create mongolab

DBの接続情報はMONGODB_URIという環境変数で取得できます。
今回のプログラムでは既にこの環境変数を使用して、MongoDBに接続するようになっています。

Heroku環境にアクセス

$ heroku open

こちらが今回作成したデモ環境です。
https://secret-scrubland-78144.herokuapp.com/

まとめ

Herokuを使えばMEAN Stackの運用環境を簡単に作成できることが分かりました。