2019年7月7日日曜日

docker-compose で nfs ボリュームを使ってみた

概要

前回 docker stack を使って Swarm 環境に docker-compose で定義したコンテナやネットワーク、ボリュームをデプロイしました
その際にボリュームがコンテナホスト間で共有できない問題を取り上げました
解決策の一つとして nfs があるようなので今回は nfs を使ったボリューム共有の方法を紹介したいと思います

環境

  • macOS 10.14.5
  • Vagrant 2.1.1
  • Ubuntu 16.04 LTS
  • docker 18.09.7
  • nfs-kernel-server 1.2.8

nfs サーバ構築

Vagrant で構築した Ubuntu 16.04 上に構築します
過去の記事で紹介しているのでそちらを参考にして構築してください

なお今回は動作確認用に nfs で exports したパスに root ユーザでファイルを 1 つ作成しています

  • date > /opt/nfs/hoge.txt

とりあえず docker で nfs ボリュームを触ってみる

docker-compose.yml を書く前にまずは docker コマンドだけで nfs ボリュームを試してみます
というかそもそも docker コマンドでの挙動を理解していないと docker-compose にしても理解不能になってしまうと思います

nfs ボリューム作成

volume create で作成できます
nfs の場合 --opt でいろいろと指定する必要があります

  • docker volume create --driver local --opt type=nfs --opt o=addr=172.28.128.3,rw,nfsvers=4 --opt device=:/opt/nfs nfs-volume

今回主に使用している --opt は以下の通りです

  • --opt type=nfs
  • --opt o=addr=172.28.128.3,rw,nfsvers=4
  • --opt device=:/opt/nfs

1 つ目はタイプを指定します
docker 18.09.7 の場合、特にボリューム用のプラグインなどを別途インストールしなくても上記の形式で記載することで nfs が使用できます
2 つ目は nfs をマウントする際のオプションを指定します
これは nfs 自体のオプションとほぼ同じになります
詳しくはこの辺りを御覧ください
なお今回は「nfs サーバの IP アドレス」「Read/Write の許可」「nfs サーバのバージョン」を指定しています
3 つ目は nfs で export したパスを指定します

これで nfs ボリュームが作成されます
inspect した結果は以下の通りです

  • docker volume inspect nfs-volume
[
    {
        "CreatedAt": "2019-07-04T01:49:07Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/nfs-volume/_data",
        "Name": "nfs-volume",
        "Options": {
            "device": ":/opt/nfs",
            "o": "addr=172.28.128.3,rw,nfsvers=4",
            "type": "nfs"
        },
        "Scope": "local"
    }
]

コンテナでマウントしてみる

では作成した nfs ボリュームをマウントしてみましょう
docker コマンドでボリュームをマウントするには -v (or --mount) オプションを使います
今回は挙動を確認しやすい redis イメージを使っています

  • docker run -d -v nfs-volume:/opt/nfs --name redis redis

これでマウントできます
コンテナが起動したら df コマンドでマウント状況を確認してみましょう

  • docker exec redis df -h
Filesystem      Size  Used Avail Use% Mounted on
overlay         9.7G  2.0G  7.7G  21% /
tmpfs            64M     0   64M   0% /dev
tmpfs           496M     0  496M   0% /sys/fs/cgroup
/dev/sda1       9.7G  2.0G  7.7G  21% /data
:/opt/nfs       9.7G  2.2G  7.6G  22% /opt/nfs
shm              64M     0   64M   0% /dev/shm
tmpfs           496M     0  496M   0% /proc/acpi
tmpfs           496M     0  496M   0% /proc/scsi
tmpfs           496M     0  496M   0% /sys/firmware

こんな感じで /opt/nfs にマウントされていれば OK です
読込できるか確認してみます

  • docker exec redis cat /opt/nfs/hoge.txt

=> Thu Jul 4 01:36:43 UTC 2019

こんな感じで事前に作成したファイルの内容が表示されれば OK です

書き込みしてみる

次に書き込みしてみましょう
先程のコンテナ上で以下のコマンドを実行してみます

  • docker exec redis /bin/sh -c "date > /opt/nfs/hoge2.txt"

問題なく書き込めると思います
また nfs サーバ側にもデータあることを確認できると思います

権限に注意

過去の記事の手順で nfs サーバを構築した場合、基本的には root ユーザでのみ書き込みができるようになります

例えば vagrant 環境なので vagrant ユーザからの読み書きができるようにする場合は nfs サーバ側でマウントポイント (/opt/nfs) の所有者を以下のように変更する必要があります

  • chown -R vagrant:vagrant /opt

また exports する場合にも no_root_squash を削除しなければいけません

  • vim /etc/exports
/opt/nfs 172.28.128.0/24(rw,no_subtree_check,async)

この設定をすると vagrant ユーザで読み書きできる nfs のパスが作成できるのですがもしこのパスをコンテナで使おうとすると読みはできますが書き込みはできません
なぜなら redis コンテナ上では root ユーザで読み書きが行われる上記の nfs サーバの設定だと権限がないためです

docker は基本的に root ユーザでコンテナは動作するため今回の nfs サーバの設定で基本的には大丈夫だと思いますがもしユーザを変える場合には注意しましょう
もし別のユーザで動作させる場合には nfs サーバで別のパスをそのユーザ専用に作成してあげると良いと思います

既存のボリュームにマウントはできないのか

redis コンテナの場合 /data にダンプデータが出力されます
基本的にはこのパスをホスト側でマウントしたりしてデータの永続化を行います
なら /data を nfs ボリュームでマウントできないか試してみました

  • docker run -d -v nfs-volume:/data --name redis redis

こんな感じでコンテナ側のマウントパスを /data に変更してあげます
これで df を実行すると以下のようになります

  • docker exec redis df -h
Filesystem             Size  Used Avail Use% Mounted on
overlay                9.7G  2.0G  7.7G  21% /
tmpfs                   64M     0   64M   0% /dev
tmpfs                  496M     0  496M   0% /sys/fs/cgroup
172.28.128.3:/opt/nfs  9.7G  2.2G  7.6G  22% /data
/dev/sda1              9.7G  2.0G  7.7G  21% /etc/hosts
shm                     64M     0   64M   0% /dev/shm
tmpfs                  496M     0  496M   0% /proc/acpi
tmpfs                  496M     0  496M   0% /proc/scsi
tmpfs                  496M     0  496M   0% /sys/firmware

これでダンプファイルが出力されるかテストしてみましょう

  • docker exec redis redis-cli set a a
  • docker stop redis

nfs サーバ側を直接確認すると dump.rdb がちゃんと作成されていました

  • ls -l /opt/nfs/dump.rdb
-rw-r--r-- 1 999 docker 102 Jul  4 02:13 /opt/nfs/dump.rdb

が気になったのは 999:docker というユーザの権限で保存されていたことです
更に上位の階層を見ると nfs フォルダが 999 ユーザの所有権になっていました

  • ls -l /opt/
drwxr-xr-x 2  999 root 4096 Jul  4 02:13 nfs

詳しく調査していないので、おそらくですが dockerd が都合の良いように nfs 側のマウントパスの所有者を書き換えたんだと思います
一応コンテナの root ユーザからは問題なく読み書きできるので挙動的にバグることはないのですが少し気持ち悪い感じはします

なおこの後再度 redis コンテナを起動してデータを確認しましたがちゃんと前のデータが復元されていました

  • docker start redis
  • docker exec redis redis-cli get a

=> a

本題: docker-compose.yml で使ってみる

ではいよいよ docker-compose.yml で nfs ボリュームを使ってみます

  • vim docker-compose.yml
version: '3.4'

services:
  redis:
    image: redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    deploy:
      placement:
        constraints: [node.role == worker]
volumes:
  redis_data:
    driver_opts:
      type: nfs
      o: "addr=172.28.128.3,rw,nfsvers=4"
      device: ":/opt/nfs"

docker コマンド時に最後にやった redis + nfs ボリュームを実現するための docker-compse.yml になります
これで stack deploy してみましょう

  • docker stack deploy -c docker-compose.yml test
  • docker stack ls
NAME                SERVICES            ORCHESTRATOR
test                1                   Swarm
  • docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
yszxwqr2fjsn        test_redis          replicated          1/1                 redis:latest        *:6379->6379/tcp

こんな感じになれば OK です
redis-cli でデータを保存しておきます

  • redis-cli set a aa

なお今回は worker 側にコンテナを立ち上げているので worker 側のホストで exec してあげれば df などでマウント状況も確認できます (前述と同じマウント結果なので省略)

あとは stack を削除してデータが nfs 上に残っているかとそれを復元できるか再度確認してみましょう

  • docker stack rm test
  • docker stack deploy -c docker-compose.yml test
  • redis-cli get a

=> aa

こんな感じで復元できれば成功です
なお nfs サーバの /opt/nfs に dump.rdb があるのも確認できました

動作するコンテナを manager 側にした場合どうなるのか

ここで疑問に思ったのは nfs ボリュームと言えど結局ドライバは local ドライバなので作成されているボリュームは worker 側にしかありません
なので結局ボリュームをコンテナホスト間で共有できていないことになってしまいます
では nfs ボリュームを manager 側にも作成すれば結局見ている nfs サーバは同じなのでボリュームが違えどデータは同じなのではどうことで試してみました
docker-compose.yml の placement.constraints を manager に変更します

  • vim docker-compose.yml
version: '3.4'

services:
  redis:
    image: redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    deploy:
      placement:
        constraints: [node.role == manager]
volumes:
  redis_data:
    driver_opts:
      type: nfs
      o: "addr=172.28.128.3,rw,nfsvers=4"
      device: ":/opt/nfs"

これで再度 redis コンテナを立ち上げてみてデータが復元できるか確認したところちゃんと復元できていました

  • docker stack rm test
  • docker stack deploy -c docker-compose.yml test
  • redis-cli get a

=> aa

なおこの後で worker 側と manager 側で docker volume ls をしてみると両方に nfs ボリュームが作成されているのが確認できると思います

  • docker volume ls
DRIVER              VOLUME NAME
local               test_redis_data

未検証: 気になったこと

  • nfs サーバ側の /etc/exports などで設定を変更した場合に docker の nfs ボリュームは作成し直さないとダメなのか

最後に

docker Swarm + docker-compose でボリュームがコンテナホスト間で共有できない問題を nfs サーバを構築し各コンテナホストに nfs ボリュームを作成することで解決してみました
おそらくこれが Swarm を使ってボリューム共有する最善手なのではないかと思います

nfs サーバは今回は VM 上に直接インストールしましたがコンテナでもいいしクラウド上に作成しても良いかなと思います
ただレイテンシーが少なからず発生するのでできればコンテナの近くに置いてあげたほうがコンテナ自体のパフォーマンスも良くなると思います

参考サイト

2019年7月6日土曜日

docker Swarm 上で docker-compose で定義したコンテナを実行してみた

概要

docker swarm + docker-compose を試してみました
docker stack という機能を使うことが実現できるのでその使い方を紹介します

環境

  • macOS 10.14.5
  • Vagrant 2.1.1
  • Ubuntu 16.04 LTS
  • docker 18.09.7

Swarm 環境構築

Vagrant でパパっと構築していきます

  • vim Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.define "vm01" do |v|
    v.vm.box = "ubuntu/xenial64"
    v.vm.network "private_network", type: "dhcp"
    v.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
  end
  config.vm.define "vm02" do |v|
    v.vm.box = "ubuntu/xenial64"
    v.vm.network "private_network", type: "dhcp"
    v.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
  end
end
  • vagrant up
  • vagrant ssh

それぞれにログインして作業します
docker をインストールして Swarm クラスタを構築します

  • sudo apt-get remove docker docker-engine docker.io containerd runc
  • sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
  • curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  • sudo apt-key fingerprint 0EBFCD88
  • sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  • sudo apt-get update
  • sudo apt-get install docker-ce docker-ce-cli containerd.io
  • sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  • sudo chmod +x /usr/local/bin/docker-compose
  • sudo gpasswd -a $USER docker
  • sudo hostnamectl set-hostname vm01

これで docker のインストールは完了です

  • docker swarm init
  • docker swarm join --token SWMTKN-1-19rwa01lw9elmef3qiz9fmgn2mkvzfvdwivyxv1bgihhj3k3wv-6mcld04qufib76sni5a1kj82n 172.28.128.3:2377

vm01 で init を実行し vm02 で join コマンドを実行しましょう

  • docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION l414hf95bwjcv52nz72hpofzh * vm01 Ready Active Leader 18.09.7 p2ph2kvwm8ygwn0wri6vzosya vm02 Ready Active 18.09.7

これで Swarm の準備は OK です

とりあえず docker-compose の作成

とりあえずコンテナを複数デプロイする docker-compose.yml を作成してみます
確認のため環境変数 DUDE に動作しているコンテナホスト名を設定しています

  • vim docker-compose.yml
version: '3.4'

services:
  nginx:
    image: nginx
    environment:
      DUDE: "{{.Node.Hostname}}"
    deploy:
      replicas: 3

これを Swarm 上にデプロイしてみます
デプロイする場合は stack deploy を使います

  • docker stack deploy -c docker-compose.yml test

スタックの一覧を確認すると「test」というスタックが確認できます

  • docker stack ls
NAME SERVICES ORCHESTRATOR test 1 Swarm

デプロイが完了したら ps コマンドで確認してみましょう

  • docker stack ps test
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS ww8tdxhvkbhq test_nginx.1 nginx:latest vm01 Running Running 3 minutes ago 0ok7rtsycvui test_nginx.2 nginx:latest vm02 Running Running 3 minutes ago 3i6hekkj0780 test_nginx.3 nginx:latest vm02 Running Running 3 minutes ago

vm01 と vm02 に分散されてコンテナがデプロイされているのがわかります
exec コンテナが動作しているホストに対して docker ps を実行しコンテナ ID か名前を取得してそれに対して実行しましょう
以下を実行するとちゃんと DUDE 変数にコンテナホスト名 vm01 が設定されているのが確認できると思います

  • docker exec test_nginx.1.ww8tdxhvkbhqh92vv822d7qx7 env

動作が確認できたらスタックを削除します
スタックを削除するとコンテナもすべて削除されます

  • docker stack rm test

ポートを使ってみる

今度はポートを使ってみます
stack deploy を使った場合、nginx プロセスが立ち上がりコンテナを自動でロードバランシングしてくれます

  • vim docker-compose.yml
version: '3.4'

services:
  nginx:
    image: valian/nginx-test-page
    ports:
      - "80:80"
    deploy:
      replicas: 3

イメージは動作確認しやすいイメージにしています
これでデプロイしてみましょう

  • docker stack deploy -c docker-compose.yml test

先程同様に test という stack が作成されています
これで vm01 or vm02 で localhost にアクセスしてみるとコンテナにアクセスできるのが確認できると思います

service について

ports を指定すると service も自動で作成されます

  • docker service ls
ID NAME MODE REPLICAS IMAGE PORTS arla23uvljz5 test_nginx replicated 3/3 valian/nginx-test-page:latest *:80->80/tcp

vm01 のコンテナホストに nginx のプロセスが立ち上がり overlay ネットワークを通して各コンテナホストにデプロイされたコンテナにアクセスすることができます
vm01 のコンテナホストに curl してみると以下のようにバランシングされていることが確認できます (この結果は vm02 から実行しても同じになります)

vagrant@vm01:~$ curl localhost
HOSTNAME=c0e0d2ae816c
vagrant@vm01:~$ curl localhost
HOSTNAME=8680d41c963b
vagrant@vm01:~$ curl localhost
HOSTNAME=4461b5233fc3

また docker ps -a で確認するとわかりますがホストのポートにはバインドしておらず dockerd が直接ルーティングしていることがわかります

  • sudo lsof -i:80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dockerd 1254 root 41u IPv6 72420 0t0 TCP *:http (LISTEN)

ボリュームを使ってみる (ホストボリュームマウント)

次にボリュームを使ってみます
まずはコンテナホストのボリュームをマウントしてみます

version: '3.4'

services:
  redis:
    image: redis
    ports:
      - "6379:6379"
    volumes:
      - ./data:/data
    deploy:
      placement:
        constraints: [node.role == manager]
  • mkdir data
  • docker stack deploy -c docker-compose.yml test

これで実行すると redis コンテナが 1 つ vm01 側で起動します
また service を確認すると 6379 へのルーティングが設定されており vm01, vm02 どちらの localhost にアクセスしても redis コンテナにアクセスできるようになっています

  • redis-cli set a a
  • redis-cli get a

=> a

また stack を rm して再度 deploy しても同じ値が再度取得できるのが確認できると思います
次に動作するコンテナホストを変更してみます

  • vim docker-compose.yml
version: '3.4'

services:
  redis:
    image: redis
    ports:
      - "6379:6379"
    volumes:
      - ./data:/data
    deploy:
      placement:
        constraints: [node.role == worker]
  • mkdir data
  • docker stack deploy -c docker-compose.yml test

これで vm02 でコンテナが起動したら先程のデータが取得できるか確認してみましょう

  • redis-cli get a

=> nil

という感じでデータがないのが確認できます
これは当然と言えば当然ですが vm01 の ./data をマウントしておりそこにデータも保存しているため起動するコンテナホストを vm02 にしたら当然保存したデータもありません

というわけで

service について

  • docker service ls
ID NAME MODE REPLICAS IMAGE PORTS i0ewsq4w9s5i test_redis replicated 1/1 redis:latest *:6379->6379/tcp

nginx のとき同様で dockerd が iptables を使って 6379 ポートをルーティングしてくれています

  • sudo iptables -L DOCKER-INGRESS
Chain DOCKER-INGRESS (1 references) target prot opt source destination ACCEPT tcp – anywhere anywhere tcp dpt:6379 ACCEPT tcp – anywhere anywhere state RELATED,ESTABLISHED tcp spt:6379 RETURN all – anywhere anywhere
  • sudo lsof -i:6379
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dockerd 1312 root 43u IPv6 90625 0t0 TCP *:6379 (LISTEN)

ボリュームを使ってみる (ボリューム領域作成)

ではホストの領域をマウントする方式ではなくボリュームを作成したらどうなるか確認してみます

  • vim docker-compose.yml
version: '3.4'

services:
  redis:
    image: redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    deploy:
      placement:
        constraints: [node.role == manager]
volumes:
  redis_data:
    driver: local

これで起動しデータをセットします

  • docker stack deploy -c docker-compose.yml test
  • redis-cli set a aa

そして docker-compose.yml 上は同じ volume を指定し動作させるコンテナホストを変更してみます
先程と同じように constraints: [node.role == worker] に変更して rm -> deploy してみましょう

  • docker stack rm test
  • docker stack deploy -c docker-compose.yml test
  • redis-cli get a

=> nil

するとデータは nil で別のボリュームを参照していることが確認できると思います 
各コンテナホストで docker volume ls してみるとわかりますがそれぞれでボリュームが作成されており領域的には別になっていることが確認できます
これを回避する方法はいくつかありますが簡単そうなのは

  • ボリュームコンテナを起動してその領域をマウントする
  • nfs ドライバを使う

かなと思います
ただ docker-compose 3 系からは volumes_from が使えないようなので前者は厳しいと思います
となると後者の nfs を選択するしかなさそうです

Tips

コンテナを起動するコンテナホストにイメージがない場合は最初に docker pull が走るためコンテナの起動が遅れます

最後に

docker Swarm + docker-compose を試してみました
基本的には stack 機能を使えば OK です
stack deploy, rm, ls など基本的なコマンドは覚えておきましょう
service と network は dockerd が良い感じにやってくれるのでそこまで苦労はしないと思います

一番辛そうなのは volume かなと思います
どうやらこれだけはデフォルトだとコンテナホスト間で共有するすべがなさそうです
NFS が一番簡単そうですが driver のインストールから必要になります

参考サイト

2019年7月5日金曜日

Bulma CSS で Navbar を使う場合にはスマホ用のハンバーガメニューの ON/OFF は自分で実装しなければならない

概要

Hero を使ってヘッダーを実装している場合、スマホ表示用にハンバーガーメニューが使えます
ボタンや CSS の処理はサンプルから引っ張ればいいのですが実際にメニューの表示/非表示を実現するには自分で JavaScript を実装しなければなりません
今回はその方法を紹介します

環境

  • macOS 10.14.5
  • Bulma 0.7.5

準備

今回は Ruby を使ってアプリを作成します

アプリ雛形作成

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor
  • mkdir views
  • touch views/index.erb
  • mkdir -p public/js
  • touch public/js/custom.js
  • touch app.rb

アプリ作成

  • vim app.rb
require 'sinatra'

get '/' do
  erb :index
end

Bulma をロードしたテンプレート作成

CDN があるのでそれを使います
jQuery も使いたいので CDN からロードしています
Hero のサンプルはこのあたり からコピペしているだけです
section タグを body 内に貼り付ければ OK です

  • views/index.erb
<html>
<head>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css" integrity="sha256-vK3UTo/8wHbaUn+dTQD0X6dzidqc5l7gczvH+Bnowwk=" crossorigin="anonymous" />
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="/js/custom.js"></script>
</head>
<body>
<section class="hero is-success is-fullheight">
  <!-- Hero head: will stick at the top -->
  <div class="hero-head">
    <header class="navbar">
      <div class="container">
        <div class="navbar-brand">
          <a class="navbar-item">
            <img src="https://bulma.io/images/bulma-type-white.png" alt="Logo">
          </a>
          <span class="navbar-burger burger" data-target="navbarMenuHeroC">
            <span></span>
            <span></span>
            <span></span>
          </span>
        </div>
        <div id="navbarMenuHeroC" class="navbar-menu">
          <div class="navbar-end">
            <a class="navbar-item is-active">
              Home
            </a>
            <a class="navbar-item">
              Examples
            </a>
            <a class="navbar-item">
              Documentation
            </a>
            <span class="navbar-item">
              <a class="button is-success is-inverted">
                <span class="icon">
                  <i class="fab fa-github"></i>
                </span>
                <span>Download</span>
              </a>
            </span>
          </div>
        </div>
      </div>
    </header>
  </div>

  <!-- Hero content: will be in the middle -->
  <div class="hero-body">
    <div class="container has-text-centered">
      <h1 class="title">
        Title
      </h1>
      <h2 class="subtitle">
        Subtitle
      </h2>
    </div>
  </div>

  <!-- Hero footer: will stick at the bottom -->
  <div class="hero-foot">
    <nav class="tabs is-boxed is-fullwidth">
      <div class="container">
        <ul>
          <li class="is-active"><a>Overview</a></li>
          <li><a>Modifiers</a></li>
          <li><a>Grid</a></li>
          <li><a>Elements</a></li>
          <li><a>Components</a></li>
          <li><a>Layout</a></li>
        </ul>
      </div>
    </nav>
  </div>
</section>
</body>
</html>

custom.js の作成

ここにハンバーガーメニューの表示/非表示を実装します
基本は is-active をトグルすれば OK です

  • vim public/js/custom.js
$(document).ready(function(){
  var burger = $('.burger');
  var menu = $('#' + burger.data('target'));
  burger.on('click', function() {
    burger.toggleClass('is-active');
    menu.toggleClass('is-active');
  });
});

. burger クラスをクリックしたら burgermenu の DOM をトグルしているだけです
menuburger.data('target')id="navbarMenuHeroC" の DOM を取得しています

動作確認

  • bundle exec ruby app.rb

localhost:4567 にアクセスしましょう
ブラウザのデベロッパーツールを起動してビューをスマホモードに切り替えましょう
ハンバーガーメニューとして表示されるのはスマホからアクセスしたときだけです

最後に

Bulma css の Hero でハンバーガーメニューを実装してみました
HTML と CSS はサンプルにありますが JS がないので自分で作る必要があるという話でした

2019年7月4日木曜日

Lazyload2.0 を使ってみた

概要

Lazyload はブラウザで画像の遅延ダウンロードを実現することができるライブラリです
画面上に初めて画像が表示されたときにロードを行うことができたりします
また 2.0 から jQuery 経由の実行は禁止されネイティブの JS で動作するようになったのでそれを試してみます

環境

  • macOS 10.14.5
  • Ruby 2.6.2p47
  • Lazyload 2.0

準備

今回は Ruby を使ってアプリを作成します

アプリ雛形作成

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor
  • mkdir views
  • touch views/index.erb
  • mkdir -p public/js
  • touch public/js/custom.js
  • touch app.rb

アプリ作成

  • vim app.rb
require 'sinatra'

get '/' do
  erb :index
end

テンプレート作成

  • vim views/index.erb
<html>
<head>
</head>
<body>
  <p><img src="https://homepages.cae.wisc.edu/~ece533/images/airplane.png"/></p>
  <p><img src="https://homepages.cae.wisc.edu/~ece533/images/arctichare.png"/></p>
  <p><img src="https://homepages.cae.wisc.edu/~ece533/images/baboon.png"/></p>
  <p><img src="https://homepages.cae.wisc.edu/~ece533/images/barbara.bmp"/></p>
  <p><img src="https://homepages.cae.wisc.edu/~ece533/images/boat.png"/></p>
</body>
</html>

適当に画像を表示するテンプレートを作成しましょう

Lazyload 2.0 を追加

まずはテンプレートに Lazyload を追加します
CDNがあるのでそれを使います

  • vim views/index.erb
<html>
<head>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/lazyload@2.0.0-rc.2/lazyload.js"></script>
  <script src="/js/custom.js"></script>
</head>
<body>
  <p><img class="lazyload" width="400" height="400" data-src="https://homepages.cae.wisc.edu/~ece533/images/airplane.png"/></p>
  <p><img class="lazyload" width="400" height="400" data-src="https://homepages.cae.wisc.edu/~ece533/images/arctichare.png"/></p>
  <p><img class="lazyload" width="400" height="400" data-src="https://homepages.cae.wisc.edu/~ece533/images/baboon.png"/></p>
  <p><img class="lazyload" width="400" height="400" data-src="https://homepages.cae.wisc.edu/~ece533/images/barbara.bmp"/></p>
  <p><img class="lazyload" width="400" height="400" data-src="https://homepages.cae.wisc.edu/~ece533/images/boat.png"/></p>
</body>
</html>

jQuery は Lazyload では使いませんが DOM の読み込みに使うので追加します
また各 img タグに class="lazyload" を追加します
そして srcdata-src に変更し画像のサイズも必須なので指定します
これを行った img タグに関しては遅延ロードの対象になります

custom.js はこの後作成します

custom.js

DOM が描画されたら lazyload() メソッドをコールする処理を実装します

  • vim public/js/custom.js
$(document).ready(function(){
  lazyload();
});

動作確認

準備ができたので動作確認してみましょう

  • bundle exec ruby app.rb

localhost:4567 にアクセスすると画像が表示されます
Lazyload を使った場合はスクロールするごとに画像が読み込まれていることがわかります

Lazyload が使われているかよくわからない場合には

プレースホルダーが使えるのでそれを使ってみましょう
要するにダウンロード中は別の読み込み画像を見せることができます
今回は事前に public/img/dummy.png に配置しています
それを src 属性を使って指定することでプレースホルダーとして使えます

  • vim views/index.erb
<html>
<head>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/lazyload@2.0.0-rc.2/lazyload.js"></script>
  <script src="/js/custom.js"></script>
</head>
<body>
  <p><img class="lazyload" width="400" height="400" src="/img/dummy.png" data-src="https://homepages.cae.wisc.edu/~ece533/images/airplane.png"/></p>
  <p><img class="lazyload" width="400" height="400" src="/img/dummy.png" data-src="https://homepages.cae.wisc.edu/~ece533/images/arctichare.png"/></p>
  <p><img class="lazyload" width="400" height="400" src="/img/dummy.png" data-src="https://homepages.cae.wisc.edu/~ece533/images/baboon.png"/></p>
  <p><img class="lazyload" width="400" height="400" src="/img/dummy.png" data-src="https://homepages.cae.wisc.edu/~ece533/images/barbara.bmp"/></p>
  <p><img class="lazyload" width="400" height="400" src="/img/dummy.png" data-src="https://homepages.cae.wisc.edu/~ece533/images/boat.png"/></p>
</body>
</html>

プレースホルダー実行後は以下のように Lazyload が使われているのがよくわかります

最後に

Lazyload2.0 を使ってみました
ライブラリを追加して既存のタグに classdata-src 属性を追加すれば使えるので簡単に導入できそうです
画像がたくさんあり一気に読み込むことでページのロードが重くなっている場合などは導入を検討してみると良いかなと思います

参考サイト

2019年7月3日水曜日

Zabbix4.2 でアラートを Slack に通知してみた

概要

Zabbix のアラートを Slack に飛ばしてみました
一応公式の perl スクリプトがあるので今回はそれを使っています

環境

  • Zabbix サーバ
    • Ubuntu 16.04 LTS
    • docker 18.09.6
    • Zabbix Server 4.2

イメージ準備

Perl スクリプトは当然ですが docker の公式イメージにはインストールされていません
なのでスクリプトインストール入りのイメージを作成します

  • mkdir docker
  • cd docker
  • vim Dockerfile
FROM zabbix/zabbix-server-mysql:ubuntu-4.2-latest

RUN apt -y update
RUN apt -y install libwww-perl libjson-xs-perl
RUN apt -y install libtest-simple-perl libtest-most-perl
RUN apt -y install git
RUN apt -y install build-essential
RUN git clone https://github.com/v-zhuravlev/zabbix-notify.git
WORKDIR zabbix-notify
RUN perl Makefile.PL INSTALLSITESCRIPT=/usr/lib/zabbix/alertscripts
RUN make test
RUN make install
  • docker build -t my_zabbix .

これでイメージの作成は完了です
alertscripts のパスは公式の docker 版の場合以下のようになっていました

  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml exec zabbix-server grep 'alertscripts' /etc/zabbix/zabbix_server.conf

=> AlertScriptsPath=/usr/lib/zabbix/alertscripts

docker-compose ファイルの作成

公式の既存の docker-compose.yaml ファイルを書き換えます

  • vim docker-compose_v3_ubuntu_mysql_latest.yaml
version: '3.5'
services:
 zabbix-server:
  image: my_zabbix
  ports:
   - "10051:10051"
  volumes:
   - /etc/localtime:/etc/localtime:ro
   - ./zbx_env/usr/lib/zabbix/externalscripts:/usr/lib/zabbix/externalscripts:ro
   - ./zbx_env/var/lib/zabbix/modules:/var/lib/zabbix/modules:ro
   - ./zbx_env/var/lib/zabbix/enc:/var/lib/zabbix/enc:ro
   - ./zbx_env/var/lib/zabbix/ssh_keys:/var/lib/zabbix/ssh_keys:ro
   - ./zbx_env/var/lib/zabbix/mibs:/var/lib/zabbix/mibs:ro
  links:
   - mysql-server:mysql-server
  ulimits:
   nproc: 65535
   nofile:
    soft: 20000
    hard: 40000
  deploy:
   resources:
    limits:
      cpus: '0.70'
      memory: 1G
    reservations:
      cpus: '0.5'
      memory: 512M
  env_file:
   - .env_db_mysql
   - .env_srv
  user: root
  depends_on:
   - mysql-server
  networks:
   zbx_net_backend:
     aliases:
      - zabbix-server
      - zabbix-server-mysql
      - zabbix-server-ubuntu-mysql
      - zabbix-server-mysql-ubuntu
   zbx_net_frontend:
  stop_grace_period: 30s
  sysctls:
   - net.ipv4.ip_local_port_range=1024 65000
   - net.ipv4.conf.all.accept_redirects=0
   - net.ipv4.conf.all.secure_redirects=0
   - net.ipv4.conf.all.send_redirects=0
  labels:
   com.zabbix.description: "Zabbix server with MySQL database support"
   com.zabbix.company: "Zabbix SIA"
   com.zabbix.component: "zabbix-server"
   com.zabbix.dbtype: "mysql"
   com.zabbix.os: "ubuntu"

 zabbix-web-apache-mysql:
  image: zabbix/zabbix-web-apache-mysql:ubuntu-4.2-latest
  ports:
   - "80:80"
   - "443:443"
  links:
   - mysql-server:mysql-server
   - zabbix-server:zabbix-server
  volumes:
   - /etc/localtime:/etc/localtime:ro
   - ./zbx_env/etc/ssl/apache2:/etc/ssl/apache2:ro
  deploy:
   resources:
    limits:
      cpus: '0.70'
      memory: 512M
    reservations:
      cpus: '0.5'
      memory: 256M
  env_file:
   - .env_db_mysql
   - .env_web
  user: root
  depends_on:
   - mysql-server
   - zabbix-server
  healthcheck:
   test: ["CMD", "curl", "-f", "http://localhost"]
   interval: 10s
   timeout: 5s
   retries: 3
   start_period: 30s
  networks:
   zbx_net_backend:
    aliases:
     - zabbix-web-apache-mysql
     - zabbix-web-apache-ubuntu-mysql
     - zabbix-web-apache-mysql-ubuntu
   zbx_net_frontend:
  stop_grace_period: 10s
  sysctls:
   - net.core.somaxconn=65535
  labels:
   com.zabbix.description: "Zabbix frontend on Apache web-server with MySQL database support"
   com.zabbix.company: "Zabbix SIA"
   com.zabbix.component: "zabbix-frontend"
   com.zabbix.webserver: "apache2"
   com.zabbix.dbtype: "mysql"
   com.zabbix.os: "ubuntu"

 zabbix-web-nginx-mysql:
  image: zabbix/zabbix-web-nginx-mysql:ubuntu-4.2-latest
  ports:
   - "8081:80"
   - "8443:443"
  links:
   - mysql-server:mysql-server
   - zabbix-server:zabbix-server
  volumes:
   - /etc/localtime:/etc/localtime:ro
   - ./zbx_env/etc/ssl/nginx:/etc/ssl/nginx:ro
  deploy:
   resources:
    limits:
      cpus: '0.70'
      memory: 512M
    reservations:
      cpus: '0.5'
      memory: 256M
  env_file:
   - .env_db_mysql
   - .env_web
  user: root
  depends_on:
   - mysql-server
   - zabbix-server
  healthcheck:
   test: ["CMD", "curl", "-f", "http://localhost"]
   interval: 10s
   timeout: 5s
   retries: 3
   start_period: 30s
  networks:
   zbx_net_backend:
    aliases:
     - zabbix-web-nginx-mysql
     - zabbix-web-nginx-ubuntu-mysql
     - zabbix-web-nginx-mysql-ubuntu
   zbx_net_frontend:
  stop_grace_period: 10s
  sysctls:
   - net.core.somaxconn=65535
  labels:
   com.zabbix.description: "Zabbix frontend on Nginx web-server with MySQL database support"
   com.zabbix.company: "Zabbix SIA"
   com.zabbix.component: "zabbix-frontend"
   com.zabbix.webserver: "nginx"
   com.zabbix.dbtype: "mysql"
   com.zabbix.os: "ubuntu"

 zabbix-agent:
  image: zabbix/zabbix-agent:ubuntu-4.2-latest
  ports:
   - "10050:10050"
  volumes:
   - /etc/localtime:/etc/localtime:ro
   - ./zbx_env/etc/zabbix/zabbix_agentd.d:/etc/zabbix/zabbix_agentd.d:ro
   - ./zbx_env/var/lib/zabbix/modules:/var/lib/zabbix/modules:ro
   - ./zbx_env/var/lib/zabbix/enc:/var/lib/zabbix/enc:ro
   - ./zbx_env/var/lib/zabbix/ssh_keys:/var/lib/zabbix/ssh_keys:ro
  links:
   - zabbix-server:zabbix-server
  deploy:
   resources:
    limits:
      cpus: '0.2'
      memory: 128M
    reservations:
      cpus: '0.1'
      memory: 64M
   mode: global
  env_file:
   - .env_agent
  user: root
  privileged: true
  pid: "host"
  networks:
   zbx_net_backend:
    aliases:
     - zabbix-agent
     - zabbix-agent-passive
     - zabbix-agent-ubuntu
  stop_grace_period: 5s
  labels:
   com.zabbix.description: "Zabbix agent"
   com.zabbix.company: "Zabbix SIA"
   com.zabbix.component: "zabbix-agentd"
   com.zabbix.os: "ubuntu"

 mysql-server:
  image: mysql:8.0
  command: [mysqld, --character-set-server=utf8, --collation-server=utf8_bin, --default-authentication-plugin=mysql_native_password]
  volumes:
   - ./zbx_env/var/lib/mysql:/var/lib/mysql:rw
  env_file:
   - .env_db_mysql
  user: root
  stop_grace_period: 1m
  networks:
   zbx_net_backend:
    aliases:
     - mysql-server
     - zabbix-database
     - mysql-database

 db_data_mysql:
  image: busybox
  volumes:
   - ./zbx_env/var/lib/mysql:/var/lib/mysql:rw

networks:
  zbx_net_frontend:
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "false"
    ipam:
      driver: default
      config:
      - subnet: 172.16.238.0/24
  zbx_net_backend:
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "false"
    internal: true
    ipam:
      driver: default
      config:
      - subnet: 172.16.239.0/24

少し長いですが書き換えているのは以下の通りです

  • Zabbix サーバのイメージをビルドした my_zabbix に変更
  • ./zbx_env/usr/lib/zabbix/alertscripts:/usr/lib/zabbix/alertscript:ro を削除
  • zabbix-proxy-sqlite3, zabbix-proxy-mysql を削除
  • zabbix-java-gateway, zabbix-snmptraps を削除

スクリプトは直接コンテナに配置していますがホストでインストールして配置してそれをマウントしても OK です
またホスト側に alertscript ディレクトリがある場合は事前に削除しておきましょう

  • sudo rm -rf zbx_env/usr/lib/zabbix/alertscripts/

削除しているのは up する際に起動するサービスを指定しても OK です

Zabbix サーバ起動

  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml up -d
  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml ps
               Name                              Command                       State                          Ports               
----------------------------------------------------------------------------------------------------------------------------------
zabbix-docker_db_data_mysql_1         sh                               Exit 0                                                     
zabbix-docker_mysql-server_1          docker-entrypoint.sh mysql ...   Up                                                         
zabbix-docker_zabbix-agent_1          /sbin/tini -- /usr/bin/doc ...   Up                                                         
zabbix-docker_zabbix-server_1         /sbin/tini -- /usr/bin/doc ...   Up                      0.0.0.0:10051->10051/tcp           
zabbix-docker_zabbix-web-apache-      docker-entrypoint.sh             Up (health: starting)   0.0.0.0:443->443/tcp,              
mysql_1                                                                                        0.0.0.0:80->80/tcp                 
zabbix-docker_zabbix-web-nginx-       docker-entrypoint.sh             Up (health: starting)   0.0.0.0:8443->443/tcp,             
mysql_1

起動しているホストの 80 番ポートにアクセスすれば管理画面が表示されます

スクリプト動作確認

コンテナに配置したスクリプトを手動で実行して動作確認します
Slack 上に Bots アプリが必要になるので作成しましょう
今回は「zabbix_bot」という名前のボットにしました
アイコンなどは適当に設定してください
必要になるのは API Token になるのでコピーしておきます

ボットが作成できたら Slack 上でチャネルに招待しましょう

  • /invite @zabbix_bot

そしてコンテナ上のスクリプトを実行してみます

  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml exec zabbix-server /bin/bash

でコンテナに入り以下を実行しましょう

  • cd /usr/lib/zabbix/alertscripts/
  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml exec zabbix-server /usr/lib/zabbix/alertscripts/zbx-notify @hawksnowlog 'PROBLEM:myHOSTNAME Temperature Failure on DAE5S Bus 1 Enclosure 1' 'Host: myHOSTNAME Trigger: PROBLEM: myHOSTNAME Temperature Failure on DAE5S Bus 1 Enclosure 1: High Timestamp: 2016.03.14 11:57:10 YEKT eventid: 100502' --api_token=xoxb-58452257553-675165639408-w75zUlSmXlqZpKh8o2HdOp7o --slack
  • ./zbx-notify @hawksnowlog 'OK:myHOSTNAME Temperature Failure on DAE5S Bus 1 Enclosure 1' 'Host: myHOSTNAME Trigger: OK: myHOSTNAME Temperature Failure on DAE5S Bus 1 Enclosure 1: High Timestamp: 2016.03.14 11:57:10 YEKT eventid: 100502' --api_token=xoxb-58452257553-675165639408-w75zUlSmXlqZpKh8o2HdOp7o --slack

以下のように通知されればスクリプトの動作確認は OK です


スクリプトが見つからない場合は再度インストールしてみましょう

  • docker-compose -f docker-compose_v3_ubuntu_mysql_latest.yaml exec zabbix-server make install

メディアの設定

Zabbix の Slack 通知の仕組みはインストールしたスクリプトを実行しているだけです
そしてそのスクリプトを実行するのはメディアになります

Administraion -> Media types
で「Create media type」を選択しましょう
そして以下のように入力します

  • Name・・・好きな名前を設定します
  • Type・・・Script
  • Script name・・・zbx-notify
  • Script parameters
    • {ALERT.SENDTO}
    • {ALERT.SUBJECT}
    • {ALERT.MESSAGE}
    • --api_token=you_token_here
    • --slack

これで Update しましょう

作成されると一覧に表示されます
ちなみに --slack_mode=alarm というパラメータも付与するとアラートが復帰した情報も通知してくれるようになります

ユーザのメディアを変更する

メンションするユーザ名は Zabbix のユーザに紐付いているメディア情報を編集します
Administraion -> Users
でメディアを編集するユーザを選択します
そして Media タブを選択し Type に先程作成した「Alert to Slack」を選択します
そして Send to にメンションしたいユーザ名を設定します

ちなみに Send to を「#general」にすればチャネルに通知できます 

アクションの作成

アラートが上がったら作成したメディアに通知するアクションを作成します
Configuration -> Actions で右上の Event source のプルダウンを「Triggers」に変更し「Create action」を選択します

Action タグで条件を設定します

  • Name・・・好きな名前を設定します
  • New condition・・・Trigger serverity equals Average

で Add しましょう
条件も好きな条件を設定して OK です

次に Operation タブを設定します

  • Default subject・・・{TRIGGER.STATUS}:{HOSTNAME}:{TRIGGER.NAME}.
  • Default message
Host: {HOSTNAME}
Trigger: {STATUS}: {TRIGGER.NAME}: {TRIGGER.SEVERITY}
Timestamp: {EVENT.DATE} {EVENT.TIME}
{TRIGGER.COMMENT}
{TRIGGER.URL}
http://192.168.99.200
Eventid: {EVENT.ID}
  • Operations
    • Operation type・・・Send message
    • Send to Users・・・Admin
    • Send only to・・・Alert to Slack (作成したメディア)

通知のメッセージはそのままでも OK ですが上記をおすすめしていたので素直に変更しました
で Add を押して追加します

これでアクションの作成は完了です
Average なトリガーが発泡されると Action 経由で Slack に通知が行きます

動作確認 (アラートを上げる)

何でもいいのでアラートを上げてみましょう
今回の場合は Average なトリガーでなければいけません
一番手っ取り早いのは監視しているホストの zabbix-agent を停止します
Monitoring -> Problems でトリガーが上がれば Slack 側にもメンションが来るのが確認できると思います

最後に

Zabbix4.2 で Slack にアラート情報を通知してみました
仕組みとしては単純でスクリプトを実行しているだけです
公式は Perl スクリプトになっていますがシェバングで Ruby などを指定すれば Ruby の通知スクリプトも作成可能です
(当然 Ruby がインストールされているイメージが必要ですが)
Perl のスクリプトを見ながら実装しても良いと思います

正直スクリプトなのでやり方は何でも OK です
直接 Zabbix から通知してもいいですが一旦 IFTTT を経由しても OK ですし Cloud Function などに渡しても OK です

参考サイト

2019年7月2日火曜日

m4a のメタデータを afinfo コマンドで確認する

概要

いつも忘れるのでメモしておきます
afinfo コマンドを使うと簡単に m4a ファイルのサイズが再生時間を確認できます
Mac であればデフォルトでインストールされています

環境

  • macOS 10.14.5
  • afinfo 2.0

使い方

  • afinfo /path/to/test.m4a
File:           /path/to/test.m4a
File type ID:   m4af
Num Tracks:     1
----
Data format:     2 ch,  32000 Hz, 'aac ' (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame        
                no channel layout.
estimated duration: 4484.977656 sec
audio bytes: 35880320
audio packets: 140158
bit rate: 63999 bits per second
packet size upper bound: 757
maximum packet size: 757
audio data file offset: 45056
not optimized
audio 143519285 valid frames + 2112 priming + 395 remainder = 143521792                                                        
format list:
[ 0] format:      2 ch,  32000 Hz, 'aac ' (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame       
Channel layout: Stereo (L R)
----

estimated duration 再生時間で audio bytes が音源自体のサイズです

2019年7月1日月曜日

Inventory ファイルから ssh/config ファイルを作成する

概要

なんとなく作ったのでメモ
すべての YAML ファイルに対応してはいない

想定 inventory ファイル

  • mkdir -p /path/to/inventory
  • touch /path/to/inventory/i1.yml
  • touch /path/to/inventory/i2.yml

i1.yml

---
i1:
  children:
    app:
      hosts:
        h1:
          ansible_host: 192.168.100.10
        h2:
          ansible_host: 192.168.100.11

i2.yml

---
i2:
  children:
    app:
      hosts:
        h3:
          ansible_host: 192.168.100.12
    util:
      hosts:
        h4:
          ansible_host: 192.168.100.13

Ruby スクリプト

  • vim export.rb
require 'yaml'

Dir.glob('/path/to/inventory/*.yml').each { |name|
  yaml = YAML.load_file("#{name}")
  ret = ""
  yaml.each { |s|
    s[1]['children'].each { |k1, v1|
      v1['hosts'].each { |k, v|
        ret += <<-"EOF"
Host #{k}
   Hostname #{v['ansible_host']}
   Port 22
   User user1
      EOF
      }
    }
  }
  puts ret
}
  • ruby export.rb

結果は以下の通り

Host h1
   Hostname 192.168.100.10
   Port 22
   User user1
Host h2
   Hostname 192.168.100.11
   Port 22
   User user1
Host h3
   Hostname 192.168.100.12
   Port 22
   User user1
Host h4
   Hostname 192.168.100.13
   Port 22
   User user1

あとはこの結果を貼り付けるなり実行時に << すれば OK

最後に

もっと良い方法はないだろうか
もっと簡潔に書けるはず