株式会社クリアコード > ククログ

ククログ


GitHub Actionsを使ったGroongaのパッケージのインストール、テストの自動化

Groongaでは、これまでもGitHub Actionsを使ってパッケージの作成を自動化したり、テストの自動化を実施してきました。
いままで自動化してきたテストは、リポジトリーにpushされたソースコードに対してビルド、テストするものでした。
これらの自動化により、リリース前に初めて問題が発覚することが少なくなり、問題が発生した段階で対処を進めることができています。

ただ、リポジトリーにpushされたソースコードに対するテストだと、各OS向けに作成したパッケージがちゃんとインストールできるか、
パッケージからインストールした環境で動作するかは確認できていませんでした。
そのため、パッケージの作成に失敗していた場合には、リリース後、パッケージからGroongaをインストールする段階にならないと問題に気がつけない状態でした。

リリース後にパッケージに問題があるとわかった場合は、再リリースすることになり、余計な時間がかかってしまいます。
そこで、リポジトリーにソースコードがpushされた段階でパッケージのインストールとパッケージからインストールしたGroongaのテストを実行するようにしました。

この記事は、作成したパッケージをインストール、テストする方法を説明したものです。
Groongaに固有の部分もありますが、各OS向けにパッケージを提供しているプロジェクトにとって参考になる情報もあると思います。

パッケージのテスト環境の構築

パッケージをテストするためには、当然テスト対象のパッケージを作成する必要がありますが、Groongaでは、既に自動化されています。
パッケージは既にできているので、この記事では、作成されたパッケージを取得するところから説明します。

前述の通りGroongaでは、リポジトリーにソースコードがpushされるたびにパッケージの作成が実行されるので、パッケージ作成後にテストを実行します。

パッケージの作成とパッケージのテストのジョブを分けても良いのですが、そのようにすると、パッケージを作成するジョブでartifactsにパッケージを保存し、
パッケージをテストするジョブでは、artifactsから必要なパッケージをダウンロードする操作が必要になり煩雑です。
(GitHub Actionsでは、パッケージ等のワークフローの成果物をartifactsとして保持できます。)

そのため、Groongaではパッケージの作成とパッケージのテストは同一のジョブで実行しています。

具体的には、以下のようにしています。

  1. Docker上にホストのディレクトリをマウント
  2. テスト用のイメージ、テストスクリプトを指定
# Test
- name: Test
  run: |
    docker run \
      --rm \
      --tty \
      --volume ${PWD}:/groonga:ro \
      ${{ matrix.test-docker-image }} \
      /groonga/${{ matrix.test-script }}

上記設定は、 https://github.com/groonga/groonga/blob/v10.0.2/.github/workflows/package.yml#L158 に記載されています。

1. Docker上にホストのディレクトリをマウント

docker run--volumeオプションを使って、ホストのディレクトリをDocker上にマウントできます。
上記では、--volume ${PWD}:/groonga:roと指定されているので、現在居るディレクトリをDcoker上の/groongaにマウントしています。

2. テスト用のイメージ、テストスクリプトを指定

${{ matrix.test-docker-image }}で実行するイメージを指定し、/groonga/${{ matrix.test-script }}で実行するテストスクリプトを指定しています。

GitHub Actions上で作成しているパッケージは、CentOS向けとDebian向けのものなので、この2つのOSのイメージを使用します。
複数のバージョンがあるので、matrixを使用し、バージョンごとにイメージを変更してテストしています。

${{ matrix.test-docker-image }}は以下のように定義されているので、Debian stretch、Debian buster、CentOS6、CentOS7、CentOS8のイメージを使用するようになっています。
テストに使用するスクリプトもOSによって異なるので、${{ matrix.test-script }}としてそれぞれのパスを指定しています。

        include:
          - label: Debian GNU/Linux stretch amd64
            id: debian-stretch-amd64
            test-docker-image: debian:stretch
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux stretch i386
            id: debian-stretch-i386
            test-docker-image: i386/debian:stretch
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux buster amd64
            id: debian-buster-amd64
            test-docker-image: debian:buster
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux buster i386
            id: debian-buster-i386
            test-docker-image: i386/debian:buster
            test-script: packages/apt/test.sh
          - label: CentOS 6
            id: centos-6
            test-docker-image: centos:6
            test-script: packages/yum/test.sh
          - label: CentOS 7
            id: centos-7
            test-docker-image: centos:7
            test-script: packages/yum/test.sh
          - label: CentOS 8
            id: centos-8
            test-docker-image: centos:8
            test-script: packages/yum/test.sh

また、テストに使用するイメージは既存のイメージを再利用せず、新規に作るのが良いです。
新規の環境でテストしないと依存ライブラリーが足りない等の問題に気がつけない可能性があるためです。

ここまでで、それぞれのDockerイメージ上でテストを実行する準備が整いました。
次は、パッケージのインストールとテストを実施します。
パッケージのインストールとテストは、テストスクリプト内で実施しています。

パッケージのインストールとテストの実施

Debian向け、CentOS向けのパッケージのテストスクリプトは、以下の場所にあります。
ここからは、これらのスクリプトの内容を説明し、どのような流れでパッケージのインストールとテストを行っているかを説明します。

Debian向け:
https://github.com/groonga/groonga/blob/v10.0.2/packages/apt/test.sh

CentOS向け:
https://github.com/groonga/groonga/blob/v10.0.2/packages/yum/test.sh

基本的な流れはどちらのスクリプトも同じで、それぞれのOSのパッケージ管理システム(Debianならapt、CentOS6,7ならyum、CentOS8ならdnf)を使って
ホストからマウントしたディレクトリにある、パッケージをインストールし、その後grntest(Groonga用のテストツール)を使ってGroongaのテストを実行しています。

ただ、CentOS6,7はRubyのバージョンが古く、grntestを使ったテストができないので、パッケージのインストールのみを確認しています。
また、CentOS8は、CentOS8向けのMessagePackのパッケージが無いため、現状ではテストを実行せずにスクリプトを終了しています。
(MessagePackがないとすべてのテストを実行できないためです。)

Debian向けパッケージのインストール、テストの流れ

まず、テストスクリプトの以下の箇所でOSのコードネームとアーキテクチャを取得します。
これらは、インストールするパッケージのパスに使用します。

code_nameには、stretchbuster等のDebianの各バージョンのコードネームが入ります。
architectureには、amd64i386等のCPUアーキテクチャが入ります。

code_name=$(lsb_release --codename --short)
architecture=$(dpkg --print-architecture)

次にインストールするパッケージのパスを指定してaptコマンドでパッケージをインストールします。
aptコマンドは、APTリポジトリを用意しなくても、以下のようにローカルに保存されているパッケージを直接指定してインストールできます。

repositories_dir=/groonga/packages/apt/repositories
apt install -V -y \
  ${repositories_dir}/debian/pool/${code_name}/main/*/*/*_{${architecture},all}.deb

インストール後、インストールが成功しているかを、groongaコマンドを使って確認します。

groonga --version

次にテスト用のディレクトリを作成し、テストスクリプトを移動します。
この段階で、i386の環境では動作しないテストを削除します。

mkdir -p /test
cd /test
cp -a /groonga/test/command ./
if [ "${architecture}" = "i386" ]; then
  rm command/suite/ruby/eval/convert/string_to_time/over_int32.test
  # TODO: debug this
  rm command/suite/select/filter/geo_in_circle/no_index/north_east.test
fi

最後にgemgrntestをインストールし、テストを実行しています。

apt install -V -y \
  gcc \
  make \
  ruby-dev
gem install grntest

export TZ=Asia/Tokyo

grntest_options=()
grntest_options+=(--base-directory=command)
grntest_options+=(--n-retries=3)
grntest_options+=(--n-workers=$(nproc))
grntest_options+=(--reporter=mark)
grntest_options+=(command/suite)
grntest "${grntest_options[@]}"
grntest "${grntest_options[@]}" --interface http
grntest "${grntest_options[@]}" --interface http --testee groonga-httpd
CentOS向けパッケージのインストール、テストの流れ

まず、テストスクリプトの以下の箇所でOSのバージョンを取得します。
これは、パッケージのパスを指定するのとパッケージ管理システムのコマンドを指定するのに使います。
(CentOSのパッケージ管理は、CentOS6,7では、yumコマンドで実行しますが、CentOS8では、dnfコマンドで実行するためです。)

version=$(cut -d: -f5 /etc/system-release-cpe)

以下の箇所でバージョン毎にパッケージ管理システムのコマンドを指定します。

case ${version} in
  6|7)
    DNF=yum
    ;;
  *)
    DNF="dnf --enablerepo=PowerTools"
    ;;
esac

以下の箇所でGroongaの公開鍵のインポートを行い、RPMパッケージをインストールします。
yumdnfコマンドもaptコマンドと同様、YUMリポジトリを用意しなくても、以下のようにローカルに保存されているパッケージを直接指定してインストールできます。

${DNF} install -y \
  https://packages.groonga.org/centos/groonga-release-latest.noarch.rpm

repositories_dir=/groonga/packages/yum/repositories
${DNF} install -y \
  ${repositories_dir}/centos/${version}/x86_64/Packages/*.rpm

インストールが成功しているかを、groongaコマンドを使って確認します。

groonga --version

CentOS6,7はRubyのバージョンが古くgrntestが動作しないためここでスクリプトを終了します。

case ${version} in
  6|7)
    exit 0
    ;;
  *)
    ;;
esac

CentOS8は、以下の箇所でgrntestをインストール後テストを実行します。
ただ、前述の通り現在は、CentOS8向けのMessagePackのパッケージが無いため、テスト実行前にexit 0でスクリプトを終了しています。

# TODO: Require msgpack for testing normalizer options
exit 0

${DNF} install -y \
  gcc \
  make \
  redhat-rpm-config \
  ruby-devel
gem install grntest

export TZ=Asia/Tokyo

grntest_options=()
grntest_options+=(--base-directory=/groonga/test/command)
grntest_options+=(--n-retries=3)
grntest_options+=(--n-workers=$(nproc))
grntest_options+=(--reporter=mark)
grntest_options+=(/groonga/test/command/suite)

grntest "${grntest_options[@]}"
grntest "${grntest_options[@]}" --interface http
grntest "${grntest_options[@]}" --interface http --testee groonga-httpd

以上のようにして、Groongaではリポジトリへのpushをトリガーとして、パッケージの作成、インストール、テストまでを自動で実行しています。
こうすることで、作成したパッケージがインストールできないといった問題を未然に防げるようになり、より安定したものをリリースできます。

各OSに向けのパッケージを配布しているプロジェクトは、上記のようなやり方を参考にして、パッケージのテストも自動化してみてはいかがでしょうか?

タグ: Groonga
2020-05-05

ノータブルコード6 - ポインターのサイズで32bit環境を見分ける

Groongaという全文検索エンジンの開発に参加している堀本です。
今回は、開発中にGroongaのソースコードから「おぉー」と思ったコードを見つけたので紹介します。

Groongaは32bit用パッケージと64bit用パッケージを配布していますが、32bit環境のときだけコンパイルオプション等を設定したいケースがあります。

例えば、32bitのWindows向けパッケージはCMakeを使用してビルドしていますが、CMakeには32bitのWindowsかどうかを判定できる定義済みの変数はありません。
WIN32という定義済みの変数がありますが、これはコンパイルターゲットが(64bitを含む)Windowsの場合にTrueになるので、32bitかどうかの判定には使えません。

そこでGroongaでは、以下のようにポインターのサイズを使って32bitかどうかを判定しています。
(CMAKE_SIZEOF_VOID_Pvoidポインタのサイズを計算してくれるCMakeの定義済みの変数です。)

  if(CMAKE_SIZEOF_VOID_P EQUAL 4)
    # 32bit
    list(APPEND MRUBY_DEFINITIONS "MRB_METHOD_T_STRUCT")
  endif()

データ型のサイズは、データ型モデルという定義にしたがって決まります。データ型モデルとはデータ型の大きさを定義したもので、OSによって異なりますが、概ね以下のどれかになります。

32bit用データ型モデル
データモデル名 short int long long long pointer
ILP32 16 32 32 64 32
LP32 16 16 32 64 32

64bit用データ型モデル
データモデル名 short int long long long pointer
LLP64 16 32 32 64 64
LP64 16 32 64 64 64
IP64 16 64 32 64 64
ILP64 16 64 64 64 64
SILP64 64 64 64 64 64

上記を見てわかるとおり、どのモデルであっても32bitの場合、ポインターのサイズは、32bit(4byte)になっています。
また、同様に64bitの場合も、ポインターのサイズはどのモデルも64bit(8byte)になっています。

現状出回っているほとんどの環境では、上記のように32bitと64bitでポインターのサイズは決まっているので、これを利用して32bit環境かどうかを判定できるのです。

様々なOSの32bit版のソフトウェアをメンテナンスしている方は、上記のような判定方法も利用してみてはいかがでしょうか?

2020-05-14

ノータブルコード7 - Rustのif式を賢く使う

組み込みGeckoプロジェクトでRustに本格的に触れ始めた畑ケです。
今回は、組み込みGeckoプロジェクトでフィードバックしたgit2-rsのコミットから「これは!」、と思ったコードを見つけたので紹介します。

まず、git2-rsというRustのcrate*1 は、libgit2というライブラリのRustバインディングです。git2-rsはRustのパッケージマネージャーのcargoの依存crateの一つで、cargoはRustのパッケージ情報を取得するときにgitの操作が必要になる場面があります。git2-rsというcrateはcargoに必要なgitの操作を担当します。

背景

git2-rsのCライブラリのバインディングを担当しているlibgit2-sysというcrateの中に、libgit2のエラーメッセージを取得する操作が書かれていなかったため、
cargoの中でgitの操作が失敗したときにはエラーコードしか報告されないという問題がありました。

そこで、次節のパッチを提出しました。

ノータブルコードとなる前のパッチ

diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs
index eba2077..9d1afba 100644
--- a/libgit2-sys/lib.rs
+++ b/libgit2-sys/lib.rs
@@ -7,6 +7,8 @@ extern crate libz_sys as libz;
 use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t};
 #[cfg(feature = "ssh")]
 use libssh2_sys as libssh2;
+use std::ffi::CStr;
+use std::ptr;
 
 pub const GIT_OID_RAWSZ: usize = 20;
 pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2;
@@ -3551,7 +3553,25 @@ pub fn init() {
         openssl_init();
         ssh_init();
         let r = git_libgit2_init();
-        assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        if r < 0 {
+            let git_error = git_error_last();
+            let mut error_msg: *mut c_char = ptr::null_mut();
+            if !git_error.is_null() {
+                error_msg = (*git_error).message;
+            }
+            if !error_msg.is_null() {
+                assert!(
+                    r >= 0,
+                    "couldn't initialize the libgit2 library: {}, error: {}",
+                    r,
+                    CStr::from_ptr(error_msg).to_string_lossy()
+                );
+            } else {
+                assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+            }
+        } else {
+            assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        }
 
         // Note that we intentionally never schedule `git_libgit2_shutdown` to
         // get called. There's not really a great time to call that and #276 has

特に以下の部分がこのパッチの要点です。

            let git_error = git_error_last();
            let mut error_msg: *mut c_char = ptr::null_mut();
            if !git_error.is_null() {
                error_msg = (*git_error).message;
            }

このパッチでは、一度error_msg変数を可変なものとして宣言し、条件分岐の中で値を変更するという書き方になっています。これはC言語のライブラリなどでよく見かける書き方です。

リファイン後のパッチ

前述のパッチは無事取り込まれましたが、当該箇所を改めてプロジェクトオーナー側でリファインされました。ミュータブルな変数を使うよりも変数束縛を使う方がRustらしいコードです。
その考えに則って修正されたパッチが以下の通りです。

diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs
index 9d1afba840..a5998af84c 100644
--- a/libgit2-sys/lib.rs
+++ b/libgit2-sys/lib.rs
@@ -8,7 +8,6 @@ use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t};
 #[cfg(feature = "ssh")]
 use libssh2_sys as libssh2;
 use std::ffi::CStr;
-use std::ptr;
 
 pub const GIT_OID_RAWSZ: usize = 20;
 pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2;
@@ -3552,30 +3551,25 @@ pub fn init() {
     INIT.call_once(|| unsafe {
         openssl_init();
         ssh_init();
-        let r = git_libgit2_init();
-        if r < 0 {
-            let git_error = git_error_last();
-            let mut error_msg: *mut c_char = ptr::null_mut();
-            if !git_error.is_null() {
-                error_msg = (*git_error).message;
-            }
-            if !error_msg.is_null() {
-                assert!(
-                    r >= 0,
-                    "couldn't initialize the libgit2 library: {}, error: {}",
-                    r,
-                    CStr::from_ptr(error_msg).to_string_lossy()
-                );
-            } else {
-                assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
-            }
-        } else {
-            assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        let rc = git_libgit2_init();
+        if rc >= 0 {
+            // Note that we intentionally never schedule `git_libgit2_shutdown`
+            // to get called. There's not really a great time to call that and
+            // #276 has some more info about how automatically doing it can
+            // cause problems.
+            return;
         }
 
-        // Note that we intentionally never schedule `git_libgit2_shutdown` to
-        // get called. There's not really a great time to call that and #276 has
-        // some more info about how automatically doing it can cause problems.
+        let git_error = git_error_last();
+        let error = if !git_error.is_null() {
+            CStr::from_ptr((*git_error).message).to_string_lossy()
+        } else {
+            "unknown error".into()
+        };
+        panic!(
+            "couldn't initialize the libgit2 library: {}, error: {}",
+            rc, error
+        );
     });
 }
 

このパッチのコードの要点を以下に抜粋します。
ミュータブルな変数のerror_msgを削除し、代わりに以下のRustのコードでは、if式の性質を使いエラーメッセージの値へerrorというラベルを付けるコードになっています。

        let git_error = git_error_last();
        let error = if !git_error.is_null() {
            CStr::from_ptr((*git_error).message).to_string_lossy()
        } else {
            "unknown error".into()
        };

このように、Rustではifは文ではなく式なので、値を返すことができます。そのため、ifで分岐した時の結果の値に上記のようにラベルづけをすることができます。
変数束縛を意識的に使い、値にラベル付けをしていくのがRustらしいコードとなるため紹介しました。

*1 Rustでは、ライブラリの事をcrateと呼びます。

2020-05-15

設定変更時にのみ起こる問題の、原因となっている設定項目を特定する

FirefoxやThunderbirdは、MCD(別名:AutoConfig)ポリシー設定を使ってある程度の設定を集中管理できます。集中管理可能な設定項目の情報は当社のサポート業務で把握している頻出設定の一覧Mozilla公式のポリシーテンプレートの説明などで調べることができ、実際の運用では、これらの資料で得た情報に基づいて書き上げた設定ファイルをFirefox(Thunderbird)に読み込ませることになります。

ただ、それですんなりいけばよいのですが、時には単純な記述ミスや、記載した設定の未知の特性の影響などによって、想定外の問題が起こる場合も度々あります。そのような場面で、多数ある設定の中から問題の原因箇所を特定するのはなかなか大変です。項目が多い場合、しらみつぶしに調査していては時間がいくらあっても足りません。

このような場面では、後退バグの発生原因となったコミットを特定する際にも使用した二分探索が有用です。

設定ファイルを対象とした二分探索の基本的な手順は、以下のようになります。

  1. 設定ファイルを前半と後半に分け、どちらか一方、例えば前半を削除する。
  2. その設定ファイルをFirefox(Thundebrird)に読み込ませ、動作を確認する。
    • 問題が再現しないようであれば、削除した部分(前半)のどこかに問題があると見なす。
      削除した部分(前半)を元に戻し、削除する範囲と残す範囲を入れ換える。
    • 問題が再現するようであれば、現在残っている部分(後半)のどこかに問題があると見なす。
  3. 残った部分を対象として、同じ操作を繰り返す。

二分探索では、しらみつぶしに調べるよりもはるかに少ない試行回数で原因箇所を特定できます。しかしながら、やり方を誤ると、問題が無い箇所を問題の原因と誤認してしまう事もあり得ます。この記事では、Firefoxの集中管理用設定ファイルを対象に二分探索を行う際のよくある注意点を紹介します。

ファイルの形式を問わない注意点

Virtual Storeの影響の排除

MCDの場合もポリシー設定の場合も、Windowsにおいて集中管理用の設定ファイルを直接編集する場合には、Virtual Storeの影響を受けないように気をつける必要があります。

現在のWindowsでは C:\Program Files 配下などの位置にファイルを保存しようとすると管理者権限での認証を求められますが、古いWindows向けアプリケーションにはそのような認証処理に非対応の物があります。そこでWindowsは、認証を行えなかった場合には暫定的に C:\Users\username 配下にファイルを保存しておき、以後 C:\Program Files 配下のファイルにそのユーザーの権限でアクセスしようとした場合は、代わりに C:\Users\username 配下に保存した方のファイルの内容を返却する、という動作を行います。この機能が「Virtual Store」です。Firefox・Thunderbirdの集中管理用設定ファイルは、一般ユーザーが勝手に内容を変更してしまうことがないよう、変更に管理者権限が必要な位置に置かれますので、不用意に編集しようとするとVirtual Storeの影響を受けてしまうことがあります。

Virtual Storeの影響を受けないようにするためには、「EmEditor」のように管理者権限での認証に対応したテキストエディターを使うか、もしくは、以下の手順で操作する必要があります。

  1. 設定ファイルを一旦デスクトップにコピーする。
  2. デスクトップにコピーしたファイルを編集する。
  3. ファイルを上書き保存する。
  4. 編集後のファイルをC:\Program Files 配下の元の位置に上書きコピーする。
ファイルのエンコーディングの維持

MCD用設定ファイルもpolicies.jsonも、エンコーディング形式は「BOM無しのUTF-8(リトルエンディアン)」が正式です。しかしながら、Windowsのバージョンが古いと、「メモ帳」を使ってファイルを編集した場合にBOMが付与されてしまったり、意図せずエンコーディングが変更されてしまったり、といったトラブルが起こり得ます。この事が原因で、編集後のファイル全体がエラーとなり、まったく動作しない状態となってしまう場合もあります。

このようなトラブルを避けるためにも、集中管理用設定ファイルの編集には、先にも名前を挙げた「EmEditor」のように「BOMの有無やテキスト保存時のエンコーディングを自由に指定でき、また、ファイルの上書き保存時には元のエンコーディングを可能な限り維持する」性質を持つテキストエディターを使用する事を強くお勧めします。

MCD(AutoConfig)の設定ファイルで二分探索を実施する場合の注意

MCD用設定ファイルはJavaScript形式なので、単純な記述ミスであればeslintで文法エラーを検出できます。

文法エラーでない原因で起こる問題を二分探索で探る際にも、その過程での文法エラーの発生には気をつける必要があります。例えば、以下のように条件分岐を伴う設定が記述されている場合:

if (getenv('USERDOMAIN' == '...') {
  lockPref('...', true);
  lockPref('...', false);
  lockPref('...', 0);
}

このような箇所が二分探索の境界上にあると、

if (getenv('USERDOMAIN' == '...') {
  lockPref('...', true);

// ここでファイルが終了しており、開かれた括弧が
// 閉じられないままになっている

// ここでファイルが始まっており、開き括弧が無いまま
// 閉じ括弧だけがある状態になっている

  lockPref('...', 0);
}

のように文法的に不正な状態が発生してしまうことがあります。二分探索の境界は、このような箇所を避けて決めるように気をつけましょう。

ポリシー設定の設定ファイル(policies.json)で二分探索を実施する場合の注意

ポリシー設定をGPOではなくpolicies.jsonで行う場合、policies.jsonは(JavaScriptではなく)JSON形式として妥当である必要があります。

例えば、以下のような内容のpolicies.jsonがあった場合:

{
  "policies": {
    "DisableAppUpdate": true,
    "ExtensionUpdate": false,
    "EnableTrackingProtection": {
      "Value": true,
      "Locked": true
    }
  }
}

以下のように単純に行で区切って二分探索すると、閉じ括弧が欠けているために文法エラーとなり、内容が読み込まれません。

{
  "policies": {
    "DisableAppUpdate": true,
    "ExtensionUpdate": false,
    "EnableTrackingProtection": {
      "Value": true,
{
  "policies": {
    "DisableAppUpdate": true,
    "ExtensionUpdate": false,

また、見落としやすい注意点として、いわゆる「ケツカンマ」の存在があります。設定項目にあたる部分だけを削除して

{
  "policies": {
    "DisableAppUpdate": true,
    "ExtensionUpdate": false,
  }
}

のように最後の項目の末尾に「,」が残ったままになっていると、これはJavaScriptの文法上はエラーにならないのですが、JSON形式としては文法エラーになります。このような箇所は必ず、

{
  "policies": {
    "DisableAppUpdate": true,
    "ExtensionUpdate": false
  }
}

のように、最後の項目の後に残った「,」を取り除いておく必要があります。

編集後のファイルがJSONとして妥当かどうかは、JSONLintで確認できます。policies.jsonを組織外に晒すことを避けたい場合は、npmパッケージのjsonlint-cliのようにオフラインで実行できる文法チェックツールを使うと、同等の検証を行えます。

まとめ

Firefox・Thundebirdの集中管理に使うMCD用設定ファイルとポリシー設定ファイルについて、問題発生時の原因を二分探索で調査する手順と、その注意点をご紹介しました。

この記事でご紹介した調査方法は多くの場合で有効ですが、残念ながら万能ではありません。設定項目の中には複数が組み合わさった場合にのみ問題が起こる物もあり、二分探索を実施しても「前半と後半それぞれだけで読み込んだら問題無いのに、全体を合わせると問題が起こる」というような不可解な状況が発生する場合があります。そのようなケースでは、ログと突き合わせての調査なども併用する必要があります。どうしても問題の原因を掴めないという場合には、当社の法人向け有償サポートがお役に立てるかもしれませんので、ぜひ一度お問い合わせください

タグ: Mozilla
2020-05-16

Groongaである時間範囲のデータを集計する方法

月単位や日単位などある時間範囲で検索したい場面は多いです。
こんな時は月単位、日単位で日付情報を格納するカラムを作り、それを用いて検索したくなります。

しかし、検索要件が変わり、新たに年単位や週単位で検索する必要が出た場合、それらの情報を格納するカラムを増やさなければなりません。
日付情報は一つのカラムに格納し、その情報を年単位や週単位に丸めて検索できれば、要件の変更に追従してカラムを増やす必要がなくなります。

Groongaでは、time_classify関数を使って日付情報をある期間で丸めることができます。
time_classify関数には以下の関数が存在し、それぞれ、年、月、週、日、曜日、時間、分、秒単位で時間を丸めることができます。

  • time_classify_year
  • time_classify_month
  • time_classify_week
  • time_classify_day
  • time_classify_day_of_week
  • time_classify_hour
  • time_classify_minute
  • time_classify_second

例えば、time_classify_yearなら同じ年の時間を丸めることができます。
つまり、2020年1月13日と2020年5月21日が、2020年1月1日 00:00:00に丸められます。

これらの関数の値は以下のようにドリルダウンのキーとして使用できるので、ある範囲の時間のデータを集計できます。
例えば以下の例では、売上台帳から、ある製品が月単位でどのくらい売れたかを集計しています。

plugin_register functions/time

table_create Sales TABLE_NO_KEY
column_create Sales name COLUMN_SCALAR ShortText
column_create Sales price COLUMN_SCALAR UInt32
column_create Sales timestamp COLUMN_SCALAR Time

load --table Sales
[
{"name": "Apple" , "price": "256", "timestamp": "2020-01-30 11:50:11.000000"},
{"name": "Apple" , "price": "256", "timestamp": "2020-05-01 10:20:00.000000"},
{"name": "Orange", "price": "122", "timestamp": "2020-05-02 11:44:12.000001"},
{"name": "Apple" , "price": "256", "timestamp": "2020-01-07 19:50:23.000020"},
{"name": "banana", "price": "88" , "timestamp": "2020-05-08 11:00:02.000000"},
{"name": "banana", "price": "88" , "timestamp": "2020-05-08 21:34:12.000001"}
]

select Sales \
  --limit -1 \
  --columns[month].stage initial \
  --columns[month].type Time \
  --columns[month].value "time_classify_month(timestamp)" \
  --drilldowns[sales_per_month_name].keys "month, name" \
  --drilldowns[sales_per_month_name].output_columns "_value.name, _nsubrecs, time_format_iso8601(_value.month)" \
  --drilldowns[sales_per_month_name].limit -1
[
  [
    0,
    1590035787.669446,
    0.003034353256225586
  ],
  [
    [
      [
        6
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "month",
          "Time"
        ],
        [
          "name",
          "ShortText"
        ],
        [
          "price",
          "UInt32"
        ],
        [
          "timestamp",
          "Time"
        ]
      ],
      [
        1,
        1577804400.0,
        "Apple",
        256,
        1580352611.0
      ],
      [
        2,
        1588258800.0,
        "Apple",
        256,
        1588296000.0
      ],
      [
        3,
        1588258800.0,
        "Orange",
        122,
        1588387452.000001
      ],
      [
        4,
        1577804400.0,
        "Apple",
        256,
        1578394223.00002
      ],
      [
        5,
        1588258800.0,
        "banana",
        88,
        1588903202.0
      ],
      [
        6,
        1588258800.0,
        "banana",
        88,
        1588941252.000001
      ]
    ],
    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ]
    }
  ]
]

上記の例では、--columns[month]を使って動的にカラムを生成しています。
Groongaには動的カラムというクエリー実行時に一時的に作成できるカラムがあります。
この動的カラムを使って、time_classify_month(timestamp)の結果をmonthカラムに格納しています。

次に--drilldowns[sales_per_month_name]を使って、同じ期間に売れた製品をグループ化します。

ポイントは、--drilldowns[sales_per_month_name].keys "month, name" のところです。
ここでは、どのカラムの値を使ってグループ化するかを指定しています。

monthを指定することで、同じ月のレコードをグループ化しています。また、nameを指定することで、製品名でもグループ化しています。

このように、monthカラムの値と、nameカラムの値をグループ化すると以下の結果になります。

    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ]
    }

_nsubrecsの値がグループ化したレコードの数を表します。
したがって、例えば["Apple",2,"2020-01-01T00:00:00.000000+09:00"]という結果なら、nameの値がApplemonthカラムの値が
2020-01-01T00:00:00.000000+09:00というレコードが2件あると解釈します。
(Timeの値は通常だとUNIX時間で表示されますが、わかりにくいのでこの例では、人間に読みやすい形式に変換して出力しています。)
つまり、2020年1月にりんごは2個売れたと解釈できます。

もう少し高度な使い方として、以下のようにドリルダウンを使ってある期間のデータを計算して加工できます。
以下の例では月毎の売上の合計値を出力しています。

select Sales \
  --limit -1 \
  --columns[month].stage initial \
  --columns[month].type Time \
  --columns[month].value "time_classify_month(timestamp)" \
  --drilldowns[sales_per_month_name].keys "month, name" \
  --drilldowns[sales_per_month_name].output_columns "_value.name, _nsubrecs, time_format_iso8601(_value.month)" \
  --drilldowns[sales_per_month_name].limit -1 \
  --drilldowns[sum_sales_per_month].keys month \
  --drilldowns[sum_sales_per_month].calc_target price \
  --drilldowns[sum_sales_per_month].calc_types SUM \
  --drilldowns[sum_sales_per_month].output_columns "time_format_iso8601(_key), _sum"
[
  [
    0,
    1590116735.057712,
    0.0004277229309082031
  ],
  [
    [
      [
        6
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "month",
          "Time"
        ],
        [
          "name",
          "ShortText"
        ],
        [
          "price",
          "UInt32"
        ],
        [
          "timestamp",
          "Time"
        ]
      ],
      [
        1,
        1577804400.0,
        "Apple",
        256,1
        580352611.0
      ],
      [
        2,
        1588258800.0,
        "Apple",
        256,
        1588296000.0
      ],
      [
        3,
        1588258800.0,
        "Orange",
        122,
        1588387452.000001
      ],
      [
        4,
        1577804400.0,
        "Apple",
        256,
        1578394223.00002
      ],
      [
        5,
        1588258800.0,
        "banana",
        88,
        1588903202.0
      ],
      [
        6,
        1588258800.0,
        "banana",
        88,
        1588941252.000001
      ]
    ],
    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ],
      "sum_sales_per_month":
      [
        [
          2
        ],
        [
          [
            "time_format_iso8601",
            null
          ],
          [
            "_sum",
            "Int64"
          ]
        ],
        [
          "2020-01-01T00:00:00.000000+09:00",
          512
        ],
        [
          "2020-05-01T00:00:00.000000+09:00",
          554
        ]
      ]
    }
  ]
]

ドリルダウンは、同一グループのカラムの値を計算できます。
具体的には、合計値と最大値、最小値、平均値を計算できます。

上記の例では、月ごとの売上を計算したいので、合計値を計算します。

どんな計算をするかは、--drilldowns[sum_sales_per_month].calc_typesで指定しています。
合計値を計算する場合はSUMを指定します。

最大値の場合はMAX、最小値の場合はMIN、平均値の場合はAVGを指定します。
計算した値は、それぞれ_sum_max_min_avgというカラムに格納されます。(これらのカラムは自動的に作成されるので、ユーザーが用意する必要はありません。)

計算対象のカラムは、--drilldowns[sum_sales_per_month].calc_targetで指定しています。
上記の例では売上を集計したいので、priceカラムを指定しています。

最後に--drilldowns[sum_sales_per_month].output_columnsで出力する情報を指定しています。
月毎の売上がわかれば良いので、月と売上の合計値が出力されれば良いことになります。

したがって、--drilldowns[sum_sales_per_month].output_columnsには、_key_sumを指定しています。
_keyはドリルダウンのキーを表すのでmonthカラムの値を出力します。
_sumはグループ内のpriceカラムの値の合計値を出力します。

結果は以下のようになり、この店舗では、2020年1月の売上は512円で2020年5月の売上は554円であることがわかります。

        [
          "2020-01-01T00:00:00.000000+09:00",
          512
        ],
        [
          "2020-05-01T00:00:00.000000+09:00",
          554
        ]

このようにして、月毎の売上の合計値を出力できました。

月単位や日単位などある時間範囲で集計したい場面に遭遇したら、time_classify関数の使用を検討してみてはいかがでしょうか?

タグ: Groonga
2020-05-22

Terraformの導入 - 検証環境をコマンドで立ち上げられるようにする その1

はじめに

クラウドサービスをコマンドラインで操作した方が楽に目的の環境作成ができると言うことに気づいた畑ケです。
Terraformと言うクラウド上のリソースを作成するツールがあります。TerraformでAWS, Azure, GCPなど各種クラウドのリソースの状態を定義し、作成できます。

AzureでWindowsのインスタンスを作成するには

AzureでWindowsのインスタンスを作成するには、以下のものを定義して作成する必要があります。

  • リソースグループ
  • バーチャルネットワーク
  • サブネット
  • ネットワークインターフェース
  • 仮想マシンの設定

また、Azureに立ち上げた仮想マシンに対して、プロビジョニングやssh, RDPで接続するためには追加で以下の設定も必要です。

  • セキュリティグループ
  • ネットワークインターフェースとセキュリティグループのアソシエーション(関連づけ)
  • パブリックIPアドレス

Azure上のWindows 10のインスタンスについてはボリュームライセンスを用意する必要があります。ライセンス認証については https://docs.microsoft.com/ja-jp/windows/deployment/windows-10-subscription-activation の記事をご覧ください。
この記事ではWindows Server 2019 Datacenterを用いて解説します。

実際に作成してみる

Terraformはディレクトリにある拡張子tfのファイルを読み込みます。また、必要なモジュールはproviderブロックで指定する必要があります。

Terraformを使用する準備
Terraformのインストール

Terraformを使用するには、まずTerraformをインストールする必要があります。
https://www.terraform.io/downloads.html から使用するプラットフォームに応じた実行ファイルをインストールします。

Azureコマンドラインフロントエンドをインストール

Terraformを使用するためには、Azureから認証情報を取得する必要があります。Azureをコマンドラインから操作するコマンドはazです。
https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli?view=azure-cli-latest を参考に使用するプラットフォーム応じた実行ファイルをインストールします。

Terraform用の認証情報の作成

Terraformを使用するには、Terraform用に認証情報を作成する必要があります。

先ほどインストールしたAzure CLIのazを使ってAzureにログインします。

$ az login

Azureへのログインが成功すると、az account listにより、以下の情報をとることができます。

$ az account list
[
  {
    "cloudName": "AzureCloud",
    "id": "00000000-0000-0000-0000-000000000000",
    "isDefault": true,
    "name": "AN USER Subscription",
    "state": "Enabled",
    "tenantId": "00000000-0000-0000-0000-000000000000",
    "user": {
      "name": "user@example.com",
      "type": "user"
    }
  }
]

ここで、idはサブスクリプションのIDです。これを、SUBSCRIPTION_ID環境変数に代入します。

$ export SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"

続いて、以下のコマンドを実行すると、appIdとpasswordと認証トークンを取得できます。

$ az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/${SUBSCRIPTION_ID}"
{
  "appId": "00000000-0000-0000-0000-000000000000",
  "displayName": "azure-cli-2020-05-18-04-58-43",
  "name": "http://azure-cli-2020-05-18-04-58-43",
  "password": "00000000-0000-0000-0000-000000000000,
  "tenant": "00000000-0000-0000-0000-000000000000
}

idの値をARM_SUBSCRIPTION_IDに設定し、appIDの値をARM_CLIUENT_IDに設定し、passwordの値をARM_CLIENT_SECRETに設定し、tenantの値をARM_TENANT_IDに設定します、

export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"

ここまでの準備で、Terraformが読み取る環境変数にAzureの認証情報を格納することに成功しました。

使用するプロバイダー

この記事ではAzureにWindowsの検証環境を作成する想定でmain.tfへTerraformのスクリプトを書き進めていきます。

そのため、azurerm プロバイダーを使用します。

provider "azurerm" {
  version = "~>2.10"
  features {}
}
リソースグループ

Azureでは、リソースを作成する前に、リソースがどのリソースグループに属するかを指定しないといけません。そのため、リソースグループを指定するリソースグループを作成します。

resource "azurerm_resource_group" "clearcode" {
  name     = "ExampleClearCodeResourceGroup"
  location = "Japan East"
}

AzureRMにおけるリソースグループに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/resource_group.html を参照してください。

バーチャルネットワーク

Azureでは、インスタンスが属するバーチャルネットワークを指定する必要があります。ローカルネットワークのIPアドレスには10.0.0.0/8のクラスAアドレスを使用することができます。*1
この例では、10.5.0.0/16(10.5.0.0 ~ 10.5.255.255)を切り出して使用することにします。

# Create a virtual network in the production-resources resource group
resource "azurerm_virtual_network" "clearcode" {
  name                = "clearcode-network"
  resource_group_name = azurerm_resource_group.clearcode.name
  location            = azurerm_resource_group.clearcode.location
  address_space       = ["10.5.0.0/16"]
}

AzureRMにおけるバーチャルネットワークに関して、詳しくは https://www.terraform.io/docs/providers/azurerm/r/virtual_network.html を参照してください。

サブネット

Azureでは、大元のバーチャルネットワークを定義したら、その次はインスタンスが直接属するサブネットを定義する必要があります。
internalサブネットを以下のように定義します。(10.5.2.0 ~ 10.5.2.255の範囲とします。)

resource "azurerm_subnet" "internal" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.clearcode.name
  virtual_network_name = azurerm_virtual_network.clearcode.name
  address_prefixes       = ["10.5.2.0/24"]
}

AzureRMのサブネットに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/subnet.html を参照してください。

ネットワークインターフェース

Azureのインスタンスの設定に行く前にさらに設定が要ります。インスタンスに紐付けるネットワークインターフェースの設定です。
このネットワークインターフェースの設定を行うことで、インスタンスにようやくネットワーク設定が紐付けされます。

resource "azurerm_network_interface" "testing" {
  name                = "clearcode-testing-instance-nic"
  location            = azurerm_resource_group.clearcode.location
  resource_group_name = azurerm_resource_group.clearcode.name

  ip_configuration {
    name                          = "testing-instance-nic"
    subnet_id                     = azurerm_subnet.internal.id
    private_ip_address_allocation = "Static"
    private_ip_address            = "10.5.2.4"
  }
}

AzureRMのネットワークインターフェースに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/network_interface.html を参照してください。

これで、インスタンスに紐付けるネットワークインターフェースの設定ができました。

Windowsインスタンス

ようやくここで、インスタンスに紐付ける最低限のリソースの作成ができました。

Windows Server 2019 Datacenterのインスタンスの設定を書き下してみます。

resource "azurerm_virtual_machine" "winservtesting" {
  name                             = "clearcode-testing-winserv-vm"
  location                         = azurerm_resource_group.clearcode.location
  resource_group_name              = azurerm_resource_group.clearcode.name
  network_interface_ids            = [azurerm_network_interface.testing.id]
  vm_size                          = "Standard_B2S"
  delete_os_disk_on_termination    = true
  delete_data_disks_on_termination = true

  storage_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }

  storage_os_disk {
    name              = "2019-datacenter-disk1"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
    os_type           = "Windows"
  }

  os_profile {
    computer_name  = "cc-winserv"
    admin_username = "clearcode"
    admin_password = "CC/changeme1"
  }

  os_profile_windows_config {
    enable_automatic_upgrades = true
    provision_vm_agent        = true
  }

  tags = {
    CreatedBy = "clearcode"
    Purpose   = "Describe Terraform instruction"
  }
}

AzureRMのバーチャルマシンについて詳しくは https://www.terraform.io/docs/providers/azurerm/r/virtual_machine.html を参照してください。

実際にデプロイしてみる

ここまでの設定が書けたら、実際にデプロイしてみましょう。今回はパブリックIPアドレスを指定していないので、インターネットからのアクセスはできません。

$ terraform init

以下のようなログが出力されれば、Terraformの初期化に成功しています。


Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "azurerm" (hashicorp/azurerm) 2.10.0...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

デプロイする前に、terraformの設定の齟齬がないかをplanサブコマンドで確認してみましょう。

$ terraform plan

# ...

Plan: 5 to add, 0 to change, 0 to destroy.

どうやらplanの段階ではエラーはないようです。

applyしてリソースを実際に作成してみましょう。

$ terraform apply -auto-approve

ここまでに作成したTerraformのスクリプトは、
https://gitlab.com/clear-code/terraform-example/-/tree/708283aaeeb8f9f389d4cdc45560d6817ce0ac83 に置いてあります。

まとめ

TerraformでAzureのインスタンスを立ち上げる最低限のところまでを解説しました。今回立ち上げたWindows Server 2019 DatacenterのインスタンスはパブリックIPアドレスを持たないため、Azure内部の同じサブネットワークのインスタンスからしか接続することができません。そのため、サンプルのTerraformスクリプトはセキュリティに関する記載がありません。
次回の記事では、セキュリティグループの作成や、公開リポジトリに置く際にtfvarsファイルを使って変数を外出しするテクニックを紹介します。

*1 RFC 1918 https://tools.ietf.org/html/rfc1918 のSection 3にて10/8プレフィックスのアドレスとして例示されている。

2020-05-25

Terraformの導入 - 検証環境をコマンドで立ち上げられるようにする その2

はじめに

クラウドサービスを扱うのにTerraformが便利ということに気づいたけれど、解説を書くとなると長くなってしまった…と気落ちしている畑ケです。

前回の記事では、TerraformでWindows系のAzureのインスタンスを立ち上げるのに最低限必要であった

  • リソースグループ
  • バーチャルネットワーク
  • サブネット
  • ネットワークインターフェース
  • 仮想マシンの設定

について解説しました。今回の記事では、

  • セキュリティグループ
  • ネットワークインターフェースとセキュリティグループのアソシエーション(関連づけ)
  • パブリックIPアドレス

の項目を解説します。ここまでの設定が流し込まれていると、RDPで接続が可能なWindows Server 2019 Datacenterのインスタンスが作成できます。

Azure上のWindows 10のインスタンスについてはボリュームライセンスを用意する必要があります。ライセンス認証については https://docs.microsoft.com/ja-jp/windows/deployment/windows-10-subscription-activation の記事をご覧ください。
この記事ではWindows Server 2019 Datacenterを用いて解説します。

変数の切り出し

Terraformには変数があり、これによりインターネットに公開できない情報を別ファイルに切り出し、Gitのリポジトリに入れないようにできます。

例えば、variable.tfに以下の変数の定義を書くことで、tfファイルで使用する変数を定義することができます。

variable "windows-username" {}
variable "windows-password" {}

これに対応するユーザー名とパスワードをterraform.tfvarsファイルで定義します。

windows-username = "clearcode"
windows-password = "CC/changeme1"

この変数を使うには、var.windows-username, var.windows-passwordとしてtfファイル中から参照できます。

main.tfファイルを以下のように書き換えると、terraform.tfvarsに定義した変数の値を参照します。

diff --git a/main.tf b/main.tf
index 0148da0..c0e23a4 100644
--- a/main.tf
+++ b/main.tf
@@ -63,8 +63,8 @@ resource "azurerm_virtual_machine" "winservtesting" {
 
   os_profile {
     computer_name  = "cc-winserv"
-    admin_username = "clearcode"
-    admin_password = "CC/changeme1"
+    admin_username = var.windows-username
+    admin_password = var.windows-password
   }
 
   os_profile_windows_config {
ネットワークセキュリティグループ

Azureにはネットワークに対して、セキュリティのルールを決める仕組みがあります。これがセキュリティグループです。

RDPの通信と、WinRMの双方向通信を許可するネットワークセキュリティグループを作成してみます。

resource "azurerm_network_security_group" "testing" {
  name                = "ClearCodeSecurityGroup"
  location            = "Japan East"
  resource_group_name = azurerm_resource_group.clearcode.name

  security_rule {
    name                       = "RDP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "WinRM"
    priority                   = 998
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "5986"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "WinRM-out"
    priority                   = 100
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "5985"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "Creating with Terraform"
  }
}

ネットワークセキュリティグループに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/network_security_group.html を参照してください。

ネットワークインターフェースとネットワークセキュリティグループのアソシエーション(関連づけ)

上節で作成したセキュリティグループはネットワークインターフェースに関連づけをしないといけません。そこで登場するのが、ネットワークインターフェースセキュリティーグループアソシエーションのリソースです。

# Connect the security group to the network interface
resource "azurerm_network_interface_security_group_association" "testing" {
  network_interface_id      = azurerm_network_interface.testing.id
  network_security_group_id = azurerm_network_security_group.testing.id
}

AzureRMのネットワークインターフェースとネットワークセキュリティグループの関連づけに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/network_interface_security_group_association.html を参照してください。

この設定では、先に定義したtestingセキュリティーグループをtestingネットワークインターフェースへ関連づけるルールを記述しています。

パブリックIPアドレス

ようやくパブリックIPアドレスを定義するリソースの解説まで到達しました。
clearcodeリソースグループへ動的なパブリックIPアドレスを定義するには以下のようにします。

resource "azurerm_public_ip" "testing" {
  name                    = "clearcode-collector-pip"
  location                = azurerm_resource_group.clearcode.location
  resource_group_name     = azurerm_resource_group.clearcode.name
  allocation_method       = "Dynamic"
  idle_timeout_in_minutes = 30

  tags = {
    environment = "clearcode-testing-pip"
  }
}

AzureRMのパプリックアドレスに関して詳しくは https://www.terraform.io/docs/providers/azurerm/r/public_ip.html を参照してください。

ここまでで、AzureのWindowsインスタンスへパブリックIPアドレスを付与するための準備が整いました。

以下のようにインスタンスのネットワークインターフェースリソースに対して、パブリックIPアドレスのリソースを紐づけることで、WindowsのインスタンスがパブリックIPアドレスを持つようにできます。

diff --git a/main.tf b/main.tf
index f8d326c..0d428b1 100644
--- a/main.tf
+++ b/main.tf
@@ -98,6 +98,7 @@ resource "azurerm_network_interface" "testing" {
     subnet_id                     = azurerm_subnet.internal.id
     private_ip_address_allocation = "Static"
     private_ip_address            = "10.5.2.4"
+    public_ip_address_id          = azurerm_public_ip.testing.id
   }
 }
 
ここまでの設定の適用

ここまでの設定を適用するには前回までのリソースを消しておかないといけません。

$ terraform destroy -auto-approve

を実行して前回までのリソースを削除しておきます。

再度、

$ terraform apply -auto-approve

を実行することで、ここまでに書き下した設定がAzureへ適用され、Terraformで設定した設定が流し込まれたインスタンスが立ち上がります。
ここまでの設定で立ち上げたWindows Server 2019 DatacenterインスタンスはRDPプロトコルで接続ができます。

ここまでに作成したTerraformスクリプトは https://gitlab.com/clear-code/terraform-example/-/tree/ab9fe8c5c4ae4420b475eb37dc2d51ec8c3af252 に置いてあります。

まとめ

TerraformでWindows Server 2019 DatacenterのインスタンスへRDPで接続できるインスタンスを作成するための設定を駆け足で解説してきました。
WinRMによるAnsibleでのプロビジョニングに関しては記事の長さの都合上さらに追加の記事で解説します。

2020-05-26

Terraformの導入 - 検証環境をコマンドで立ち上げられるようにする その3

はじめに

プロビジョニングツールはAnsibleが好きなのだけれど、Windowsに対しては一手間必要なのが大変だなぁと思っている畑ケです。

前回までの記事で、Windows Server 2019 Datacenterの検証環境をTerraformの機能を使ってAzureに構築する方法を解説しました。今回はその第3回目です。
Windowsの機能でWinRMというWindowsのプロビジョニングに使用できる機能があります。

Ansibleはプロビジョニングツールの一種で、Windowsに対するプロビジョニングツールもサポートしています。

Azure上のWindows 10のインスタンスについてはボリュームライセンスを用意する必要があります。ライセンス認証については https://docs.microsoft.com/ja-jp/windows/deployment/windows-10-subscription-activation の記事をご覧ください。
この記事ではWindows Server 2019 Datacenterを用いて解説します。

WindowsインスタンスのWinRMを有効化する

WinRMを有効化するには、例えば、スタートアップコマンドに有効化を行うコマンドを登録する必要があります。

Invoke-WebRequest -Uri https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 -OutFile ConfigureRemotingForAnsible.ps1
powershell -ExecutionPolicy RemoteSigned ConfigureRemotingForAnsible.ps1
Remove-Item -path ConfigureRemotingForAnsible.ps1 -force

上記のスクリプトをsettings.ps1として保存し、configディレクトリの下に配置します。

$ tree -L 1 config                                                                                                                                                                    
config
└── settings.ps1

また、AzureのCustomData領域からsettings.ps1を別のファイル名として取得し、スタートアップ時に実行するコマンド群をXMLで定義します。

<FirstLogonCommands>
    <SynchronousCommand>
        <CommandLine>cmd /c "mkdir C:\terraform"</CommandLine>
        <Description>Create the Terraform working directory</Description>
        <Order>11</Order>
    </SynchronousCommand>
    <SynchronousCommand>
        <CommandLine>cmd /c "copy C:\AzureData\CustomData.bin C:\terraform\winrm.ps1"</CommandLine>
        <Description>Move the CustomData file to the working directory</Description>
        <Order>12</Order>
    </SynchronousCommand>
    <SynchronousCommand>
        <CommandLine>powershell.exe -sta -ExecutionPolicy Unrestricted -file C:\terraform\winrm.ps1</CommandLine>
        <Description>Execute the WinRM enabling script</Description>
        <Order>13</Order>
    </SynchronousCommand>
</FirstLogonCommands>

このXMLをconfig以下に配置すると、configディレクトリは以下のファイルを含みます。

$ tree -L 1 config
config
├── FirstLogonCommands.xml
└── settings.ps1

0 directories, 2 files
diff --git a/main.tf b/main.tf
index 0d428b1..6568904 100644
--- a/main.tf
+++ b/main.tf
@@ -130,15 +130,43 @@ resource "azurerm_virtual_machine" "win10testing" {
     computer_name  = "cc-winserv"
     admin_username = var.windows-username
     admin_password = var.windows-password
+    custom_data    = file("./config/settings.ps1")
   }
 
   os_profile_windows_config {
     enable_automatic_upgrades = true
     provision_vm_agent        = true
+    winrm {
+      protocol = "http"
+    }
+    # Auto-Login's required to configure WinRM
+    additional_unattend_config {
+      pass         = "oobeSystem"
+      component    = "Microsoft-Windows-Shell-Setup"
+      setting_name = "AutoLogon"
+      content      = "<AutoLogon><Password><Value>${var.windows-password}</Value></Password><Enabled>true</Enabled><LogonCount>1</LogonCount><Username>${var.windows-username}</Username></AutoLogon>"
+    }
+    # Unattend config is to enable basic auth in WinRM, required for the provisioner stage.
+    additional_unattend_config {
+      pass         = "oobeSystem"
+      component    = "Microsoft-Windows-Shell-Setup"
+      setting_name = "FirstLogonCommands"
+      content      = file("./config/FirstLogonCommands.xml")
+    }
   }
 
   tags = {
     CreatedBy = "clearcode"
     Purpose   = "Describe Terraform instruction"
   }
+
+  connection {
+    host     = azurerm_public_ip.testing.ip_address
+    type     = "winrm"
+    port     = 5985
+    https    = false
+    timeout  = "2m"
+    user     = var.windows-username
+    password = var.windows-password
+  }
 }

埋め込むと、上記のdiffのようになります。

Ansibleのインベントリを作成する
Ansibleに必要なライブラリをインストールする

Ansibleではプロビジョニング対象の情報をインベントリというファイルから取得してプロビジョニングを行います。
Windowsへプロビジョニングする際には、さらにWinRMというPythonのライブラリも必要です。

Python3で動作させる際には、venvを用いて環境をリポジトリ固有のものにします。

$ python3 -m venv management
$ source ./management/bin/activate  

により、システムから隔離されたmanagement環境に入ります。

$ pip3 install ansible pywinrm

を実行し、Ansibleとpywinrmをインストールします。

Terraformでインベントリを作成する

Terraformでローカルにファイルを作成するには、localプロバイダーをインストールします。

provider "local" {
  version = "~>1.4"
}

localプロバイダーをTerraformにインストールするため、再度terraform initを実行します。

Terraformの実行中のデータを格納するdataソースを定義していきます。
また、実行が終了した時にパブリックIPアドレスを出力してくれるoutputもついでに定義していきます。

data "azurerm_public_ip" "testing" {
  name                = azurerm_public_ip.testing.name
  resource_group_name = azurerm_virtual_machine.winservtesting.resource_group_name
}

output "win10testing_instance_public_ip_address" {
  value = data.azurerm_public_ip.testing.ip_address
}

AnsibleのインベントリとPlayBookを入れるためのansibleディレクトリを作成します。

$ mkdir ansible

Ansibleのインベントリはlocalファイルリソースにてローカルに出力します。

resource "local_file" "inventory" {
  filename = "ansible/hosts"
  content  = <<EOL
[windows]
${data.azurerm_public_ip.testing.ip_address}

[windows:vars]
ansible_user=${var.windows-username}
ansible_password=${var.windows-password}
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
EOL
}
設定の適用

ここまでの設定を適用するには前回までのリソースを消しておかないといけません。

$ terraform destroy -auto-approve

を実行して前回までのリソースを削除しておきます。

再度、

$ terraform apply -auto-approve

を実行することで、ここまでに書き下した設定がAzureへ適用され、Terraformで設定した設定が流し込まれたインスタンスが立ち上がります。
ここまでの設定を流し込むことで、WinRMを用いたAnsibleによるプロビジョニングを行う準備ができたWindows Server 2019 Datacenterインスタンスが立ち上がってきます。

WinRMの有効化に成功している場合、Ansibleのwin_pingモジュールを使用するとpongが返ってきます。

$ ansible windows -i ansible/hosts -m win_ping -vvv 

# ...

0.0.0.0 | SUCCESS => {
    "changed": false,
    "invocation": {
        "module_args": {
            "data": "pong"
        }
    },
    "ping": "pong"
}

ここまでに作成したTerraformのスクリプトは https://gitlab.com/clear-code/terraform-example/-/tree/1430e3146c3a27c770fedf099cee3b79facea19b に置いてあります。

まとめ

Azure上のWindows Server 2019 DatacenterのインスタンスをWinRMを有効化してAnsibleによるプロビジョニングを実行する準備を整えるところまで複数回に分けて解説しました。
WindowsのWinRMを有効化したインスタンスを立ち上げることができれば、必要な設定を入れ込んだインスタンスを常時起動・休止されている必要はなく、必要な時に作成・不要な時に破棄することができるようになります。
クラウドサービスはとても便利ですが、必要な設定がきちんと入っているか、必要なリソースがちゃんと確保されているか、インスタンスタイプの変更をdiffで取得できるか、など管理コンソールから確認するのには一手間必要です。
EC2インスタンスやそれに準じたリソースを扱う際には、Terraformの設定に落とし込み設定変更が必要な際にはdiffとして出力することができる仕組みを整えてみてはいかがでしょうか。

2020-05-27

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|