Groongaでは、これまでもGitHub Actionsを使ってパッケージの作成を自動化したり、テストの自動化を実施してきました。
いままで自動化してきたテストは、リポジトリーにpushされたソースコードに対してビルド、テストするものでした。
これらの自動化により、リリース前に初めて問題が発覚することが少なくなり、問題が発生した段階で対処を進めることができています。
ただ、リポジトリーにpushされたソースコードに対するテストだと、各OS向けに作成したパッケージがちゃんとインストールできるか、
パッケージからインストールした環境で動作するかは確認できていませんでした。
そのため、パッケージの作成に失敗していた場合には、リリース後、パッケージからGroongaをインストールする段階にならないと問題に気がつけない状態でした。
リリース後にパッケージに問題があるとわかった場合は、再リリースすることになり、余計な時間がかかってしまいます。
そこで、リポジトリーにソースコードがpushされた段階でパッケージのインストールとパッケージからインストールしたGroongaのテストを実行するようにしました。
この記事は、作成したパッケージをインストール、テストする方法を説明したものです。
Groongaに固有の部分もありますが、各OS向けにパッケージを提供しているプロジェクトにとって参考になる情報もあると思います。
パッケージをテストするためには、当然テスト対象のパッケージを作成する必要がありますが、Groongaでは、既に自動化されています。
パッケージは既にできているので、この記事では、作成されたパッケージを取得するところから説明します。
前述の通りGroongaでは、リポジトリーにソースコードがpushされるたびにパッケージの作成が実行されるので、パッケージ作成後にテストを実行します。
パッケージの作成とパッケージのテストのジョブを分けても良いのですが、そのようにすると、パッケージを作成するジョブでartifacts
にパッケージを保存し、
パッケージをテストするジョブでは、artifacts
から必要なパッケージをダウンロードする操作が必要になり煩雑です。
(GitHub Actionsでは、パッケージ等のワークフローの成果物をartifacts
として保持できます。)
そのため、Groongaではパッケージの作成とパッケージのテストは同一のジョブで実行しています。
具体的には、以下のようにしています。
# 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 に記載されています。
docker run
の--volume
オプションを使って、ホストのディレクトリをDocker上にマウントできます。
上記では、--volume ${PWD}:/groonga:ro
と指定されているので、現在居るディレクトリをDcoker上の/groonga
にマウントしています。
${{ 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がないとすべてのテストを実行できないためです。)
まず、テストスクリプトの以下の箇所でOSのコードネームとアーキテクチャを取得します。
これらは、インストールするパッケージのパスに使用します。
code_name
には、stretch
やbuster
等のDebianの各バージョンのコードネームが入ります。
architecture
には、amd64
やi386
等の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
最後にgem
でgrntest
をインストールし、テストを実行しています。
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
まず、テストスクリプトの以下の箇所で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パッケージをインストールします。
yum
やdnf
コマンドも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という全文検索エンジンの開発に参加している堀本です。
今回は、開発中にGroongaのソースコードから「おぉー」と思ったコードを見つけたので紹介します。
Groongaは32bit用パッケージと64bit用パッケージを配布していますが、32bit環境のときだけコンパイルオプション等を設定したいケースがあります。
例えば、32bitのWindows向けパッケージはCMakeを使用してビルドしていますが、CMakeには32bitのWindowsかどうかを判定できる定義済みの変数はありません。
WIN32
という定義済みの変数がありますが、これはコンパイルターゲットが(64bitを含む)Windowsの場合にTrue
になるので、32bitかどうかの判定には使えません。
そこでGroongaでは、以下のようにポインターのサイズを使って32bitかどうかを判定しています。
(CMAKE_SIZEOF_VOID_P
はvoid
ポインタのサイズを計算してくれるCMakeの定義済みの変数です。)
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
# 32bit
list(APPEND MRUBY_DEFINITIONS "MRB_METHOD_T_STRUCT")
endif()
データ型のサイズは、データ型モデルという定義にしたがって決まります。データ型モデルとはデータ型の大きさを定義したもので、OSによって異なりますが、概ね以下のどれかになります。
データモデル名 | short | int | long | long long | pointer |
---|---|---|---|---|---|
ILP32 | 16 | 32 | 32 | 64 | 32 |
LP32 | 16 | 16 | 32 | 64 | 32 |
データモデル名 | 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版のソフトウェアをメンテナンスしている方は、上記のような判定方法も利用してみてはいかがでしょうか?
組み込み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と呼びます。
FirefoxやThunderbirdは、MCD(別名:AutoConfig)やポリシー設定を使ってある程度の設定を集中管理できます。集中管理可能な設定項目の情報は当社のサポート業務で把握している頻出設定の一覧やMozilla公式のポリシーテンプレートの説明などで調べることができ、実際の運用では、これらの資料で得た情報に基づいて書き上げた設定ファイルをFirefox(Thunderbird)に読み込ませることになります。
ただ、それですんなりいけばよいのですが、時には単純な記述ミスや、記載した設定の未知の特性の影響などによって、想定外の問題が起こる場合も度々あります。そのような場面で、多数ある設定の中から問題の原因箇所を特定するのはなかなか大変です。項目が多い場合、しらみつぶしに調査していては時間がいくらあっても足りません。
このような場面では、後退バグの発生原因となったコミットを特定する際にも使用した二分探索が有用です。
設定ファイルを対象とした二分探索の基本的な手順は、以下のようになります。
二分探索では、しらみつぶしに調べるよりもはるかに少ない試行回数で原因箇所を特定できます。しかしながら、やり方を誤ると、問題が無い箇所を問題の原因と誤認してしまう事もあり得ます。この記事では、Firefoxの集中管理用設定ファイルを対象に二分探索を行う際のよくある注意点を紹介します。
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」のように管理者権限での認証に対応したテキストエディターを使うか、もしくは、以下の手順で操作する必要があります。
C:\Program Files
配下の元の位置に上書きコピーする。MCD用設定ファイルもpolicies.jsonも、エンコーディング形式は「BOM無しのUTF-8(リトルエンディアン)」が正式です。しかしながら、Windowsのバージョンが古いと、「メモ帳」を使ってファイルを編集した場合にBOMが付与されてしまったり、意図せずエンコーディングが変更されてしまったり、といったトラブルが起こり得ます。この事が原因で、編集後のファイル全体がエラーとなり、まったく動作しない状態となってしまう場合もあります。
このようなトラブルを避けるためにも、集中管理用設定ファイルの編集には、先にも名前を挙げた「EmEditor」のように「BOMの有無やテキスト保存時のエンコーディングを自由に指定でき、また、ファイルの上書き保存時には元のエンコーディングを可能な限り維持する」性質を持つテキストエディターを使用する事を強くお勧めします。
MCD用設定ファイルはJavaScript形式なので、単純な記述ミスであればeslintで文法エラーを検出できます。
文法エラーでない原因で起こる問題を二分探索で探る際にも、その過程での文法エラーの発生には気をつける必要があります。例えば、以下のように条件分岐を伴う設定が記述されている場合:
if (getenv('USERDOMAIN' == '...') {
lockPref('...', true);
lockPref('...', false);
lockPref('...', 0);
}
このような箇所が二分探索の境界上にあると、
if (getenv('USERDOMAIN' == '...') {
lockPref('...', true);
// ここでファイルが終了しており、開かれた括弧が
// 閉じられないままになっている
や
// ここでファイルが始まっており、開き括弧が無いまま
// 閉じ括弧だけがある状態になっている
lockPref('...', 0);
}
のように文法的に不正な状態が発生してしまうことがあります。二分探索の境界は、このような箇所を避けて決めるように気をつけましょう。
ポリシー設定を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用設定ファイルとポリシー設定ファイルについて、問題発生時の原因を二分探索で調査する手順と、その注意点をご紹介しました。
この記事でご紹介した調査方法は多くの場合で有効ですが、残念ながら万能ではありません。設定項目の中には複数が組み合わさった場合にのみ問題が起こる物もあり、二分探索を実施しても「前半と後半それぞれだけで読み込んだら問題無いのに、全体を合わせると問題が起こる」というような不可解な状況が発生する場合があります。そのようなケースでは、ログと突き合わせての調査なども併用する必要があります。どうしても問題の原因を掴めないという場合には、当社の法人向け有償サポートがお役に立てるかもしれませんので、ぜひ一度お問い合わせください。
月単位や日単位などある時間範囲で検索したい場面は多いです。
こんな時は月単位、日単位で日付情報を格納するカラムを作り、それを用いて検索したくなります。
しかし、検索要件が変わり、新たに年単位や週単位で検索する必要が出た場合、それらの情報を格納するカラムを増やさなければなりません。
日付情報は一つのカラムに格納し、その情報を年単位や週単位に丸めて検索できれば、要件の変更に追従してカラムを増やす必要がなくなります。
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
の値がApple
でmonth
カラムの値が
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
関数の使用を検討してみてはいかがでしょうか?
クラウドサービスをコマンドラインで操作した方が楽に目的の環境作成ができると言うことに気づいた畑ケです。
Terraformと言うクラウド上のリソースを作成するツールがあります。TerraformでAWS, Azure, GCPなど各種クラウドのリソースの状態を定義し、作成できます。
AzureでWindowsのインスタンスを作成するには、以下のものを定義して作成する必要があります。
また、Azureに立ち上げた仮想マシンに対して、プロビジョニングやssh, RDPで接続するためには追加で以下の設定も必要です。
Azure上のWindows 10のインスタンスについてはボリュームライセンスを用意する必要があります。ライセンス認証については https://docs.microsoft.com/ja-jp/windows/deployment/windows-10-subscription-activation の記事をご覧ください。
この記事ではWindows Server 2019 Datacenterを用いて解説します。
Terraformはディレクトリにある拡張子tfのファイルを読み込みます。また、必要なモジュールはproviderブロックで指定する必要があります。
Terraformを使用するには、まずTerraformをインストールする必要があります。
https://www.terraform.io/downloads.html から使用するプラットフォームに応じた実行ファイルをインストールします。
Terraformを使用するためには、Azureから認証情報を取得する必要があります。Azureをコマンドラインから操作するコマンドはazです。
https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli?view=azure-cli-latest を参考に使用するプラットフォーム応じた実行ファイルをインストールします。
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 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プレフィックスのアドレスとして例示されている。
クラウドサービスを扱うのにTerraformが便利ということに気づいたけれど、解説を書くとなると長くなってしまった…と気落ちしている畑ケです。
前回の記事では、TerraformでWindows系のAzureのインスタンスを立ち上げるのに最低限必要であった
について解説しました。今回の記事では、
の項目を解説します。ここまでの設定が流し込まれていると、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アドレスを定義するリソースの解説まで到達しました。
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でのプロビジョニングに関しては記事の長さの都合上さらに追加の記事で解説します。
プロビジョニングツールは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を用いて解説します。
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ではプロビジョニング対象の情報をインベントリというファイルから取得してプロビジョニングを行います。
Windowsへプロビジョニングする際には、さらにWinRMというPythonのライブラリも必要です。
Python3で動作させる際には、venvを用いて環境をリポジトリ固有のものにします。
$ python3 -m venv management
$ source ./management/bin/activate
により、システムから隔離されたmanagement環境に入ります。
$ pip3 install ansible pywinrm
を実行し、Ansibleとpywinrmをインストールします。
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として出力することができる仕組みを整えてみてはいかがでしょうか。