2019年5月31日金曜日

k8s の StatefulSet を試してみた

概要

StatefulSet は Pod 名が常に同じ名前で起動し続ける機能です
これと Headless Service を組み合わせることで Pod の IP アドレスが変わっても名前を使って常に同じ Pod を参照し続けることができます
例えばクラスタ環境を構築する場合などに使います
今回は nginx コンテナを使って StatefulSet と Headless Service の挙動を確認してみました

環境

  • macOS 10.14.5
  • minikube v0.28.2

StatefulSet の yml ファイル

今回は nginx のコンテナを StatefulSet を使ってデプロイしてみます

  • vim nginx_sts.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /tmp
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

StatefulSet で構築した Pods には replicas で指定した分の Pods が作成されます
また Pods には 0 から順番にインデックス番号が振られ Pods が再作成されても同じ番号が振られます
volumeClaimTemplates を定義するのもポイントで StatefulSet の場合はデフォルトで PersistentVolumeClaim ありの状態で Pods が上がってくるのが基本です
今回は /tmp にマウントしています
DocumentRoot が /usr/share/nginx/html なのでここをマウントしてもいいのですが index.html が上書きでなくなってしまいます
PersistentVolume に cp してもいいのですが今回はテストなのでマウントポイントを変更することで対応しています

matchLabelslabelsapp: nginx は必ず同じラベルを振るようにしてください
Pods に振った app: nginx というラベルに対して StatefulSet のルールを適用するためです

これで StatefulSet をデプロイしましょう
replicas で指定した数になるまで Pods が作成されます

  • kubectl apply -f nginx_sts.yml

Service の yml ファイル

Service もポイントで Headless Service という仕組みを使うことで Pods 同士を名前でアクセスできるようにします

  • vim nginx_svc.yml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx

clusterIP: None にするのがポイントです
これで Service をデプロイしましょう

  • kubectl apply -f nginx_svc.yml

今回作成したリソースは以下の通りです

  • kubectl get sts,pv,pvc,po,svc
NAME                   DESIRED   CURRENT   AGE
statefulset.apps/web   3         3         32m

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   REASON   AGE
persistentvolume/pvc-160b82d3-81a9-11e9-b396-08002719d78a   1Gi        RWO            Delete           Bound    default/www-web-0   standard                32m
persistentvolume/pvc-21951e03-81a9-11e9-b396-08002719d78a   1Gi        RWO            Delete           Bound    default/www-web-1   standard                32m
persistentvolume/pvc-28dd3ed2-81a9-11e9-b396-08002719d78a   1Gi        RWO            Delete           Bound    default/www-web-2   standard                32m

NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/www-web-0   Bound    pvc-160b82d3-81a9-11e9-b396-08002719d78a   1Gi        RWO            standard       32m
persistentvolumeclaim/www-web-1   Bound    pvc-21951e03-81a9-11e9-b396-08002719d78a   1Gi        RWO            standard       32m
persistentvolumeclaim/www-web-2   Bound    pvc-28dd3ed2-81a9-11e9-b396-08002719d78a   1Gi        RWO            standard       32m

NAME        READY   STATUS    RESTARTS   AGE
pod/web-0   1/1     Running   0          32m
pod/web-1   1/1     Running   0          32m
pod/web-2   1/1     Running   0          32m

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   120d
service/nginx        ClusterIP   None         <none>        80/TCP    2m

ちなみに PersistentVolume は standard という StorageClass を使っており standard は何かというとホストマシンの特定のパスをマウントする StorageClass になります

  • kubectl describe pv pvc-160b82d3-81a9-11e9-b396-08002719d78a
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /tmp/hostpath-provisioner/pvc-160b82d3-81a9-11e9-b396-08002719d78a

でこのホストは何かと言うと minikube で起動している Virtualbox の VM になります
実際に ssh して確認すると該当のパスが存在するのが確認できます

  • minikube ssh
  • ls /tmp/hostpath-provisioner/pvc-160b82d3-81a9-11e9-b396-08002719d78a/

Pods を再作成しても同じ状態で上がってくることを確認する

StatefulSet の機能を確認していきます
冒頭でも少し触れましたが StatefulSet 経由で作成した Pod にはインデックス番号が振られます
そして一度振られた番号は何度 Pod を再作成しても同じ状態で起動します
その挙動を確認してみます

まず Headless Service が有効になっているか確認します
各 Pod が DNS を使って名前でアクセスできれば OK です
nslookup が nginx コンテナでは使えないので確認用の Pod を作成します

  • kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 172.17.0.7 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 172.17.0.8 web-1.nginx.default.svc.cluster.local
/ # nslookup web-2.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-2.nginx
Address 1: 172.17.0.9 web-2.nginx.default.svc.cluster.local

こんな感じで StatefulSet 経由で作成した各 Pod が Headless Service のおかげで名前でアクセスできるようになっています

ではこの状態で Pod を削除してみましょう

  • kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
pod "web-2" deleted

しばらくすると StatefulSet が再度 Pod を作成してくれます
Running になったら再度 nslookup コマンドを実行してみましょう
先ほどと同じ名前で IP が引けることが確認できると思います
Pod を再作成すると IP が変わることがありますが名前が常に同じ状態なので名前さえわかっていれば常に同じ Pod にアクセスすることができるようになります
これが StatefulSet のメイン機能になります

nginx にアクセスするにはどうすればいいか

Pod に直接 exec すればいいのですがそれだと面倒な場合が多いです
一番てっとり早いのはもう一つ公開用の Service をデプロイするのが簡単です

  • vim nginx_svc_ext.yml
apiVersion: v1
kind: Service
metadata:
  name: nginx-ext
  labels:
    app: nginx
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  • kubectl apply -f nginx_svc_ext.yml
  • kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        120d
nginx        ClusterIP   None             <none>        80/TCP         2m
nginx-ext    NodePort    10.102.236.113   <none>        80:31481/TCP   1m

ちゃんと各 Pod にアクセスできているか確認するために index.html を書き換えます

  • for i in 0 2; do kubectl exec web-$i -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'; done

あとは minikube 経由でアクセスすれば nginx が見えると思います

  • curl $(minikube ip):31481

ちゃんと Headless Service もあるので Pods 内で名前解決も引き続きできると思います

後処理

  • kubectl delete sts web
  • kubectl delete pvc www-web-0 www-web-1 www-web-2
  • kubectl delete svc nginx nginx-ext

最後に

minikube で StatefulSet を試してみました
ポイントは

  • PersistentVolumeClaim と組み合わせて使う
  • Headless Service を使うことで Pod を名前で引けるようにする
  • 常に同じ Pod 名で起動するので名前で引ければ常に同じ Pod にアクセスすることができる

になるかなと思います
今回のサンプルの場合は PersistentVolumeClaim は必須ではないかなと思います
が、Pod が削除された場合に同じ Pod 名で起動するけどデータがないというのは少しステートフルとは違うかなと思うので基本は PersistentVolumeClaim と組み合わせて使うのが良いと思います

よく Deployment と比較されますが Deployment は Pod 名が固定にはならないので、そこがステートレスと言われているポイントなのかなと思います

参考サイト

2019年5月30日木曜日

minikube で Redis コンテナを立ち上げる方法を考える

概要

シングルノード、Sentinel、Cluster のそれぞれを k8s 上でコンテナとして起動しアクセスする方法を考えます
k8s 環境は minikube を使います 

環境

  • macOS 10.14.5
  • minikube v0.28.2

minikube 起動

  • minikube start

シングルノードで起動

まずは redis 1 台で起動させてみます

Deployment 作成

Pod 直接ではなく Deploment を使います

  • vim redis_deployment.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: redis-master
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: master
    spec:
      containers:
      - name: master
        image: redis
        ports:
        - containerPort: 6379

特にポイントはないです
labels に関しては好きなラベルを設定してください
あとで Service と紐付けるのに使います

  • kubectl apply -f redis_deployment.yml
  • kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
redis-master-86f6bffc7f-xrzht   1/1     Running   0          7m

デプロイして Pod が起動すれば OK です
redis-cli コマンドを使ってコンテナが起動しているか確認しましょう

  • kubectl exec redis-master-86f6bffc7f-xrzht redis-cli info replication
# Replication
role:master
connected_slaves:0
master_replid:695cd5de82eb682d3ebd2f20dc39dc1cb3ee05a6
master_replid2:0000000000000000000000000000000000000000

Service 作成

ホストマシンからアクセスできるように Service を作ります

  • vim redis_service.yml
apiVersion: v1
kind: Service
metadata:
  name: redis-single
spec:
  type: NodePort
  selector:
    role: single
  ports:
  - protocol: TCP
    port: 6379
    targetPort: 6379

Ingress は面倒なので type: NodePort を使います
minikube -> VirtualBox -> コンテナという経路でアクセスすることになります

  • kubectl apply -f redis_service.yml
  • redis-cli -h 192.168.99.100 -p 31267 info replication

で先ほどと同じ結果が得られると思います
ちなみにこれらの IP アドレスとポートは Mac から見える minikube 用の Virtualbox のIP とポートになります

  • minikube ip
  • minikube service redis-single --format "{{.Port}}"

ちなみにこの状態で Deployment の replicas: 1replicas: 3 などにすると Pod が 3 台になります
そして上記の redis-cli でアクセスすると各コンテナに分散してアクセスしているので確認できると思います

Sentinel で起動

次に Sentinel 環境を考えます
戦略として master, slave, sentinel それぞれで Deployment と Serivce を作ってお互いを通信する感じです

master Deployment 作成

まず master 用のコンテナを作成するための Deployment を作成します

  • vim redis_master_deployment.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: redis-master
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: master
    spec:
      containers:
      - name: master
        image: redis
        ports:
        - containerPort: 6379
  • kubectl apply -f redis_master_deployment.yml

これは先程とほぼ変わりません

  • kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
redis-master-86f6bffc7f-kx4sc   1/1     Running   0          3m
  • kubectl exec redis-master-86f6bffc7f-kx4sc redis-cli info replication
role:master
connected_slaves:0

master Service 作成

次に master 用のコンテナに他のコンテナからアクセスできるように Service を定義します

  • vim redis_master_service.yml
apiVersion: v1
kind: Service
metadata:
  name: redis-master
spec:
  type: NodePort
  selector:
    role: master
  ports:
  - protocol: TCP
    port: 6379
    targetPort: 6379
  • kubectl apply -f redis_master_service.yml
  • kubectl get ep redis-master

これで master にアクセスするためのエンドポイントが表示されます

NAME           ENDPOINTS         AGE
redis-master   172.17.0.7:6379   3s

slave Deplyment 作成

次に slave コンテナ用の Deployment を作成します

  • vim redis_slave_deployment.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: redis-slave
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: slave
    spec:
      containers:
      - name: slave
        image: redis
        ports:
        - containerPort: 6379
        command: ["redis-server"]
        args: ["--slaveof", "172.17.0.7", "6379"]

master の Deployment に command を追加しています
--slaveof オプションを使って master のエンドポイントを指定することで slave として redis-server が立ち上がるようにしています

これで起動し redis-cli でレプリケーションの状態を確認してみましょう

  • kubectl apply -f redis_slave_deployment.yml
  • kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
redis-master-86f6bffc7f-kx4sc   1/1     Running   0          6m
redis-slave-5556967686-l7t58    1/1     Running   0          4m
  • kubectl exec redis-slave-5556967686-l7t58 redis-cli info replication
role:slave
master_host:172.17.0.7
master_port:6379

こんな感じで master のエンドポイントを参照し slave になっていれば OK です

slave Service 作成

failover した際に slave にアクセスすることになるので slave も Service を作成しておきます

  • vim redis_slave_service.yml
apiVersion: v1
kind: Service
metadata:
  name: redis-slave
spec:
  type: NodePort
  selector:
    role: slave
  ports:
  - protocol: TCP
    port: 6379
    targetPort: 6379
  • kubectl apply -f redis_slave_service.yml
  • kubectl get ep redis-slave
NAME          ENDPOINTS         AGE
redis-slave   172.17.0.8:6379   17s

sentinel Deployment 作成

作成した master/slave を Sentinel で監視し自動 failover できるようにします
まず Sentinel 用のイメージを作成します

  • mkdir sentinel
  • cd sentinel
  • vim sentinel.conf
port 26379
dir /tmp
sentinel monitor mymaster 172.17.0.7 6379 1
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 5000

sentinel monitor で指定する IP は master のエンドポイントを指定します

  • vim Dockerfile
FROM redis

ADD sentinel.conf /etc/redis/sentinel.conf
RUN chown redis:redis /etc/redis/sentinel.conf

CMD ["redis-server", "/etc/redis/sentinel.conf", "--sentinel"]
  • eval $(minikube docker-env)
  • docker build -t sentinel:v1 .

これでイメージが minikube で起動した Virtualbox 上に作成されます
このイメージを使って Sentinel の Deloyment を定義します

  • vim redis_sentinel_deployment.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: redis-sentinel
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: sentinel
    spec:
      containers:
      - name: sentinel
        image: sentinel:v1
        ports:
        - containerPort: 26379

containerPort は sentinel.conf で指定したポートに合わせます
また image も先程作成したイメージに書き換えます
バージョンの指定も忘れずに行いましょう

  • kubectl apply -f redis_sentinel_deployment.yml
  • kubectl get po
NAME                              READY   STATUS    RESTARTS   AGE
redis-master-86f6bffc7f-kx4sc     1/1     Running   0          18m
redis-sentinel-6d775867b9-pqtpj   1/1     Running   0          9s
redis-slave-5556967686-l7t58      1/1     Running   0          16m

これで master, slave, sentinel の必要なコンテナが揃いました

sentinel Service 作成

Sentinel API を叩くので一応 Service 登録しておきます

  • vim redis_sentinel_service.yml
apiVersion: v1
kind: Service
metadata:
  name: redis-sentinel
spec:
  type: NodePort
  selector:
    role: sentinel
  ports:
  - protocol: TCP
    port: 26379
    targetPort: 26379
  • kubectl apply -f redis_sentinel_service.yml

ホストから Sentinel API にアクセスできるか確認します

  • minikube ip
  • minikube service redis-sentinel --format "{{.Port}}"

でホストからアクセスする IP とポートを確認し redis-cli を実行します

  • redis-cli -h $(minikube ip) -p 31293 sentinel get-master-addr-by-name mymaster
1) "172.17.0.7"
2) "6379"

こんな感じでちゃんと master が見えていれば OK です

動作確認

failover させてちゃんと master が切り替わるか確認します
k8s には Pod を停止する方法はないので Deployment を削除します

  • kubectl delete deploy redis-master

このときの Sentinel のログは以下の通りです

  • kubectl logs -f redis-sentinel-6d775867b9-pqtpj
1:X 28 May 2019 07:17:12.368 # +failover-state-reconf-slaves master mymaster 172.17.0.7 6379
1:X 28 May 2019 07:17:12.428 # +failover-end master mymaster 172.17.0.7 6379
1:X 28 May 2019 07:17:12.429 # +switch-master mymaster 172.17.0.7 6379 172.17.0.8 6379
1:X 28 May 2019 07:17:12.431 * +slave slave 172.17.0.7:6379 172.17.0.7 6379 @ mymaster 172.17.0.8 6379
1:X 28 May 2019 07:17:17.449 # +sdown slave 172.17.0.7:6379 172.17.0.7 6379 @ mymaster 172.17.0.8 6379

ちゃんと failover していることが確認できます
slave の Service にアクセスすると master に昇格していることがわかると思います

  • redis-cli -h $(minikube ip) -p 32109 info replication
role:master
connected_slaves:0

slave は 0 台になっています
これは元 master がまだダウンしているためです
再度 master を Deployment してみましょう

  • kubectl apply -f redis_master_deployment.yml

Sentinel のログを見ていると以下のようなログが流れると思います

1:X 28 May 2019 07:24:34.164 # -sdown slave 172.17.0.7:6379 172.17.0.7 6379 @ mymaster 172.17.0.8 6379
1:X 28 May 2019 07:24:44.133 * +convert-to-slave slave 172.17.0.7:6379 172.17.0.7 6379 @ mymaster 172.17.0.8 6379

これで再度 slave の数を確認すると connected_slaves:1 になっていることが確認できると思います
実際に master と slave に対して redis-cliget/set してみると更に挙動を確認できると思います

$ redis-cli -h $(minikube ip) -p 32582 set a a
(error) READONLY You can't write against a read only replica.
$ redis-cli -h $(minikube ip) -p 32109 set a a
OK
$ redis-cli -h $(minikube ip) -p 32582 get a
"a"

Cluster で起動

ちょっと長くなってしまったので別記事で紹介したいと思います
どうやら Redis Cluster の場合はステートフルなので StatefulSets を使ったほうが良いみたいです

最後に

k8s 上に Redis のシングルノード環境と Sentinel 環境を構築してみました
どちらも Deployment と Service を使えば構築できることがわかりました
特に Sentinel の方は Service を使わないと各ロール間で通信できないので必須になると思います

ネットでいろいろ調べると他にも方法がありそうでした
Pod だけでやる方法も頑張ればできるかなと思います (ConfigMap を使った方法もあるかなと思います)
結局は docker 上にコンテナを立てるのが目的でそれを実現するのに k8s のどの機能を使うのかという話になるかなと思います
今回は Deployment と Serivce を使った方法を紹介しましたが、それ以外を使っても実現することはできるということです
この辺りは k8s の経験がものを言うかなと思っています

参考サイト

2019年5月29日水曜日

Ruby から docker コンテナを起動する

概要

過去 Ruby から docker を操作しました
イメージの操作がメインだったので今回はコンテナの操作をしてみました

環境

  • macOS 10.14.5
  • docker for Mac 18.09.2
  • Ruby 2.6.2p47

準備

  • bundle init
  • vim Gemfile
gem "docker-api"
  • bundle install --path vendor

コンテナを作成

今回は redis コンテナを作成します
イメージは事前に pull しておきましょう

  • docker pull redis

コンテナを作成するスクリプトです
docker for Mac で Remote API を使う場合は docker.sock を使います
2376 ポートで LISTEN することはできないためです

require 'docker'

Docker.url = 'unix:///var/run/docker.sock'
container = Docker::Container.create(
  "name" => 'my_redis', # lower case in a querystring
  "Image" => 'redis:latest',
  "PortBindings" => {
    "6379/tcp" => [
      {
        "HostPort" => "6379"
      }
    ]
  }
)
container.start
# pp Docker::Container.all(:all => true)
puts Docker::Container.all(:filters => { :name => ["my_redis"] }.to_json).first.info["Names"]

コンテナの作成は Docker::Container.create を使います
必要なパラメータを引数で指定します
name はクエリストリングのパラメータになっています
クエリストリングは基本的に先頭が小文字になっているので注意しましょう
先頭が大文字になっているのはリクエストボディとして送信されるパラメータになります
今回は ImagePortBindings を指定します
Image は必須です
PortBindings はホストから redis にアクセスするために使います
docker create コマンドで言うところの -p パラメータです

コンテナを作成したら start メソッドを呼び出すことでコンテナを起動できます
作成したら Docker::Container.all でコンテナの一覧を取得しています
all: trueps -a になります
:filters ps -f になります

コンテナに exec する

起動済みのコンテナに対して exec コマンドを実行します
起動済みのコンテナをインスタンスとして生成する方法は Docker::Container.get を使います
コンテナ ID またはコンテナ名を指定します

require 'docker'

container = Docker::Container.get('my_redis')
p container.exec(['redis-cli', 'info', 'server'])

実行するコマンドは配列で指定するのがポイントです
サブコマンドがある場合は別の要素として指定してます
レスポンスは実行結果の「標準出力」「標準エラー」「ステータスコード」が配列で戻ってきます

コンテナの停止/削除

require 'docker'

container = Docker::Container.get('my_redis')
container.stop
container.remove
# container.delete

stop, remove を使います
delete もエイリアスとして登録されています

最後に

Ruby で docker コンテナを操作してみました
Docker::Container クラスのインスタンスを作ってから操作する感じなります
パラメータ名などは Docker Remote API をそのまま使って使っているのでどんなパラメータを指定すればいいかは Docker Remote API のリファレンスを参照するのが良いと思います

参考サイト

2019年5月28日火曜日

【GCP】Cloud Scheduler を使ってみた

概要

GCP の Cloud Scheduler は cron サービスです
3 ジョブまでは無料で使えるので試してみました

環境

  • macOS 10.14.5
  • gcloud 247.0.0

HTTP + Get

コンソールから作成しました

Cloud Scheduler のページアクセスし「ジョブを作成」を選択します

入力した情報は以下の通りです

  • 名前・・・job1 (好きな名前で OK)
  • 頻度・・・*/1 * * * * (テストのため 1 分に 1 回実行)
  • タイムゾーン・・・日本
  • ターゲット・・・HTTP
  • URL・・・リクエストしたい好きな URL
  • HTTP メソッド・・・GET

これで「作成」を選択します

ジョブが作成されるとスケジュール通りに実行されます
結果が「成功」になっていれば OK です
手動で実行することもできます

ログは Stackdriver に自動的に格納されます
スケジュール時のリクエストのログが残るようです
レスポンスに関してはレスポンスコードしか残らずレスポンスボディやヘッダは残らないようです

HTTP + POST + JSON

コンソールからではヘッダが設定できないので gcloud コマンドから作成します

gcloud beta scheduler jobs create http job2 \
--time-zone "Asia/Tokyo" \
--schedule="*/1 * * * *" \
--uri="https://kaka-request-dumper.herokuapp.com/" \
--headers="Content-Type=application/json" \
--http-method="POST" \
--message-body="{\"key1\":\"value1\"}"

--headers オプションで Content-Type=application/json を指定しましょう
--message-body は文字列しか指定できないのでエスケープして JSON を指定しましょう
これで実行するとちゃんと JSON ボディが設定されてサーバ側にリクエストされます

Pub/Sub

特定のトピックにペイロードを送信することもできます
事前に Pub/Sub のコンソールからトピックとサブスクリプションを作成しておきましょう

あとはジョブを作成します
ターゲットを「Pub/Sub」にトピックに事前に作成トピックを指定しましょう
サブスクリプションを指定する項目はコンソールにはないようです

ペイロードを受け取るサブスクライバーは何でも OK です
今回は Ruby で実装しました (参考)

  • bundle init
  • vim Gemfile
gem "google-cloud-pubsub"
  • bundle install --path vendor
  • touch app.rb
require 'google/cloud/pubsub'

pubsub = Google::Cloud::PubSub.new(
  project_id: 'pjct-123456',
  credentials: './pjct-123456-abcd12345.json'
)
topic = pubsub.topic 'cloud_scheduler_topic'
sub = pubsub.subscription ''
subscriber = sub.listen do |msg|
  msg.acknowledge!
  puts msg.data
  puts msg.message_id
  puts msg.attributes
end
subscriber.start
sleep
  • bundle exec ruby app.rb

しばらく待っていれば以下のようにメッセージが表示されるのが確認できると思います

bundle exec ruby app.rb 
hoge
560942199166650
{}
hoge
560943876532507
{}

App Engine HTTP

事前にプロジェクト内に App Engine を使って適当にアプリを起動しておきましょう

あとはそのアプリに使ってリクエストするようなジョブを作成するだけです
今回は App Engine を default というサービス配下に作成しました
違うサービス配下に作成した場合は適宜変更してください

最後に

Cloud Scheduler を試してみました
現状は 3 つのターゲットをが指定可能なようです
MemoryStore に値を入れることができたり GCE の定期的な起動や停止、何かしらのメッセージやコマンドが渡せたできるようになると面白いかなと思いました
今後に期待かなと思います

参考サイト

2019年5月27日月曜日

RedisCloud を使ったみた

概要

RedisCloud は redislabs が提供するクラウドサービスです
30MB までの使用であれば無料で使うことが可能です
今回は登録から実際に Redis インスタンスを作成し redis-cli での接続テストまで行ってみました

環境

  • macOS 10.14.5
  • redis-cli 5.0.5

アカウント登録

まずはアカウント登録します
メールアドレスがあれば登録できます

メールが届くとアカウント登録するフォームの URL が記載されているのでアクセスして登録しましょう
Company は個人利用なので「personal」と入れておきました

そのままプランを設定しましょう

今回は 30MB の無料枠を使うので Essentials プランを選択します

そしてサブスクリプションですが Cache の「30MB free」を選択しましょう
Cache 以外にも Standard と Multi-AZ が選択できます
とりあえず今回は検証だけなので Cache にしておきましょう
クラウドも AWS or GCP が選択できるのでお好きなほうを選択してください

インスタンス作成

次に Redis インスタンスの作成画面になります
基本はデフォルトのままで OK です
Database Name にだけ好きな名前を入力してください

Protocol は Redis にしましょう
Cache を選択しているので Replication や Data Persistence は自動で「Disabled」「None 」になっています
アクセスするにはエンドポイントとパスワードが必要になります
エンドポイントはインスタンス作成後に確認できます
パスワードは Access Control & Security に自動で入力されるので右側のパスワードを表示するボタンを押してメモしておきましょう

Data Eviction Policy はデータを自動で削除するポリシーを選択します
今回はデフォルトの volatile-lru を選択しています
volatile-lruexpire が設定されたキーの expire が来た順番から削除してくれるポリシーです
Alert Settings は指定の値までメモリ消費した場合にアラートを上げることができます
今回はデフォルトの 80% のままにしています
アラートはアカウント登録時のメールアドレスに送信されるようです

入力内容を確認したら「Activate」を選択しましょう

エンドポイント確認

作成されたらエンドポイントを確認しましょう
インスタンスが作成されるまで Endpoint の欄には何も表示されていません

redis-cli で動作確認

では redis-cli で確認します
-h オプションでホストを指定し -p でポートを指定しましょう

redis-cli -h redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com -p 15671                     
redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com:15671> set a b                                                               
(error) NOAUTH Authentication required
redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com:15671> auth xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OK
redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com:15671> set a b
OK
redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com:15671> get a
"b"

あとはアクセスする前に auth コマンドを実行しましょう
インスタンス作成時に確認したパスワードをここで入力します

Ruby から動作確認

Ruby からも確認してみました
gem のインストール手順などは省略しますが簡単な動作確認のスクリプトは以下の通りです

require 'redis'

redis = Redis.new(url: "redis://:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@redis-15671.c16.us-east-1-2.ec2.cloud.redislabs.com:15671")

redis.set('b', 'b')
puts redis.get('b')

ポイントはパスワードの前のコロンを忘れないようにしましょう
URI のフォーマットで user:pass と指定する必要があるためです
ユーザは今回指定が必要ないためコロンの前に何も記述していないだけです

最後に

redislabs の RedisCloud を使ってみました
かなりシンプルな作りで簡単に使えると思います
ElastiCache や MemoryStore はクラウド内部からしか使えないのが前提でしたが RedisCloud は外部のどこからでも使うことができます
故にアンセキュアではあるのですがそこはエンドポイント名とパスワードの複雑さでカバーしている感じだと思います

30MB であれば無料で使えるのでとりあえず Redis がほしい場合などにはかなり重宝するかなと思います
プロダクションで使う場合には Pro プランや Standard, Multi-AZ サブスクリプションを検討しましょう

2019年5月26日日曜日

【GCP】Cloud Memory を使ってみた

概要

GCP の Redis サービス MemoryStore を使ってみました
インスタンスの作成から redis-cli を使った接続確認まで行いました

環境

  • macOS 10.14.5
  • gcloud 247.0.0

インスタンス作成

まずはインスタンスを作成します
コンソール画面から作成しても OK ですが今回は gcloud を使いました

  • gcloud beta redis instances create myredis --size=1 --region=us-central1 --redis-version=redis_4_0

タイプは「基本」で作成されます
基本はシンプルなシングル構成になります
メモリ容量は最小の 1GB にしています
Redis のバージョンは MemoryStore で使える最新の 4.0 にしています

またインスタンスの作成には beta コマンドを使います

インスタンス確認

create が完了したらインスタンスの状態を確認しましょう

  • gcloud redis instances describe myredis --region=us-central1
authorizedNetwork: projects/pjct-123456/global/networks/default
createTime: '2019-05-22T07:20:21.365383747Z'
currentLocationId: us-central1-b
host: 10.0.0.3
locationId: us-central1-b
memorySizeGb: 1
name: projects/pjct-123456/locations/us-central1/instances/myredis
port: 6379
redisVersion: REDIS_4_0
reservedIpRange: 10.0.0.0/29
state: READY
tier: BASIC

エンドポイントの IP やポートなどが表示されると思います
MemoryStore では FQDN ではなく IP アドレス (10.0.0.3) を使ってアクセスすることになります

MemoryStore は基本的に GCP 内のリソース (Compute Engine VM インスタンス、Google Kubernetes Engine クラスタ、Cloud Functions、App Engine フレキシブル環境、App Engine スタンダード環境) からしかアクセスできません

redis-cli 接続確認

今回は GCE インスタンスを 1 台作成して動作確認しました
また動作確認用の GCE と MemoryStore のインスタンスは同一ネットワーク (default) に所属するようにしてください

  • gcloud redis instances describe myredis --region us-central1 | grep default
authorizedNetwork: projects/pjct-123456/global/networks/default
  • gcloud compute instances describe vm01 | grep ' network:'
network: https://www.googleapis.com/compute/v1/projects/pjct-123456/global/networks/default

GCE に SSH ログインしたら先程確認したエンドポイントに対して動作確認してみます
ちなみに GCE は Ubuntu18.04 LTS のイメージを元に作成しました

  • apt -y install redis-tools
  • redis-cli -h 10.0.0.3
# Server
redis_version:4.0.14
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:c361ea82ce4c0f02
redis_mode:standalone
os:Linux 4.14.94+ x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:6.3.0
process_id:1
run_id:288517b3521abb484270dcf09846499bcc79489f
tcp_port:6379
uptime_in_seconds:985
uptime_in_days:0
hz:10
lru_clock:15006935
executable:/data/redis-server
config_file:/usr/local/etc/redis/redis.conf

こんな感じでホストを指定するだけでアクセスできます

料金

  • Cloud MemoryStore (アイオワ、1GB)・・・$0.049 (参考)
  • GCE (アイオワ、f1.micro)・・・$0.000 (参考)

後始末

Cloud MemoryStore インスタンスの削除

  • gcloud beta redis instances delete myredis --region=us-central1

GCE インスタンスの削除

  • gcloud compute instances delete vm01

最後に

Cloud MemoryStore を試してみました
インスタンスを作成するだけで Redis を使えるようになるので簡単です
注意点としては GCP の特定のリソースのみからしかアクセスできない点です
GCE を使ってグローバルな IP アドレスからポートフォーワードなどすれば外部からでもアクセスできるようになるとは思います

参考サイト

2019年5月25日土曜日

AWS ElastiCache for Redis 入門

概要

AWS の ElastiCache は Memcashed or Redis が選択できます
今回は Redis を構築して実際に redis-cli で接続できるところまで確認してみました

環境

  • macOS 10.14.4
  • AWS ElasciCache for Redis (2019/05/21 時点)
  • AWS EC2 (2019/05/21 時点)

クラスタ作成

ElastiCache for Redis を使うにはまずクラスタを作成しなければいけません
クラスタには Redis が動作するためのノードやアクセスを制御するセキュリティグループ、所属させる VPC ネットワークなどを指定します
また Redis の場合はクラスタ構成を選択することもできます
今回は検証目的で格安で進めたいのでシングルノードで選択します
またノードのスペックも一番小さいものにします

「今すぐ始める」を選択します

クラスタの設定画面になります
名前はクラスタ名になります
好きな名前を設定しましょう
ノードのタイプは一番小さい cache.t2.micro を選択します
なお cache.t2.micro はオハイオでは選択できますがリージョンによっては選択できないリージョンもあるのでご注意ください

VPC の設定をします
既存のでも OK です

セキュリティグループの選択をします
バックアップも不要なのでチェックを外しています
データをインポートやメンテナンス機能も特に使わないので指定しません

あとは作成できるのを待ちましょう
ステータスが available になればクラスタの内にノードが作成されています
なお ElastiCache は内部的には EC2 ですが EC2 のコンソールに移動して確認しようとしてもできません

動作確認用の EC2 インスタンス作成

ElastiCache は基本的に EC2 および AWS サービス内からのみアクセスできます
外部からも接続することはできますが結局 NAT 用のインスタンスが必要になるので素直に動作用の EC2 を作成します
作成するスペックは何でも OK ですが今回は以下のようにしました

  • Amazon Linux 2 AMI (HVM), SSD Volume Type
  • t2.nano
  • ElastiCache のクラスタと同一のセキュリティグループ

作成できたらプライベートキーを使ってアクセスできる確認しましょう
セキュリティグループに SSH の許可が必要な場合は入れてください

  • ssh -i ec2.pem ec2-user@ec2-18-218-71-107.us-east-2.compute.amazonaws.com

あとは ElastiCache に接続確認するための redis-cli もインストールしておきます

  • sudo amazon-linux-extras install redis4.0

セキュリティグループ変更 (任意)

これは任意です
もし確認用の EC2 を ElastiCache のクラスタとは別のセキュリティグループに所属させている場合はそのセキュリティグループからアクセスできるようにしましょう
また動作確認用の EC2 に SSH できるルールを追加してあげましょう

エンドポイント確認

redis-cli でアクセスするエンドポイントを確認します
作成したクラスタの詳細を表示し「プライマリエンドポイント」の部分を探しましょう

動作確認

あとは EC2 からコマンドで確認すれば OK です
redis-cli-h オプションを使って先程のエンドポイントを指定すれば OK です

[ec2-user@ip-172-31-46-33 ~]$ redis-cli -h cluster1.0n143f.0001.use2.cache.amazonaws.com
cluster1.0n143f.0001.use2.cache.amazonaws.com:6379> info server
# Server
redis_version:5.0.4
redis_git_sha1:0
redis_git_dirty:0
redis_build_id:0
redis_mode:standalone
os:Amazon ElastiCache
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:0.0.0
process_id:1
run_id:b85954932494109d4a2520b07ffcd6d7d7bfca2c
tcp_port:6379
uptime_in_seconds:2300
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:15000582
executable:-
config_file:-

料金

  • ElastiCache (オハイオ、cache.t2.micro)・・・$0.017/1hour (参考)
  • EC2 (オハイオ、t2.nano)・・・$0.0058USD/1hour (参考)

後始末

  • ElastiCache のクラスタの削除
  • 動作確認した EC2 の削除

最後に

ElastiCache に入門してみました
クラスタを作成して指定されたエンドポイントにアクセスするだけで使えるのでコマンドやアプリはそのままで向き先だけ変更すればすぐに使えるのは嬉しい点かなと思います
また Redis Cluster が必要であれば構築/運用も行ってくれます

原則的には EC2 からのアクセスのみを許可しているのでたとえば別のクラウドサービスなどからアクセスしたい場合には NAT インスタンスを立てる工夫が必要になります
EC2 に EIP を付与しその EIP に外部からアクセスがあったら ElastiCache のエンドポイントに流すような DNAT の設定を入れる感じだと思います (参考)

参考サイト

2019年5月24日金曜日

Mac 上で nomad 入門

概要

前回 Vagrant 上で nomad に入門しました
後々気づいたのですがシングルノードであれば Mac のみでも検証できることがわかったので紹介します

環境

  • macOS 10.14.5
  • nomad 0.9.1
  • docker for Mac 18.09.2

nomad インストール

  • brew install nomad
  • nomad -v

=> Nomad v0.9.1

agent 起動

  • nomad agent -dev

ジョブ作成 (.nomad ファイル作成)

  • nomad job init

example.nomad が作成されます

ジョブ起動

  • nomad job run example.nomad

ジョブ確認

  • nomad status job
ID       Type     Priority  Status   Submit Date
example  service  50        running  2019-05-21T22:55:43+09:00
  • nomad job status example
Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created    Modified                                                   
fc8aa4fb  aa7e5005  cache       0        run      running  1m19s ago  1m18s ago

Allocation 動作確認

  • nomad alloc status fc8aa4fb
Task Resources
CPU        Memory           Disk     Addresses
4/500 MHz  992 KiB/256 MiB  300 MiB  db: 127.0.0.1:21181
  • redis-cli -p 21181 info server

ジョブ停止

  • nomad job stop example

コンテナが削除されていることを確認します

最後に

シングルノードであれば Mac だけでも検証はできました

2019年5月23日木曜日

vcsim を使ってみた

概要

環境

  • macOS 10.14.4
  • golang 1.11.5
  • Ruby 2.6.2p47

vcsim インストール

  • go get -u github.com/vmware/govmomi/vcsim

govc がない場合は動作確認で使うのでインストールしておきましょう

  • go get -u github.com/vmware/govmomi/govc

vcsim 起動

$GOPATH/bin には PATH を通しているものとします

  • vcsim

これで localhost:8989 で起動します

環境変数

govc を使って動作確認してみます
vcsim を起動すると環境変数を設定するように言われるので素直に設定します

  • export GOVC_URL=https://user:pass@127.0.0.1:8989/sdk GOVC_SIM_PID=62667 GOVC_INSECURE=1

GOVC_INSECURE=1 を忘れずに入れましょう

動作確認

まずは govc を使って確認してみます

リソースの一覧を表示

  • govc find

vCenter の情報を表示

  • govc about

VM の一覧を取得

  • govc find vm
  • govc find . -type m

VM の詳細を取得

  • govc vm.info DC0_H0_VM0

VM のシャットダウン

  • govc vm.power -off DC0_H0_VM0

ちなみに -on で VM を起動できます

VM の設定変更

  • govc vm.change -vm DC0_H0_VM0 -name=DC0_H0_VM2

ホストの一覧を取得

  • govc find . -type h

その他 govc の使い方はこちらを参照してください

rbvmomi で動作確認

せっかくなので Ruby からも動作確認してみました

  • bundle init
  • vim Gemfile
gem "rbvmomi"
  • bundle install --path vendor
  • vim app.rb
require 'rbvmomi'

vim = RbVmomi::VIM.connect(
  host: 'localhost',
  port: '8989',
  user: 'user',
  password: 'pass',
  insecure: 'true'
)

# List vms
dc = vim.serviceInstance.find_datacenter('DC0') || fail('datacenter not found')
dc.vmFolder.childEntity.grep(RbVmomi::VIM::VirtualMachine).find do |x| 
  puts x.name
end

# Search a vm
vm = dc.find_vm('DC0_H0_VM0') || fail('VM not found')

# ReconfigVM_Task
# pp vm.config
spec = {}
spec['name'] = 'DC0_H0_VM2'
ret = vm.ReconfigVM_Task(spec: spec).wait_for_completion
  • bundle exec ruby app.rb

という感じで使えます

Tips 使える関数の一覧

  • curl -sk https://user:pass@127.0.0.1:8989/about

vcsim で使える SOAP API の一覧が確認できます
さすがにすべての API をサポートしているわけではなさそうです

最後に

vcsim を使ってみました
vCenter 環境を簡単に構築できない場合に便利かなと思います
govmomi のレポジトリで管理されているので p-r などは govmomi 自体に送ることになります

参考サイト

2019年5月22日水曜日

nomad 超入門

概要

nomad は Hashicorp 社が開発するコンテナスケジューリングツールです
位置づけとして k8s と同じ部類かなと思います
今回は Vagrant 上で nomad を簡単に触ってみました

環境

  • macOS 10.14.4
  • Vagrant 2.1.1
  • Ubuntu 16.04 (コンテナホスト)
    • nomad 0.8.6
    • consul 1.4.0
    • docker 18.09.6

nomad インストール (任意)

  • brew install nomad

ここ からバイナリをダウンロードして PATH に配置しても OK です
今回は基本的に Vagrant 上で作業を進めるので Mac 上へのインストールは任意です

nomad でコンテナを動かすサーバの準備

Vagrant を使います
公式の GettingStarted に Ubuntu16 用の Vagrantfile があるのでそれを使います
使わない場合は docker, nomard, consul がインストールされているホストを準備すれば OK です

  • mkdir nomad_host1
  • cd nomad_host1
  • wget 'https://raw.githubusercontent.com/hashicorp/nomad/master/demo/vagrant/Vagrantfile'
  • vagrant up

VM が起動したら vagrant ssh して中身を確認しましょう

vagrant@nomad:~$ docker -v
Docker version 18.09.6, build 481bc77
vagrant@nomad:~$ consul -v
Consul v1.4.0
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
vagrant@nomad:~$ nomad -v
Nomad v0.8.6 (ab54ebcfcde062e9482558b7c052702d4cb8aa1b+CHANGES)

少し nomad のバージョンが低いですが大丈夫でしょう
また推奨は 3 から 5 台のホストで構成されるクラスタですが今回は入門なので 1 台で行きます

nomad agent の起動

  • sudo nomad agent -dev

フォアグラウンドで起動するのでターミナルは戻ってきません
別のターミナルを開いて動作している確認しましょう

vagrant@nomad:~$ nomad node status
ID        DC   Name   Class   Drain  Eligibility  Status
84c8013c  dc1  nomad  <none>  false  eligible     ready
vagrant@nomad:~$ nomad server members
Name          Address    Port  Status  Leader  Protocol  Build  Datacenter  Region
nomad.global  127.0.0.1  4648  alive   true    2         0.8.6  dc1         global

ちなみに Mac からアクセスしたい場合は

  • nomad server members -address=http://10.0.2.15:4646

のような感じで -address を指定すれば OK です
ただ今回の Vagrantfile を使った場合は hostonly アダプタで VM が起動しているため Mac からアクセスできないのと agent が 127.0.0.1 で起動するのでバインドアドレスを

  • sudo nomad agent -dev -bind=0.0.0.0

という感じで指定しましょう

ジョブの作成

nomad はジョブ単位でコンテナを起動します
ジョブは .nomad という拡張子が付くファイルを作成することで定義します

  • nomad job init

これで example.nomad というファイルが作成されます

job "example" {
  datacenters = ["dc1"]
  type = "service"
  update {
    max_parallel = 1
    min_healthy_time = "10s"
    healthy_deadline = "3m"
    progress_deadline = "10m"
    auto_revert = false
    canary = 0
  }
  migrate {
    max_parallel = 1
    health_check = "checks"
    min_healthy_time = "10s"
    healthy_deadline = "5m"
  }
  group "cache" {
    count = 1
    restart {
      attempts = 2
      interval = "30m"
      delay = "15s"
      mode = "fail"
    }
    ephemeral_disk {
      size = 300
    }
    task "redis" {
      driver = "docker"
      config {
        image = "redis:3.2"
        port_map {
          db = 6379
        }
      }
      resources {
        cpu    = 500 # 500 MHz
        memory = 256 # 256MB
        network {
          mbits = 10
          port "db" {}
        }
      }
      service {
        name = "redis-cache"
        tags = ["global", "cache"]
        port = "db"
        check {
          name     = "alive"
          type     = "tcp"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

コメントを削除すると上記のようになっています
サンプルのジョブでは Redis コンテナが起動するようなジョブになっています

ジョブ起動

.nomad ファイルを元にコンテナを起動してみましょう

  • nomad job run example.nomad
==> Monitoring evaluation "bd2147b2"
    Evaluation triggered by job "example"
    Allocation "946c0f27" created: node "8a89cc14", group "cache"
    Evaluation within deployment: "f48565d1"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "bd2147b2" finished with status "complete"

という感じでコンテナが起動すれば OK です

ジョブの確認

ジョブの一覧を確認してみます

  • nomad job status
ID       Type     Priority  Status   Submit Date
example  service  50        running  2019-05-21T06:20:28Z

詳細も確認できます

  • nomad status example
Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created    Modified
946c0f27  8a89cc14  cache       0        run      running  5m28s ago  4m33s ago

Allocations を確認するとホスト側で LISTEN しているポートなどが確認できます

  • nomad alloc status 946c0f27
g"
Task Resources
CPU        Memory            Disk     IOPS  Addresses
2/500 MHz  1012 KiB/256 MiB  300 MiB  0     db: 127.0.0.1:30684

Task Events:
Started 

また Allocations でログも確認できます

  • nomad alloc logs 946c0f27 redis

試しに表示されたポートにアクセスするとちゃんと Redis が起動していることが確認できると思います

  • sudo apt -y install redis-tools
  • redis-cli -p 30684 info server

ジョブを更新する

次にジョブを更新してみます
group ディレクティブの count を 1 から 3 に上げてみます

group "cache" {
  count =3 
  ...
}

これを適用してみます

  • nomad job plan example.nomad
+/- Job: "example"
+/- Task Group: "cache" (2 create, 1 in-place update)
  +/- Count: "1" => "3" (forces create)
      Task: "redis"

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 18
To submit the job with version verification run:

nomad job run -check-index 18 example.nomad

When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

実行中のタスクは check-index を指定して確認することができます

  • nomad job run -check-index 18 example.nomad
e.nomad
==> Monitoring evaluation "eb1d87df"
    Evaluation triggered by job "example"
    Allocation "423dca4c" created: node "8a89cc14", group "cache"
    Allocation "f9e5ff00" created: node "8a89cc14", group "cache"
    Allocation "946c0f27" modified: node "8a89cc14", group "cache"
    Evaluation within deployment: "979cd34b"
    Allocation "423dca4c" status changed: "pending" -> "running"
    Allocation "f9e5ff00" status changed: "pending" -> "running"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "eb1d87df" finished with status "complete"

complete になっていれば変更完了です
確認してみましょう
ジョブの Allocations を確認すると 3 つコンテナが起動していることが確認できると思います

  • nomad status example
Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created     Modified
423dca4c  8a89cc14  cache       1        run      running  2m38s ago   2m21s ago
f9e5ff00  8a89cc14  cache       1        run      running  2m38s ago   2m22s ago
946c0f27  8a89cc14  cache       1        run      running  16m14s ago  2m28s ago

docker ps -a で確認してもコンテナが 3 台起動していることが確認できると思います
ポートを確認する場合は alloc でそれぞれ確認しましょう

ジョブの停止

  • nomad job stop example

これでコンテナが停止します
エージェントをいきなり停止してもコンテナの停止/削除が行われます

クラスタにホストを追加する

今回はここまでです
公式は次にホスト追加の手順に進むので興味があれば試してみてください
サーバモードとクライアントモードで agent を個別に動作させることでクラスタを構築する感じです
サーバモードは管理画面もあるので更に便利に使えると思います

最後に

nomad に入門してみました
.nomad というファイルにコンテナのスケジューリングの定義をすることでコンテナの配置やライフサイクルを管理してくれる感じです

k8s は yamlhaml を使って定義するのがそこが一番の違いかなと思います
ただ k8s には Ingress や StorageClass など他のシステムとシームレスにつながる機能や istio などのサードパーティのツールも充実しているので機能的には k8s のほうが豊富かなと思います

k8s ほどたくさんの機能はいらないけど簡単なコンテナスケジューリング、クラスタ管理はしたいという場合に便利かなと思います

参考サイト

2019年5月21日火曜日

Redis Cluster を試してみた

概要

これまで Redis Sentinel を試してきました
Redis には Cluster の機能もありこれを使えばノードを増やすことでスケールアウトすることができます
今回は Mac 上に Redis Cluster 環境を構築しついでに Ruby からも操作してみました

環境

  • macOS 10.14.4
  • Redis 5.0.5
  • Ruby 2.6.2p47

各ノードの作成

プロセスであれば OK なので適当にディレクトリを作成して redis-server プロセスを起動します

  • mkdir 7000 7001 7002
  • touch 7000/redis.conf 7001/redis.conf 7002/redis.conf
    vim 7000/redis.conf
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

redis.conf は 7001, 7002 ディレクトリ配下でも作成しましょう
また port の部分は 7001, 7002 に書き換えましょう

  • cd 7000
  • redis-server redis.conf

あとは各ディレクトリに移動して conf ファイルを指定して起動します

クラスタの構築

起動した各プロセスを元にクラスタを構築します
クラスタを構築するには redis-cli --cluster create コマンドを使います

  • redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002
>>> Performing hash slots allocation on 3 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: ad0cd036fa5cc5e87f3c46efeda4efa06ac866da 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: 7da858c6200f22ec2b8395a56e6f950e8c72685e 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
M: c99ba957f8d81c4a38e77a275043730ab77aeb33 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
...
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: ad0cd036fa5cc5e87f3c46efeda4efa06ac866da 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: c99ba957f8d81c4a38e77a275043730ab77aeb33 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
M: 7da858c6200f22ec2b8395a56e6f950e8c72685e 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

16384 スロットが均等に割り振られれば OK です

localhost で指定すると ERR Invalid node address specified: localhost:7000 になるので IP で指定します
また --cluster-replicas 1 を指定すると各 master に対する slave ノードを作成することができます
今回は master 用の 3 台しかプロセスを用意していないので指定できません

動作確認

クラスタが構築できたら動作確認してみましょう

  • redis-cli -c -p 7000 set a a

=> OK

  • redis-cli -c -p 7001 get a

=> "a"

  • redis-cli -c -p 7002 set b b

=> OK

  • redis-cli -c -p 7000 get b

=> "b"

どこからも set/get ができることを確認しましょう
またノードの状態も確認できます

  • redis-cli -p 7000 cluster nodes

新しいノードの追加

ノードを 4 台に増やしてみます
reshard する点がポイントです

  • mkdir 7003
  • touch 7003/redis.conf
  • vim 7003/redis.conf
port 7003
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
  • cd 7003
  • redis-server redis.conf

プロセスを起動したら追加します

  • redis-cli --cluster add-node 127.0.0.1:7003 127.0.0.1:7000

コマンドは redis-cli --cluster add-node を使います
新ノードのIP:ポート既存ノードのIP:ポート のフォーマットで指定します

>>> Adding node 127.0.0.1:7003 to cluster 127.0.0.1:7000
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: ad0cd036fa5cc5e87f3c46efeda4efa06ac866da 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: c99ba957f8d81c4a38e77a275043730ab77aeb33 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
M: 7da858c6200f22ec2b8395a56e6f950e8c72685e 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:7003 to make it join the cluster.
[OK] New node added correctly.

これでノードの追加ができました
が、このままではデータが保存されません
reshard することで新しいノードにもデータが分散されるようにします

  • redis-cli --cluster reshard 127.0.0.1:7000

いろいろと聞かれます
まずは「4096」を入力しましょう
これは 16384 / 4 の値になります
4 は 4 台で分散しているためです
次に「0eb468ab4a1766f2e9c31ba75a9ae880563ae462」を入力します
これは新規に追加した redis-server の ID になります
各ノードの ID は redis-cli -p 7000 cluster nodes などで確認できます
3 つ目に「all」を入力します
これはデータをノード全体で reshard することを意味します
最後に「yes」で reshard が始まります
reshard が始まるとずらーっとログが流れ始めるので待ちます

完了すると 4096 スロットずつに分散されていることが確認できます

  • redis-cli --cluster info 127.0.0.1:7000
127.0.0.1:7000 (ad0cd036...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:7002 (c99ba957...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:7003 (0eb468ab...) -> 0 keys | 4096 slots | 0 slaves.
127.0.0.1:7001 (7da858c6...) -> 0 keys | 4096 slots | 0 slaves.
[OK] 2 keys in 4 masters.
0.00 keys per slot on average.

ノードの削除

削除する際も reshard が必要です
コマンドは redis-cli --cluster del-node を使います

  • redis-cli --cluster del-node 127.0.0.1:7003 0eb468ab4a1766f2e9c31ba75a9ae880563ae462

フォーマットは 削除するノードのIP:ポート 削除するノードID という感じで指定します
おそらく以下のようなエラーが出ると思います
[ERR] Node 172.17.0.5:6379 is not empty! Reshard data away and try again.
ようするにデータがあるから指定のノードをクラスタから削除できないよというエラーになります
削除したいノードからデータを移すのも reshard を使います

  • redis-cli --cluster reshard 127.0.0.1:7000

入力する情報が異なります

まずは「4096」を入力します
これは移したいノードが持つスロット数になります
そして「ad0cd036fa5cc5e87f3c46efeda4efa06ac866da」を入力します
これはデータの移し先のノードになるので削除するノードではなく残すノードの ID を指定しましょう
そして「0eb468ab4a1766f2e9c31ba75a9ae880563ae462」->「done」を入力します
ここに削除するノードの ID を入力します
今回は 1 台だけなので 1 ID 分入力したら done で終了します
そして「yes」で reshard を開始します

完了するとノードが削除されて redis-server プロセスが停止します

  • redis-cli --cluster info 127.0.0.1:7000
127.0.0.1:7000 (ad0cd036...) -> 1 keys | 8192 slots | 0 slaves.
127.0.0.1:7002 (c99ba957...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:7001 (7da858c6...) -> 1 keys | 4096 slots | 0 slaves.
[OK] 3 keys in 3 masters.
0.00 keys per slot on average.

スロットのバランスがかなり悪いので再度 5461reshard してあげると良いかなと思います

クラスタ解体

--cluster create コマンドはありますが delete コマンドはないようです
もし丁寧にやるのであれば最後の 1 台になるまで reshard して最後に 1 台のプロセスを kill する感じかなと思います
もうすでにアクセスがなくデータが飛んでもいいのであればいきなり kill でも良いと思います

Ruby から操作する

せっかくなので Ruby から操作してみました
redis-rb を使います

  • bundle init
  • vim Gemfile
gem "redis"
  • bundle install --path vendor
  • vim app.rb
require 'redis'

nodes = (7000..7002).map { |port| "redis://127.0.0.1:#{port}" }
redis = Redis.new(cluster: nodes)

puts redis.get('c')
  • bundle exec ruby app.rb

=> c

こんな感じです
Redis オブジェクトを初期化する際にクラスタ内に存在するノードを指定する必要があります
なので add-nodedel-node でノード数が変わる場合はアプリケーションも変更する必要がありそうです

最後に

Redis Cluster を試してみました
redis-server プロセスを立ち上げてあとは redis-cli --cluster create で構築すれば使えるといった感じです
基本は master で上がって来ます
必要であれば slave を用意することもできます

Sentinel に比べてスケーラビリティは高いと思います
サーバのスペックが足りなくなったら新規で redis-server プロセスを追加すれば良いだけだからです
ただ reshard など管理する項目も増えるので運用は大変になりそうなイメージです
master が一定数存在する必要もあります
例えば今回は 3 台構成を紹介しましたが最低 3 台必要になります (1, 2 台でもクラスタは組めますが公式でも3 台以上を推奨しています)
なので 1 台ダウンするとクラスタが壊れてしまうためうまく読み書きできなくなってしまいます
slave を用意すれば Cluster でも failover させることができるので可用性を担保することはできますが管理する台数は増えます

この辺りは用途に合わせた使い分けかなと思うのでどっちが良い悪いという話ではないかなと思います

参考サイト

2019年5月20日月曜日

redis-rb を使えば Sentinel の failover が発生しても常に master にアクセスしてくれる

概要

前回 docker 上に redis + Sentinel 環境を構築してみました
問題点として failover が発生した場合には常に master の IP アドレスを追跡する仕組みがアプリ側に必要だということがわかりました
Ruby には redis を操作するライブラリに redis-rb があります
実はこれを使えばライブラリ側で master の追跡をしてくれます
今回は簡単なアプリを作成して挙動を確認してみました

環境

  • macOS 10.14.4
  • docker 18.09.2
  • Redis 5.0.3

アプリ作成

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "redis"
  • vim app.rb
require 'sinatra'
require 'redis'

class MyApp < Sinatra::Base
  SENTINELS = [{ host: "sentinel", port: 26379 }]
  redis = Redis.new(url: "redis://mymaster", sentinels: SENTINELS, role: :master)

  get '/get' do
    key = params['key']
    redis.get(key)
  end

  get '/set' do
    key = params['key']
    value = params['value']
    redis.set(key, value)
  end
end

SENTINELS で指定するホスト名は docker-compose.yml で指定する Sentinel のサービス名を指定します
Redis.new で指定する URL は常に master になるように sentinel.conf で指定した master 名を指定します

  • vim config.ru
require './app.rb'
run MyApp

アプリは動作確認しやすいように Sinatra を使って Web アプリにしています
getset を実装することで常に master にアクセスできているかを確認します

Dockerfile 作成

アプリ用の Dockerfile を作成します

  • vim Dockerfile_app
FROM ruby

ADD . /home
WORKDIR /home
RUN gem install bundler
RUN bundle install --path vendor

EXPOSE 9292

CMD ["bundle", "exec", "rackup", "config.ru", "-o", "0.0.0.0"]

docker-compose.yml 修正

作成したアプリを docker-compose.yml に追加します

  • vim docker-compose.yml
version: '2'
services:
  master:
    image: redis
  slave:
    image: redis
    command: redis-server --slaveof master 6379
    links:
      - master
  sentinel:
    build:
      context: .
      dockerfile: Dockerfile
    links:
      - master
      - slave
  app:
    build:
      context: .
      dockerfile: Dockerfile_app
    ports:
      - 9292:9292
    links:
      - sentinel
      - master
      - slave

app サービスを追加しています
それ以外は前回 の設定と同じです

sentinel.conf と Sentinel 用の Dockerfile

これは前回と同様なのでそちらを参照してください

ビルド

アプリと Sentinel 用のイメージをビルドします

  • docker-compose build

コンテナ起動

ではコンテナを起動します

  • docker-compose up -d

動作確認

まずはこれで get/set できることを確認しましょう

  • curl localhost:9292/set?key=a&value=a
  • curl localhost:9292/get?key=a

=> a

set した値が取得できれば問題なく動作しています
では failover させても問題なく動作するか確認しましょう
まずは Sentinel と slave をスケールさせます

  • docker-compose scale sentinel=3
  • docker-compose scale slave=4

そして master をダウンさせてみましょう

  • docker-compose stop master

master と salve の一覧が変わっていることを確認します

  • docker-compose exec sentinel redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
  • docker-compose exec sentinel redis-cli -p 26379 sentinel slaves mymaster | grep -A 1 'name'

master が failover したのを確認できたら再度 get/set できるか確認してみましょう

  • curl localhost:9292/set?key=a&value=b
  • curl localhost:9292/get?key=a

=> b

という感じで値が更新できると思います
以上から redis-rb 経由で Sentinel 環境にアクセスすれば master の failover を自動で追跡してくれることがわかります

最後に

redis-rb を使って Sentinel 環境にアクセスしてみました
failover にも対応しているので Ruby + Sentinel 環境の場合には必須かなと思います
ただ、docker の世界のサービス名と redis の世界の master/slave のロールは相変わらず齟齬が発生しているのでそこは注意が必要です
現在どれが master でどれが slave か確認するには Sentinel API の get-master-addr-by-nameslaves を使うようにしましょう