forループ

Bashスクリプトのforループについて書きます。

まずは基本から。

#!/bin/bash
for i in a b c ; do
  echo ">> $i"
done

上記では変数 i に対して in の右側にある値を一つづつ代入しループ処理をしています。結果は次のようになります。

❯ ./bin/for-loop.sh
>> a
>> b
>> c

ループさせたい値を配列として指定することもできます。例えば、同じ出力にするには次のように書きます。

#!/bin/bash
array=(a b c)
for i in ${array[@]} ; do
  echo ">> $i"
done

配列は ${array[@]} と書かなくてはいけません。 array だけだと、先頭要素のみを表現することになるからです。そうすると、次のように出力されてしまいます。

❯ ./bin/for-loop.sh
>> a

forループでファイルを処理することもできます。例えば次のようなファイルがあったとします。

This is first line.
This is second line.

次のようにすれば、ファイルをループ処理できます。

#!/bin/bash
for i in $(cat $HOME/bin/sample) ; do
  echo ">> $i"
done

結果は次のようになり、改行、またはスペース単位で処理されていることがわかります。

❯ ./bin/for-loop.sh
>> This
>> is
>> first
>> line.
>> This
>> is
>> second
>> line.

しかし、一行ずつ処理したい方がよくあるケースかもしれません。その場合はIFSを改行に設定します。

#!/bin/bash
IFS=$'\n'
for i in $(cat $HOME/bin/sample) ; do
  echo ">> $i"
done

IFSに改行を指定するには上記のように $ とシングルクォートで囲う必要がある点に注意してください。実行すると次のようになります。

❯ ./bin/for-loop.sh
>> This is first line.
>> This is second line.

パラメータ置換

シェルではパラメータ置換を使って、パラメータを取得する時に必要な形に置き換えることができます。

例えば、シェルスクリプトでパラメータが指定されていない場合のデフォルト値をセットしたいかもしれません。そんな時にパラメータ置換を使えば簡単に実現できます。

パラメータのデフォルト値の割当

適当なBashスクリプトを用意しました。このファイルは tmp.sh で保存します。

#!/bin/bash
echo ${1:-"default value"}

パラメータ置換を使わない場合、 $1 で第一パラメータにアクセスできますが、上記のようにすることでパラメータが与えられない場合のデフォルト値を指定できます。この方法を取れば複雑な処理を書かずにかつ、シンプルな記法なので可読性も担保できます。

実際に呼び出してみると次のような出力になります。

❯ tmp.sh "Hello Bash"
Hello Bash
❯ tmp.sh
default value

引数を与えなかった2つ目の呼び出しでデフォルト値が出力されたのがわかると思います。

パラメータが与えられない場合にエラーで処理を中断する

パラメータ置換はデフォルト値を指定する以外の書き方も存在します。使用イメージがわきやすいものとして、パラメータが指定されない場合にエラーで処理を中断する記法を紹介します。

#!/bin/bash
echo ${1?requried parameter}
echo World

この例ではパラメータが与えられていない場合にメッセージを表示します。

❯ tmp.sh Hello
Hello
World
❯ tmp.sh
/home/bookstore/bin/tmp.sh: 行 2: 1: requried parameter

KubernetesでLivenessProbeを試す

Kubernetesはコンテナの健全性を測定するために3つの仕組みを持っています。(StartupProbe / LivenessProbe / ReadinessProbe)今回はその一つ、LivenessProbeを実際に試してみたいと思います。

Probeについて、公式ドキュメントは下記が該当します。 https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes

Probeはそれぞれ異なる役割を持っています。LivenessProbeはコンテナをいつ再起動するかを判断します。再起動が必要な状況、例えばメモリリークなどに適用させるのが良いとされています。また、Podを再起動するわけではないことに気をつけてください。

環境

> kind --version
kind version 0.17.0

> docker --version
Docker version 23.0.3, build 3e7cbfd

> cat /etc/os-release
NAME="Fedora Linux"
VERSION="37 (Workstation Edition)"
ID=fedora
VERSION_ID=37
VERSION_CODENAME=""
PLATFORM_ID="platform:f37"
PRETTY_NAME="Fedora Linux 37 (Workstation Edition)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:37"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f37/system-administrators-guide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=37
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=37
SUPPORT_END=2023-11-14
VARIANT="Workstation Edition"
VARIANT_ID=workstation

必要なリソースの作成

次のコマンドでマニフェストファイルの雛形を作成します。kubectlコマンドのリファレンスはこちらを参考にしました。また、kubectlコマンドに対しkのエイリアスを設定しているので適宜読み替えてください。

https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#-em-deployment-em-

k create deployment wiremock --image=wiremock/wiremock:2.35.0 --dry-run=client -o yaml > deployment.wiremock.yaml

イメージで指定している通り、今回wiremockというツールを使ってLivenessProbeを試してみたいと思います。

この時点でのマニフェストファイルは以下のようになっています。Probeは一つも定義されていません。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: wiremock
  name: wiremock
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wiremock
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: wiremock
    spec:
      containers:
      - image: wiremock/wiremock:2.35.0
        name: wiremock
        resources: {}
status: {}

ここからLivenessProbeを定義していきます。Probeがコンテナの健全性をチェックする方式は3種類が用意されています。(拡張すれば増やせるんでしょうか?)

  • exec
  • httpGet
  • tcpSocket

今回は一番使われていそうなhttpGetを使いたいと思います。

wiremockを設定し特定のパスでアクセスした際HTTP ステータスが200で返ってくるようにします。以下のマニフェストに書き換え、Deploymentをapplyできるようにします。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wiremock
  name: wiremock
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wiremock
  strategy: {}
  template:
    metadata:
      labels:
        app: wiremock
    spec:
      containers:
      - image: wiremock/wiremock:2.35.0
        name: wiremock
        resources: {}

以下のコマンドでDeploymentを作成します。

k apply -f deployment.wiremock.yaml

wiremockにアクセスできるよう、次のコマンドでポートフォワードします。

k port-forward wiremock-6984ddd466-xp2vv 8080:8080

ターミナルを別で開き、次のコマンドでwiremockの設定をします。

curl -X POST \
    --data '{ "request": { "url": "/get/this", "method": "GET" }, "response": { "status": 200, "body": "Here it is!\n" }}' \
    http://localhost:8080/__admin/mappings/new

これで、次のように200が返ってくるパスを設定できました。

curl -sS -v http://localhost:8080/get/this
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /get/this HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.85.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Matched-Stub-Id: a364d640-b003-46ee-987f-06203495bbea
< Vary: Accept-Encoding, User-Agent
< Transfer-Encoding: chunked
<
Here it is!
* Connection #0 to host localhost left intact

これでwiremockを設定することができましたが、この手順だとLivnessProbeが始まる前に設定を終わらせるのが難しそうです。仮に終わらないとコンテナが再起動されてしまい、一からやり直す事になってしまいます。なので、最初からこのようなレスポンスを返すようにしないといけません。

wiremockのDockerイメージは /home/wiremock/mappings に存在するjsonファイルを起動時に読み込みます。この仕組を利用し、コンテナ起動時に先程のレスポンスが返ってくるようにします。

jsonファイルの配置はコンテナイメージをビルドする時にコピーする方法もありますが、今回はConfigMapを使おうと思います。

apiversion: v1
kind: configmap
metadata:
  name: wiremock-mappings
immutable: true
data:
  livness_probe_mapping.json: |
    {
      "request": {
        "url": "/get/this",
        "method": "GET"
      },
      "response": {
        "status": 200,
        "body": "here it is!\n"
      }
    }

Deploymentのマニフェストファイルも更新し、上記のConfigMapを使うように設定します。同時に、アクセスログを標準出力へ書き込むよう、起動時引数を追加します。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wiremock
  name: wiremock
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wiremock
  strategy: {}
  template:
    metadata:
      labels:
        app: wiremock
    spec:
      containers:
      - image: wiremock/wiremock:2.35.0
        args:
          - "--verbose"
        name: wiremock
        volumeMounts:
          - name: wiremock-mappings-configmap
            mountPath: /home/wiremock/mappings
            readOnly: true
        resources: {}
      volumes:
      - name: wiremock-mappings-configmap
        configMap:
          name: wiremock-mappings

以下のコマンドでどちらのリソースも作成します。

> k apply -f deployment.wiremock.yaml
> k apply -f configmap.wiremock.yaml

先程試したようにcurlコマンドを打つとコンテナ起動直後からレスポンスが返ってくるようになります。

LivenessProbeの定義

Deploymentのマニフェストファイルを更新しLivenessProbeを設定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wiremock
  name: wiremock
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wiremock
  strategy: {}
  template:
    metadata:
      labels:
        app: wiremock
    spec:
      containers:
      - image: wiremock/wiremock:2.35.0
        args:
          - "--verbose"
        name: wiremock
        volumeMounts:
          - name: wiremock-mappings-configmap
            mountPath: /home/wiremock/mappings
            readOnly: true
        livenessProbe:
          httpGet:
            path: "/get/this"
            port: 8080
          periodSeconds: 5
          timeoutSeconds: 1
          failureThreshold: 3
        resources: {}
      volumes:
      - name: wiremock-mappings-configmap
        configMap:
          name: wiremock-mappings

設定値では、LivenessProbeは5秒間隔で実行され(periodSeconds)、1秒以内にレスポンスが200~399であることを確認します(timeoutSeconds)。3回連続で失敗した場合、ヘルスチェックが不合格となります(failureThreshold)。それぞれの設定項目を整理すると次のようになります。

  • periodSeconds ... 実行間隔(秒)
  • timeoutSeconds ... タイムアウト時間(秒)
  • failureThreshold ... 失敗と判断する、不合格回数

LivenessProbeの動作を確認する

それぞれのリソースを作成し、実際に動作していることを観測してみようと思います。Deploymentに関連付けてPodが作成されるので、コンテナのログを見てみましょう。

2023-04-10 11:15:04.017 Verbose logging enabled
2023-04-10 11:15:04.572 Verbose logging enabled
 /$$      /$$ /$$                     /$$      /$$                     /$$
| $$  /$ | $$|__/                    | $$$    /$$$                    | $$
| $$ /$$$| $$ /$$  /$$$$$$   /$$$$$$ | $$$$  /$$$$  /$$$$$$   /$$$$$$$| $$   /$$
| $$/$$ $$ $$| $$ /$$__  $$ /$$__  $$| $$ $$/$$ $$ /$$__  $$ /$$_____/| $$  /$$/
| $$$$_  $$$$| $$| $$  \__/| $$$$$$$$| $$  $$$| $$| $$  \ $$| $$      | $$$$$$/
| $$$/ \  $$$| $$| $$      | $$_____/| $$\  $ | $$| $$  | $$| $$      | $$_  $$
| $$/   \  $$| $$| $$      |  $$$$$$$| $$ \/  | $$|  $$$$$$/|  $$$$$$$| $$ \  $$
|__/     \__/|__/|__/       \_______/|__/     |__/ \______/  \_______/|__/  \__/

port:                         8080
enable-browser-proxying:      false
disable-banner:               false
no-request-journal:           false
verbose:                      true

2023-04-10 11:15:08.686 Request received:
10.244.1.1 - GET /get/this

Host: [10.244.1.14:8080]
User-Agent: [kube-probe/1.25]
Accept: [*/*]
Connection: [close]



Matched response definition:
{
  "status" : 200,
  "body" : "here it is!\n"
}

Response:
HTTP/1.1 200
Matched-Stub-Id: [ff2d9379-40f5-4554-be54-25296345a647]

2023-04-10 11:15:13.624 Request received:
10.244.1.1 - GET /get/this

Host: [10.244.1.14:8080]
User-Agent: [kube-probe/1.25]
Accept: [*/*]
Connection: [close]



Matched response definition:
{
  "status" : 200,
  "body" : "here it is!\n"
}

Response:
HTTP/1.1 200
Matched-Stub-Id: [ff2d9379-40f5-4554-be54-25296345a647]

2023-04-10 11:15:18.624 Request received:
10.244.1.1 - GET /get/this

Host: [10.244.1.14:8080]
User-Agent: [kube-probe/1.25]
Accept: [*/*]
Connection: [close]



Matched response definition:
{
  "status" : 200,
  "body" : "here it is!\n"
}

Response:
HTTP/1.1 200
Matched-Stub-Id: [ff2d9379-40f5-4554-be54-25296345a647]

少し分かりづらいですが、5秒間隔で /get/this にGETリクエストが来ていることがわかります。Deploymentをdescribeしてみると、様々な情報を確認できます。

> k describe deploy wiremock
Name:                   wiremock
Namespace:              default
CreationTimestamp:      Tue, 11 Apr 2023 21:45:25 +0900
Labels:                 app=wiremock
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=wiremock
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app=wiremock
  Containers:
   wiremock:
    Image:      wiremock/wiremock:2.35.0
    Port:       <none>
    Host Port:  <none>
    Args:
      --verbose
    Liveness:     http-get http://:8080/get/this delay=0s timeout=1s period=5s #success=1 #failure=3
    Environment:  <none>
    Mounts:
      /home/wiremock/mappings from wiremock-mappings-configmap (ro)
  Volumes:
   wiremock-mappings-configmap:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      wiremock-mappings
    Optional:  false
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   wiremock-856c89cccd (1/1 replicas created)
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  2m12s  deployment-controller  Scaled up replica set wiremock-856c89cccd to 1

Pod Template > Containers > wiremock > Liveness をよく見てみると設定されているLivnessProbeが反映されていることがわかります。

2つのマニフェストファイルを一つにまとめる

今回、DeploymentとConfigMapの2つのYAMLを書きました。これは一つのファイルにまとめることができます。次のようになります。

apiVersion: v1
kind: ConfigMap
metadata:
  name: wiremock-mappings
immutable: true
data:
  livness_probe_mapping.json: |
    {
      "request": {
        "url": "/get/this",
        "method": "GET"
      },
      "response": {
        "status": 200,
        "body": "here it is!\n"
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wiremock
  name: wiremock
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wiremock
  strategy: {}
  template:
    metadata:
      labels:
        app: wiremock
    spec:
      containers:
      - image: wiremock/wiremock:2.35.0
        args:
          - "--verbose"
        name: wiremock
        volumeMounts:
          - name: wiremock-mappings-configmap
            mountPath: /home/wiremock/mappings
            readOnly: true
        livenessProbe:
          httpGet:
            path: "/get/this"
            port: 8080
          periodSeconds: 5
          timeoutSeconds: 1
          failureThreshold: 3
        resources: {}
      volumes:
      - name: wiremock-mappings-configmap
        configMap:
          name: wiremock-mappings

KubernetesのProbeの種類と仕組み

kubernetesではコンテナの健康状態を計測するためにProbeと言う仕組みがある。

  • Startup Probe
    • コンテナのアプリケーションが起動したかどうか。起動に時間がかかるアプリケーションが、他二つのProbeによって利用不可、削除されることを防ぐための仕組み。
  • Readiness Probe
    • アプリケーションがリクエストを受け付ける状態であるかどうか。このProbeの成功、失敗によって利用可、利用不可の判断が下される。Serviceは利用不可なコンテナをトラフィックの転送先から自動的に除外し、利用可能になったら転送先に戻す。その他、DeploymentやStatefulSetでも、コンテナのスケール、更新処理にて利用される。このProbeは失敗したとしてもコンテナが破棄されることはない。
  • Liveness Probe
    • コンテナが正常に動作しているかどうか。失敗すると、コンテナは破棄され、再作成される。

Clojureのget-inの使い方メモ

シゴトで使うようになったclojure。get-inなる関数が気になったので使い方をチョット調べました。

(ns getting-started-clojure.get-in)

(def m {:username "sally"
        :profile {:name "Sally Clojurian"
                  :address {:city "Austin" :state "TX"}}})

(comment
  (get-in m [:profile :name])
  ;; "Sally Clojurian"
  (get-in m [:profile :address :city])
  ;; "Austin"
  (get-in m [:profile :address :city :number] "not found")
  ;; "not found"
  )

(def n [{:username "first"} {:username "second"}])

(comment
  (get-in n [0 :username])
  ;; "first"
  )

リストやマップから特定の値を名前通り取り出すもののよう。

マップの場合はキーを。リストの場合は0始まりの添字を使うみたいですね。

neovim (vim) のヘルプコマンドの走り書き その1

  • https://neovim.io/doc/user/helphelp.html#helphelp
  • 引数なしで :help コマンドを打つと helpfile オプションで指定されたファイルが開く。
  • 通常は help.txt が開くと思う。
  • 以下のコマンドで helpfile オプションの値を確認できる。
  • また、タグが指定された場合には、 runtimepath オプションで指定されたディレクトリの doc/tags ファイルの中から検索を行う。
  • この場合も同じく、 :set runtimepath? で設定されている値を確認できる。
    • ヘルプウィンドウの初期高さは helpheight オプションで変更できる。デフォルトは20。

  • ヘルプファイルを見ていて、特定の項目にジャンプする方法は2つある。
  • CTRL-] コマンドを使う。
    • コマンド名、またはオプション名の上にカーソルを移動し、 CTRL-] でその項目にジャンプできる。
  • :ta {subject} コマンドを使う。
    • ジャンプ先から戻るには CTRL-T または CTRL-O コマンドを使う。

  • 特定のトピックについてヘルプファイルを全検索したい場合がある。(大体そう)
  • その場合、次の手順でいい感じに検索ができる。
  • lh {subject} コマンドでヘルプを検索。
    • この場合、grep検索ができる。
    • ロケーションリストで該当箇所を記憶してくれる。
  • :lopen コマンドでロケーションリストを表示する。
  • 各項目にいい感じにジャンプできる。

Clojure Leiningen その5

テスト

  • lein test コマンドでテストを実行。
  • 例。
vagrant@ubuntu2204:~/source/temp$ lein test

lein test temp.core-test

lein test :only temp.core-test/a-test

FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
  actual: (not (= 0 1))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Subprocess failed (exit code: 1)
  • ↑ テストが一件落ちていると警告が出ている。
  • ↑ この場合、テストが少ないので問題ないが、テストが増えてくると時間がかかるようになってくる。名前空間を指定すれば、そこだけテストを実行できる。
  • 例。
vagrant@ubuntu2204:~/source/temp$ lein test temp.core-test

lein test temp.core-test

lein test :only temp.core-test/a-test

FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
  actual: (not (= 0 1))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Subprocess failed (exit code: 1)
  • 存在しない名前空間を指定したらどうなるんだろう?
vagrant@ubuntu2204:~/source/temp$ lein test temp.core

lein test user

Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
  • エラーは出ないので注意。代わりに(?) Ran 0 tests とメッセージが出る。
  • REPLで clojure.test/run-tests を使ってもテストを実行できる。この場合、JVMをいちいち起動しないので高速にテストを実行できる。
vagrant@ubuntu2204:~/source/temp$ lein repl
nREPL server started on port 38937 on host 127.0.0.1 - nrepl://127.0.0.1:38937
REPL-y 0.5.1, nREPL 0.9.0
Clojure 1.11.1
OpenJDK 64-Bit Server VM 17.0.5+8-LTS
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

temp.core=> (use 'clojure.test)
nil
temp.core=> (clojure.test/run-tests)

Testing temp.core

Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
{:test 0, :pass 0, :fail 0, :error 0, :type :summary}
temp.core=> 
  • Ran 0 tests と言われた。
  • よく見ると実行しているテストの名前空間がテストの名前空間ではない。
  • 名前空間を指定するとうまく実行できた。どうやらデフォルトではREPLで今いる名前空間のテストを実行するみたい。
temp.core=> (require 'temp.core-test)
nil
temp.core=> (clojure.test/run-tests 'temp.core-test)

Testing temp.core-test

FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
  actual: (not (= 0 1))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
{:test 1, :pass 0, :fail 1, :error 0, :type :summary}
temp.core=> 
  • run-tests は繰り返し実行する時間の短縮にはなるが、テストコードを変更した場合(またはその他プロダクションコードを変更した場合も)REPLの世界に変更を取り込まないと反映されない。そのため、変更の取り込みを簡単に行う仕組みを別途整える必要がある。