yu nkt’s blog

nkty blog

I'm an enterprise software and system architecture. This site dedicates sharing knowledge and know-how about system architecture with me and readers.

vimでyamlのオートインデントを設定

Kubernetesyamlなど、最近はVimyaml (yml)ファイルを記述する機会が増えました。 ただ、Vimのデフォルトのオートインデントだと、yamlファイルはかなり扱いにくいと思います。 今回は、Vimで快適にyamlファイルを編集するための設定を書きます。

最終形だけ欲しい場合は、他の設定も含まれていますが、こちらをご覧ください。

github.com

プラグイン

まず、プラグインをインストールします。 私はNeoBundleを利用しているため、.vimrcの、NeoBundleプラグインを指定する場所に、このように書きます。

NeoBundle 'chase/vim-ansible-yaml'

chase/vim-ansible-yamlプラグインは、filetypeがansibleのファイルを対象に、yamlファイルのオートインデントを正しく(多くの人の思い通りに)するプラグインです。

スペース数

次に、インデントのスペース数を設定しましょう。 私は、普段は4なのですが、yamlファイルは2であることが多いので、拡張子に合わせてこのように記載します。

" Indent width
if has("autocmd")
  "ファイルタイプの検索を有効にする
  filetype plugin on
  "ファイルタイプに合わせたインデントを利用
  filetype indent on
  "sw=softtabstop, sts=shiftwidth, ts=tabstop, et=expandtabの略
  autocmd FileType c           setlocal sw=4 sts=4 ts=4 et
  autocmd FileType html        setlocal sw=4 sts=4 ts=4 et
  autocmd FileType ruby        setlocal sw=2 sts=2 ts=2 et
  autocmd FileType js          setlocal sw=4 sts=4 ts=4 et
  autocmd FileType zsh         setlocal sw=4 sts=4 ts=4 et
  autocmd FileType python      setlocal sw=4 sts=4 ts=4 et
  autocmd FileType scala       setlocal sw=4 sts=4 ts=4 et
  autocmd FileType json        setlocal sw=4 sts=4 ts=4 et
  autocmd FileType yml        setlocal sw=2 sts=2 ts=2 et
  autocmd FileType yaml        setlocal sw=2 sts=2 ts=2 et
  autocmd FileType html        setlocal sw=4 sts=4 ts=4 et
  autocmd FileType css         setlocal sw=4 sts=4 ts=4 et
  autocmd FileType scss        setlocal sw=4 sts=4 ts=4 et
  autocmd FileType sass        setlocal sw=4 sts=4 ts=4 et
  autocmd FileType javascript  setlocal sw=4 sts=4 ts=4 et
endif

ポイントは、ymlとyamlの行です。

空行の後の行はインデントをクリア

空行があるというのは、複数のyamlファイルを書くときなどだと思います。 そのため、空行があったら、インデントをリセットして欲しいと思います。 その設定は、以下のようにかけば良いです。 これは、chase/vim-ansible-yamlプラグインのオプション設定です。

let g:ansible_options = {'ignore_blank_lines': 0}

yaml/ymlファイルをansibleファイルと見なす

再度書きますが、chase/vim-ansible-yamlプラグインは、filetypeがansibleのファイルを対象に、yamlファイルのオートインデントを正しく(多くの人の思い通りに)するプラグインです。 ですので、普通にyamlファイルやymlファイルを開いただけでは、filetypeがyamlとなっているため、このプラグインが機能しません。

そこで、ファイルタイプをansibleと見なすための設定をします。

vim ~/.vim/filetype.vim

ここに、以下のように記載します。

augroup filetypedetect
  au BufRead,BufNewFile *.yaml setfiletype ansible
  au BufRead,BufNewFile *.yml  setfiletype ansible
augroup END

これで、yamlファイルとyamlファイルが、ansibleと見なされるようになります。

おわりに

これで、yamlファイルとymlファイルが快適に作成出来るようになると思います。 具体的には、

apiVersion: apps/v1

と書いた後に、改行をしたら、もともとは勝手にインデントが追加されていたと思いますが、インデントなしで次の行が書けます。

また、元々の設定では、

metadata: 
  name

このnameの後に、コロンを入力したら、自動的にインデントが上がってしまう現象などもあると思いますが、それもなくなります。

補足

ちなみに、Vimでは、行のインデントを上げる(1つ左にする)のは、Ctrl+t、インデントを下げるのはCtrl+d、です。 Vimを記述しているなら、これは覚えておくと便利だと思います。

Makefile内のechoコマンドで$ (ダラー)をそのまま出力させる

Makefile内のechoコマンドで$ (ダラー)をそのまま出力させる方法です。 全く情報を見つける事が出来ませんでしたが、試行錯誤してたら出来ました。

環境:bash (バージョン:4.2.46(2)-release (x86_64-redhat-linux-gnu))

Makefileにこのように書きます。

build:
    @echo Current '$$'http_proxy is ${http_proxy}

(インデントはタブにしてください。はてブの都合上、タブに出来ない…)

ポイントは、$$を、シングルクオーテーションで囲むところです。 囲まないと、次のhttp_proxyの環境変数の値が表示されてしまいます。

では、実行してみます。

$ export http_proxy=http://huga
$ make build
Current $http_proxy is http://huga

ちゃんと、$が表示されました。

プロキシ環境下の自前GitLabにアクセスする方法

背景

Gitに関して、何度もプロキシ関連でハマってきて、何度も同じようなことをググってきたので、自分で整理します。

環境

アカウントのプロキシ設定

まず、アカウントにプロキシを設定します。 プロキシの情報は、ログイン時に自動的に設定するようにすればよいので、.bash_profileに記載しておきます。 もしなければ、作成しましょう。

touch $HOME/.bash_profile

httpとhttpsで、両方同じプロキシ and/or 認証情報でよければ、以下のように記載します。

PROXY_INFO="http://(user):(pass)@(host):(port)"
export http_proxy=${PROXY_INFO}
export https_proxy=${PROXY_INFO}
export HTTP_PROXY=${PROXY_INFO}
export HTTPS_PROXY=${PROXY_INFO}
NOPROXY_INFO="localhost,(自前GitLabのホスト)"
export no_proxy=${NOPROXY_INFO}
export NO_PROXY=${NOPROXY_INFO}

もし、httpとhttpsで異なるプロキシ and/or 認証情報が必要であれば、それぞれ個別に記載してください。

これを反映します。

source $HOME/.bash_profile

Gitのプロキシ設定

次に、Gitにプロキシを設定しましょう。

git config --global http.proxy $http_proxy
git config --global https.proxy $https_proxy
git config --global http.sslVerify false
git config --global http.proxyAuthMethod 'basic'

3つ目のコマンドは、オレオレ認証局のカギでもいいこととする、というものなので、セキュリティ的にはマズいです。 そこをよく理解したうえでの自己責任、ということにさせてください。

4つ目のコマンドは、プロキシサーバがサポートしている認証方式によって、basicかどうか異なります。 プロキシによって、異なるかもしれませんし、設定が不要かもしれません。 自分の環境では、4つ目を設定していなかったら、このようなエラーが出てました。

$ git clone https://aaaaa/bbbbb/ccccc.git
Cloning into 'ccccc' ...
fatal: unable to access 'https://aaaaa/bbbbb/ccccc.git/': Invalid file descriptor

下記、ご参考。

github.com

AWS EC2上にkubesprayでk8s環境を構築

背景

kubernetesの構築から運用まで、少しずつ勉強しようと思い、EC2に環境構築を試してみたのですが、かなりハマったので、その共有をします。 今時、自らEC2に環境構築する人は少ないかもしれませんが、練習ということで。

環境

  • k8sを構築する環境
  • ansibleコマンドを叩くローカル
    • Mac Mojave
    • WindowsのWSLでも試していますが、こちらはうまく行かない・・・(Macよりもさらにハマっているので、解決したら別の記事で解説します)

作業

ここからの試行錯誤の流れはかなり長いので、ansible-playbookが動くまでとその後とで分けて説明します。

ansible-playbookが動くまで

まずは、何も考えずにGetting Started通りに進めました。

Getting Startedでは、一番最初にrequirements.txtに書かれたモジュールをインストールすることになっています。 その中にansibleがあるので、一応homebrewでインストールしたansibleは削除しておきました。

その上で、以下のように実行。


$ sudo pip install -r requirements.txt
$ cp -rfp inventory/sample inventory/mycluster
$ declare -a IPS=(x.x.x.1 x.x.x.2 x.x.x.3)
$ CONFIG_FILE=inventory/mycluster/hosts.ini python3 contrib/inventory_builder/inventory.py ${IPS[@]}
Traceback (most recent call last):
  File "contrib/inventory_builder/inventory.py", line 36, in 
    from ruamel.yaml import YAML
ModuleNotFoundError: No module named 'ruamel'

ruamelがないと怒られます。 なぜrequirements.txtに記載がないのか不思議ですが、手作業でインストールしましょう。

$ sudo pip install ruamel.yaml
$ CONFIG_FILE=inventory/mycluster/hosts.ini python3 contrib/inventory_builder/inventory.py ${IPS[@]}
Traceback (most recent call last):
  File "contrib/inventory_builder/inventory.py", line 36, in <module>
    from ruamel.yaml import YAML
ModuleNotFoundError: No module named 'ruamel'

また同じエラーが出ます。 よく見たら、入力したコマンドにはpython3を使っていますが、requirements.txtによるモジュールのインストールには、単なるpip (つまりpython2.7)を使っています。 なぜGetting Startedがこうなっているのか謎ですが、pip3でインストールしましょう。

$ sudo pip3 install -r requirements.txt
$ sudo pip3 install ruamel.yaml
$ CONFIG_FILE=inventory/mycluster/hosts.ini python3 contrib/inventory_builder/inventory.py ${IPS[@]}
Traceback (most recent call last):
  File "contrib/inventory_builder/inventory.py", line 391, in <module>
    sys.exit(main())
  File "contrib/inventory_builder/inventory.py", line 388, in main
    KubesprayInventory(argv, CONFIG_FILE)
  File "contrib/inventory_builder/inventory.py", line 77, in __init__
    self.yaml_config = yaml.load(self.hosts_file)
  File "/usr/local/lib/python3.7/site-packages/ruamel/yaml/main.py", line 331, in load
    return constructor.get_single_data()
  File "/usr/local/lib/python3.7/site-packages/ruamel/yaml/constructor.py", line 109, in get_single_data
    node = self.composer.get_single_node()
  File "/usr/local/lib/python3.7/site-packages/ruamel/yaml/composer.py", line 87, in get_single_node
    event.start_mark,
ruamel.yaml.composer.ComposerError: expected a single document in the stream
  in "inventory/mycluster/hosts.ini", line 4, column 1
but found another document
  in "inventory/mycluster/hosts.ini", line 15, column 1

最後のコマンドでエラーが発生しました。

このコマンドは、k8sが構築されるVMのIPを、hostsファイルに設定するためのコマンドのようですが、hosts.iniがパースできないと言われます。 気になるのは、ruamel.yaml.compopser.ComposerErrorです。 メッセージ的にymlを扱いそうなのに、hosts.iniを修正しようとしているのが原因のような気がします。

そこで、hosts.iniの部分をhosts.ymlに変更してみましょう。

$ CONFIG_FILE=inventory/mycluster/hosts.yml python3 contrib/inventory_builder/inventory.py ${IPS[@]}
DEBUG: Adding group all
DEBUG: Adding group kube-master
DEBUG: Adding group kube-node
DEBUG: Adding group etcd
DEBUG: Adding group k8s-cluster
DEBUG: Adding group calico-rr
DEBUG: adding host node1 to group all
DEBUG: adding host node2 to group all
DEBUG: adding host node3 to group all
DEBUG: adding host node1 to group etcd
DEBUG: adding host node2 to group etcd
DEBUG: adding host node3 to group etcd
DEBUG: adding host node1 to group kube-master
DEBUG: adding host node2 to group kube-master
DEBUG: adding host node1 to group kube-node
DEBUG: adding host node2 to group kube-node
DEBUG: adding host node3 to group kube-node

思いの外うまくいきました。

残るは、最後のコマンド、ansible-playbookを叩くのみです。

ansible-playbookを最後まで通す

ansible-playbookを叩く前に、二つ気をつけることがあります。 一つは、Getting Startedでは、パスワード認証のようですが、EC2では秘密鍵認証なので、オプションを追加する必要があります。 -uオプションでEC2のUbuntuのユーザー名(デフォルトならubuntu)、--private-keyオプションで秘密鍵のパスを指定しましょう。

もう一つ気をつけることとして、先ほどhostsファイルをymlで作ったので、指定するhostsファイルは、hosts.iniではなく、hosts.ymlにする必要があります。

ではそれを踏まえて、コマンドを入力しましょう。

$ ansible-playbook -i inventory/mycluster/hosts.yml --become --become-user=root cluster.yml -u ubuntu --private-key=~/.ssh/private.pem

(長いので省略)

TASK [kubernetes/preinstall : Stop if ip var does not match local ips] *************************************************************************************************************
Sunday 07 April 2019  19:18:33 +0900 (0:00:00.110)       0:00:57.115 **********
fatal: [node1]: FAILED! => {
    "assertion": "ip in ansible_all_ipv4_addresses",
    "changed": false,
    "evaluated_to": false,
    "msg": "Assertion failed"
}
fatal: [node2]: FAILED! => {
    "assertion": "ip in ansible_all_ipv4_addresses",
    "changed": false,
    "evaluated_to": false,
    "msg": "Assertion failed"
}
fatal: [node3]: FAILED! => {
    "assertion": "ip in ansible_all_ipv4_addresses",
    "changed": false,
    "evaluated_to": false,
    "msg": "Assertion failed"
}

途中でエラーが出て止まりました。

ip in ansible_all_ipv4_addressesググると、以下のStackoverflowのスレッドでヒントを見つけました。

The check is if ip is actually a local ip address. It's not a bug. You can't tell etcd to bind to the floating IP address. You should set access_ip instead to specify the floating IP

ipローカルアドレスになっているか確認しましょう。このメッセージは決してバグなどではありません。etcdにfloating IPアドレスを伝えることはできないのです。その代わりaccess_ipにfloating IPアドレスをセットしましょう。

github.com

floating IPアドレスとは、OpenStackで外部からアクセスするためにインスタンスに割り当てるIPアドレスです。 EC2でいうパブリックIPです。

現状のhosts.ymlを確認してみましょう。

$ cat inventory/mycluster/hosts.yml
all:
  hosts:
    node1:
      ansible_host: x.x.x.1
      ip: x.x.x.1
      access_ip: x.x.x.1
    node2:
      ansible_host: x.x.x.2
      ip: x.x.x.2
      access_ip: x.x.x.2
    node3:
      ansible_host: x.x.x.3
      ip: x.x.x.3
      access_ip: x.x.x.3
  children:
    kube-master:
      hosts:
        node1:
        node2:
    kube-node:
      hosts:
        node1:
        node2:
        node3:
    etcd:
      hosts:
        node1:
        node2:
        node3:
    k8s-cluster:
      children:
        kube-master:
        kube-node:
    calico-rr:
      hosts: {}

先ほどのStackOverflowのスレッドの下の方に書かれている通り、ansible_hostというキー名をansible_ssh_hostに変え、さらにipの値にローカルIPをセットしてみました。

これで、先どのansible-playbookを実行してみましょう。

$ ansible-playbook -i inventory/mycluster/hosts.yml --become --become-user=root cluster.yml -u ubuntu --private-key=~/.ssh/private.pem

(長いので省略)

TASK [kubernetes/preinstall : Stop if access_ip is not pingable] *******************************************************************************************************************
Sunday 07 April 2019  21:05:52 +0900 (0:00:00.111)       0:00:11.392 **********
fatal: [node1]: FAILED! => {"changed": true, "cmd": ["ping", "-c1", "x.x.x.1"], "delta": "0:00:10.002630", "end": "2019-04-07 12:06:03.175649", "msg": "non-zero return code", "rc": 1, "start": "2019-04-07 12:05:53.173019", "stderr": "", "stderr_lines": [], "stdout": "PING x.x.x.1 (x.x.x.1) 56(84) bytes of data.\n\n--- x.x.x.1 ping statistics ---\n1 packets transmitted, 0 received, 100% packet loss, time 0ms", "stdout_lines": ["PING x.x.x.1 (x.x.x.1) 56(84) bytes of data.", "", "--- x.x.x.1 ping statistics ---", "1 packets transmitted, 0 received, 100% packet loss, time 0ms"]}
fatal: [node2]: FAILED! => {"changed": true, "cmd": ["ping", "-c1", "x.x.x.2"], "delta": "0:00:10.002654", "end": "2019-04-07 12:06:03.184854", "msg": "non-zero return code", "rc": 1, "start": "2019-04-07 12:05:53.182200", "stderr": "", "stderr_lines": [], "stdout": "PING x.x.x.2 (x.x.x.2) 56(84) bytes of data.\n\n--- x.x.x.2 ping statistics ---\n1 packets transmitted, 0 received, 100% packet loss, time 0ms", "stdout_lines": ["PING x.x.x.2 (x.x.x.2) 56(84) bytes of data.", "", "--- x.x.x.2 ping statistics ---", "1 packets transmitted, 0 received, 100% packet loss, time 0ms"]}
fatal: [node3]: FAILED! => {"changed": true, "cmd": ["ping", "-c1", "x.x.x.3"], "delta": "0:00:10.003199", "end": "2019-04-07 12:06:03.188290", "msg": "non-zero return code", "rc": 1, "start": "2019-04-07 12:05:53.185091", "stderr": "", "stderr_lines": [], "stdout": "PING x.x.x.3 (x.x.x.3) 56(84) bytes of data.\n\n--- x.x.x.3 ping statistics ---\n1 packets transmitted, 0 received, 100% packet loss, time 0ms", "stdout_lines": ["PING x.x.x.3 (x.x.x.3) 56(84) bytes of data.", "", "--- x.x.x.3 ping statistics ---", "1 packets transmitted, 0 received, 100% packet loss, time 0ms"]}

先ほどのエラーは乗り越えましたが、Pingが通らない、というエラーが出ています。

気になるのは、access_ip is not pingableという言葉。 ノード間の通信に使われるIPアドレスは、access_ipのようです。 そこで、再度、hosts.ymlを見直し、access_ipにローカルIPを入れてみました。

しかしそれでも同じところで止まります。

では、本当にPingは返らないのか、確認してみます。 インスタンスSSHでログインして、別のインスタンスにプライベートIPでPingを飛ばしてみます。 そうしたら、確かに返ってきませんでした。

この原因はかなり呆気ないもので、セキュリティグループが正しく設定されていないことでした。 許可するインバウンドを、検証している自宅のIPのみ許可する設定にしていたため、ノード間ではPingも不通だったようです。 プライベートIPだったら、セキュリティグループに記述しなくても疎通できると勘違いしていました。

改めてセキュリティグループの設定し直し、もう一度ansible-playbookを実行。

今回は、かなり成功しているような感じで、処理がどんどん進んでいきます。

しかし、途中でこんなログも。ただ、ignoringと出ているので、ひとまずそのままにしておきます。

TASK [etcd : Configure | Check if etcd cluster is healthy] *************************************************************************************************************************
Sunday 07 April 2019  23:44:50 +0900 (0:00:00.131)       0:04:42.813 **********
fatal: [node2]: FAILED! => {"changed": false, "cmd": "/usr/local/bin/etcdctl --endpoints=https://(ローカルIP 1):2379,https://(ローカルIP 2):2379,https://(ローカルIP 3):2379 cluster-health | grep -q 'cluster is healthy'", "delta": "0:00:00.012281", "end": "2019-04-07 14:44:50.868068", "msg": "non-zero return code", "rc": 1, "start": "2019-04-07 14:44:50.855787", "stderr": "Error:  client: etcd cluster is unavailable or misconfigured; error #0: dial tcp (ローカルIP 3):2379: getsockopt: connection refused\n; error #1: dial tcp (ローカルIP 2):2379: getsockopt: connection refused\n; error #2: dial tcp (ローカルIP 1):2379: getsockopt: connection refused\n\nerror #0: dial tcp (ローカルIP 3):2379: getsockopt: connection refused\nerror #1: dial tcp (ローカルIP 2):2379: getsockopt: connection refused\nerror #2: dial tcp (ローカルIP 1):2379: getsockopt: connection refused", "stderr_lines": ["Error:  client: etcd cluster is unavailable or misconfigured; error #0: dial tcp (ローカルIP 3):2379: getsockopt: connection refused", "; error #1: dial tcp (ローカルIP 2):2379: getsockopt: connection refused", "; error #2: dial tcp (ローカルIP 1):2379: getsockopt: connection refused", "", "error #0: dial tcp (ローカルIP 3):2379: getsockopt: connection refused", "error #1: dial tcp (ローカルIP 2):2379: getsockopt: connection refused", "error #2: dial tcp (ローカルIP 1):2379: getsockopt: connection refused"], "stdout": "", "stdout_lines": []}
...ignoring

ただ、最終的には、このような形で終了し、どうやら成功したみたいです。

PLAY RECAP *************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0
node1                      : ok=396  changed=117  unreachable=0    failed=0
node2                      : ok=333  changed=101  unreachable=0    failed=0
node3                      : ok=298  changed=88   unreachable=0    failed=0

試しに、kubectlコマンドでk8s環境の状況を確認しましょう。

$ ssh -i ~/.ssh/private.pem ubuntu@x.x.x.1
$ sudo su
# kubectl get nodes
NAME    STATUS   ROLES         AGE   VERSION
node1   Ready    master,node   23m   v1.13.5
node2   Ready    master,node   22m   v1.13.5
node3   Ready    node          22m   v1.13.5

うまくいけているようです。

まとめ

Kubespray自体のREADMEに、かなり違和感を感じるものの、誤字なのか自分の環境が特殊なのか分からず、Pull requestを出して良いものか悩ましい状況です。 他の方は、何も困らずに勧められるんでしょうか・・・。

その他、知識不足もあり、苦戦しましたが、無事週末に自分のk8s環境を整えることができました。 k8sライフを楽しみたいと思います。

WSLの方は、追って追記したいと思います。

EC2インスタンスへansibleで疎通確認

目的

EC2にあるインスタンスの構成管理をしようと思いますが、その前に、そのインスタンスへの疎通確認をしてみました。

今までansibleをローカルの環境構築にしか使ってなかったとは言え、まさかここで詰まることがあるとは…。

環境

  • 構成管理対象のリモート環境
  • 手元の環境
    • Windows10
      • WSL (Ubuntu18.04)
      • Ansible: 2.7.10 (pipでインストール)
    • Mac Mojave 10.14.4
      • Ansible: 2.7.9 (homebrewでインストール)

EC2のインスタンスは、kubesprayでk8s環境を作ってみようと思い、用意しました。 手元の環境が二つあるのは、後述するように、ハマった時の原因切り分けのために二つ使っていただけです。 手元に二つの環境がないといけないわけではありません。

作業

まずは、EC2でインスタンスを立ち上げます。コンソール画面からやりました。 秘密鍵は、~/.ssh/の中に入れておきました。chmod 400 秘密鍵を忘れずに。

ansibleでpingが通るかを確認しましょう。

$ echo "インスタンスのIP" > hosts
$ ansible -i hosts -m ping -u ubuntu --private-key=~/.ssh/秘密鍵 インスタンスのIPアドレス

出た出力がこちら。

インスタンスのIP | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: muxclient: master hello exchange failed\r\nFailed to connect to new control master",
    "unreachable": true
}

ansibleに不慣れとはいえ、まさかここからつまずくとは。 かなりハマったのですが、Macで試したら、次のような出力が出て、ヒントになりました。

インスタンスのIP | FAILED! => {
    "changed": false,
    "module_stderr": "Shared connection to インスタンスのIP closed.\r\n",
    "module_stdout": "/bin/sh: 1: /usr/local/bin/python: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}

このエラーの真意は、「sshで接続した先で、ansibleを実行しようとしたけど、pythonがインストールされてないよ」ということのようです。 しかし、今やりたいのは、作成したインスタンスsshで入れるか、そして試しにpingがとばせられるかどうか、なので、 pingを実行するのは手元の環境です。

このように、手元の環境でansibleを実行したい場合は、hostsファイルを以下のように変える必要があります。

インスタンスのIP ansible_connection=local

これでもう一度ansibleコマンドを実行すると、うまく行きました。

$ ansible -i hosts -m ping -u ubuntu --private-key=/Users/y-nakata/.ssh/秘密鍵 インスタンスのIP
インスタンスのIP | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

感想

WSLとMacとで、Ansibleの出力が違うのは謎ですが、WSLのエラーメッセージで意味がわかる人がどれだけいるのか…。

SafetyとLiveness

はじめに

今回は、昨年のACM PODC (Principle of Distributed Computing)でDijkstra賞を受賞した、以下の発表について解説します。

B. Alpern, F. B.Schneider, "Defining liveness," in proceedings of published in Information Processing Letters, vol. 21, no. 4, pp. 181-185, Oct. 1985.

歴史的背景

1960年代から、ITシステムが徐々にビジネスの重要な業務を肩代わりするようになりました。 その当時は、トランザクションを扱うシステムが出始め、日本でも、みどりの窓口で今でも後継が使われているマルスというトランザクション管理システムが生まれた時期です。

システムは大規模化して行き、仕様も複雑になっていきました。 「こんな場合(異常系も含めて)に、どう処理するべきか」の"こんな場合"を網羅的にリストアップし、その全ての対応を考えるのは、人間の頭(想像)だけでは限界がありました。 しかし、システムがインフラの一部に組み込まれていく中で、「仕様漏れで障害が起きました」は許されるはずがありません。 そのため、システムが思い通りに(安全に)動くか、を定量的に知ることは、非常に重要なトピックでした。

そんな中、1977年にLeslie Lamport氏が"Proving the Correctness of Multiprocess Programs"という論文を出し、この中で初めてSafetyLivenessという用語が誕生します。 これ以降、現在でもSafetyとLivenessと言う用語は使われ続けています。

今回紹介する論文は、Livenessを数式的に定義し、さらに正しく動作するシステムが持つあらゆる特徴は、SafetyとLivenessの積集合に含まれることを証明しました。

前提知識

まずは、この論文を読むに当たっての前提知識を説明します。

LivenessとSafety

二つとも、システムやプログラムが持つ特徴(Property)です。 これら二つは、Concurrency controlの巨匠、Lamport先生の論文で最初に登場しました。

L. Lamport, "Proving the Correctness of Multiprocess Programs," IEEE Transaction on Software Engineering, vol. SE-3, no. 2, pp. 125-143, Mar. 1977.

それぞれ、以下のように表現されます。

  • Liveness: 最終的に良いことが起きる
  • Safety: 悪いことが起きない

ここでいう良いことと悪いこととは、以下のような意味です。

  • 良いこと(Good):システムやプログラムが、開発者やユーザーが期待した計算を終えること、または期待した状態になること
  • 悪いこと(Bad):エラーが発生すること、またはエラー状態に陥ったりデッドロックなどにより終了状態以外の状態から抜け出せなくなること

Livenessは、例えばNoSQLでよく言われる結果整合性 (Eventual consistency)が分かりやすいかもしれません。

悪いこと(Bad)の例

具体的に何をGoodとし、Badとするかは、そのプログラムの要件や状況次第としか言えません。 例えば、エラーが発生した場合にそれをBadとして良いのですが、必ずしもBadが発生したからといってシステムが救いようのない状態に陥ると断定する必要はありません。 Badなことが起きたあとでも、Goodなことが起きて、システムが正常状態に復帰することはあって良いのです。

以下に一般的な悪いことの例を記載します。

  • Deadlock: 複数のプロセスが、互いにロックしているリソースの解放を、半永久的に待ち続けてシステムが止まる現象

デッドロックは、複数のプロセスが複数のリソースを同時に利用して処理する状況において発生し得ます。 S2PLのように、順番にロックをとっていく仕組みだと発生し得ます。 ちなみに、デッドロックが発生しうる状況は、Coffman conditionと呼ばれます。

  • Critical section: 複数の処理で共有された単一のリソース

適切なロックの処理を行わずに、複数のプロセスがクリティカルセクションに同時に行われると、データの不整合が発生するなどの破綻がおきます。

内容

論文の内容に入っていきましょう。

この論文のメインポイントの一つは、Livenessの定義を数式的に行なったことです。

ですが、式を見る前に持って頂きたいイメージがあります。 それは、プログラムにはたくさんの処理が定義されており、一つの処理が行われると、そのプログラムを動かすシステムの状態が変わっていく、ということです。 状態遷移図を知っている人からすると当たり前かもしれません。 式を見る上では、状態遷移図を頭に浮かべ、その上でLivenessの定義が、最終的にGoodな状態に到達可能かどうか、を表しているか確認してみてください。

では、式を見ていきましょう。

\forall \alpha: \alpha \in S^{*}: (\exists \beta: \beta \in S^{\omega}:\alpha\beta\models P)

式の中に登場する変数の説明は以下の通りです。

  • S
    • システムの状態の集合
  • S^{\omega}
    • システムの状態の無限長の列
    • この列の要素は、状態の方ではなく、その状態遷移を引き起こした実行(executions)とする
    • 世の中のあらゆるプログラムにおけるexecutionは、S^{\omega}の要素としてモデル化される
  • S^{*}
    • システムの状態の有限長の列
    • S^{\omega}と同じく、この列の要素は、その状態を引き起こした(部分)実行 (partial executions)とする
  • \alpha\beta
    • 実行\alphaの後に実行\betaが発生することを意味する
  • \alpha\beta \models P
    • execution \alpha\betaで到達する状態が特徴Pを持つことを意味する

\alpha\beta \models Pから見ていきましょう。 これは、「\alphaが発生した後に、\betaが発生して到達した状態が、Pという特徴に含まれる」という意味です。

次に、\exists \beta: \beta \in S^{\omega}を見ていきましょう。 これは、「ありとあらゆる状態遷移(つまりS^{\omega})の中で探したら、○○を満たす\betaがある」という意味です。

では、\exists \beta: \beta \in S^{\omega}:\alpha\beta\models Pを繋げましょう。 これは、「ありとあらゆる状態遷移(つまりS^{\omega})の中には、実行\alphaが発生した後の状態から、Pという特徴に含まれる状態に到達しうる実行\betaが少なくとも1つはある」という意味になります。

最後に\alpha \in S^{*}ですが、これはそのプログラムでありえるシステムの状態遷移の中のあらゆる実行\alphaを指します。

ですので、全部まとめると、「プログラムとしてありえる全ての状態からは、その後Pという特徴を持つ状態に移行するための実行が必ずある」となります。 そのため、Pがシステムとして正常な状態とするなら、そこに行き着く過程が必ずある、ということです。 よって、この式を満たすプログラム(状態遷移)は、Livenessということです。

おまけ

論文では、他の先行研究で提案されたLivenessの定義についても記載されており、上記の定義がそれと比べて直感的に的を得ていることを説明しています。

あと、すでに提案されているSafetyの定義についても触れられています。

 \forall \sigma: \sigma \in S^{\omega}:
\sigma \not\models P \rightarrow (\exists i:0\leq i:(\forall \beta: \beta \in S^{\omega}: \sigma_{i}\beta \not\models P))

P に含まれない状態に陥ってしまったら、どんな遷移が発生しても、もうPに含まれる状態になることはない」という意味ですね。

ここまで感覚がつかめてきたら、他のLivenessの定義も理解できてくると思います。 あとは、読者の皆様におまかせします。

余談(おわりに)

以前、会社内で、システムの挙動を状態遷移で設計するか、フローチャートで設計するか、と話題になったことがあります。 フローチャートは、正常性の動きの流れを示す、という意味では良く、チームに新しい開発者が参加した際などには役にたつかもしれません。 しかし、システムの健全性という観点で言うと、発生する実行や状態の網羅性を捉えることができるという意味では状態遷移ですね。

Squash commit

背景

APIの仕様をRAMLで記述する際に、api-designerを使っていますが、そのContribution guideに以下のような記述がありました。

Before submitting a pull request, you need to execute squash commits. Submit the pull request afterwards to have them considered for merging into the main API Designer repo;

プルリクエストを送信する前に、スカッシュコミットを実行する必要があります。 その後にプルリクエストを送信すれば、それらの変更がAPI Designerのメインのリポジトリへマージして良いか検討されます。

あまり会社内の普段の開発では使わないのですが、OSSの開発に携わったりする場合は、api-designerのようにPull request前にsquash commitを要求するケースも多々あるので、知っておいた方が良いでしょう。

Squash commitとは

端的に言うと、複数回のコミットを一つにまとめるための手段です。

コミットの粒度は諸説あり、問題が起きた時に戻せられる粒度、細かい機能が完成した(テストが通る)粒度などがありますが、往々にして一つのコミットは一回のPull requestに対し細かすぎることがあります。 そうすると、複数回のコミットが詰まったPull requestを提出することになると思いますが、それではOSSのメンテナーはPull requestのレビューがしにくくなります。

そのため、OSSのメンテナーの要望として、複数回のコミットを一回のコミットにまとめてほしいという場合があります。 ここでSquash commitを使います。 Squash commitを行い、複数回のコミットを一回のコミットにまとめた上で、Pull requestを提出するのです。

Squash commitのやり方

あるブランチに複数回のコミットがなされていたとして、別のブランチにその複数回のコミットを一回のコミットとしてマージします。

下が、squash commitの例です。

~/dev $ mkdir squash-practice

~/dev $ cd squash-practice/

~/dev/squash-practice $ git init
Initialized empty Git repository in /Users/y-nakata/dev/squash-practice/.git/

~/dev/squash-practice [master #]$ touch README.md

~/dev/squash-practice [master #%]$ git add .

~/dev/squash-practice [master +]$ git commit -m "Add README"
[master (root-commit) 6466ea0] Add README
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

~/dev/squash-practice [master]$ git checkout -b readme
Switched to a new branch 'readme'

~/dev/squash-practice [readme]$ echo "This is a project for practice of squash commit" > README.md

~/dev/squash-practice [readme *]$ git commit -am "Add description"
[readme faaa0b4] Add description
 1 file changed, 1 insertion(+)

~/dev/squash-practice [readme]$ echo "This is second line" >> README.md

~/dev/squash-practice [readme *]$ git commit -am "Add second line"
[readme 2559fc0] Add second line
 1 file changed, 1 insertion(+)

~/dev/squash-practice [readme]$ git checkout -b create-readme master
Switched to a new branch 'create-readme'

~/dev/squash-practice [create-readme]$ git merge --squash readme
Updating 6466ea0..2559fc0
Fast-forward
Squash commit -- not updating HEAD
 README.md | 2 ++
 1 file changed, 2 insertions(+)

~/dev/squash-practice [create-readme +]$ git commit -m "Create README to show description"
[create-readme 61e0a68] Create README to show description
 1 file changed, 2 insertions(+)

~/dev/squash-practice [create-readme]$ git log
commit 61e0a685984d8a233da4a75d3b2c579ea8e422f2 (HEAD -> create-readme)
Date:   Thu Jan 3 23:27:58 2019 +0900

    Create README to show description

commit 6466ea08ede8f3478919945136926431e4681d3c (master)
Date:   Thu Jan 3 23:22:10 2019 +0900

    Add README

README.mdを作成してから、readmeブランチをmasterブランチから作成しています。

readmeブランチでは、README.mdを2回修正しており、それぞれの修正に対してコミットが行われています。 つまり、2回コミットが行われています。

その後、create-readmeブランチをmasterブランチから作成し、readmeブランチの内容をcreate-readmeブランチにマージします。 この時のgit merge--squashオプションをつけています。

最後にgit commitをし、git logで履歴を確認しています。 create-readmeブランチでは一回のコミットがあったように見えています。

まとめ

長期的に開発されるOSSや大規模なプロジェクトでは、コミットログも他人から見られるので、わかりやすい履歴をつけておきましょう。 今回は、そのための主要な方法であるsquash commitについて、メモしておきました。