2021年3月30日火曜日

tavern で REST API テスト超入門

概要

python の tavern は REST API をテストするのに特化したテストフレームワークです
テストケースを yaml で記載するだけでテストを書くことができます
今回はよく使いそうなサンプルケースを試してみました

環境

  • macOS 11.2.3
  • Python 3.8.7
    • tavern 1.14.1

インストール

  • pipenv install tavern

テストファイルの命名規則

test_x.tavern.yaml という感じで定義します
x の部分に好きなテスト名を入力しましょう
例えば test_get-id.tavern.yaml という感じでテストを定義できます

とりあえず GET をコール

  • vim test_sample_get.tavern.yaml
---

test_name: Sample Get Test

stages:
  - name: Make sure we have the right method
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: GET
    response:
      strict: False
      status_code: 200
      json:
        path_info: /
        method: GET
  • pipenv run pytest test_sample_get.tavern.yaml

or

  • pipenv run tavern-ci test_sample_get.tavern.yaml

テスト自体は pytest を経由して行います
ファイル名を指定しない場合は命名規則に従っているファイルをすべてテストしてくれます
tavern-ci というコマンドを使ってもテストできます
こちらの場合は pytest にないオプションを使ってテストできます
ログファイルを名を指定するとテスト結果をログファイルに記載したりできます

strict: False

response の検証時に strict: False を指定することでレスポンス全体の検証ではなく一部の key の検証のみにすることができます (参考)
レスポンスの JSON 全体と照合させたい場合には True もしくは設定自体を削除しましょう

前の結果を使う

---

test_name: Sample Save Data Test

stages:
  - name: Make sure we have the right method
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: GET
    response:
      strict: False
      status_code: 200
      json:
        path_info: /
        method: GET
      save:
        json:
          returned_method: method

  - name: Make sure we have the right returned_method
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: GET
      params:
        returned_method: "{returned_method:s}"
    response:
      strict: False
      status_code: 200
      json:
        query_string: "returned_method={returned_method:s}"

save でデータを保存できます
保存したデータは "{val_name:type}" という形式で参照できます
val_name は save 時に指定したキー名で type は型を指定します
上記であれば文字列情報を格納した returned_method を参照するので string である s を指定しています

特定の状態になるまでポーリングする

---

test_name: Sample Polling Test

stages:
  - name: Make sure we have the right state
    max_retries: 3
    delay_after: 5
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: GET
    response:
      status_code: 200
      json:
        status: ready

max_retriesdelay_after を使います
前者はリトライ回数で後者は失敗後にウェイトする秒数を指定します 
非同期 API などではよく使う機能かなと思います
上記はサンプルですべてエラーになります
リトライの上限に達すると tavern.util.retry:retry.py:59 Stage 'Make sure we have the right state' did not succeed in 3 retries. という感じのエラーが表示されます

POST + JSON ボディを送信する

---

test_name: Sample Post Test

stages:
  - name: Make sure we have the right body
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: POST
      json:
        name: hawksnowlog
    response:
      strict: False
      status_code: 200
      json:
        method: POST
        body: !raw '{"name": "hawksnowlog"}'

request にそのまま json を含まれば OK です
レスポンスの検証時に文字列内に ブラケット {} が含まれる場合は !raw を使うことで変数展開されずにそのまま検証できます

クエリストリングを送信する

---

test_name: Sample Query String Test

stages:
  - name: Make sure we have the right query_string
    request:
      url: https://kaka-request-dumper.herokuapp.com/
      method: GET
      params:
        name: hawksnowlog
    response:
      strict: False
      status_code: 200
      json:
        method: GET
        query_string: "name=hawksnowlog"

先程の POST 時のままで method を GET に変更すれば OK です

最後に

他にも Python スクリプトを直接コールしたりする機能や他のテスト YAML ファイルを include したり any 機能で型判定だけする機能などもあるので興味があれば調べてみると良いと思います

参考サイト

2021年3月29日月曜日

flower を docker-compose で使う方法

概要

過去 に flower を使う方法を紹介しました
今回は docker-compose で使用する方法を紹介します

環境

  • celery 4.4
  • flower 1.0.0

docker-compose

version: '3.8'
services:
  broker:
    image: redis
  flower:
    image: mher/flower
    ports:
      - "5555:5555"
    deploy:
      placement:
        constraints:
          - node.hostname == worker1
    command: flower --broker=redis://broker:6379 --address=0.0.0.0

ポイント

デフォルトポートは 5555 なのでホスト側にバインドしてあげます
deploy の定義は stack deploy 対応です
各種パラメータは command で指定しています
環境変数でも可能ですが今回はコマンド引数にしました
--address=0.0.0.0 にしなくても動作します

2021年3月26日金曜日

docker swram の証明書は初期構築時に期限切れの期限を伸ばしておいたほうがいいかも

概要

docker swarm は通信に tls を使うことができます
通信するポートは選択できますがデフォルトで swarm は自己証明書を内部的に生成しています
その証明書の期限がデフォルトだと 2160 時間 (90日) になっています
基本的には自動で更新してくれるので問題ないのですがログなどが出るので初期構築のときに期限を延ばしておいてもいいかもしれません

環境

  • Ubuntu18.04
  • docker 20.10.5

期限を短くして動作確認

最短で 1 時間にまで短縮できます

  • docker swarm update --cert-expiry 1h0m0s
  • docker swarm ca --rotate

これで期限が 1 時間になった証明書が再配布されます
試しに確認してみましょう

  • cat /var/lib/docker/swarm/certificates/swarm-node.crt | openssl x509 -noout -dates
notBefore=Mar 25 22:03:00 2021 GMT
notAfter=Mar 26 00:03:00 2021 GMT

JST に直すと時間は 09:03 に期限切れとなります

期限が近くなるとどうなるか

内部的に自動で証明書の更新を行うようなログが流れると思います

  • journalctl -xu docker.service
Mar 26 09:10:00 swarm2 dockerd[26069]: time="2021-03-26T09:10:00.000149868+09:00" level=info msg="renewing certificate" module=node/tls node.id=ojnx8crctp5vb5rjmrf8uln7b node.role=swarm-worker

証明書の期限が 09:03 でそのあとに流れた renewing のログが 09:10 になります
期限切れのあとで renewing のバッチが流れると証明書の期限が自動的に延伸されているのが確認できると思います

  • cat /var/lib/docker/swarm/certificates/swarm-node.crt | openssl x509 -noout -dates
notBefore=Mar 25 23:10:00 2021 GMT
notAfter=Mar 26 01:10:00 2021 GMT

問題点

これの問題点としては以下の 2 つあります

  • 証明書の期限切れと同時に更新してくれるわけではない
  • 証明書更新のログが流れ続けるので syslog を逼迫する可能性がある

特に大きな問題になるわけではないですが長い間蓄積すると結構な量になったりする恐れがあります

期間を延伸する

なので初期構築の段階で延伸してあげるのが良いかなと思います

  • docker swarm update --cert-expiry 876000h0m0s
  • docker swarm ca --rotate

上記だと 100 年になります
無限はできないので数を大きくしましょう

最後に

自分で SSL 証明書を設定している場合にはそれを管理すれば良いのですがそうでない場合はデフォルトだと自己証明書が作られるようです
更新などは勝手にやってくれているようなので基本面倒を見る必要はないのですが自己証明書が使われていることは認識しておいたほうが良いかもしれません

2021年3月25日木曜日

iPhone と Mac の USB 接続が安定しないときに確認すること

iPhone と Mac の USB 接続が安定しないときに確認すること

概要

有線で iPhone と Mac を接続して同期や撮影をする際に接続が不安定になることがあります
その場合に自分が試すことを紹介します

環境

  • macOS 11.2.3
  • iOS 14.4.2

ケーブルの挿し直し

まずはこれをやってみましょう
Mac 側の USB 端子と iPhone 側のライトニングケーブルを挿し直して安定するか確認します

ライトニングケーブルの向きを変える

ライトニングケーブルには上下がないので向きを変えて指し直して見ましょう

USB のポートを変更してみる

Mac 側で使用する USB ポートを別の場所に変更してみましょう
ポートが壊れている場合に十分な電源供給が行われずに不安定になることがありそうです

ケーブルの故障

Mac 側の電源ケーブルや接続しているライトニングケーブルが故障している場合は当然不安定になります
破れていて中の線がむき出しになっている場合は電力不足になりがちです

ケーブルをまっすぐにする

意外と有効なのがこの方法です
普段ケーブルを束ねて保存しているとケーブルが曲がった状態になりがちです
その状態で接続すると不安定になることがありました
一旦ケーブルを抜いて手である程度まっすぐにすることで安定することがあります
あまり紹介されていない方法かなと思うので一度試してみても良いかなと思います

ちなみに接続中もピンと張っていないと安定しないことがあります
最悪 Mac はテーブルで iPhone は地面において強制的にピンと張るなどの工夫が必要です

ケーブルを直接接続しないで USB ハブなどを挟んで接続する

これも意外と安定します
Apple 純正のケーブルを Mac に直接接続しないで間に USB の延長ケーブルやハブを挟んでみましょう

CPUやメモリの空き容量を増やす

プロセスが大量に起動しリソースを食っている場合に接続が不安定になることがあります
おそらくファンなどを動かすのに電力を使い USB 側の電力が不足するために不安定になっているのではと推測できます
一度不要なプロセスを停止してから再度接続してみましょう

Mac を再起動する

最終手段として Mac を再起動してみましょう
更に再起動時に iPhone を接続状態にしておくと復帰時に接続が安定していることがありました

iPhone の機種によっても安定度が違う

自分の環境では iPhoneXR はかなり不安定なのですが iPhone6 はケーブルが曲がっていたりしても接続が常に安定しています

iPhone 側の OS バージョンや必要電力の違いによっても安定感は変わってくるのかもしれません

最後に

単純に故障の場合はどうしようもないので諦めましょう
ケーブルが複数ある場合は別のケーブルに変えてみるのもありだと思います

2021年3月24日水曜日

docker configs 使ってみた

概要

docker config は k8s の ConfigMap のように使えます
ただし条件があり stack deployo (swarm 環境) でしか使えません
今回は 2 台の swarm 環境に対して docker config を使ってみました

環境

  • Ubuntu18.04
  • docker 20.10.5

動作確認用アプリの作成

こちらを参考に作成しました
動けばいいのでどんなアプリでも OK です
Sinatra アプリと Dockerfile まで作成しましょう

docker build & push

swarm 環境に対して stack デプロイする場合には事前にイメージをどこかのコンテナレジストリに配置しておく必要があります
build ディレクティブが stack deploy では使えないためこれは必須の作業となります
例えば以下のように実行します

  • docker build -t your.registry.com/user/app:latest .
  • docker push your.registry.com/user/app:latest

config として登録する default.conf の作成

今回は nginx のコンフィグファイルを docker config で使ってみます
まずはファイル自体を作成しましょう
これも過去の紹介記事と同じ default.conf の内容で OK です

config の作成

docker-compose.yml に configs として定義もするのですがどうやら事前に config を swarm 環境に対して作成しおく必要があるようです
docker-compose.yml に定義している configs が swarm 環境にないと「config not found」と言われてエラーになってしまいます

  • docker -H 192.168.100.10:2376 config create my_default_conf ./default.conf

swarm 環境の master ノードに対して実行しましょう

docker-compose.yml の作成

stack deploy で使用する docker-compose.yml を作成していきます
ポイントは configs で事前に作成しておいた config を指定するのとコンテナ側でそのコンフィグファイルをどこに配置するのか指定する部分です

ersion: '3.8'
services:
  web:
    image: nginx:latest
    ports:
      - 80:80
    depends_on:
      - app
    configs:
      - source: my_default_conf
        target: /etc/nginx/conf.d/default.conf
    deploy:
      replicas: 2
      placement:
        max_replicas_per_node: 1
  app:
    image: your.registry.com/user/app:latest

configs:
  my_default_conf:
    file: ./default.conf
    external: true

ちゃんと各ノードに config が散らばっているのかを確認するために nginx は各ノードに最低 1 台デプロイされるようにしました
また app サービス側では build は使用できないので事前にコンテナレジストリに push しておいたイメージを指定します

動作確認

  • docker -H 192.168.100.10:2376:2376 stack deploy -c docker-compose.yml test --with-registry-auth

これで各 swarm のノードの IP:80 にアクセスしてどちらも app からのレスポンスが返ってくれば OK です
swarm 環境からレジストリにアクセスできる必要もあるのでファイアウォールのルールなども確認しましょう
レジストリの認証情報は実行しているマシンですでに docker login しているのであれば --with-registry-auth で認証情報を渡すことができます

最後に

k8s の ConfigMap っぽく使えそうではあります
残念なのは事前に docker config create しなければいけない点かなと思います
本当は stack deploy で同時に config create もしてくれるのが良いのですがそれはしてくれないようです
また stack deploy のみサポートしているのも残念な感じはします

参考サイト

2021年3月23日火曜日

Ubuntu18.04 に rbenv の最新版をインストールする方法

概要

apt ではなくインストールツールを使いましょう

環境

  • Ubuntu 18.04
  • rbenv 1.1.2-44

rbenv インストール

  • curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-doctor | bash

.bash_profile 編集

  • vim ~/.bash_profile
export RBENV_ROOT="$HOME/.rbenv"
export PATH="$RBENV_ROOT/bin:$PATH"

ruby インストール

  • rbenv install -l | grep '3.0.0'
  • rbenv install 3.0.0

ruby を PATH に通して使う

  • echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  • rbenv global 3.0.0
  • ruby -v

=> 3.0.0

参考サイト

2021年3月22日月曜日

Python で docker stack deploy する場合は docker-py ではなく pipenv install python-on-whales を使う必要がある

概要

docker-py は stack コマンド系をサポートしていません
そもそも stack コマンドはクライアント側の機能なので docker-py になくても納得なのは納得です
どうしても python から stack を使いたい場合は CLI を直接コールするか python-on-whales というライブラリを使います
今回は python-on-whales の簡単なサンプルを紹介します
python-on-whales もまだ未成熟なプロジェクトなので機能の未実装やバグなどは含まれている想定です

環境

  • Ubuntu 18.04
  • docker 20.10.5
  • Python 3.8.3
  • python-on-whales 0.15.0

インストール

  • pipenv install python-on-whales

サンプルコード

  • vim test.py
from python_on_whales import docker

output = docker.stack.deploy("from_python", "/path/to/docker-compose.yml")
print(output)

stacks = docker.stack.list()
for stack in stacks:
    print(stack)

processes = docker.stack.ps("from_python")
for process in processes:
    print(vars(process))

services = docker.stack.services("from_python")
for service in services:
    print(vars(service))
output = docker.stack.remove("from_python")
print(output)

実行

  • pipenv run python test.py

リモートホストに実行したい場合は

  • DOCKER_HOST=192.168.100.10:2376 pipenv run python test.py

という感じで実行します

最後に

まだまだ開発段階なので機能が揃っていません
ですが自分で CLI を組んでやるよりかは良さそうな気がします
今後の成長に期待して心中するのはありかもしれません
あとは stack 系だけこれを使ってそれ以外の docker API は docker-py を使うというのもありだとは思います

2021年3月19日金曜日

ssh-agent で ssh-add を自動で入力する方法

概要

ssh-agent を使ってパスワードやパスフレーズの管理を行っている場合、マシンがリブートした場合や再ログインした場合は ssh-agent のセッションがなくなるため再度 ssh-add しなければなりません
本当はセキュリティのことも踏まえて毎回パスフレーズを入力するのが良いのですが面倒なので自動化する方法を紹介します
あまりよろしくはない方法なので自己責任でお願いします

環境

  • Ubuntu 18.04

SSH_ASKPASS を使う

ssh-add をするときに環境変数 SSH_ASKPASS を設定すれば勝手にそこからパスワードを拾って登録してくれるという機能です
SSH_ASKPASS に実行可能なシェルスクリプトを設定すればその結果をパスワードとして使ってくれます

  • vim echo_pass.sh
#!/bin/sh
echo "xxxxxx"
  • chmod +x echo_pass.sh

xxxxxx の部分はパスワードを設定してください
あまりよろしくないという点はここでパスワードを平分で保存しているのでアンセキュアになるという点です
何かしらの方法で暗号化/復号化するような仕組みのシェルスクリプトになっていればある程度はセキュアになるかなと思います
今回はテストなので平文で進めます

SSH_ASKPASS + ssh-add で自動登録するコマンド

以下のような感じで使います
実際叩いてみるとわかりますがプロンプトで止まることはなく鍵が登録されることがわかると思います

  • DISPLAY=:0.0 SSH_ASKPASS=/path/to/echo_pass.sh setsid ssh-add /path/to/private_key.pem </dev/null

あとは動作確認として対象のサーバに ssh ログインしてみましょう
ちゃんとパスワードの入力を求められることなくログインできるようになっていると思います

おまけ: ssh-agent を自動起動する

上記のコマンドは ssh-agent が起動していることが前提です
ログイン時にてっとり早く起動させたい場合は以下の情報を ~/.bash_profile などに記載します

if [ -z "$SSH_AUTH_SOCK" ]; then
   # Check for a currently running instance of the agent
   RUNNING_AGENT="`ps -ax | grep 'ssh-agent -s' | grep -v grep | wc -l | tr -d '[:space:]'`"
   if [ "$RUNNING_AGENT" = "0" ]; then
        # Launch a new instance of the agent
        ssh-agent -s &> .ssh/ssh-agent
   fi
   eval `cat .ssh/ssh-agent`
fi

これでログイン時に勝手に ssh-agent が起動します
ただしこの場合だとログインすることが前提なのでサーバが再起動した際には上がってきません
サーバが起動した際に ssh-agent を起動させたい場合は systemd 配下 で起動させましょう

最後に

ssh を使うツール (ansible など) にも応用できるのでプロンプトが出て処理が止まっちゃうようなジョブなどに使うと良いかなと思います
何度も言いますがパスワード平文は危険なので何かしら工夫して使うようにしてください

参考サイト

2021年3月18日木曜日

Gitlab 公式の helm chart でワイルドカード SSL 証明書を設定する方法

概要

Let’sEncrypt などで取得したワイルドカード SSL 証明書を使って Gitlab の helm chart に SSL 設定をする方法を紹介します
内部的には kubernetes の nginx-ingress が使われているのでそれの tls 機能を使います

環境

  • kubernetes 1.19.3
  • helm 3.5.2
  • Gitlab helm chart 4.9.3
  • kubernetes nginx-ingress-controller 0.41.2

手順

とりあえず先に手順を紹介します
そのあとでポイントなどを紹介します

証明書登録

Let’sEncrypt などで取得したワイルドカード証明書を secret に登録します

  • kubectl create secret tls gitlab-tls --cert=/path/to/tls.crt --key=/path/to/tls.key

デプロイ

helm chart をデプロイします
ワイルドカードを使って動作させる場合の最低限のオプションを指定しています
global.hosts.domain は自身で登録した DNS の FQDN を指定してください

helm install gitlab gitlab/gitlab \
  --timeout 600s \
  --set global.hosts.domain=your.domain.com \
  --set nginx-ingress.controller.service.type=NodePort \
  --set certmanager.install=false \
  --set global.ingress.configureCertmanager=false \
  --set global.ingress.tls.secretName=gitlab-tls

説明

Let’sEncrypt の SSL ワイルドカード証明書取得時に指定したコモンネームと同じコモンネームを global.hosts.domain に指定します
ワイルドカードドメインだからと言って例えばサブドメイン (global.hosts.domain=sub.your.domain.com) を指定したりするとエラーになります
nginx-ingress-controller の Pod のログを見るとわかりますが証明書に設定されているコモンネームと global.hosts.domain で指定されているコモンネームが異なっているというエラーが出ているとうまく設定できていないことになります
必ず取得時と同じコモンネームを指定しましょう


nginx-ingress.controller.service.type=NodePort は今回オンプレミス環境にデプロイするので指定しています
デフォルトだと type=Loadbalancer でデプロイされてしまいます
特に type=Loadbalancer でも問題ないのですが気持ち悪いので NodePort に変えておきます


certmanager.install=falseglobal.ingress.configureCertmanager=falseglobal.ingress.tls.secretName=gitlab-tls はワイルドカード SSL を使うのに必須の設定になります
secretName の部分は事前に secret に登録した際の名前を指定しましょう

動作確認

Pod がすべて動作しているか確認します
gitlab-webservice-default が動作していれば OK だと思います

  • kubectl get pod

あとは svc を調べてアクセスするポートを確認します

  • kubectl get svc

IP は nginx-ingress-controller Pod がデプロイされているノードの IP にアクセスします
調べ方は -o wide を使うと Pod が乗っているノードが確認できるのでそこからノードの一覧を使って IP を確認しましょう

  • kubectl get pod -o wide
  • kubectl get node -o wide

あとは IP を DNS にレコードに登録して gitlab.your.domain.com:port という感じでアクセスすれば Gitlab にアクセスできるはずです

補足

Gitlab 公式の helm chart の場合 nginx-ingress-controller の Pod は 2 つデプロイされます
デフォルトだとどのノードで動作するかは完全にランダムになるので再デプロイなどをすると nginx-ingress-controller の Pod の場所が変わりアクセスする IP も変わるので注意しましょう
特に DNS などを設定している場合はレコード情報を書き換えることになるのでそこも注意しましょう

解決策としては nginx-ingress-controller の Pod をノードの数と同じだけデプロイするように変更するか (もしくは Damonset を使うか) nodeSelector を使って nginx-ingress-controller の Pod を必ず特定のノードにデプロイするようにすれば IP を固定することができると思います

トラブルシューティング

再度やり直す場合は helm の delete と delete では削除されないリソースも削除してからやり直しましょう

  • helm delete gitlab
  • for i in `kubectl get all,cm,secret,ing,job,pvc -A -o name | grep gitlab`; do kubectl delete ${i}; done;

最後に

ワイルドカード証明書のコモンネームと global.hosts.domain の指定は必ず一致するようにしましょう

参考サイト

2021年3月17日水曜日

Ruby でシングルトンなクラスを作成する方法

概要

Singleton は 1 つのクラスから 1 つのオブジェクトのみを生成させることができる機能です
システム全体で共有して使うようなクラスが必要な場合に使うパターンが多いです

環境

  • Ruby 3.0.0p0

サンプルコード

  • vim app.rb
require 'singleton'

class TestSingleton
  include Singleton

  attr_accessor :counter

  def initialize
    @counter = 0
  end
end

ts1 = TestSingleton.instance
ts1.counter += 1
p ts1.counter
p ts1.object_id

ts2 = TestSingleton.instance
ts2.counter += 1
p ts2.counter
p ts2.object_id

TestSingleton.new # => NoMethodError

同一の object_id が返ってくることと counter の値が共有されていることが確認できると思います
なお Singleton にしたクラスでは new ができなくなります

new もできるようにする

method_missing を使います

require 'singleton'

class TestSingleton
  include Singleton

  attr_accessor :counter

  def initialize
    @counter = 0
  end

  def self.method_missing(method, *args, &block)
    TestSingleton.instance
  end
end

ts1 = TestSingleton.instance
ts1.counter += 1
p ts1.counter
p ts1.object_id

ts2 = TestSingleton.instance
ts2.counter += 1
p ts2.counter
p ts2.object_id

ts3 = TestSingleton.new # => NoMethodError
ts3.counter += 1
p ts3.counter
p ts3.object_id

デメリット

コードが密結合になりやすいのでユニットテストが書きづらくなるのが個人的には一番のデメリットかなと思います
DI などによる回避方法が簡単な方法かなと思います

参考サイト

2021年3月16日火曜日

Python で kubernetes を操作する超入門

概要

kubernetes の API をコールするのに公式の Python クライアントがあります
今回は超入門ということで redis の Pods を k8s 上に構築してみました

環境

  • kubernetes v1.19.3
  • Python 3.8.3
  • pipenv 2020.8.13

インストール

  • pipenv install kubernetes

進め方

リファレンスを読みながら API をコールするための yaml 情報を Python で構築いく感じになります
フィールドがオブジェクトだったり dict だったりするのでそれをリファレンスで確認して何を使用するのか指定するのかを確認しながらコーディングしていくとやりやすいと思います

まずはバージョンを取得する

  • vim show_version.py
from kubernetes import client, config

config.load_kube_config()

v1 = client.VersionApi()
print(v1.get_code())
  • pipenv run python show_version.py

redis の PersistentVolumeClaim を作成する

storageClass は nfs を使って事前に作成しておきます
https://hawksnowlog.blogspot.com/2021/03/how-to-use-nfs-as-storageclass-server-on-kubernetes.html

  • vim apply_pvc.py
from kubernetes import client, config

config.load_kube_config()

api_instance = client.CoreV1Api()
body = client.V1PersistentVolumeClaim()
namespace = "default"
body.metadata = client.V1ObjectMeta(
  name="pvc1"
)
body.spec = client.V1PersistentVolumeClaimSpec(
  access_modes = [
    "ReadWriteOnce"
  ],
  resources = client.V1ResourceRequirements(
    requests = {
      "storage": "100Mi"
    }
  ),
  storage_class_name = "nfs",
)

api_response = api_instance.create_namespaced_persistent_volume_claim(namespace, body)
print(api_response)
  • pipenv run python apply_pvc.py
  • kubectl get pvc pvc1
NAME   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       AGE
pvc1   Bound    pvc-b0386fce-8745-4e24-9cbe-e79b6602e671   100Mi      RWO            nfs                15s

redis の Deployment を作成する

長いですが Pod spec なども同じ用に作成するだけです

  • vim apply_deploy.py
from kubernetes import client, config

config.load_kube_config()

api_instance = client.AppsV1Api()
body = client.V1Deployment()
namespace = "default"
body.metadata = client.V1ObjectMeta(
  name="redis"
)
body.spec = client.ExtensionsV1beta1DeploymentSpec(
  replicas = 1,
  selector = client.V1LabelSelector(
    match_labels = {
      "name": "redis"
    }
  ),
  template = client.V1PodTemplateSpec(
    metadata = client.V1ObjectMeta(
      labels = {
        "name": "redis"
      }
    ),
    spec = client.V1PodSpec(
      containers = [
        client.V1Container(
          image = "redis:6",
          name = "redis",
          volume_mounts = [
            client.V1VolumeMount(
              mount_path = "/data",
              name = "pvc1"
            )
          ]
        )
      ],
      restart_policy = "Always",
      volumes = [
        client.V1Volume(
          name = "pvc1",
          persistent_volume_claim = client.V1PersistentVolumeClaimVolumeSource(
            claim_name = "pvc1"
          )
        )
      ]
    )
  )
)

api_response = api_instance.create_namespaced_deployment(namespace, body)
print(api_response)
  • pipenv run python apply_deploy.py
  • kubectl get pod redis-6965c4dcd7-7mptl
NAME                     READY   STATUS    RESTARTS   AGE
redis-6965c4dcd7-7mptl   1/1     Running   0          9m31s

redis の Service を作成する

NodePort を使ってアクセスします

  • vim apply_svc.py
from kubernetes import client, config

config.load_kube_config()

api_instance = client.CoreV1Api()
body = client.V1Service()
namespace = "default"
body.metadata = client.V1ObjectMeta(
  name="redis-svc"
)
body.spec = client.V1ServiceSpec(
  type = "NodePort",
  selector = {
    "name": "redis"
  },
  ports = [
    client.V1ServicePort(
      name = "6379",
      port = 6379,
      target_port = 6379,
      protocol = "TCP"
    )
  ]
)

api_response = api_instance.create_namespaced_service(namespace, body)
print(api_response)
  • pipenv run python apply_svc.py
  • kubectl get svc redis-svc
NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
redis-svc   NodePort   10.105.106.45   <none>        6379:31806/TCP   5s

動作確認

  • redis-cli -h 192.168.100.10 -p 31806

おまけ: yaml からリソースを作成する

  • vim from_file.py
from kubernetes import client, config, utils

config.load_kube_config()
k8s_client = client.ApiClient()
ret = utils.create_from_yaml(k8s_client, "./pvc.yml")
print(ret)

最後に

慣れればリファレンス片手に簡単にコーディングできるようになると思います
動的に変わるようなマニフェストを作成したい場合やテンプレート化して管理したい場合に使えます

2021年3月15日月曜日

nginx-ingress-controller で All hosts are taken by other resources

概要

どうやら同一ドメインに対するルールは同一ファイルに定義しないとダメなようです

環境

  • Ubuntu18.04
  • kubernetes v1.20.4

対応策

例えば foo.bar.com という Host でアクセスする場合に以下のように 2 つのファイルを作成してはダメなようなです

  • vim ingress1.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example-ingress1
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
        - path: /app1
          backend:
            serviceName: service1
            servicePort: 5678
  • vim ingress2.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example-ingress2
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
        - path: /app2
          backend:
            serviceName: service2
            servicePort: 5678

これで ingress1 -> ingress2 の順番で起動すると ingress2 のほうが All hosts are taken by other resources になると思います
この場合 paths の定義を統合して 1 つのファイルにしてから apply してあげましょう

  • vim ingress.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example-ingress2
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
        - path: /app1
          backend:
            serviceName: service1
            servicePort: 5678
        - path: /app2
          backend:
            serviceName: service2
            servicePort: 5678

どうしてもファイルを分けたい場合は host 自体を別の名前にする必要があります

2021年3月14日日曜日

複数の deployment で podAntiAffinity を使う場合は対象の namespace を指定する必要がある

概要

podAntiAffinity は Pod をノードにデプロイする際のルールを指定することができる
podAffinity と似ていますが podAntiAffinity の場合は特定の条件に合致する Pod とは別のノードにデプロイしたい場合に使うルールになります
通常 Deployment で使い replicas で複数の Pod をデプロイする場合にそれらの Pod を異なるホストにデプロイしたい場合に使います
しかし複数の Deployment 定義を YAML ファイルでしている場合にはそれぞれの YAML ファイルを横断して affinity ルールを適用する必要があります
その場合にはルールを適用する対象の namespace を指定することで横断的に affinity ルールを適用することができます

環境

  • k8s 1.19.3

普通に podAntiAffinity を使う

まずは普通に podAntiAffinity を使う例を紹介します
1 つの Deployment 内で各ノードに 1 つ Pod をデプロイしたい場合には replicas をノード数分指定し podAntiAffinity を使います
今回は 6 ノードあるクラスタに対してデプロイするので replicas:6 を指定しています

  • vim pod_anti_affinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-multi
spec:
  selector:
    matchLabels:
      app: web
  replicas: 6
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-multi
        image: nginx:latest
  • kubectl apply -f pod_anti_affinity.yaml

これでデプロイすると確かに各ノードに 1 つずつ重複することなくデプロイされることが確認できます

  • kubectl get pod -o wide | grep web-multi
web-multi-db6d7cd4f-2b559 1/1 Running 0 11s 10.233.2.31 tt9lr <none> <none> web-multi-db6d7cd4f-7ql5h 1/1 Running 0 11s 10.233.5.14 ifov6 <none> <none> web-multi-db6d7cd4f-cqq4l 1/1 Running 0 11s 10.233.4.8 qdhf7 <none> <none> web-multi-db6d7cd4f-hm4sm 1/1 Running 0 11s 10.233.3.7 1rfxw <none> <none> web-multi-db6d7cd4f-m76ck 1/1 Running 0 11s 10.233.0.31 00wcr <none> <none> web-multi-db6d7cd4f-w9wbx 1/1 Running 0 11s 10.233.1.24 bdzss <none> <none>

異なる namespace 間で podAntiAffinity を使う場合

本題です
例えば以下のように Deployment の定義が複数ある場合を考えます
そういった場合には namespaces を追加して affinity ルールを横断的に適用する namespace を指定します

  • vim pod_anti_affinity_with_ns.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: web1
spec:
  selector:
    matchLabels:
      app: web
  replicas: 1
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web
            topologyKey: "kubernetes.io/hostname"
            namespaces:
              - web2
              - web3
      containers:
      - name: web
        image: nginx:latest

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: web2
spec:
  selector:
    matchLabels:
      app: web
  replicas: 1
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web
            topologyKey: "kubernetes.io/hostname"
            namespaces:
              - web1
              - web3
      containers:
      - name: web
        image: nginx:latest

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: web3
spec:
  selector:
    matchLabels:
      app: web
  replicas: 1
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web
            topologyKey: "kubernetes.io/hostname"
            namespaces:
              - web1
              - web2
      containers:
      - name: web
        image: nginx:latest

こうすることで

web1 は web2 と web3 によってデプロイされた app=web というラベルを持つ Pod とは異なるノードにデプロイされる

といったルール付けをすることができるようになります
試しに何度かデプロイしてみると必ずそれぞれの Pod が別のノードにデプロイされるのが確認できると思います

  • kubectl get pod --all-namespaces -o wide | grep -e web[1-3]
web1 web-86d5665f89-2fb54 1/1 Running 0 9s 10.233.3.14 1rfxw <none> <none> web2 web-78b54b56fd-cj2tn 1/1 Running 0 9s 10.233.4.16 qdhf7 <none> <none> web3 web-c9469f65-v87cb 1/1 Running 0 9s 10.233.5.20 ifov6 <none> <none>

おまけ: 特定のノードに固定した場合は nodeSelector を使う

piVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      name: web
  template:
    metadata:
      labels:
        name: web
    spec:
      nodeSelector:
        kubernetes.io/hostname: pool2-tt9lr
      containers:
        - image: nginx:latest
          name: web

最後に

  • podAffinity は条件に合致する Pod があるホストやゾーンと同じノードにデプロイする
  • podAntiAffinity は条件に合致する Pod がないホストやゾーンのノードにデプロイする

と覚えておけば良いと思います

参考サイト

2021年3月13日土曜日

k8s 上にデプロイした postgresql で docker-entrypoint-initdb.d が動作しない

概要

k8s 上に PostgreSQL をデプロイしたのですが初期化用の SQL スクリプト /docker-entrypoint-initdb.d/init.sql が動作しなかったので原因を調査してみました
対処方法を紹介します

環境

  • k8s v1.20.4

解決方法その1: configMap で defaultMode を指定する

基本は configMap で sql ファイルを /docker-entrypoint-initdb.d/init.sql という感じでマウントすることになると思うのですがデフォルトだと実行権限がないので defaultMode で実行権限を付与してあげましょう

      volumes:
        - name: postgres-pvc
          persistentVolumeClaim:
            claimName: postgres-pvc
        - name: postgres-config
          configMap:
            name: postgres-config
            defaultMode: 0755

解決方法その2: データ領域のデータを一旦削除する

pvc を使ってデータを永続化した場合そのディレクトリにすでに Postgres のデータディレクトリなどが作成されていると /docker-entrypoint-initdb.d/init.sql の実行がスキップされてしまいます
なので削除するなり再度 pvc を作成するなどして対応しましょう

参考: k8s のリソースファイル

  • vim pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
spec:
  capacity:
    storage: 100Mi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: postgres-sc
  local:
    path: /mnt/postgres
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1
  • vim pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  creationTimestamp: null
  labels:
    io.kompose.service: postgres-pvc
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
  storageClassName: postgres-sc
status: {}
  • vim cm.yaml
apiVersion: v1
data:
  init.sql: |
    create user user001 with password 'xxxxxxxxxx';
    create database database1 owner user001;
    alter role user001 with superuser;
kind: ConfigMap
metadata:
  creationTimestamp: "2021-03-01T01:37:55Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:data:
        .: {}
        f:init.sql: {}
    manager: kubectl-create
    operation: Update
    time: "2021-03-01T01:37:55Z"
  name: postgres-config
  namespace: default
  resourceVersion: "359270"
  uid: ea4605e6-4d98-4b35-a004-dcf3688d8f63
  • vim deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose -f docker-compose-multi.yml convert
    kompose.version: 1.22.0 (955b78124)
  creationTimestamp: null
  labels:
    io.kompose.service: postgres
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      io.kompose.service: postgres
  strategy:
    type: Recreate
  template:
    metadata:
      annotations:
        kompose.cmd: kompose -f docker-compose-multi.yml convert
        kompose.version: 1.22.0 (955b78124)
      creationTimestamp: null
      labels:
        io.kompose.network: "true"
        io.kompose.service: postgres
    spec:
      containers:
        - env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
            - name: POSTGRES_PASSWORD
              value: xxxxxxxxxx
          image: postgres:11.10
          name: postgres
          resources: {}
          volumeMounts:
            - mountPath: /var/lib/postgresql/data/pgdata
              name: postgres-pvc
            - mountPath: /docker-entrypoint-initdb.d
              name: postgres-config
      nodeSelector:
        kubernetes.io/hostname: node1
      restartPolicy: Always
      volumes:
        - name: postgres-pvc
          persistentVolumeClaim:
            claimName: postgres-pvc
        - name: postgres-config
          configMap:
            name: postgres-config
            defaultMode: 0755
status: {}

2021年3月12日金曜日

k8s 上に Jenkins Server をデプロイして使ってみた

概要

kubeadm で構築した k8s 上に Jenkins をデプロイしてみました
helm を使ってデプロイしています

環境

  • Ubuntu 18.04
  • k8s v1.20.4
  • helm 3.5.2

pv の作成

Jenkins 用の PersistentVolume を作成しておきます
pvc に自動で結びつくように claimRef をしっかり定義しておきます

  • vim jenkins_pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-pv
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  claimRef:
    namespace: default
    name: jenkins
  storageClassName: local-storage
  local:
    path: /mnt/jenkins
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1
  • kubectl apply -f jenkins_pv.yml

Jenkins のデプロイ

  • helm repo add jenkins https://charts.jenkins.io
  • helm repo update
  • helm install jenkins jenkins/jenkins

とりあえずそのままデプロイしています
リポジトリを追加して install するだけです

Pods の確認

Pod が立ち上がっていれば成功です

  • kubectl get pod jenkins-0
NAME        READY   STATUS    RESTARTS   AGE
jenkins-0   2/2     Running   0          6m59s

Loadbalancer として使える Ingress がある場合はそのまま Ingress リソースを追加するだけで良いのですが NodePort を使ってノードの IP を使う場合はこのままだとアクセスできません
なので chart のパラメータを少しいじっして NodePort からアクセスできるようにしてみます

upgrade する

  • helm upgrade --set controller.serviceType=NodePort jenkins jenkins/jenkins

install 時でも良いのですが --set controller.serviceType=NodePort を付与することでノード側の IP でバインドしてくれます
service を確認してみます

  • kubectl get svc jenkins
NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
jenkins   NodePort   10.109.115.80   <none>        8080:31441/TCP   30m

動作確認

http://192.168.100.10:31441/ にアクセスすると Jenkins のログイン画面になります
admin ユーザのトークンは secret に格納されています

  • kubectl exec --namespace default -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/chart-admin-password && echo

ジョブを作成して実行してみると jenkins-agent がジョブ実行用の Pods を作成してそこでビルドが実行されているのが確認できると思います

Tips: カスタマイズできる項目の取得 values

  • helm show values jenkins/jenkins

これで values.yml が取得できます
chart 自体の構成を見たい場合は

  • helm show chart jenkins/jenkins

で取得できます

参考サイト

2021年3月11日木曜日

k8s admin dashboard をデプロイする方法

概要

kubeadm など自分で構築した環境にデプロイしてみます

環境

  • k8s 1.19.3
  • admin UI 2.0.0

マニフェストダウンロード

  • wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml

Service 編集 NodePort 化

  • vim recommended.yaml
kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  type: NodePort # <- ここを追記
  ports:
    - port: 443
      targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard

デプロイ

  • kubectl apply -f recommended.yaml

Service 確認

  • kubectl get svc -n kubernetes-dashboard
NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE
dashboard-metrics-scraper   ClusterIP   10.105.49.239    <none>        8000/TCP        11s
kubernetes-dashboard        NodePort    10.105.192.142   <none>        443:31574/TCP   11s

ノードのIP:31574 にアクセスするとダッシュボードにアクセスできます

ログインユーザの作成

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kubernetes-dashboard
EOF
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kubernetes-dashboard
EOF
  • kubectl -n kubernetes-dashboard get secret $(kubectl -n kubernetes-dashboard get sa/admin-user -o jsonpath="{.secrets[0].name}") -o go-template='{{printf "%s\n" .data.token | base64decode}}{{"\n"}}'

ここで表示されるトークンを使ってログインできます

最後に

NodePort 化しないで Ingress を使っても OK です

おまけ: metrics-server もデプロイする

  • kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

これで dashbord で CPU とメモリのメトリックが見れるようになります
もし tls 関連でエラーが出てうまく metrics-server がデプロイできない場合は以下を試してください

  • kubectl edit deploy -n kube-system metrics-server
... 以下抜粋
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: metrics-server
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        k8s-app: metrics-server
    spec:
      containers:
      - args:
        - --cert-dir=/tmp
        - --secure-port=4443
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
        - --kubelet-use-node-status-port
        - --kubelet-insecure-tls # <- ここを追加
        image: k8s.gcr.io/metrics-server/metrics-server:v0.4.2

参考サイト

2021年3月10日水曜日

alertmanager で起動する URI を変更する方法

概要

デフォルトだと / で受けますが nginx 配下で別の URI で受けたい場合には Alertmanager 側の URI も変更しましょう

環境

  • Alertmanager 0.21.0

web.external-url を指定する

  • docker run -d -p 9093:9093 --name alertmanager -v $(pwd):/alertmanager prom/alertmanager --config.file=/alertmanager/alertmanager.yml --web.external-url=http://localhost:9093/am

k8s で指定する場合は

deployments あたりで args を使いましょう

...
    spec:
      securityContext:
        fsGroup: 0
      containers:
        - args:
            - --config.file=/home/alertmanager.yml
            - --web.external-url=http://alertmanager:9093/am
          image: prom/alertmanager:v0.21.0
          name: alertmanager
...

2021年3月9日火曜日

nginx-ingress-controller で SSL を有効にする方法

概要

証明書を登録しそれを Ingress の定義ファイルで参照するだけで有効にできます

環境

  • Ubuntu18.04
  • kubernetes v1.20.4

証明書作成

何でも OK です
LetsEncrypt などで作成する場合はこちらを参考に取得してください

証明書登録

k8s の secret リソースに登録します
例えば証明書と鍵を ~/certs/tls.crt~/certs/tls.key に配置した場合は以下のコマンドで登録できます

  • kubectl create secret tls my-tls --key ~/certs/tls.key --cert ~/certs/tls.crt

確認は get を使いましょう
また yaml に保存したい場合は -o yaml を使います

  • kubectl get secret
  • kubectl get secret my-tls -o yaml

Ingress 定義作成

Ingress を定義する際は tls を使って SSL で受けるホストを指定します
また登録した証明書は secretName: my-tls という感じで参照します

  • vim ingress.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: registry-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  tls:
  - hosts:
    - foo.bar.com
    secretName: my-tls
  rules:
  - host: foo.bar.com
    http:
      paths:
        - path: /
          backend:
            serviceName: web
            servicePort: 80

参考サイト

2021年3月8日月曜日

kubeadm で構築したノードが NotReady になった場合の対処方法

概要

ノード自体が再起動した際にクラスタのステータスが NotReady から復旧しなくなった場合の対処方法です

環境

  • Ubuntu 18.04
  • k8s v1.20.4

kubelet が起動しているか確認する

Ubuntu の場合は systemd 配下で管理されていると思います

  • systemctl start kubelet

これで起動してあげれば OK です

swapoff する

うまく kubelet が起動しない場合は swapoff もちゃんとされているか確認してみましょう

  • swapoff -a

systemd 配下で kubelet を管理しているのであればこれで大抵は Ready になるはずです
ちゃんと kubelet が上がってくればノード側で必要なコンテナが起動し Ready に変わります

2021年3月7日日曜日

kubernetes で nfs をマウントして使う方法

kubernetes で nfs をマウントして使う方法

概要

k8s で nfs を使うにはいろいろな方法がありますが nfs-subdir-external-provisioner という helm chart が便利そうだったので使ってみました

環境

  • kubernetes v1.19.3
  • nfs-subdir-external-provisioner 4.0.2

nfs サーバの準備

こちらを参考に Ubuntu 上に構築しています
また nfs は no_root_squash を設定しましょう
export しているパスは / にしています

nfs-subdir-external-provisioner のインストール

helm を使います
リポジトリを add したら install します

  • helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
    --set nfs.server=192.168.100.10 \
    --set nfs.path=/ \
    --set storageClass.name=nfs

nfs サーバの IP と export したパスを指定しましょう
storageClass.name は好きな storageClass 名を指定しましょう
storageClass が作成され nfs 用の Pod が作成されていれば OK です

  • kubectl get storageclass
NAME  PROVISIONER                                     RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs   cluster.local/nfs-subdir-external-provisioner   Delete          Immediate           true                   4m30s
  • kubectl get pod
NAME  PROVISIONER                                     RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs   cluster.local/nfs-subdir-external-provisioner   Delete          Immediate           true                   4m30s

動作確認

nfs が使用できるか確認してみます

PersistentVolumeClaim の作成

pv は不要です
まずは pvc を作成します
必ず storageClassName: nfs を指定しましょう

  • vim pvc1.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc1
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: nfs
  • kubectl apply -f pvc1.yml

動作確認用の redis Pod の作成

Pod で nfs をマウントしてデータを配置します
何でも OK です
今回は redis のバックアップデータでも配置してみます

  • vim redis.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      name: redis
  template:
    metadata:
      labels:
        name: redis
    spec:
      containers:
        - args:
            - redis-server
            - --appendonly
            - "yes"
          image: redis:6
          name: redis
          resources: {}
          volumeMounts:
            - mountPath: /data
              name: pvc1
      restartPolicy: Always
      volumes:
        - name: pvc1
          persistentVolumeClaim:
            claimName: pvc1
  • kubectl apply -f redis.yml

これで redis Pod ができれば OK です
Pod の中を確認するとちゃんと指定のパスにバックアップデータが作成されているのが確認できます

  • kubectl get pod
NAME                                              READY   STATUS    RESTARTS   AGE
nfs-subdir-external-provisioner-59cccbc98-h8cnl   1/1     Running   0          91m
redis-87846d68-7pl9c                              1/1     Running   0          79s
  • kubectl exec redis-87846d68-7pl9c -- ls /data

=> appendonly.aof

alpine Pod を作成してデータがあるか確認してみる

別の Pod を作成して同じ pvc をマウントしてみて先程作成された redis のバックアップデータがあるか確認してみましょう

  • vim alpine.yml
apiVersion: v1
kind: Pod
metadata:
  name: alpine-test
spec:
  containers:
    - image: alpine:latest
      name: alpine-test
      resources: {}
      volumeMounts:
        - mountPath: /data
          name: pvc1
      command: ["ls"]
      args: ["/data"]
  restartPolicy: Never
  volumes:
    - name: pvc1
      persistentVolumeClaim:
        claimName: pvc1
  • kubectl apply -f alpine.yml
  • kubectl logs alpine-test

=> appendonly.aof

ちゃんと redis のバックアップがあることが確認できました

別の pvc を作成してみる

1 つの storageClass から別の pvc を作成してみるとどうなるか確認してみました

  • vim pvc2.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc2
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: nfs
  • kubectl apply -f pvc2.yml

これで pvc2 をマウントしてデータ領域を確認すると redis のバックアップデータがないことが確認できます
つまり同じ nfs の export パスを使っているにも関わらず別のデータ領域を使っていることがわかります

  • vim alpine2.yml
apiVersion: v1
kind: Pod
metadata:
  name: alpine-test2
spec:
  containers:
    - image: alpine:latest
      name: alpine-test2
      resources: {}
      volumeMounts:
        - mountPath: /data
          name: pvc2
      command: ["ls"]
      args: ["/data"]
  restartPolicy: Never
  volumes:
    - name: pvc2
      persistentVolumeClaim:
        claimName: pvc2
  • kubectl apply -f alpine2.yml
  • kubectl logs alpine-test

=> 表示されない

nfs 内ではどうなっているか

VM で適当に nfs をマウントして中身を見てみると pvc ごとにディレクトリが作成されているようです
これが別の pvc を作成してもデータ領域が異なっているように見えた原因になります

ls /mnt/test/
default-pvc1-pvc-8d750fd7-9e5e-4392-ab87-eb99196777e3  default-pvc2-pvc-ecfd3eca-9b43-412b-91c7-037588cd16c5

最後に

これで pvc ごとに nfs の export を作成する必要がなくなるのでかなり便利かなと思います
データ領域的には 1 つの領域を共有しているので pvc の使いすぎによる別 pvc の逼迫には注意しましょう

おまけ: デフォルトの storageClasss として使う方法

--set storageClass.defaultClass=true

参考サイト

2021年3月6日土曜日

k8s でローカルストレージを共有する場合はサイドカーを使わなければならない

概要

ホスト間で共有 PersistentVolume などを使いたいときは S3 または nfs などのネットワークストレージを使いましょう
それ以外でホストの領域を使ってコンテナ間でストレージを共有したい場合はサイドカーを使います

環境

  • Ubuntu 18.04
  • k8s v1.20.4

ローカルストレージが共有できるパターン

  • vim pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  restartPolicy: Never
  volumes:
  - name: test-vol
    emptyDir: {}
  containers:
  - name: web1
    image: nginx:latest
    volumeMounts:
    - name: test-vol
      mountPath: /home
  - name: echo
    image: alpine:latest
    volumeMounts:
    - name: test-vol
      mountPath: /home
    command: ["/bin/sh"]
    args: ["-c", "echo This message from alpine container > /home/msg.txt"]

これで web Pods 内にある web1 コンテナで共有領域にアクセスするとファイルがあるのが確認できます

  • kubectl apply -f pod.yml
  • kubectl exec web -c web1 -- cat /home/msg.txt

ローカルストレージが共有できないパターン

pv から pvc を作成して別のホストにデプロイするような Pods で pvc を使おうとしても共有できません

  • vim pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: test-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local
  hostPath:
    path: "/mnt/test"
  • vim pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local
  • vim deploy_web1.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web1
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: web1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: web1
    spec:
      containers:
        - image: nginx:latest
          name: web1
          resources: {}
          volumeMounts:
            - mountPath: /home
              name: test-pvc
      nodeSelector:
        kubernetes.io/hostname: node1
      restartPolicy: Always
      volumes:
        - name: test-pvc
          persistentVolumeClaim:
            claimName: test-pvc
status: {}
  • vim deploy_web2.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web2
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: web2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: web2
    spec:
      containers:
        - image: nginx:latest
          name: web2
          resources: {}
          volumeMounts:
            - mountPath: /home
              name: test-pvc
      nodeSelector:
        kubernetes.io/hostname: node2
      restartPolicy: Always
      volumes:
        - name: test-pvc
          persistentVolumeClaim:
            claimName: test-pvc
status: {}
  • kubectl apply -f pv.yml
  • kubectl apply -f pvc.yml
  • vim deploy_web1.yml
  • vim deploy_web2.yml

これで web1 の Pods と web2 の Pods で共有している領域にアクセスしてもファイルを共有することができません

参考サイト

2021年3月5日金曜日

kubernetes 公式の ingress-nginx を試してみた

概要

前回は Nginx 社が開発している nginx-ingress-controller を試してみました
今回は kubernetes 公式が開発している ingress-nginx を試してみました
ほぼ同じですがデプロイ方法や機能が実感異なります

環境

  • Ubuntu 18.04
  • k8s v1.20.4
  • nginx 1.19.6

普通にデプロイ

helm を使います

  • helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
  • helm repo update
  • helm install ingress-nginx ingress-nginx/ingress-nginx

動作確認

NodePort で Service が Expose されているのでそこにアクセスすれば nginx の 404 画面にアクセスできます
Service へのアクセスは前回同様 Host と Path ベースの Ingress を定義する感じになります

削除

helm を使って削除します

  • helm delete ingress-nginx -n ingress-nginx

hostNetwork を使う

NodePort を使った場合は 30000 番ポート以上のハイポートで LISTEN することになります
ingress-nginx を使うとホストネットワークを使ってポートバインドすることもできます
290 行目あたりにある Deployment の設定を手動で変更します

  • wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.44.0/deploy/static/provider/baremetal/deploy.yaml
  • vim deploy.yaml
# Source: ingress-nginx/templates/controller-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    helm.sh/chart: ingress-nginx-3.23.0
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/version: 0.44.0
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/component: controller
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/instance: ingress-nginx
      app.kubernetes.io/component: controller
  revisionHistoryLimit: 10
  minReadySeconds: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/instance: ingress-nginx
        app.kubernetes.io/component: controller
    spec:
      hostNetwork: true # <- ここを追記

あとはデプロイするだけです

  • kubectl apply -f deploy.yaml

削除

helm を使っていないので namespace ごと削除しましょう

  • kubectl delete ns ingress-nginx

トラブルシューティング

Nginx 版の nginx-ingress-controller がすでにデプロイされている環境にデプロイしようとすると以下のエラーが発生しました
kubernetes 版と Nginx 版は共存しないほうが良さそうです

The IngressClass "nginx" is invalid: spec.controller: Invalid value: "k8s.io/ingress-nginx": field is immutable

最後に

helm を使って簡単にインストールできるのでこちらのほうが良いかもしれません

参考サイト