20191125のdockerに関する記事は12件です。

Unity + DockerでUnityプロジェクトからAARファイルを自動生成

はじめに

UnityのSceneをAndroidのSubView(FrameLayout)として埋め込むまで
https://qiita.com/_nonono/items/253aa15d6027ecc8ad66
このような記事があり、AndroidにUnityのシーンをFrameLayoutに埋め込むことができます(便利

Unityで開発せずビルド成果物(AARファイル)のみ必要な場合
1.指定のバージョンのUnityをインストール
2.Unityを立ち上げAndroidプロジェクトをExport
3.Android Studioでビルド
上の3つの作業を手作業で行うので一苦労だと思います
Unity、Android Studioのバージョンを意識したりしなければならいので良い方法が必要です

「なんとか良い方法を!」と思って Unityの入ったDockerイメージを使ってAARの生成を自動化 してみたので、作業を下に記していきます

TL;DR

いきなり一連の作業をスクリプトにできないので、dockerの中で作業を1つずつして、最後にスクリプトにまとめます

やったこと

  1. docker pull gableroux/unity3d
  2. Dockerコンテナに入りライセンスファイルを用意
  3. ライセンスファイルをコピー
  4. ビルド用クラスを作成
  5. Unityプロジェクトをビルド
  6. build.gradleとAndroidManifest.xmlを編集
  7. GradleでAndroidプロジェクトをビルド
  8. build.shを書いて完全自動化

実際の環境

・MacBook Pro (15-inch, 2016) macOS 10.13.6(17G10007)
・docker desktop ver. 2.1.0.5(40693)

1. Unityの入ったDockerイメージをpullする

まずは、Unityの入っているDockerイメージをインターネットから落としてきます

次のコマンドを実行すると最新版のコンテナが自分のローカルに保存されます

Dockerイメージをpull
docker pull gableroux/unity3d

しかし、このイメージではAndroidプロジェクトを生成/ビルドができないので、UnityバージョンとUnityプロジェクトのビルドターゲットを指定しなければなりません。

この記事では、 Unity 2018.4.5f1 でビルドターゲットをAndroid とするので、Dockerイメージのtagに2018.4.5f1-androidを指定します
※目的のバージョンのイメージが無い場合があるので、 DockerHub のページで確認しておきましょう

目的のDockerイメージをpull
docker pull gableroux/unity3d:2018.4.5f1-android

docker imagesを実行するとpullしてきたイメージが確認できます

DockerImageの確認
cha84rakanal$ docker images
REPOSITORY            TAG                  IMAGE ID            CREATED             SIZE
gableroux/unity3d     2018.4.5f1-android   3cc6afa5c2bd        3 months ago        6.7GB

docker run -it {image name} bash を実行すればイメージからコンテナが作成され、Dokcer内のターミナルに接続できる
なので、接続してUnityAndroid SDKがあるかを確認しておきます

UnityとAndroidSDKの確認
cha84rakanal$ docker run -it --rm gableroux/unity3d:2018.4.5f1-android bash
root@7462f939f4d0:/# echo $ANDROID_HOME
/opt/android-sdk-linux
root@7462f939f4d0:/# ls /opt/
android-sdk-linux  Unity  Unity-2018.4.5f1
root@7462f939f4d0:/# 

2. Dockerコンテナに入ったUnity用のライセンスを生成する(1回のみ・手作業)

普段使うmacOSやWindowsでUnityを一番最初に起動すると次の画面が出てアクティベーションをする必要があります
アクティベーション画面

次の作業は、DockerコンテナにインストールされているUnityのアクティベーションを行います

まず、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます
パスワードを平打ちするので気をつけましょう

Dockerコンテナの立ち上げ
cd /path/to/project
docker run -it --rm \
-e "UNITY_USERNAME=username@example.com" \
-e "UNITY_PASSWORD=example_password" \
-e "TEST_PLATFORM=linux" \
-e "WORKDIR=/root/project" \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
bash

次に、Dockerコンテナ内で以下のコマンドを実行します

アクティベーションをする
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-logFile /dev/stdout \
-batchmode \
-username "$UNITY_USERNAME" -password "$UNITY_PASSWORD"

すると、出力に次のようなXMLが表示されるので、表示されたXMLを unity3d.alf というファイル名で保存します

ターミナルでの出力
LICENSE SYSTEM [2017723 8:6:38] Posting <?xml version="1.0" encoding="UTF-8"?><root><SystemInfo><IsoCode>en</IsoCode><UserName>[...]

もし、XMLではなく401エラーが出た場合は、2段階認証を切りましょう
GoogleでサインインFaceBookでサインイン でUnity IDを作成している場合でも 401エラーがでるので、メールアドレスから作成したUnityIDを使いましょう

401エラー
Can't activate unity: No sufficient permissions while processing request HTTP error code 401

unity3d.alf が用意できたら https://license.unity3d.com/manual にアクセスします。アクセスしたらサイトの指示に従ってunity3d.alfをアップロード、質問に答えていきましょう。すべて終わるとUnity_v2018.x.ulfUnity_v2019.x.ulfがダウンロードできるので保存しておきます。
このUnity_v*.x.ulfファイルは今後必要になるので大切に保管しておきます

プロジェクトチームの誰かがこの作業をやれば、Unityのバージョンを変えない限り、
3以降の作業でUnity_v2018.x.ulfを使いまわすことができます(多分

※アクティベーションの詳しい手順は このページ に書いてありますが、まとめとしてこの記事にも記しています

3. Dockerコンテナに入ったUnityにライセンスを当てる

2の手順でダウンロードしてきたファイルUnity_v2018.x.ulfをUnityプロジェクトのルートディレクトにおきます

Unity_v2018.x.ulfの設置
/path/to/project
├── Assets
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf

次に、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます

Dockerコンテナの立ち上げ
cd /path/to/project
docker run -it --rm \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
bash

ここから先の作業7.が終わるまでは、dockerコンテナ内のターミナルでexitをしてはいけません

ライセンスファイルをUnityプロジェクトのルートディレクトに設置できたら、dockerコンテナ内のターミナルで次のコマンドを実行します。これでライセンスファイルの設置は完了です

Unityのライセンスファイルを設置
set -e
set -x
mkdir -p /root/.cache/unity3d
mkdir -p /root/.local/share/unity3d/Unity/
set +x
cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf

4. ビルド用クラスを作成してAssets/Scripts/Editorに配置

Unityプロジェクトをコマンドラインからビルドできるように次のC#ソースをAssets/Scripts/Editorに設置します

ApplicationBuild.cs
using UnityEngine;
using System;
using System.Linq;

public static class ApplicationBuild {

    private static string[] GetAllScenePaths() {
        return EditorBuildSettings.scenes
            .Where(scene => scene.enabled)
            .Select(scene => scene.path)
            .ToArray();
    }

    public static void AndroidBuild() {

        string[] scenes = GetAllScenePaths();
        BuildPipeline.BuildPlayer(scenes, "./Build/", BuildTarget.Android, BuildOptions.AcceptExternalModificationsToPlayer);

    }

}

公式ドキュメント を確認すると、 ビルドのパラメータを色々設定できます。
今回は、プロジェクトのエクポートパスは./Build 、ビルドターゲットはAndroidなので BuildTarget.Android 、AndroidのビルドはExternalで行うのでオプションに BuildOptions.AcceptExternalModificationsToPlayer を指定しています

BuildPipeline.BuildPlayer
BuildPlayer(EditorBuildSettingsScene[] levels, string locationPathName, BuildTarget target, BuildOptions options)

パラメータ
levels The Scenes to be included in the build. If empty, the currently open Scene will be built. Paths are relative to the project folder (Assets/MyLevels/MyScene.unity).
locationPathName 成果物の保存先のパス
target ビルドする BuildTarget
options ビルドしたプレイヤーを実行するか、などの追加の BuildOptions

5. コマンドラインからUnityプロジェクトをビルド

次のコマンドでUnityプロジェクトをコマンドラインからビルドできます

コマンドラインからUnityプロジェクトをビルド
/opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuild

ログは ./build.log に保存されるので、エディタ等で確認するとビルドが進んでるのがわかります
ビルド中は、ターミナルに何かこれといって表示されるものは無いです(無視できるエラーとかは出る

ビルドが完了するとログに次の行が記録されます

build.log
Exiting batchmode successfully now!

※成果物の保存先のパス ./Buildディレクトリがすでにあるとビルドが停止してしまうので消しておきましょう

6. APKではなくAARを作成するようにbuild.gradleとAndroidManifest.xmlを編集

5の作業が終わり、ビルドが成功すると、./BuildディレクトリにAndroidプロジェクトが生成されます

コマンドラインでのビルド後
/path/to/project
├── Assets
├── Build
│   └── {Project Name}
│       ├── build.gradle
│       ├── gradle.properties
│       ├── libs
│       ├── local.properties
│       ├── proguard-unity.txt
│       └── src
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf

デバイスにインストールする.apkの作成であれば、gradle コマンドでビルドして終了です
今回は、.aarの作成なので、build.gradleAndroidManifest.xmlを編集する必要があります

build.gradle

  • apply plugin: 'com.android.application'apply plugin: 'com.android.library' に変更
  • applicationId 'com.project.unitytest' を削除
  • bundle をコメントアウト(Unity2018.3.x以降??)
build.gradle
--- apply plugin: 'com.android.application'
+++ apply plugin: 'com.android.library'

--- applicationId 'com.project.unitytest'  

--- bundle {
+++ /*bundle {
        language {
            enableSplit = false
        }
        density {
            enableSplit = false
        }
        abi {
            enableSplit = true
        }
--- }
+++ }*/

AndroidManifest.xml

  • 該当のintent-filterタグとその子をすべてコメントアウト
AndroidManifest.xml
<!--<intent-filter>-->
<!--<action android:name="android.intent.action.MAIN" />-->
<!--<category android:name="android.intent.category.LAUNCHER" />-->
<!--<category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
<!--</intent-filter>-->

UnityプロジェクトからAARファイルの生成を行うには、この作業も自動化しておく必要があるので、
UnityのPostProcessの機構を使って自動化しておきます

[Unity] PostProcessでビルド後に処理を差し込む
https://qiita.com/edo_m18/items/346439f7678218e85e69

build.gradleの文字コードは UTF-8 で書き出すこと、 C#のでのUTF8EncodingUTF-8 with BOMであるので注意が必要。 UTF-8 with BOMbuild.gradlegradleに渡すとエラーになるので注意
※UnityのPostProcessBuildにはこの記事ではふれません
build.gradlebundleのコメントアウトだが このページ によれば次に示す置換でも問題ない

build.gradle
--- bundle {
+++ splits {
        language {
---         enableSplit = false
+++         enable false
        }
        density {
---         enableSplit = false
+++         enable false
        }
        abi {
---         enableSplit = true
+++         enable true
        }
    }
bundle部分を処理するC#の例
build_text = build_text.Replace("bundle {", "splits {");
build_text = build_text.Replace("enableSplit = false", "enable false");
build_text = build_text.Replace("enableSplit = true", "enable true");

7. コマンドラインでAndroidプロジェクトをビルド

残る作業は./Buildディレクトリに生成されたAndroidプロジェクトをビルドしてAARファイルを生成するだけです
生成されたAndroidプロジェクトのディレクトリ(build.gradleがあるディレクトリ)に移動して次のコマンドを実行するだけです

AARファイルを生成
gradle bundleDebugAar
AARファイルを生成
gradle bundleReleaseAar

デバッグとリリースがあるので必要に応じて使い分けましょう

ビルドが成功するとプロジェクトのディレクトリにbuildディレクトリが生成されて、
その中に {PROJECT_NAME}-debug.aar {PROJECT_NAME}-release.aar が生成されます

ビルド後
/path/to/project
├── Assets
├── Build
│   └── {Project Name}
│       ├── build
│       │   └── outputs
│       │       └── aar
│       │           ├── {PROJECT_NAME}-debug.aar
│       │           └── {PROJECT_NAME}-release.aar
│       ├── build.gradle
│       ├── gradle.properties
│       ├── libs
│       ├── local.properties
│       ├── proguard-unity.txt
│       └── src
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf

8. 作業をシェルスクリプトにまとめる

4から7の作業をまとめて、コマンド一行でAARファイルの生成できるようにします
ライセンスファイルはUnityプロジェクトのルートディレクトリに設置して、6の作業はUnityのPostProcessBuildで自動化しておきましょう

そうして作業をまとめたスクリプトが次になります

build.sh
#!/usr/bin/env bash

set -e
set -x
mkdir -p /root/.cache/unity3d
mkdir -p /root/.local/share/unity3d/Unity/
set +x

cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf

cd ~/project/ && rm -r ./Build && /opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuild
cd ~/project/Build/{Project Name} && gradle bundleDebugAar

Unityプロジェクトのルートディレクトリに移動して、dockerコンテナ内でbuild.shを実行します

AARの自動ビルド
cd /path/to/project
chmod 777 build.sh
docker run -it --rm \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
/bin/bash -c "/root/project/build.sh"

さいごに

これでコマンドを実行してビルドされるのを待つだけになりました!
これをBitbucket PipelineCircleCIGitHub Actions とかのCIツールに載せていけるといいですね
まとめている間に、既にmacOSやWindowsにUnityがインストールされていて、かつ、Android SDKがインストールされているなら、Docker上じゃなくていいかなと思いました

余談

実際の出力
cha84rakanal$ time docker run -it --rm -e "WORKDIR=/root/project" -v "$(pwd):/root/project" gableroux/unity3d:2018.4.5f1-android /bin/bash -c "/root/project/build.sh"
+ mkdir -p /root/.cache/unity3d
+ mkdir -p /root/.local/share/unity3d/Unity/
+ set +x
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default
/home/builduser/buildslave/unity/build/Editor/Platform/Linux/UsbDevices.cpp:UsbDevicesQuery

Welcome to Gradle 5.1.1!

Here are the highlights of this release:
 - Control which dependencies can be retrieved from which repositories
 - Production-ready configuration avoidance APIs

For more details see https://docs.gradle.org/5.1.1/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

> Task :help

Welcome to Gradle 5.1.1.

To run a build, run gradle <task> ...

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle --help

To see more detail about a task, run gradle help --task <task>

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 5s
1 actionable task: 1 executed
debugger-agent: Unable to listen on 28
Starting a Gradle Daemon (subsequent builds will be faster)

Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.5.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 2m 7s
21 actionable tasks: 21 executed

real    33m9.065s
user    0m0.162s
sys     0m0.178s
cha84rakanal$ 

timeコマンドで実行時間を図ったら30分もビルドしてるので、dockerコンテナ内よりホスト側でビルドした方がはやいやんけw

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerとQEMUでRaspbianイメージをカスタマイズする

TL;DR

dockerとqemuでRaspbianのイメージファイル.imgにchrootし、直接カスタマイズする

何がうれしい

  • 設定がコード化できる
  • 同じ環境をたくさん作れる
  • 完成物が小さい
  • PCだけで作業できる(シリアルケーブル不要)

作業用Dockerイメージ

FROM ubuntu:18.04
RUN apt update &&\
    apt install -y kpartx libarchive-tools wget qemu-user-static binfmt-support --no-install-recommends &&\
    apt clean &&\
    rm -rf /var/lib/apt/lists/*

適当に作業用ディレクトリを作成し、マウントする

$ mkdir workspace
$ docker build -t nullsnet/raspbian_customizer .
$ docker run --rm -v ${PWD}/workspace:/workspace -it nullsnet/raspbian_customizer bash

Ubuntu in Docker

ここからdockerのUbuntu内での作業

マウント

作業用ディレクトリに移動した後、Raspbianのイメージファイルをダウンロード&展開する

$ cd workspace
$ wget https://downloads.raspberrypi.org/raspbian_lite_latest -O-|bsdtar -xvf
$ image=$(ls -1tr *.img)
$ echo $image
2019-09-26-raspbian-buster-lite.img

kpartxでマッピングして、適当なディレクトリにマウントする
loop[0-9]p1bootパーティション、loop[0-9]p2rootfsパーティションになる

$ kpartx -av $image
$ add map loop8p1 (253:16): 0 524288 linear 7:8 8192
$ add map loop8p2 (253:17): 0 3858432 linear 7:8 532480
$ mkdir rootfs boot
$ mount $(ls /dev/mapper/loop*p1) boot
$ mount $(ls /dev/mapper/loop*p2) rootfs

この時点でbootrootfsの中身が触れるようになる
ファイルを改変するだけの設定で良いなら、ここまででよい

$ ls rootfs
bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
$ ls raspbian-boot/
COPYING.linux           bcm2708-rpi-cm.dtb      bcm2710-rpi-2-b.dtb       bcm2711-rpi-4-b.dtb  fixup.dat     fixup4x.dat   issue.txt     kernel8.img  start4cd.elf  start_db.elf
LICENCE.broadcom        bcm2708-rpi-zero-w.dtb  bcm2710-rpi-3-b-plus.dtb  bootcode.bin         fixup4.dat    fixup_cd.dat  kernel.img    overlays     start4db.elf  start_x.elf
bcm2708-rpi-b-plus.dtb  bcm2708-rpi-zero.dtb    bcm2710-rpi-3-b.dtb       cmdline.txt          fixup4cd.dat  fixup_db.dat  kernel7.img   start.elf    start4x.elf
bcm2708-rpi-b.dtb       bcm2709-rpi-2-b.dtb     bcm2710-rpi-cm3.dtb       config.txt           fixup4db.dat  fixup_x.dat   kernel7l.img  start4.elf   start_cd.elf

chroot

qemu-arm-staticをコピーする

$ cp /usr/bin/qemu-arm-static rootfs/usr/bin/

ld.so.preloadを書き換える
chroot時は$PLATFORMが定義されないため,これをやっておかないとバイナリ実行時にエラーが出てしまう

$ sed -i 's/${PLATFORM}/v7l/' rootfs/etc/ld.so.preload

rootfsにchrootしてbashを起動する

$ chroot rootfs /bin/bash

これであとは好きなように設定できる

raspi-config

raspi-configによる設定も可能

$ raspi-config nonint do_hostname raspi
$ raspi-config nonint do_wifi_country JP
$ raspi-config nonint do_change_timezone 'Asia/Tokyo'
$ raspi-config nonint do_change_locale ja_JP.UTF-8

apt

aptも当然可能

$ apt update
$ apt upgrade -y
$ apt install -y git

systemd

sshを有効にしておき、実機起動直後にすぐ接続できるようにもできる

$ systemctl enable ssh

docker

dockerのインストールだってできる

$ curl -sSL https://get.docker.com/ | sh
$ systemctl enable docker

Shell Script

設定をシェルスクリプトにしておき、chroot時に呼び出すこともできる

$ chroot rootfs /bin/bash -C YOUR_SCRIPT.sh

後片付け

chrootを抜けた後、qemu-arm-staticを削除してld.so.preloadを元に戻す

$ exit
$ rm rootfs/usr/bin/qemu-arm-static
$ sed -i 's/v7l/${PLATFORM}/' rootfs/etc/ld.so.preload

イメージファイルをアンマウントしてマッピングを削除する

$ umount boot rootfs
$ rmdir boot rootfs
$ kpartx -d $image
$ rm /dev/mapper/loop*

SDカードの作成

作成した*.imgファイルを、普通にdd等でSDカードへ書き込めばよい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kea DHCPでAct-Sbyのhot-standby構成を組む

ゴール

Kea DHCPを用いてAct-Sbyのhot-standby High-Availability構成を組み,動作させることができること.

About Kea DHCP

Kea DHCPとは,ISC DHCPの後継として開発されているオープンソースのDHCPサーバプログラムである. MPL2.0ライセンスのもとISCのGitlabで公開されている.最新のメジャーバージョンはISC DHCPとの違いは下記のように書かれている.

1. モジュラーコンポーネントデザインとHooksモジュールによる拡張
2. REST APIによるオンラインRe-configuration
3. 既存のシステムとのインテグレーション可能なデザイン

1つ目に述べられているhooksモジュールについては,ユーザ自身でこのhooksモジュールを書くことが可能であることを示している.これによりKea DHCPの独自のモジュールを実装し,より柔軟なサービス提供ができるように考慮されている.今回対象とするHA構成についても,このhooksモジュールとして実装されているHA機能を提供するライブラリをincludeして利用する形で実現する.

2つ目に述べられているREST APIによるconfigurationについては,このKea DHCPにはkea-ctrl-agentが内包されており,このagentがREST APIの仲介者となりkea-dhcp4プロセス(DHCPv4)やkea-dhcp6プロセス(DHCPv6)に対して各種設定のgetやset等の様々な操作をすることが可能である.またKea DHCPはconfigを変更した際にrestartが極力必要とならないよう設計されている.多くのconfiguratinにおいては,configのreloadを行うことでconfigの適用を行うことができる.

最後に,既存システムとのインテグレーションについて述べられているが,これはどうもKea DHCPのconfigurationを,configファイルだけでなくDatabaseを用いることでも行うことができる点を指しているようである.これにより,実行環境とconfigurationを分離することができることがこの3つ目の要旨であるようだ.たしかに,実行環境とconfigの環境が異なり,さらにDBを用いて管理をすることができるのは一部のIT管理者にとっては有効かもしれない.

Kea DHCPのHA構成とISC DHCPのFailover

かつてのISC-DHCPではHA機能は提供されておらず,Failover機能のみ提供されているものであった.しかしながらKea DHCPではrelease 1.4.0よりHA構成が適用可能となった.リリースノートからも分かる通り,1.4.0は2018年6月にリリースされたものであり,まだ1年少々しか経過していない.そのためまだ機能が十分でない部分等も多くあると推察できる.1.4.0のリリース当初はこのHA機能はPremium featureとして提供されるようであったが,現在は通常のhooks libraryとして提供されている.

ISC-DHCPのFailoverとKea DHCPのHA構成については下記で述べられている.

Kea High Availability vs ISC DHCP Failover - Kea DHCP

比較してみると,ISCのFailoverがDHCPv4のみに適用可能だったのに対し,Keaではv4とv6の両方に対して適用可能となっている.他には,ISCでは2つのサーバでの冗長構成のみしか組むことができなかったが,Keaでは2つのactiveサーバと無限のバックアップサーバを用いることが可能である.他にもリースDBのレプリケーション等がKeaでは実施可能であったり,ISC DHCPではOMAPIであったが,Keaではより一般的となっているREST APIによるconfigurationを採用するなど,多くの点でKea DHCPはISC DHCPをfollowする形となっているが,一部機能においてはISCよりデグレしている点が見受けられる.たとえばLoadBalancingだと,ISCではFlexible LBが可能であったが,Keaでは50/50のLBモードしか現状選択することができない.いずれもRFC3074に準拠しているようだ.また,Lazy lease updates(MCLT)の項目についてはISC DHCPでは可能であったがKeaではこれを許さず,リースアップデートが完全に完了してからでなければclientへresponseを送らないなど,より厳密となっている.このLazy lease updates(MCLT)については特にHA構成を組んだ場合のパフォーマンスに大きく影響することとなる.

Kea DHCPでHA構成を組む

Introduction

では早速Kea DHCPを用いたHA構成を組んでいく.
今回はKea DHCP verion 1.5.0を利用する.基本的にはマニュアルを熟読の上設定を行なっていくこととなる.v1.5.0では下記を参照することとなるだろう.

Kea Administrator Reference Manual

HA構成を組む上ではこのマニュアルのうち 15.4.8. ha: High Availability をrefer するとよい.
また,HA構成において,HA構成を組んだホスト間のリースアップデート等はkea-ctrl-agentを介したREST API経由で実施されることとなる.そのため,Kea DHCPのREST API,kea-ctrl-agentについての理解も深めておくとより理解が深まると思う.

Build and Configuration

今回はdockerを用いて検証環境を構成してHA構成を組むことを試みる.

下記のdocker file とdocker-compose.ymlを用いてkea-dhcp2台をデプロイし,その間をkea-mngネットワークで繋いでおく.docker compose upする前に,docker network create kea-mng をしておく必要がある.これはHA構成を組むにあたりheartbeatlease-updateをREST APIで実施するためのmanagement network相当のものである.L3疎通性さえあれば問題はない.

FROM ubuntu:latest

ENV KEA_DHCP_VERSION=1.5.0
ENV LOG4_CPLUS_VERSION=1.2.1

RUN apt-get update && apt-get install -y iputils-ping net-tools traceroute nmap tcpdump iproute2 python3 vim  jq curl automake libtool pkg-config build-essential ccache libboost-dev libboost-system-dev liblog4cplus-dev libssl-dev && \
    curl -SL https://downloads.isc.org/isc/kea/${KEA_DHCP_VERSION}/kea-${KEA_DHCP_VERSION}.tar.gz | tar -xzC /root && \
    cd /root/kea-${KEA_DHCP_VERSION} && \
    ./configure --enable-shell && \
    make -s -j$(nproc) && \
    make install && \
    ldconfig
version: '3'
services:
  kea_1:
    image: kea:1.5.0
    container_name: kea_1
    privileged: true
    networks:
      - kea-mng
    tty: true

  kea_2:
    image: kea:1.5.0
    container_name: kea_2
    privileged: true
    networks:
      - kea-mng
    tty: true

networks:
    kea-mng:
        external: true

環境が構築できたら,実際にconfigを行っていく.
manualを見て分かるとおり,HA構成の実現にはhooks libraryを用いる.
今回はDHCPv4のデーモンに対してHA構成を行う例を示す.DHCPv6に対しても似たconfigで実現できると思われる.

早速HA構成を組むためのconfigを見ていこう.

マニュアルのHA構成のconfigサンプルとしてこのような例がある.

{
  "Dhcp4": {

    ...

    "hooks-libraries": [
        {
            "library": "/usr/lib/hooks/libdhcp_lease_cmds.so",
            "parameters": { }
        },
        {
            "library": "/usr/lib/hooks/libdhcp_ha.so",
            "parameters": {
                "high-availability": [ {
                    "this-server-name": "server1",
                    "mode": "load-balancing",
                    "heartbeat-delay": 10000,
                    "max-response-delay": 10000,
                    "max-ack-delay": 5000,
                    "max-unacked-clients": 5,
                    "peers": [
                        {
                            "name": "server1",
                            "url": "http://192.168.56.33:8080/",
                            "role": "primary",
                            "auto-failover": true
                        },
                        {
                            "name": "server2",
                            "url": "http://192.168.56.66:8080/",
                            "role": "secondary",
                            "auto-failover": true
                        },
                        {
                            "name": "server3",
                            "url": "http://192.168.56.99:8080/",
                            "role": "backup",
                            "auto-failover": false
                        }
                    ]
                } ]
            }
        }
    ],

    ...

  }
}

hooks-librariesとしてlibdhcp_ha.soを読み込む.デフォルトでは/user/lib/hooks配下に存在するが,keaのinstall pathによっては異なってくるので注意しよう.基本的にはkeaのroot配下の./lib/hooks配下にあると考えて良さそうだ.またHA構成を組む場合,lease-updateを送受信してリース情報を交換する必要がある.これを行うためにlibdhcp_lease_cmds.soを読み込む必要がある.
読み込んでいないとlease情報がAPI経由でやり取りすることができない.たとえばこのlibdhcp_lease_cmd.soを読み込まずにlease情報をgetしようとすると,下記のようになる.

root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "lease4-get-all", "service": [ "dhcp4" ] }' http://localhost:8080/ | jq .[]
{
  "result": 2,
  "text": "'lease4-get-all' command not supported."
}

さらに各種パラメータのsettingもここで実施する.パラメータの意味はざっくりと下記の通りである.

this-server-name: HA構成のうち自身を指すユニークな名前.
mode: HAのモード.
heartbeat-delay: パートナーへのハートビートの送信間隔.[ms]
max-response-delay: パートナーをdownと判定するまでの時間.[ms]
max-ack-delay: パートナーがクライアントからの通信に応答するべきmax時間.これを超えるとunackedと判定される.[ms]
max-unacked-clients: パートナーを障害と判定するunackedクライアント数.
peers: HAを構成するホストの指定

これらをマニュアルを見つつ適切に設定することとなる.
このうち,動作を大きく変えるのが mode である.このパラメータはHAをどのモードで動作させるかを決定する.
Kea DHCPのHA動作モードはload-balancinghot-standbyのうちから選ぶ.
想像できる通り,load-balancingモードでは50/50のact-actによる構成,hot-standbyモードではact-sbyのホットスタンバイ構成となる.

今回はまずhot-standbyモードとして下記の通り設定することとする.
primary に相当するのがkea_1でありIPアドレスは172.18.0.3,secondaryに相当するのがkea_2でありIPアドレスは172.18.0.2とした.

this-server-name: kea_1 //(2台目は"kea_2")
mode: hot-standby
heartbeat-delay: 10000
max-response-delay: 60000
max-ack-delay: 10000
max-unacked-clients: 0
"peers": [
    {
        "name": "kea_1",
        "url": "http://172.18.0.3:8080/",
        "role": "primary",
        "auto-failover": true
    },
    {
        "name": "kea_2",
        "url": "http://172.18.0.2:8080/",
        "role": "secondary",
        "auto-failover": true
    }
]

上記を踏まえてconfigを作成し,各サーバに適用して動作を確認してみよう.

たとえば私の検証では下記のようなconfgをkea_1に投入した.kea_2もほぼ同様であるが,"this-server-name" 等を適切に変更する.

{
"Dhcp4": {
    "interfaces-config": {
        "interfaces": ["eth0"],
        "dhcp-socket-type": "raw",
        "outbound-interface": "use-routing"
    },
    "control-socket": {
        "socket-type": "unix",
        "socket-name": "/tmp/kea-dhcp4-ctrl.sock"
    },
    "lease-database": {
        "type": "memfile",
        "persist": true,
        "name": "/usr/local/etc/kea/kea-leases4.csv",
        "lfc-interval": 1800
    },
    "expired-leases-processing": {
        "reclaim-timer-wait-time": 10,
        "flush-reclaimed-timer-wait-time": 25,
        "hold-reclaimed-time": 3600,
        "max-reclaim-leases": 100,
        "max-reclaim-time": 250,
        "unwarned-reclaim-cycles": 5
    },

    "renew-timer": 1200,
    "rebind-timer": 2400,
    "valid-lifetime": 3600,

    "option-data": [
        {
            "name": "domain-name-servers",
            "data": "8.8.8.8, 8.8.4.4"
        },
        {
            "name": "default-ip-ttl",
            "data": "0xf0"
        }
    ],

    "hooks-libraries": [
        {
            "library": "/usr/local/lib/hooks/libdhcp_lease_cmds.so",
            "parameters": { }
        },
        {
            "library": "/usr/local/lib/hooks/libdhcp_ha.so",
            "parameters": {
                "high-availability": [ {
                    "this-server-name": "kea_1",
                    "mode": "hot-standby",
                    "heartbeat-delay": 10000,
                    "max-response-delay": 10000,
                    "max-ack-delay": 5000,
                    "max-unacked-clients": 5,
                    "peers": [
                      {
                        "name": "kea_1",
                        "url": "http://172.18.0.2:8080/",
                        "role": "primary",
                        "auto-failover": true
                      },
                      {
                        "name": "kea_2",
                        "url": "http://172.18.0.3:8080/",
                        "role": "standby",
                        "auto-failover": true
                      }
                    ]
                } ]
            }
        }
    ],

    "reservation-mode": "disabled",
    "host-reservation-identifiers": [ "hw-address" ],

    "subnet4": [
          {
                "id": 1,
                "subnet": "172.18.0.0/16",
                "pools": [
                    { "pool": "172.18.1.1 - 172.18.1.250" }
                ],
                "option-data": [
                    {
                        "name": "routers",
                        "data": "172.18.0.1"
                    }
                ]
          }
    ]
},

"Logging":
{
  "loggers": [
    {
        "name": "kea-dhcp4",
        "output_options": [
            {
                "output": "/usr/local/var/log/kea-dhcp4.log",
                "maxver": 8,
                "maxsize": 204800,
                "flush": true
            }
        ],
        "severity": "DEBUG",
        "debuglevel": 99
    }
  ]
}
}

まずはkea_1でkeaを起動する.今回,起動にはkeactrlコマンドを用いた.
起動後にRESTAPIでha-heartbeatを送信し,それぞれのサーバのステータスを見てみよう.

# kea_1
root@0d5f73b7ffc7:/# keactrl start
INFO/keactrl: Starting /usr/local/sbin/kea-dhcp4 -c /usr/local/etc/kea/kea-dhcp4.conf
INFO/keactrl: Starting /usr/local/sbin/kea-ctrl-agent -c /usr/local/etc/kea/kea-ctrl-agent.conf
root@0d5f73b7ffc7:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:50:37 GMT",
      "state": "waiting"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@0d5f73b7ffc7:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
curl: (7) Failed to connect to 172.18.0.2 port 8080: Connection refused
root@0d5f73b7ffc7:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:50:42 GMT",
      "state": "partner-down"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@0d5f73b7ffc7:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
curl: (7) Failed to connect to 172.18.0.2 port 8080: Connection refused

起動後,kea_1はstateが waitingから,partner-downにシフトしていることが見受けられる.
kea_2に対しても同様のリクエストを行なっているが,kea_2ではまだkeaを起動していないためConnection refusedが返っている.
kea_1を起動し,kea_2を起動していない場合,kea_1はこの状態に収束する.

ここでkea_2を起動する.

# kea_2
root@d864a28cbf84:/# keactrl start
INFO/keactrl: Starting /usr/local/sbin/kea-dhcp4 -c /usr/local/etc/kea/kea-dhcp4.conf
INFO/keactrl: Starting /usr/local/sbin/kea-ctrl-agent -c /usr/local/etc/kea/kea-ctrl-agent.conf
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:37 GMT",
      "state": "partner-down"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:41 GMT",
      "state": "waiting"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:47 GMT",
      "state": "partner-down"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:49 GMT",
      "state": "ready"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:54 GMT",
      "state": "hot-standby"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:55 GMT",
      "state": "ready"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.3:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:57 GMT",
      "state": "hot-standby"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]
root@d864a28cbf84:/# curl -sS -X POST -H "Content-Type: application/json" -d '{ "command": "ha-heartbeat", "service": [ "dhcp4" ] }' http:///172.18.0.2:8080/ | jq
[
  {
    "arguments": {
      "date-time": "Fri, 25 Oct 2019 17:51:59 GMT",
      "state": "hot-standby"
    },
    "result": 0,
    "text": "HA peer status returned."
  }
]

ログを見てみる.

# kea_1
root@0d5f73b7ffc7:/# grep INFO /usr/local/var/log/kea-dhcp4.log | grep ha-hooks
2019-10-25 17:50:27.312 INFO  [kea-dhcp4.ha-hooks/1358] HA_CONFIGURATION_SUCCESSFUL HA hook library has been successfully configured
2019-10-25 17:50:27.312 INFO  [kea-dhcp4.ha-hooks/1358] HA_INIT_OK loading High Availability hooks library successful
2019-10-25 17:50:27.359 INFO  [kea-dhcp4.ha-hooks/1358] HA_LOCAL_DHCP_DISABLE local DHCP service is disabled while the kea_1 is in the WAITING state
2019-10-25 17:50:27.359 INFO  [kea-dhcp4.ha-hooks/1358] HA_SERVICE_STARTED started high availability service in hot-standby mode as primary server
2019-10-25 17:50:37.372 INFO  [kea-dhcp4.ha-hooks/1358] HA_STATE_TRANSITION server transitions from WAITING to PARTNER-DOWN state, partner state is UNDEFINED
2019-10-25 17:50:37.372 INFO  [kea-dhcp4.ha-hooks/1358] HA_LEASE_UPDATES_DISABLED lease updates will not be sent to the partner while in PARTNER-DOWN state
2019-10-25 17:50:37.372 INFO  [kea-dhcp4.ha-hooks/1358] HA_LOCAL_DHCP_ENABLE local DHCP service is enabled while the kea_1 is in the PARTNER-DOWN state
2019-10-25 17:51:52.821 INFO  [kea-dhcp4.ha-hooks/1358] HA_STATE_TRANSITION server transitions from PARTNER-DOWN to HOT-STANDBY state, partner state is READY
2019-10-25 17:51:52.821 INFO  [kea-dhcp4.ha-hooks/1358] HA_LEASE_UPDATES_ENABLED lease updates will be sent to the partner while in HOT-STANDBY state
2019-10-25 17:53:11.474 INFO  [kea-dhcp4.ha-hooks/1358] HA_DEINIT_OK unloading High Availability hooks library successful
root@0d5f73b7ffc7:/#
# kea_2
root@d864a28cbf84:/# grep INFO /usr/local/var/log/kea-dhcp4.log | grep ha-hooks
2019-10-25 17:51:33.880 INFO  [kea-dhcp4.ha-hooks/901] HA_CONFIGURATION_SUCCESSFUL HA hook library has been successfully configured
2019-10-25 17:51:33.880 INFO  [kea-dhcp4.ha-hooks/901] HA_INIT_OK loading High Availability hooks library successful
2019-10-25 17:51:33.928 INFO  [kea-dhcp4.ha-hooks/901] HA_LOCAL_DHCP_DISABLE local DHCP service is disabled while the kea_2 is in the WAITING state
2019-10-25 17:51:33.928 INFO  [kea-dhcp4.ha-hooks/901] HA_SERVICE_STARTED started high availability service in hot-standby mode as standby server
2019-10-25 17:51:45.738 INFO  [kea-dhcp4.ha-hooks/901] HA_STATE_TRANSITION server transitions from WAITING to SYNCING state, partner state is PARTNER-DOWN
2019-10-25 17:51:45.738 INFO  [kea-dhcp4.ha-hooks/901] HA_LEASE_UPDATES_DISABLED lease updates will not be sent to the partner while in SYNCING state
2019-10-25 17:51:45.738 INFO  [kea-dhcp4.ha-hooks/901] HA_SYNC_START starting lease database synchronization with kea_1
2019-10-25 17:51:45.743 INFO  [kea-dhcp4.ha-hooks/901] HA_LEASES_SYNC_LEASE_PAGE_RECEIVED received 0 leases from kea_1
2019-10-25 17:51:45.745 INFO  [kea-dhcp4.ha-hooks/901] HA_SYNC_SUCCESSFUL lease database synchronization with kea_1 completed successfully in 6.724 ms
2019-10-25 17:51:45.745 INFO  [kea-dhcp4.ha-hooks/901] HA_STATE_TRANSITION server transitions from SYNCING to READY state, partner state is PARTNER-DOWN
2019-10-25 17:51:45.745 INFO  [kea-dhcp4.ha-hooks/901] HA_LEASE_UPDATES_DISABLED lease updates will not be sent to the partner while in READY state
2019-10-25 17:51:56.910 INFO  [kea-dhcp4.ha-hooks/901] HA_STATE_TRANSITION server transitions from READY to HOT-STANDBY state, partner state is HOT-STANDBY
2019-10-25 17:51:56.911 INFO  [kea-dhcp4.ha-hooks/901] HA_LEASE_UPDATES_ENABLED lease updates will be sent to the partner while in HOT-STANDBY state
2019-10-25 17:51:56.911 INFO  [kea-dhcp4.ha-hooks/901] HA_LOCAL_DHCP_ENABLE local DHCP service is enabled while the kea_2 is in the HOT-STANDBY state
2019-10-25 17:53:12.373 INFO  [kea-dhcp4.ha-hooks/901] HA_DEINIT_OK unloading High Availability hooks library successful
root@d864a28cbf84:/#

これらをみてみると,起動直後,これらのHAは下記のような順序で状態遷移していることがわかる.

2019-10-25 17:50:27.359:: kea_1: waiting     , kea_2: UNDEFINED
2019-10-25 17:50:37.372:: kea_1: partner-down, kea_2: UNDEFINED
2019-10-25 17:51:33.928:: kea_1: partner-down, kea_2: waiting
2019-10-25 17:51:45.738:: kea_1: partner-down, kea_2: syncing
2019-10-25 17:51:45.745:: kea_1: partner-down, kea_2: ready
2019-10-25 17:51:52.821:: kea_1: hot-standby,  kea_2: ready
2019-10-25 17:51:56.910:: kea_1: hot-standby , kea_2: hot-standby

このそれぞれのstateの意味はkeaのdocumentを参照し,簡単に理解をまとめると下記の通りである.ほぼgoogle翻訳.

- Backup: backup-serverの平常状態で,この状態であればアクティブなサーバーからlease-updateを受信する.

- hot-standby: hot-standbyモードのアクティブサーバーの平常状態.hot-standbyモードにおけるプライマリサーバーとスタンバイサーバーは,通常この状態をとる.プライマリサーバーはDHCPクエリに応答し,スタンバイサーバーが存在する場合,スタンバイサーバーとバックアップサーバーにlease-updateを送信する

- load-balancing: load-balancingモードのアクティブなサーバーの平常状態.loadbalancingモードにおけるプライマリサーバーとセカンダリサーバーは,通常のこの状態をとる.両方のサーバーがDHCPクエリに応答し,リース更新を相互に送信し,バックアップサーバーが存在する場合にはバックアップサーバーに送信する.

- partner-down: partnerがオフラインであることを検出すると,アクティブサーバーはこの状態に移行する.バックアップサーバが利用できなくともサーバーはこの状態に移行しない.パートナーダウン状態では,サーバーはすべてのDHCPクエリに応答し,'通常時に現在使用できないpartnerによって処理されるべきクエリ'にも応答する.

- ready: lease-databaseをアクティブなパートナーと同期した後、アクティブなサーバーはこの状態となる.この状態は、パートナーに通知される可能性がある(partner-down状態にある可能性が高いため、通常の操作に戻る可能性が高い.その場合,ready 状態にあるサーバーも続けて通常の動作を開始する.)

- syncing: アクティブなサーバーはこの状態に移行して,アクティブなパートナーからリースを取得し,ローカルリースデータベースを更新します.この状態になると,dhcp-disableを発行して,リースの取得元のパートナーのDHCPサービスを無効にします. DHCPサービスは最大60秒間無効になります.その後,同期パートナーが再び停止し,サービスを再度有効にできない場合に備えて,自動的に有効になります.同期が完了すると,同期サーバーはdhcp-enableを発行して,パートナーのDHCPサービスを再度有効にします.同期操作は同期です.サーバーは,パートナーからの応答を待っており,リースの同期が行われている間は何もしていません.データベースをパートナーと同期しないように構成されているサーバー,つまりsync-leases構成パラメーターがfalseに設定されている場合,この状態に移行することはありません.代わりに,待機状態から準備完了状態に直接移行します.

- terminated: ha-hooksライブラリがHA構成を組めず,問題を解決するために手動オペレーションが必要な場合にアクティブサーバーはこの状態となる.HAセットアップの様々なの問題により,サーバーがこの状態になる可能性がある.. Kea 1.4.0リリースの時点でのHAを終了させる唯一の問題は,各サーバーのクロックが60秒以上離れている場合である.terminated 状態のサーバーは選択されたHAモード(ロードバランシングまたはホットスタンバイ)に基づいてDHCPクライアントに応答し続けますが,lease-updateは交換されず,ha-heartbeatは送信されない.terminated状態になると,サーバーは再起動されるまでこの状態となる.オペレータはサーバーを再起動する前に,根本原因を解決する必要がある.

- wainting: 起動された各サーバーはこの状態となる.バックアップサーバーは,この状態からバックアップ状態に直接移行する.アクティブなサーバーは,その状態を確認するためにpartnerにha-heartbeatを送信する.pertnerがオフラインであると考えられる場合,サーバーはpartner-down状態に移行する.そうでない場合,sync-leases構成パラメーターの設定に応じてsyncing状態またはready状態に移行する.両方のサーバーがwaiting状態(同時起動)にあると思われる場合,プライマリサーバーが先に状態遷移する.プライマリサーバーがready状態に移行するまで,セカンダリサーバーまたはスタンバイサーバーはwaiting状態のままとなる.

hot-stanbuyモードでのDHCPリース動作

HA構成を組むことができていることが確認できたので,この状態でDHCPのアドレスリースを行い,HA構成に受けるアドレスリース時の振る舞いを確認していこうと思う.

DHCPリースを模擬するために,今回はdhtestを利用した.

構築してhot-standby HA構成のサーバ群が接続されているNW上に存在するdhtest用VMで,下記の通りコマンドを実行する.

./dhtest -i <interface> -m <mac_addr>

mac_addr はdhtestホストから送出するDHCPパケットのchaddr フィールドに入るMACアドレスと思えばよい.
これを数回繰り返し実施した時の振る舞いを見てみる.
dhtestホストでは下記のようにアドレスリースが模擬できているようである.

[root@a50202e240fb dhtest-master]# ./dhtest -i eth0 -m 00:00:00:11:22:33
DHCP discover sent   - Client MAC : 00:00:00:11:22:33
DHCP offer received  - Offered IP : 172.18.1.1
DHCP request sent  - Client MAC : 00:00:00:11:22:33
^[[ADHCP ack received  - Acquired IP: 172.18.1.1
[root@a50202e240fb dhtest-master]# ./dhtest -i eth0 -m 00:00:00:11:22:34
DHCP discover sent   - Client MAC : 00:00:00:11:22:34
DHCP offer received  - Offered IP : 172.18.1.2
DHCP request sent  - Client MAC : 00:00:00:11:22:34
DHCP ack received  - Acquired IP: 172.18.1.2
[root@a50202e240fb dhtest-master]# ./dhtest -i eth0 -m 00:00:00:11:22:35
DHCP discover sent   - Client MAC : 00:00:00:11:22:35
DHCP offer received  - Offered IP : 172.18.1.3
DHCP request sent  - Client MAC : 00:00:00:11:22:35
DHCP ack received  - Acquired IP: 172.18.1.3
[root@a50202e240fb dhtest-master]#

この時,kea_1kea_2 ホストでパケットキャプチャを行うと.

## kea_1
root@0d5f73b7ffc7:~# tcpdump -i eth0 port 67 or  port 68 -v -n
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^[[O06:44:35.308246 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:33, length 251, xid 0x6a521a43, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:33
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Discover
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:35.311244 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.1.68: BOOTP/DHCP, Reply, length 290, xid 0x6a521a43, Flags [none]
          Your-IP 172.18.1.1
          Client-Ethernet-Address 00:00:00:11:22:33
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: Offer
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
06:44:35.311902 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:33, length 263, xid 0x6a521a43, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:33
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Request
            Requested-IP Option 50, length 4: 172.18.1.1
            Server-ID Option 54, length 4: 172.18.0.3
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:36.316180 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.1.68: BOOTP/DHCP, Reply, length 290, xid 0x6a521a43, Flags [none]
          Your-IP 172.18.1.1
          Client-Ethernet-Address 00:00:00:11:22:33
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: ACK
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
06:44:38.007073 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:34, length 251, xid 0x23552c97, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:34
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Discover
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:38.009206 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.2.68: BOOTP/DHCP, Reply, length 290, xid 0x23552c97, Flags [none]
          Your-IP 172.18.1.2
          Client-Ethernet-Address 00:00:00:11:22:34
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: Offer
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
06:44:38.009418 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:34, length 263, xid 0x23552c97, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:34
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Request
            Requested-IP Option 50, length 4: 172.18.1.2
            Server-ID Option 54, length 4: 172.18.0.3
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:39.014721 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.2.68: BOOTP/DHCP, Reply, length 290, xid 0x23552c97, Flags [none]
          Your-IP 172.18.1.2
          Client-Ethernet-Address 00:00:00:11:22:34
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: ACK
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
06:44:40.623865 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:35, length 251, xid 0x4a57a2d2, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:35
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Discover
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:40.625059 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.3.68: BOOTP/DHCP, Reply, length 290, xid 0x4a57a2d2, Flags [none]
          Your-IP 172.18.1.3
          Client-Ethernet-Address 00:00:00:11:22:35
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: Offer
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
06:44:40.625322 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:35, length 263, xid 0x4a57a2d2, Flags [none]
          Client-Ethernet-Address 00:00:00:11:22:35
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            DHCP-Message Option 53, length 1: Request
            Requested-IP Option 50, length 4: 172.18.1.3
            Server-ID Option 54, length 4: 172.18.0.3
            Parameter-Request Option 55, length 5:
              Subnet-Mask, BR, Default-Gateway, Domain-Name
              Domain-Name-Server
06:44:42.630578 IP (tos 0x10, ttl 128, id 0, offset 0, flags [DF], proto UDP (17), length 318)
    172.18.0.3.67 > 172.18.1.3.68: BOOTP/DHCP, Reply, length 290, xid 0x4a57a2d2, Flags [none]
          Your-IP 172.18.1.3
          Client-Ethernet-Address 00:00:00:11:22:35
          Vendor-rfc1048 Extensions
            Magic Cookie 0x63825363
            Subnet-Mask Option 1, length 4: 255.255.0.0
            Default-Gateway Option 3, length 4: 172.18.0.1
            Domain-Name-Server Option 6, length 8: 8.8.8.8,8.8.4.4
            Lease-Time Option 51, length 4: 3600
            DHCP-Message Option 53, length 1: ACK
            Server-ID Option 54, length 4: 172.18.0.3
            RN Option 58, length 4: 1200
            RB Option 59, length 4: 2400
## kea_2
root@d864a28cbf84:~# tcpdump -i eth0 port 67 or port 68 -v -n
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
06:44:35.308297 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:33, length 251, xid 0x6a521a43, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:33
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Discover
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server
06:44:35.311922 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:33, length 263, xid 0x6a521a43, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:33
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Request
      Requested-IP Option 50, length 4: 172.18.1.1
      Server-ID Option 54, length 4: 172.18.0.3
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server
06:44:38.007102 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:34, length 251, xid 0x23552c97, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:34
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Discover
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server
06:44:38.009439 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:34, length 263, xid 0x23552c97, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:34
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Request
      Requested-IP Option 50, length 4: 172.18.1.2
      Server-ID Option 54, length 4: 172.18.0.3
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server
06:44:40.623903 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 279)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:35, length 251, xid 0x4a57a2d2, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:35
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Discover
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server
06:44:40.625346 IP (tos 0x0, ttl 64, id 0, offset 0, flags [none], proto UDP (17), length 291)
    0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:00:00:11:22:35, length 263, xid 0x4a57a2d2, Flags [none]
    Client-Ethernet-Address 00:00:00:11:22:35
    Vendor-rfc1048 Extensions
      Magic Cookie 0x63825363
      DHCP-Message Option 53, length 1: Request
      Requested-IP Option 50, length 4: 172.18.1.3
      Server-ID Option 54, length 4: 172.18.0.3
      Parameter-Request Option 55, length 5:
        Subnet-Mask, BR, Default-Gateway, Domain-Name
        Domain-Name-Server

これら2つのパケットキャプチャをみて理解できるとおり,
kea_1はクライアントからの3つ全てのDHCP要求(DISCOVERY, REQUEST)において,クライアントに対してOFFER, ACK を返しており,kea_2はクライアントからの要求には全く応答パケットを送出していないことがわかる.
これはHAモードのうち,primaryの1台のみがDHCPに応答するhot-standbyの動作となっていることがわかる.

またこの時のlease-updateを見てみよう.

## kea_1

root@0d5f73b7ffc7:/# tcpdump -i eth0 port 8080 -v -n
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
06:44:35.313735 IP (tos 0x0, ttl 64, id 57517, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0x066f), seq 220565762:220566113, ack 2766104306, win 254, options [nop,nop,TS val 26311210 ecr 26310630], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335075, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:33", "ip-address": "172.18.1.1", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:35.315597 IP (tos 0x0, ttl 64, id 2855, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x0bf1), seq 1:157, ack 351, win 235, options [nop,nop,TS val 26311210 ecr 26311210], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:35 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:35.315738 IP (tos 0x0, ttl 64, id 57518, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xd5cc), ack 157, win 262, options [nop,nop,TS val 26311211 ecr 26311210], length 0
06:44:38.012408 IP (tos 0x0, ttl 64, id 57519, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0xfd19), seq 351:702, ack 157, win 262, options [nop,nop,TS val 26311480 ecr 26311210], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335077, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:34", "ip-address": "172.18.1.2", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:38.015389 IP (tos 0x0, ttl 64, id 2856, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x07cf), seq 157:313, ack 702, win 243, options [nop,nop,TS val 26311480 ecr 26311480], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:38 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:38.015425 IP (tos 0x0, ttl 64, id 57520, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xd1ae), ack 313, win 270, options [nop,nop,TS val 26311480 ecr 26311480], length 0
06:44:40.625311 IP (tos 0x0, ttl 64, id 57521, offset 0, flags [DF], proto TCP (6), length 176)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x58cc (incorrect -> 0x473e), seq 702:826, ack 313, win 270, options [nop,nop,TS val 26311745 ecr 26311480], length 124: HTTP, length: 124
  POST / HTTP/1.1
  Content-Length: 53
  Content-Type: application/json

  { "command": "ha-heartbeat", "service": [ "dhcp4" ] }[!http]
06:44:40.632575 IP (tos 0x0, ttl 64, id 2857, offset 0, flags [DF], proto TCP (6), length 303)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x594b (incorrect -> 0x866a), seq 313:564, ack 826, win 243, options [nop,nop,TS val 26311746 ecr 26311745], length 251: HTTP, length: 251
  HTTP/1.1 200 OK
  Content-Length: 142
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:40 GMT

  [ { "arguments": { "date-time": "Tue, 29 Oct 2019 06:44:40 GMT", "state": "hot-standby" }, "result": 0, "text": "HA peer status returned." } ][!http]
06:44:40.632606 IP (tos 0x0, ttl 64, id 57522, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xce1a), ack 564, win 279, options [nop,nop,TS val 26311746 ecr 26311746], length 0
06:44:41.628464 IP (tos 0x0, ttl 64, id 57523, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0xfb10), seq 826:1177, ack 564, win 279, options [nop,nop,TS val 26311845 ecr 26311746], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335080, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:35", "ip-address": "172.18.1.3", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:41.633534 IP (tos 0x0, ttl 64, id 2858, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x0080), seq 564:720, ack 1177, win 252, options [nop,nop,TS val 26311846 ecr 26311845], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:41 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:41.633621 IP (tos 0x0, ttl 64, id 57524, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xcb4f), ack 720, win 287, options [nop,nop,TS val 26311846 ecr 26311846], length 0
06:44:42.637356 IP (tos 0x0, ttl 64, id 43254, offset 0, flags [DF], proto TCP (6), length 176)
    172.18.0.2.42824 > 172.18.0.3.8080: Flags [P.], cksum 0x58cc (incorrect -> 0x7e2b), seq 1837942929:1837943053, ack 2537261301, win 254, options [nop,nop,TS val 26311946 ecr 26310830], length 124: HTTP, length: 124
  POST / HTTP/1.1
  Content-Length: 53
  Content-Type: application/json

  { "command": "ha-heartbeat", "service": [ "dhcp4" ] }[!http]
06:44:42.640120 IP (tos 0x0, ttl 64, id 59900, offset 0, flags [DF], proto TCP (6), length 303)
    172.18.0.3.8080 > 172.18.0.2.42824: Flags [P.], cksum 0x594b (incorrect -> 0xb605), seq 1:252, ack 124, win 227, options [nop,nop,TS val 26311946 ecr 26311946], length 251: HTTP, length: 251
  HTTP/1.1 200 OK
  Content-Length: 142
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:42 GMT

  [ { "arguments": { "date-time": "Tue, 29 Oct 2019 06:44:42 GMT", "state": "hot-standby" }, "result": 0, "text": "HA peer status returned." } ][!http]
06:44:42.640722 IP (tos 0x0, ttl 64, id 43255, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.2.42824 > 172.18.0.3.8080: Flags [.], cksum 0x5850 (incorrect -> 0x01b8), ack 252, win 262, options [nop,nop,TS val 26311946 ecr 26311946], length 0
## kea_2

root@d864a28cbf84:/# tcpdump -i eth0 port 8080 -v -n
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
06:44:35.313767 IP (tos 0x0, ttl 64, id 57517, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0x066f), seq 220565762:220566113, ack 2766104306, win 254, options [nop,nop,TS val 26311210 ecr 26310630], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335075, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:33", "ip-address": "172.18.1.1", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:35.315569 IP (tos 0x0, ttl 64, id 2855, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x0bf1), seq 1:157, ack 351, win 235, options [nop,nop,TS val 26311210 ecr 26311210], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:35 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:35.315755 IP (tos 0x0, ttl 64, id 57518, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xd5cc), ack 157, win 262, options [nop,nop,TS val 26311211 ecr 26311210], length 0
06:44:38.012495 IP (tos 0x0, ttl 64, id 57519, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0xfd19), seq 351:702, ack 157, win 262, options [nop,nop,TS val 26311480 ecr 26311210], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335077, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:34", "ip-address": "172.18.1.2", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:38.015323 IP (tos 0x0, ttl 64, id 2856, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x07cf), seq 157:313, ack 702, win 243, options [nop,nop,TS val 26311480 ecr 26311480], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:38 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:38.015447 IP (tos 0x0, ttl 64, id 57520, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xd1ae), ack 313, win 270, options [nop,nop,TS val 26311480 ecr 26311480], length 0
06:44:40.625346 IP (tos 0x0, ttl 64, id 57521, offset 0, flags [DF], proto TCP (6), length 176)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x58cc (incorrect -> 0x473e), seq 702:826, ack 313, win 270, options [nop,nop,TS val 26311745 ecr 26311480], length 124: HTTP, length: 124
  POST / HTTP/1.1
  Content-Length: 53
  Content-Type: application/json

  { "command": "ha-heartbeat", "service": [ "dhcp4" ] }[!http]
06:44:40.632539 IP (tos 0x0, ttl 64, id 2857, offset 0, flags [DF], proto TCP (6), length 303)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x594b (incorrect -> 0x866a), seq 313:564, ack 826, win 243, options [nop,nop,TS val 26311746 ecr 26311745], length 251: HTTP, length: 251
  HTTP/1.1 200 OK
  Content-Length: 142
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:40 GMT

  [ { "arguments": { "date-time": "Tue, 29 Oct 2019 06:44:40 GMT", "state": "hot-standby" }, "result": 0, "text": "HA peer status returned." } ][!http]
06:44:40.632623 IP (tos 0x0, ttl 64, id 57522, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xce1a), ack 564, win 279, options [nop,nop,TS val 26311746 ecr 26311746], length 0
06:44:41.628608 IP (tos 0x0, ttl 64, id 57523, offset 0, flags [DF], proto TCP (6), length 403)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [P.], cksum 0x59af (incorrect -> 0xfb10), seq 826:1177, ack 564, win 279, options [nop,nop,TS val 26311845 ecr 26311746], length 351: HTTP, length: 351
  POST / HTTP/1.1
  Content-Length: 279
  Content-Type: application/json

  { "arguments": { "expire": 1572335080, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:35", "ip-address": "172.18.1.3", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }[!http]
06:44:41.633418 IP (tos 0x0, ttl 64, id 2858, offset 0, flags [DF], proto TCP (6), length 208)
    172.18.0.2.8080 > 172.18.0.3.33988: Flags [P.], cksum 0x58ec (incorrect -> 0x0080), seq 564:720, ack 1177, win 252, options [nop,nop,TS val 26311846 ecr 26311845], length 156: HTTP, length: 156
  HTTP/1.1 200 OK
  Content-Length: 48
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:41 GMT

  [ { "result": 0, "text": "IPv4 lease added." } ][!http]
06:44:41.633649 IP (tos 0x0, ttl 64, id 57524, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.3.33988 > 172.18.0.2.8080: Flags [.], cksum 0x5850 (incorrect -> 0xcb4f), ack 720, win 287, options [nop,nop,TS val 26311846 ecr 26311846], length 0
06:44:42.637308 IP (tos 0x0, ttl 64, id 43254, offset 0, flags [DF], proto TCP (6), length 176)
    172.18.0.2.42824 > 172.18.0.3.8080: Flags [P.], cksum 0x58cc (incorrect -> 0x7e2b), seq 1837942929:1837943053, ack 2537261301, win 254, options [nop,nop,TS val 26311946 ecr 26310830], length 124: HTTP, length: 124
  POST / HTTP/1.1
  Content-Length: 53
  Content-Type: application/json

  { "command": "ha-heartbeat", "service": [ "dhcp4" ] }[!http]
06:44:42.640413 IP (tos 0x0, ttl 64, id 59900, offset 0, flags [DF], proto TCP (6), length 303)
    172.18.0.3.8080 > 172.18.0.2.42824: Flags [P.], cksum 0x594b (incorrect -> 0xb605), seq 1:252, ack 124, win 227, options [nop,nop,TS val 26311946 ecr 26311946], length 251: HTTP, length: 251
  HTTP/1.1 200 OK
  Content-Length: 142
  Content-Type: application/json
  Date: Tue, 29 Oct 2019 06:44:42 GMT

  [ { "arguments": { "date-time": "Tue, 29 Oct 2019 06:44:42 GMT", "state": "hot-standby" }, "result": 0, "text": "HA peer status returned." } ][!http]
06:44:42.640696 IP (tos 0x0, ttl 64, id 43255, offset 0, flags [DF], proto TCP (6), length 52)
    172.18.0.2.42824 > 172.18.0.3.8080: Flags [.], cksum 0x5850 (incorrect -> 0x01b8), ack 252, win 262, options [nop,nop,TS val 26311946 ecr 26311946], length 0

上記より,kea_1からkea_2に向けて

{ "arguments": { "expire": 1572335075, "force-create": true, "fqdn-fwd": false, "fqdn-rev": false, "hostname": "", "hw-address": "00:00:00:11:22:33", "ip-address": "172.18.1.1", "state": 0, "subnet-id": 1, "valid-lft": 3600 }, "command": "lease4-update", "service": [ "dhcp4" ] }

というjsonフォーマットでlease-updateが送出され,

{ "result": 0, "text": "IPv4 lease added." }

という形で応答していることがわかる.
Kea DHCPにおいてはこのようにREST APIを用いてlease-updateの交換を行うことが特徴の一つであると言えるだろう.

まとめ

というわけで,無事Kea DHCPを使ってhot-standbyモードのHA構成を組むことができました.
本記事では以下のことを実施しました.

  • ISC DHCPの後継であるKea DHCPについて紹介し,冗長構成の違いを紹介した.
  • ISCのKea DHCPを用いてhot-standbyモードのHA構成を組むことについて紹介した.
  • hot-standyモードのHAにおいて実際にクライアントを模擬してDHCPによるアドレスリースを行い,2つのサーバの応答の違いをパケットキャプチャによって確認した.
  • HA構成における2サーバ間のlease-updateについて,REST APIによって行われていることを確認した.

今後は,load-balancingモードでのHA構成の構築と,hot-standby構成との振る舞いの違いや,HA構成を取ることによるパフォーマンス影響,HA構成で本当に冗長化ができているのか,等について見てみようと考えています.

References

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker と ECS Scheduled Taskを使って定期的におじさんからメッセージが届く仕組みを作る😚💕

皆さんはおじさんは好きカナ?( ̄ー ̄?
僕は好きだヨ!!✋( ̄▽ ̄ ♥ 💕
定期的におじさんからメッセージが届いたら素敵じゃないカナ?😚😘😄
と言うわけで作ってみタヨ!!(^з<)❗😃✋💕

概要

ECS Scheduled Taskを使っておじさんから定期的に Slack メッセージが届く仕組みを作ります。
おじさんのメッセージ生成には Ojichat を使わせていただきました。

greymd/ojichat

環境構築

AWS CLI用のIAMユーザーの作成

AWS CLI用のIAMユーザーを作成します。以下の記事を参考にしながらIAMユーザーを作成します。
今回は Terraform の例もあるため、 Admin権限を持つユーザーを作成しました。
イメージをプッシュする権限のみ必要な場合は ecr:GetAuthorizationToken を付与してください。
SecretAccessKey は再発行されないため、分からなくなってしまったらユーザーを再作成する必要があります。

IAM ユーザーの作成 (コンソール)
イメージのプッシュ

AWS CLI のインストール

AWS CLI をインストールし、設定を行います。インストール方法は下記記事を参考にしてください

macOS に AWS CLI をインストールする
AWS CLI の設定

インストールが完了したら AWS CLI の設定を行い、AWS CLI から S3 のバケット一覧を取得できることを確認します。(動作確認)

$ aws --version
$ aws configure
$ aws s3api list-buckets  # S3 のバケット一覧を表示するコマンド

Slack に Ojichat の絵文字 :ojichat: を作成する

:ojichat: を追加するとテンション上がります。😚💕

カスタム絵文字を追加する

Slack のWebHookURL を作成する

おじさんからの Slackメッセージを受け取るのに必要です。
WebHook の設定を行うことで、curlコマンドで簡単にメッセージを Slack に送信することができます。

SlackのWebhook URL取得手順

イメージを作成

おじさんメッセージの生成 & Slack にメッセージを飛ばす Docker イメージを作ります。
同ディレクトリ に Dockerfileentrypoint.sh を作成し、以下のコマンドでビルドします。
entrypoint.sh 中の {your_channnel_name} にはおじさんからのメッセージを受け取りたいチャンネル名を入力してください。
「おのじゅん」は私のあだ名です。

Dockerfile
FROM golang:1.13.4-stretch
RUN go get -u github.com/greymd/ojichat
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh
#!/bin/bash

COMMENT=$(ojichat $NAME)
echo $COMMENT
curl -X POST --data-urlencode "payload={\"channel\": \"##{your_channnel_name}\", \"username\": \"Ojichat\", \"text\": \"$COMMENT\", \"icon_emoji\": \":ojichat:\"}" $WEB_HOOK_URL
$ docker build -t ojichat:latest --no-cache .

ビルドができたら以下のコマンドでおじさんからSlackにメッセージが届くことを確認します。
NAME にはおじさんに呼んで欲しい名前を、WEB_HOOK_URL には Slack のWebHookのURLを入力します。

$ docker run --rm -e NAME=おのじゅん -e WEB_HOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzzz ojichat:latest

スクリーンショット 2019-11-26 15.10.26.png

無事おじさんからメッセージを受け取ることができました。

おじさんを ECR にプッシュする

おじさんイメージがきちんと動くことがわかったので、次はおじさんイメージを ECR にプッシュします。
GUI からおじさんを格納するリポジトリを作成します。

スクリーンショット 2019-11-26 15.13.05.png

プッシュコマンドを表示 ボタンを押すとイメージのビルドからプッシュまでの手順が表示されます。
その手順に従ってイメージを ECR にプッシュします。

$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker build -t ojichat .
docker tag onojun-ojichat:latest {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/onojun-ojichat:latest
docker push {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/onojun-ojichat:latest

おじさんを動かすロールを作成する

おじさんイメージは Fargate 上で動かします。
必要な権限を持つロールを作成します。

ojichat-policy を作成します。

スクリーンショット 2019-11-26 15.39.39.png

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

次に ojichat-roleを作成し、ojichat-policyをアタッチさせ、信頼関係を編集します。

スクリーンショット 2019-11-26 15.42.34.png

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "ecs.amazonaws.com",
          "ecs-tasks.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

おじさんタスク定義を作成する

おじさんのタスク定義を作成します。

スクリーンショット 2019-11-26 16.05.26.png

スクリーンショット 2019-11-26 16.05.51.png

スクリーンショット 2019-11-26 16.05.58.png

必要な環境変数を設定します。
今回はハードコードしてしまいましたが、WEB_HOOK_URL などのクレデンシャル情報は ParameterStore に格納し、それを FromValue で参照するとタスク定義に書かずに済みます。

おじさんクラスターを作成し、おじさんタスクを実行する

ojichat-cluster を作成し、作成したタスク定義 ojichat-task からタスクを実行します。

スクリーンショット 2019-11-26 16.09.23.png

おじさんを ScheduledTask で動かす

Fargate にいるおじさんからメッセージが届いたら、次はおじさんのメッセージを自動化します。
スケジュールタスクを設定することで実現することができます。

スクリーンショット 2019-11-26 16.19.39.png

僕はおじさんが好きなので1分ごとにメッセージが飛ぶようにしました。

スクリーンショット 2019-11-26 16.40.15.png

Terraform

########################
## Credential Infos
########################
provider "aws" {
  access_key = local.access_key
  secret_key = local.secret_key
  region     = "ap-northeast-1"
}

########################
## ECR
########################
# ECS Repository
resource "aws_ecr_repository" "repository" {
  name = "ojichat"
}

# Repositry Policy
# Permit pull image
resource "aws_ecr_repository_policy" "repository_policy" {
  repository = aws_ecr_repository.repository.name
  policy     = data.aws_iam_policy_document.repository_policy.json
}

data "aws_iam_policy_document" "repository_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchCheckLayerAvailability",
      "ecr:BatchGetImage"
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }
}


########################
## IAM
########################
# IAM Policy
resource "aws_iam_policy" "policy" {
  name        = "ojicaht-policy"
  description = "for ojichat"
  policy      = data.aws_iam_policy_document.policy.json
}

data "aws_iam_policy_document" "policy" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "ecs:RunTask",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "iam:PassRole"
    ]
    resources = ["*"]
  }
}

# IAM Role
resource "aws_iam_role" "role" {
  name               = "ojichat-role"
  description        = "role for ojichat"
  assume_role_policy = data.aws_iam_policy_document.role.json
}

data "aws_iam_policy_document" "role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com", "events.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "schedule_policy_attachment" {
  role       = aws_iam_role.role.name
  policy_arn = aws_iam_policy.policy.arn
}

resource "aws_iam_role_policy_attachment" "event_role_policy_attachment" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole"
}

########################
## ECS
########################
# cluster
resource "aws_ecs_cluster" "cluster" {
  name = "ojichat-cluster"
}

# task difinition
resource "aws_ecs_task_definition" "task_definition" {
  family                   = "ojichat-task"
  task_role_arn            = aws_iam_role.role.arn
  container_definitions    = data.template_file.container_definitions.rendered
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.role.arn
}

data "template_file" "container_definitions" {
  template = <<EOF
[
  {
    "cpu": 0,
    "environment": [
      {
        "name": "WEB_HOOK_URL",
        "value": "${local.web_hook_url}"
      },
      {
        "name": "NAME",
        "value": "おのじゅん"
      }
    ],
    "name": "ojichat",
    "image": "${aws_ecr_repository.repository.repository_url}",
    "logConfiguration": {
      "logDriver": "awslogs",
      "secretOptions": null,
      "options": {
        "awslogs-group": "/ecs/ojichat-task",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "essential": true
  }
]
EOF
}

# scueduled task
resource "aws_cloudwatch_event_rule" "schedule_rule" {
  name                = "ojichat-event"
  schedule_expression = "cron(* * * * ? *)"
  is_enabled          = true
}

resource "aws_cloudwatch_event_target" "fargate_scheduled_task" {
  rule     = aws_cloudwatch_event_rule.schedule_rule.name
  arn      = aws_ecs_cluster.cluster.arn
  role_arn = aws_iam_role.role.arn

  ecs_target {
    task_definition_arn = aws_ecs_task_definition.task_definition.arn
    task_count          = 1
    launch_type         = "FARGATE"

    network_configuration {
      subnets          = [local.subnets_id]
      assign_public_ip = true
    }
  }
}

参考

greymd/ojichat
Amazon Elastic Container Service とは
Amazon ECS クラスター
Amazon ECS タスク定義
Amazon ECS サービス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nuxtで作った静的ファイルをgolangのechoでホスティングする。

背景

機能の内容は言えませんが、外部APIを利用した既存の機能の簡易版のツールを作りたい。そういった依頼が舞い込んできました。
すでにその機能は提供されていますが、画面仕様のもと多くの項目の入力が必要で、客先でさくっと処理させるには不向きでした。
そのため、既存機能の登録機能のみを切り出してほしいとのことでした。
二つ返事で了承。
今後のことを考えフロントエンドは別プロジェクトにし、APIは既存のものを利用。
外部のAPIはフロントエンドでのスクリプト読み込み後のAjax送信とサーバーサイドでのポストの処理があり、特定のホスト名と特定のIPにしか対応してませんでした。
それは困った。
ということで、nuxtで静的サイトとしてgenerateしたものを既存のプロジェクトでホスティングさせてしまえば、ホスト名とIPの課題は解決するということでやり方を書いておきます。

※ソースは抜粋しか載せられないのでその点はご理解ください。

流れ

  1. nuxtでプロジェクトを作成しgenerateすることで静的サイトへ
  2. golangとechoでできた既存システムでgenerateした静的サイトをホスティング
  3. リリース方法を検討していい感じにリリースできるようにする。

nuxtでプロジェクトを作成しgenerateすることで静的サイトへ

  • ソースを載せられないのでいい感じにご自身で実装してください。

静的サイトへgenerate

  • なんのこっちゃない。あまりやったことないがとりあえずlocalで
yarn generate
  • generate?めんどくさいのでpackage.jsonをいじって
package.json
"gen": "nuxt generate",
yarn gen
  • distに静的なファイルが出力されました。 静的なファイルっていうくらいなので、要はHTMLを生で書いたのと同じ状態ですよね?ということで、ブラウザでこのファイルを開く
  • おや?スクリプト(画面のイベント)が発火してない?そんなことある? どうやらnuxt generateしても、ルート情報が正しくないと、生成されたスクリプトを読み込めないらしい。 この場合だと当然、/hoge/hoge/index.htmlでアクセスしてる。少し調べるとわかりますが、どうやらgithubpageやnetlifyやS3でホスティングしないと正常に動かないらしい。 なるほど。

じゃあ、当社で使っているbacklogで「ブラウザで直接開く」なるものがあるので、まずはそいつでホスティングさせてみましょう。

  • backlog「ブラウザで直接開く」で開いても正しく動かない。
    なるほど。URL見ればわかるが、これはHTMLをただブラウザで開いてるだけか。。。ホスティングサービスとは別物。。。
    gitと名乗る以上極力githubに寄せてほしいところだが、できないものはできないのでどうしようもない。

  • じゃあ、githubでpage作るか。
    はい、そうです。金払わんとできない。金はない。やめる。

  • じゃあ、いったんnetlifyで。
    いける!どうやらホスティングさせてあげれば正しく動くみたい。ということが分かったので、nuxtはこんなところにしておく。

  • ホスティングする際のエントリーポイントをルートに追加
    http://hostname/nuxtprojectで受ける予定なので、nuxt.config.jsに以下を追加

  router: {
    base: '/nuxtproject' 
  }

golangとechoでできた既存システムでgenerateした静的サイトをホスティング

  • まずはgitから落としてプロジェクトへぶっこむ
cd echoproject
git clone <nuxtprojectURL>

echoのプロジェクト直下にぶっこむ。
depやglideで入れたかったのですが、なかなかてこずってできなかったので、直接ぶっこむ。

  • 次にgenerateする
cd nuxtproject
yarn gen

後で、distもリポジトリにぶっこむつくりにしますが、いったんはこんな感じでOK.

  • ホスティングする

非常に簡単。やっぱりFWって素晴らしい。

e.Static("/nuxtproject", "nuxtproject/dist")

これだけでOK。

  • nuxtprojectを編集した際は毎回cloneしてgenerateする必要があるよ。

こっからいろいろ考える。

  • パッケージ化しようとしたけど、うまくできなかったので、以下の流れで本番のモジュールに組み込む
  1. distをリポジトリ保存する。
  2. echoprojectのデプロイ時にcloneする。
  3. 環境により変更すべき値を考慮する。

distをリポジトリ保存する。

  • .gitignoreでdistを外し、yarn generateした後にpushする。
    さすがに毎回手でやるのはだるい。本当ならビルド環境用意してそいつにやらせてあげればいいのだけれども、サーバー用意するのがめんどいうえ、金がない。
    なのでローカルで必ずやるようにする。

  • どうやらgitの機能にHookがあるらしい。
    コミット前、プッシュ前様々なタイミングでフックができるらしいが、今回はコミットする前を選択。
    .git/hooks/pre-commitにコマンド書いとけばいいらしい。

#!/bin/sh
yarn
yarn gen
git add .

exit 0
  • インストールして静的なファイルを生成、できたファイルを管理対象にして終わり。pushは自分でやる これで常にdistが最新される。

echoprojectのデプロイ時にcloneする。

本来ならここでビルドすればよいのだが、dokerのイメージにnuxtとnodeとnpmとyarnを入れるのは避けたかったので、クローンするだけ。その際今回のnuxtprojectはパブリックなリポジトリではないので、いじいじする。
gitはnetrcの認証情報をもとにリポジトリからとってくることができるらしい。じゃあ簡単だ。
echoprojectにechoproject_nutrcをまずは作ります。

# 自分の情報を設定してね。
machine <git_host>
login <git_user_id>
password <git_user_pwd>

これをdockerfileで ~/.netrcにしてやればOK

RUN cp echoproject_nutrc ~/.netrc

環境により変更すべき値を考慮する。

当然開発環境と本番環境は分かれてまっせということ。
echoprojectではENVという環境変数で振り分けてますが、nuxtはどうしましょう。
よし、どちらもgenerateしてホスティングするdistの名称を変えればOKじゃね?ということでそのようにします。

  1. nuxt.config-dev.jsを作る

  2. nuxt.config-dev.jsでgenerateの際の出力フォルダを指定

  generate: {
    subFolders: false,
    dir: './dist_dev'
  },
  1. package.jsonに下記を追加
  2. "gen-dev":"nuxt generate --config-file nuxt.config-dev.js",
    
  3. .git/hooks/pre-commitにyarn gen-devを追加

これでdistとdist_devができるので後はechoprojectでdistをホスティングするのかdist_devをホスティングするの指定してあげればOK。

非常にむりやりではありますが、今回はこのようにしましたとさ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Docker-02】Docker for Macで使い捨てMySQL環境の作成

はじめに

SQLの練習をしたい時にローカルで使い捨てできる環境あると良いなーと思っていたらDockerで簡単に実現できそうなのでやってみました。
仮想環境の知識が乏しい時はAWSに立てるか?とか考えてたので正気じゃないですね:innocent:

本記事の執筆にあたり、「Dockerで使い捨てのMySQL環境を用意する。事前データを投入して起動する。」を出典とさせて頂きました。この場を借りてお礼申し上げます!

やってみる

Docker Desctop for Macのインストール

公式からインストールします。
https://docs.docker.com/docker-for-mac/install/

書いてて思い出しましたが、確かブラウザでDockerの起動とか練習するSaasがあったような。。?
そちらでもMySQL環境作成できるかもしれませんね。本記事では割愛させて頂きます。

コンテナの起動

MySQL公式のイメージを使用するので、いきなり起動。便利すぎる。

コマンドは改行せずに1行で書き切ってもOKです。
パスワードは確か英小文字大文字数字の8行とか9行じゃないとダメだったような気がしますが、適当に入力してもログインできました。なんでだw

サイトに倣い、ポートは43306をマッピング。3306:3306でも良いと思います。

$ docker container run --rm -d \
> -e MYSQL_ROOT_PASSWORD=okome \
> -p 43306:3306 --name mysql mysql:5.7

mysqlに接続

ローカルのシェルでmysqlしたら怒られた。
「mysql」コンテナのシェルを起動した後にmysqlに接続するのが正解のようです。
パスワードはコンテナ起動時に指定した「okome」を入力。

$ docker exec -it mysql /bin/bash
# mysql -uroot -p
Enter password:

mysql>

うおお動いた!早い!かんたん!
特に.mysqlは指定していないのでまっさらの初期状態ですが、勉強目的の使い捨て環境と考えるなら全然問題ないですね。捗るぜ。。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

事前にデータ投入して起動する

Dockerで使い捨てのMySQL環境を用意する。事前データを投入して起動する。」のリンクにあった「事前にデータ投入をした MySQL Docker イメージを作る場合は /docker-entrypoint-initdb.d を活用すると便利」を拝読すると、docker-entrypoint-initdb.d/にテーブルや実データを定義したgzやgzipを置けばコンテナ起動時に読み込んでくれるとのこと。

Dockerfileをサイト通りに記述し、公式からダウンロードしたworld.sql.gzを同じディレクトリに配置します。

$ cat Dockerfile
FROM mysql:5.7

COPY world.sql.gz /docker-entrypoint-initdb.d/world.sql.gz

$ ls -l
Dockerfile
world.sql.gz

配置後はイメージをビルドし、起動。

$ docker build -t mysql-image .
$ docker container run -e MYSQL_ROOT_PASSWORD=okome --name mysqlserver -d mysql-image

シェルを起動してmysqlに接続。
world databaseできとるやん。。素晴らしすぎる。

$ docker container exec -it mysqlserver /bin/bash
# mysql -uroot -p
Enter password:
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| world              |
+--------------------+
5 rows in set (0.00 sec)

mysql> use world
Database changed
mysql> show tables;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.01 sec)

おわりに

実際に手を動かすと、Dockerの可搬性の高さを実感できますね。
DB周りの知識が弱いんで、これを使ってガリガリ手を動かします!

オプションも散らかってきたので整理しなきゃですね。
あと薄々気づいてはいたのですが、dockerコマンドのcontainerって省略できたんですね。
それが一番有益だったかもしれないw

簡単ですが以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Docker】とにかく触ってみる(06:Docker Machine作成/失敗版)

最初に

まだ基本的なことがあやふやなので、CentOS上にDocker環境を構築するところから再開してみます。

10回くらい構築すれば覚えると思うけど、、、

Dockerのインストール

この作業は流石に慣れて来ました。

teraterm
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io

sudo systemctl start docker
sudo systemctl enable docker
docker --version
実行結果
Docker version 19.03.5, build 633a0ea

Dockerイメージの確認

teraterm
sudo docker images
実行結果
REPOSITORY          TAG                 IMAGE ID            CREATED            SIZE

ubuntuの取得

前回PUSHしたubuntuをPULLしてみます。

teraterm
sudo docker pull  wakataka3/dummy_ubuntu:ver191124
実行結果
ver191124: Pulling from wakataka3/dummy_ubuntu
7ddbc47eeb70: Pull complete
c1bbdc448b72: Pull complete
8c3b70e39044: Pull complete
45d437916d57: Pull complete
Digest: sha256:134c7fe821b9d359490cd009ce7ca322453f4f2d018623f849e580a89a685e5d
Status: Downloaded newer image for wakataka3/dummy_ubuntu:ver191124
docker.io/wakataka3/dummy_ubuntu:ver191124
teraterm
sudo docker images
実行結果
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
wakataka3/dummy_ubuntu   ver191124           775349758637        3 weeks ago         64.2MB

???
IMAGE IDが前回コミットしたものと異なったので確認したところ、思い切りubuntuにタグ付けしてからPUSHしていました。。。

こういう地味なミスが毎回あるので、実機確認しておかないと怖いんですよね。。。

teraterm
sudo docker tag ubuntu wakataka3/dummy_ubuntu:ver191124
sudo docker push wakataka3/dummy_ubuntu:ver191124

Docker Machineについて

Docker Machineとは、Dockerエンジンを搭載した仮想マシンの作成、起動、停止、再起動などをコマンドラインから実行できるツールのこと。
本稿ではDocker Machineをインストールしていないので、エラーが出てきました。

teraterm
sudo docker-machine ls
実行結果
sudo: docker-machine: command not found

そのため、別のEC2を起動し、Docker Machineをインストールしてみます。
毎回新しく設定しないと、どの操作が有効だったかの切り分けが出来ないのでサクッと作れるAWSはここでも重宝しています。

公式ページからコマンドをコピー&ペーストします。

teraterm
curl -L https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-`uname -s`-`uname -m` >/tmp/docker-machine &&
    chmod +x /tmp/docker-machine &&
    sudo cp /tmp/docker-machine /usr/local/bin/docker-machine

docker-machine version
実行結果
docker-machine version 0.16.2, build bd45ab13
teraterm
docker-machine ls
実行結果
NAME   ACTIVE   DRIVER   STATE   URL   SWARM   DOCKER   ERRORS

Docker Machine作成(失敗)

teraterm
docker-machine create --driver amazonec2 \
    --amazonec2-access-key xxxxxxxxxxxxxxxxxxxx \
    --amazonec2-secret-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    --amazonec2-vpc-id vpc-xxxxxxxxxxxxxxxxx \
    --amazonec2-subnet-id subnet-xxxxxxxxxxxxxxxxx \
    --amazonec2-region ap-northeast-1 \
    --amazonec2-ami ami-0c1c738e580f3e01f \
    aws-waka-taka

その他オプションについてはDockerドキュメントを参照します。

実行結果
Running pre-create checks...
Creating machine...
(aws-waka-taka) Launching instance...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Error creating machine: Error detecting OS: Too many retries waiting for SSH to be available.  Last error: Maximum number of retries (60) exceeded

SSHの接続時間超過はしていましたが、AWSのEC2インスタンス上では問題なく起動していました。
念の為、「docker ls」で確認してみます。

teraterm
docker-machine ls
実行結果
NAME           ACTIVE   DRIVER      STATE   URL   SWARM   DOCKER    ERRORS
aws-waka-taka  -    amazonec2   Running   tcp://3.112.201.223:2376        Unknown   Unable to query docker version: Cannot connect to the docker engine endpoint

ここで行き詰まりました。。。
諦めかけて1個ずつ確認していったところ、キーペアが「aws-waka-taka」になっている。
こんなキーペアは作った記憶がにないので、Docker Machine作成時にキーペアを指定しなかったから?

腑に落ちないけれど、それしか心当たりがないので、キーペアを指定して再度作成。

Docker Machine作成リトライ(失敗)

teraterm
docker-machine create --driver amazonec2 \
    --amazonec2-access-key xxxxxxxxxxxxxxxxxxxx \
    --amazonec2-secret-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    --amazonec2-vpc-id vpc-xxxxxxxxxxxxxxxxx \
    --amazonec2-subnet-id subnet-xxxxxxxxxxxxxxxxx \
    --amazonec2-region ap-northeast-1 \
    --amazonec2-ssh-keypath xxxxxxx \
    --amazonec2-ami ami-0c1c738e580f3e01f \
    aws-waka-taka2

ひとまず、本件は30分考えて分からなからなかったので保留します。
新規環境作り直してもう一度、どこが駄目だったか見直してみます。
そう思ったけど、Docker Machineは前回飛ばしたAutomated Buildと違ってクリティカルなので、もう少し調べてみようと思います。

最後に

本稿は失敗手順の記事ですが、もう少し勉強してどこが悪かったのか見極めるために残しておきます。
この手の失敗って絶対にどこかでショボいミスをしているだけだろうから…

ちなみに、参考書ではVirtual Boxを使っていたので、次回はCentOSにVirtual Boxをインストールして試してみようと思います。

EC2にVirtual Boxって微妙な気がしますが、独自色出せるほどの知識がないので、愚直にやっていくことにします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでGeforceを使ってSpleeterを回す

Spleeter(https://github.com/deezer/spleeter) という、python+Tensorflowを使った、音声分離アプリケーションがあります。これを使うと楽曲のファイルからヴォーカルだけを抜き出したりできます。

使い方も上のサイトやこちらに書いてあったりするのですが、nvidia-dockerが古いだとか、GPU使ってないだとかで決め手に欠けています。ですので今まとも?な方法のメモ書きを残します。(本当は漏れのない手順書を書きたいとこでしたが、実施から時間が経って記憶から抜けている部分もあります・・)

自分で構築するのが面倒な人は、参考の1, 2を使うとよいでしょう。ただし録音に使ったデータがクラウド上に保存されるのかどうかは保証できません。

動作確認環境

  1. AMD Ryzen 3700X
  2. ASRock B450M Steel Regend
  3. メモリ: 32GB
  4. (ブランド不明)Geforce GTX 1080Ti
  5. Ubuntu 18.04 server
  6. nvidia公式driver

導入手順

  1. nvidia driverを入れる
    ホストマシン(Dockerを呼び出すマシン)にnvidiaの公式ドライバを入れます。おそらくCUDAドライバあたりも必要です。

  2. Docker ceを入れる
    最新のnvidia-dockerがDocker ce 19.03を要求するので、入れます。Ubuntu標準dockerではダメです。
    https://docs.docker.com/install/linux/docker-ce/ubuntu/

  3. nvidia-dockerを入れる
    最近のはnvidia-docker2がdeprecatedされており、最新のnvidia-dockerではspleeterに書かれているコマンドでは実行出来ないので注意してください。
    https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)

  4. spleeterを入れて使う
    https://github.com/deezer/spleeter/wiki/2.-Getting-started#using-docker-image
    nvidia-dockerではなく、docker run --gpus all等を使う必要があります(allは手持ちのGPUによって変えてください)。
    例: docker run --gpus all -v $(pwd)/output:/output deezer/spleeter:gpu separate -i audio_example.mp3 -o /output

参考

  1. spleeter使って分離してくれるサービス https://moises.ai/
  2. Spleeterを簡単に使えるGoogle colaboratoryのノートを作成しました. https://qiita.com/Ryo0o0oOO/items/65acd38f4034800388c0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

openapi-generatorで手軽にスタブサーバとクライアントの生成

はじめに

手元のOASでさくっとスタブサーバやクライアントを作りたい時用に本記事をまとめました。

Web版SwaggerEditorからでもスタブサーバのソースコード一式生成してくれるのですが、
業務でNDA契約がどうのこうので使えないケースがあるみたいなのでそうゆう時に役立つかもです。

今回紹介するOASのバージョンは3.0です。(OAS 3.0)

今回はDockerを使用します。

作業環境

Windows 10 pro
Docker version 19.03.2
docker-compose version 1.24.1

Dockerfileの準備

下記のDockerfileをプロジェクト直下に用意します。

FROM openjdk:8-jdk-alpine

WORKDIR /app

RUN apk --update add bash nodejs npm maven git
RUN rm -rf /var/cache/apk/*ls
RUN git clone https://github.com/openapitools/openapi-generator /generator
RUN cd /generator && mvn clean package
RUN npm install -g swagger-cli
  • javaを使うのでJDKの入ったイメージをベースにしています。(DockerHub)
  • 作業ディレクトリは/app直下にしています。
  • 自動生成ツールにOpenAPI Generatorを使用するのでCloneします。
  • Mavenを使用してopenapi-generatorのJavaプログラムをビルドします。
  • swagger-cliはOASをYAMLからJSONに変換したりするのに便利なので入れてます。

docker-compose.ymlの準備

下記のdocker-compose.ymlをプロジェクト直下に用意します。

docker-compose.yml
version: '3'
services:
  tools:
    build: ./
    volumes:
      - .:/app
    tty: true
    command: sh
  • 前述で用意したDockerfileをビルドしてバックグラウンドでコンテナを起動する想定で作成しています。
  • ホスト側のカレントディレクトリをコンテナ側の作業ディレクトリにマウントしています。

Swaggerの準備

Swaggerファイルをプロジェクト直下に用意します。
今回はサンプルとして、getpostのシンプルなAPIのOASを用意しました。

openapi.yml
openapi: 3.0.0
info:
  title: Sample API
  version: 0.0.1
servers:
  - url: http://localhost:3000/api
    description: SampleApiServer
tags:
  - name: Sample
paths:
  /getTest:
    get:
      tags: [ "Sample" ]
      summary: Get test.
      description: Get test.
      operationId: getTest
      parameters:
        - in: query
          name: name
          schema:
            type: string
      responses:
        '200':
          description: Sample
  /postTest:
    post:
      tags: [ "Sample" ]
      summary: Post test.
      description: Post test.
      operationId: postTest
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '200':
          description: Sample

実践

コンテナを起動します。(けっこう時間かかります、長くて10分くらい?

PS C:\Users\user\Desktop\project> docker-compose up -d --build

コンテナの中に入ります。

PS C:\Users\user\Desktop\project> docker-compose exec tools bash

OASをYAML形式からJSON形式に変換します。

bash-4.4# swagger-cli bundle -o openapi.json openapi.yml

ExpressなNodeJSスタブサーバを生成します。

bash-4.4# java -jar /generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
   -i openapi.yml \
   -g nodejs-express-server \
   -o server

Javascriptなクライアントを生成します。

bash-4.4# java -jar /generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
   -i openapi.yml \
   -g javascript \
   -o client

ここまでやると下のような感じで、クライアントとスタブサーバが自動生成されているかと思います。
1.png

※Javascript以外の言語でも生成できるのでOpenAPI Generatorをご参照ください。

おまけ

どうせなら、SwaggerUIとSwaggerEditorもDockerで立ててしまいましょう。
下記をdocker-compose.ymlに追記します。

docker-compose.yml
  swagger-ui:
    image: swaggerapi/swagger-ui
    volumes:
      - ./openapi.yml:/usr/share/nginx/html/openapi.yml
    environment:
      API_URL: openapi.yml
    ports:
      - "9001:8080"

  swagger-editor:
    image: swaggerapi/swagger-editor
    ports:
      - "9002:8080"

コンテナを起動します。

PS C:\Users\user\Desktop\project> docker-compose up -d --build

localhost:9001でSwagger UIに、localhost:9002でSwagger Editorにアクセスできるかと思います。
2.png

以上です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Heroku】デプロイ後にcode=H14 desc="No web processes running"

事象

話の流れとしてはこの記事の続きになります。

Dockerで開発環境構築をしたRailsアプリのコンテナをpushすることができたのですが、その後

$ heroku addons:create heroku-postgresql:hobby-dev

$ heroku container:release "コンテナ名"

$ heroku open

として、サイトを開いてみると、

スクリーンショット 2019-09-24 7.44.05.png

とHerokuのエラーが発生していました。

解決方法

まず、画面に書かれている通り、コンソール上でheroku logs --tailを実行してみたところ

$ heroku logs --tail
2019-09-10T22:53:00.595256+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/" host=safe-basin-05606.herokuapp.com request_id=e797e1d4-8327-4285-b525-986ed50ea467 fwd="160.237.76.19" dyno= connect= service= status=503 bytes= protocol=https
2019-09-10T22:53:01.484857+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=safe-basin-05606.herokuapp.com request_id=f8c9be65-f445-4b0d-a4fa-83b27c76762f fwd="160.237.76.19" dyno= connect= service= status=503 bytes= protocol=https

と出力されました。

ログによるとcode=H14 desc="No web processes running"が発生しているようなので、この文言で検索してみると、こちらの公式ドキュメントを発見。

公式ドキュメントによると、heroku ps:scale web=1を叩けとのことなので、叩いてみると、

$ heroku ps:scale web=1
 ›   Warning: heroku update available from 7.26.2 to 7.29.0.
Scaling dynos... !
 ▸    Couldn't find that process type (web).

と出力され、失敗してしまいました。

改めて、Web上でherokuのBuildlogを見てみると、

スクリーンショット 2019-10-26 13.04.19.png

と表示されていました。

どうやら、heroku.ymlが含まれていないことが原因の模様で、解決するにはheroku.ymlを作成するか、heroku stack:set heroku-18を実行する必要があるとのこと。

そこでまずはheroku.ymlを作成する方針をとってみました。

Buildlogに記載されていた公式ドキュメントを参考に下記のシンプルなheroku.ymlを作成しました。

heroku.yml
build:
  docker:
    web: Dockerfile

その後

$ heroku stack:set container
$ git push heroku master

を実行し、再度heroku ps:scale web=1を実行してみると、

$ heroku ps:scale web=1
Scaling dynos... done, now running web at 1:Free

となり成功しました。

改めてURLにアクセスしてみると、Herokuに関するエラーは消え、Railsにおいて、DBが見つからないエラーが発生していたので、公式サイト
を参考に

$ heroku run rake db:migrate

としてマイグレーションを実行したところ、とうとうお馴染みの画面がお出迎えしてくれました。

スクリーンショット 2019-10-26 13.17.10.png

原因

公式ドキュメントによると、webサイトに対してdynosが割り当てられていないのが原因で、H14が発生してしまう模様で、それを割り当てるためのコマンドがheroku ps:scale web=1です。

ちなみにdynosとは、dynoの複数形で、公式のdyno解説記事によると、Herokuで使用されるコンテナのことをdynoと呼んでいるそうです。

ただ、heroku.ymlを作成するまで、heroku ps:scale web=1が成功しなかった理由は今の所わかっていません。ご存知の方がいれば、ご教示いただきますと幸いです。

また、heroku.ymlを作成せず、Buildlogに記載されていた、heroku stack:set heroku-18を実行したらどうなっていたかというのも気になりますので、同じ症状に出会い試された方がいれば、ご教示いただけますと幸いです。

まとめ

Herokuデプロイ後にcode=H14 desc="No web processes running"が発生したら、
heroku ps:scale web=1を試してみてください。
このコマンドが成功しない場合は、heroku.ymlを作成した後で再度試してみてください。

この記事が少しでも誰かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました。

参考文献

公式ドキュメント

Heroku Error Codes
https://devcenter.heroku.com/articles/error-codes#h14-no-web-dynos-running

Building Docker Images with heroku.yml
https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#getting-started

Getting Started on Heroku with Rails 5.x
https://devcenter.heroku.com/articles/getting-started-with-rails5#migrate-your-database

dyno:Heroku プラットフォームの中核
https://jp.heroku.com/dynos

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tensorflow Dockerでjupyter notebookを使う

docker runで起動

公式サイトにあるように、jupyterのタグが付いたイメージをpullして、下記のようにrunすれば、jupyter notebookが起動する。

$ docker run -it -p 8888:8888 tensorflow/tensorflow:nightly-py3-jupyter

ホストのウェブブラウザでhttp://127.0.0.1:8888/?token=...を開く。
/tfがルートとして開く。

コンテナ内で bash シェル セッションを開始後に起動

コンテナ内で bash シェル セッションを開始する。

$ docker run -it -p 8888:8888 tensorflow/tensorflow:nightly-py3-jupyter bash

コンテナ内で下記のコマンドを実行して、jupyter notebookが起動する。

# jupyter notebook --port 8888 --ip=0.0.0.0 --allow-root

ホストのウェブブラウザでhttp://127.0.0.1:8888/?token=...を開く。
コンテナ内の任意のディレクトリをルートとして、jupyter notebookが起動することができる。

(上の例では、jupyter付きのイメージを使っているが、jupyterなしのイメージを起動後にpipで入れた場合でも可能。)

参考

https://qiita.com/tand826/items/0c478bf63ead75427782
https://qiita.com/ciela/items/0e0392f600c92b93d7c6

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tensorflow(GPU) Docker環境構築

Tensorflow(GPU)のDocker環境構築に関する備忘録。

環境

Ubuntu 18.04
CPU: Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
GPU: GeForce GTX 1080

Tensorflow Dockerの要件を確認

https://www.tensorflow.org/install/docker?hl=ja#tensorflow_docker_requirements

Docker 19.03のインストール

https://docs.docker.com/install/linux/docker-ce/debian/

NVIDIA Container Toolkitのインストール

https://github.com/NVIDIA/nvidia-docker#ubuntu-16041804-debian-jessiestretchbuster

Note that with the release of Docker 19.03, usage of nvidia-docker2 packages are deprecated since NVIDIA GPUs are now natively supported as devices in the Docker runtime.

(Docker 19.03のリリースでは、NVIDIA GPUがDockerランタイムのデバイスとしてネイティブにサポートされるようになったため、nvidia-docker2パッケージの使用は推奨されないことに注意してください。)

nvidia-docker2をインストールせずに、「Quickstart」の説明に従って、nvidia-container-toolkitをインストールする。

NVIDIA Container Runtimeのインストール

InstallationDocker Engine setupを実施。

補足

NVIDIA Container Runtimeをインストールしないと、

$ sudo docker run --runtime=nvidia --rm nvidia/cuda nvidia-smi

のようにruntimeを指定して実行すると、

docker: Error response from daemon: Unknown runtime specified nvidia.
See 'docker run --help'.

とエラーが表示される。

Why do I get the error Unknown runtime specified nvidia?

TensorFlow の Docker イメージをダウンロード

最新リリース版Tensorflow(GPU)、python3、jupyter付きのイメージをpullする例。
(py3を指定しないと、python2となるので注意。)

$ docker pull tensorflow/tensorflow:latest-gpu-py3-jupyter

Tensorflow(GPU) Dockerの動作確認

tensorflow < 2.0のイメージの場合。
(公式ページには、こちらが載っている。2019.11.25)

docker run --runtime=nvidia -it --rm tensorflow/tensorflow:latest-gpu-py3-jupyter \
       python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))"

tensorflow2.0<=のイメージの場合。

docker run --runtime=nvidia -it --rm tensorflow/tensorflow:latest-gpu-py3-jupyter \
       python -c "import tensorflow as tf; print(tf.reduce_sum(tf.random.normal([1000, 1000])))"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む