Packer v1.7 - initコマンドでプラグインをインストール

Packer v1.7からプラグインを特定のルールに沿って作成されたGitHub Releasesからダウンロード〜インストールできるpacker initコマンドが追加されました。

最近さくらのクラウド向けPackerプラグイン sacloud/packer-plugin-sakuracloudでこのpacker initに対応しましたので今回はその紹介をしていきます。

packer initコマンド

Packer v1.7からpacker initコマンドが追加されました。

www.hashicorp.com

従来はPackerに組み込まれていないビルダーやプロビジョナーなどのプラグインは手動でインストールしていましたが、packer initを実行することでプラグインのダウンロード〜インストールを行ってくれるようになりました。

Terraformにおけるterraform initみたいな感じです。

ただ、Packerには今の所状態を管理する機能はありませんのでpacker initプラグインのインストールのみを行います。
また、プラグインのダウンロードは今の所GitHub上の公開リポジトリからのみ可能とのことです。

なおpacker initのドキュメントでは Currently って書いてあるのが目につくので、将来的にいろいろ拡張されそうな気配を感じます。

packer initの使い方

ドキュメントはこちらです。

www.packer.io

まずpacker initを行うためにはいくつかの条件があります。

古くからあるJSONテンプレートでは利用できない点に注意が必要です。

これらの条件を満たすプラグインであれば、以下のようなrequired_pluginsブロックをテンプレートファイルに記載した上でpacker initでインストール可能です。

packer {
  required_plugins {
    sakuracloud = {
      version = ">= 0.7.0"
      source = "github.com/sacloud/sakuracloud"
    }
  }
}
# プラグインのインストール
$ packer init template.pkr.hcl

なお、v1.7時点では従来Packer本体に組み込まれていたビルダーやプロビジョナーはまだ別リポジトリに分離されておらず、packer initなしで利用可能です。
また、packer initに対応していないプラグインでも手動インストールすることで利用は可能です。

ただし、Packer v1.7ではPacker本体とプラグインとの間の通信プロトコルのバージョンが変更されている(マイナーバージョンという概念が追加された)ため、古いプラグインはそのままだと利用できないケースもあります。

プラグインとの通信プロトコルバージョンによるエラーメッセージの例:

Error: Failed to load source type

The protocol of this plugin (protocol version 4 and lower) was deprecated,
please use a newer version of this plugin.Or use an older version of Packer (pre
1.7) with this plugin.

利用例: さくらのクラウド向けプラグイン

packer initの利用例としてさくらのクラウド向けプラグインでの利用方法を紹介します。

github.com

packer-plugin-sakuracloud v0.7からpacker initに対応しています。
(packer initに対応するために名称をpacker-builder-sakuracloudから変更しました)

以下のようなテンプレートを用意することでプラグインのインストール〜packer buildの実行が可能です。

# template.pkr.hcl

# required_pluginブロックで利用するプラグインを宣言
packer {
  required_plugins {
    sakuracloud = {
      version = ">= 0.7.0"
      source = "github.com/sacloud/sakuracloud"
    }
  }
}

source "sakuracloud" "example" {
  zone  = "is1a"

  os_type   = "centos8"
  password  = "input-your-password"
  disk_size = 20
  disk_plan = "ssd"

  core        = 1
  memory_size = 1

  archive_name        = "packer-example-centos"
  archive_description = "description of archive"
}

build {
  sources = [
    "source.sakuracloud.example"
  ]
  provisioner "shell" {
    inline = [
      "echo 'hello!'",
    ]
  }
}

テンプレートを用意し以下のコマンドで実行していきます。

# APIキーを環境変数に設定
$ export SAKURACLOUD_ACCESS_TOKEN=xxx
$ export SAKURACLOUD_ACCESS_TOKEN_SECRET=xxx

# プラグインのインストール
$ packer init template.pkr.hcl
Installed plugin github.com/sacloud/sakuracloud v0.7.0 in "~/.packer.d/plugins/github.com/sacloud/sakuracloud/packer-plugin-sakuracloud_v0.7.0_x5.0_darwin_amd64"

# buildの実行
$ packer build template.pkr.hcl

従来の手動インストールと比べると手軽に利用できるようになりました。

GitHub Container Registryでのpacker-plugin-sakuracloudのDockerイメージの配布

なおpacker-plugin-sakuracloud v0.7からDockerHubに加えGitHub Container RegistryでのDockerイメージ配布も行われるようになりました。

github.com

Dockerで利用する場合は次のようなコマンドで実行します。

$ docker run -it --rm \
         -e SAKURACLOUD_ACCESS_TOKEN \
         -e SAKURACLOUD_ACCESS_TOKEN_SECRET \
         -v $PWD:/work \
         -w /work \
         ghcr.io/sacloud/packer:latest build template.pkr.hcl

ベースイメージにhashicorp/packer:lightを使うようになりました。
従来通りのsacloud/packerも継続して利用可能です。

(プラグイン開発者向け)プラグイン側がpacker initに対応するには

Packerプラグインpacker initに対応するにはいくつかの条件があります。

  • Packer Plugin SDKに対応していること
    (かつmulti-component RPC serverであること)
  • プラグインのバイナリの名前がpacker-plugin-*という形式(従来はpacker-builder-*packer-provisioner-*だった)であること
    (これはmulti-component pluginsと呼ばれる。従来のものはsingle-component plugins)
  • 所定の手順GitHub Releasesでリリースすること

詳細は以下のドキュメントに記載されています。

www.packer.io

新しくプラグインを作成する場合はscaffoldが提供されています。

github.com

Packer v1.6以前にプラグインを提供していた場合向けの移行ガイドやPacker Plugin SDKへのマイグレーションツールも提供されています。

www.packer.io

github.com

packer initに対応するかはともかくとしても、Packer Plugin SDKへの移行はしておかないとPacker v1.7でエラー(The protocol of this plugin~)がでるようですので Packer v1.6以前のプラグインは早めにマイグレーションした方が良さそうです。

以上です。

BBC micro:bit v2でmruby/cを動かしてみる

最近息子と一緒にBBC micro:bitを触って遊んでいます。

今日はこのmicro:bit上でmruby/cを動かしてみましたのでメモを残しておきます。

(写真撮ったあとにBBCBCCtypoしてるのに気付きました。。。)

はじめに

micro:bitとは

micro:bitBBC(英国放送協会)が主体となって開発されたシングルボードコンピューターです。学校での情報教育(プログラミング)などで利用されているとのことです。

microbit.org

こちらのスイッチエデュケーションさんのサイトに特徴がまとめられています。

switch-education.com

micro:bit の特徴
- LEDやボタン、センサーなどをあらかじめ搭載しています
- パソコンやタブレット、さまざまな環境でプログラミングできます
- 段階的にプログラミングを学ぶことができます
- 拡張パーツをつなげれば、さまざまな作品を作ることができます

値段も2,000円程度(執筆時点)と手ごろな価格になっており、手軽に触り始めることが出来ます。

こちらはRaspberry Piと比べてみた写真です。手のひらサイズですね。

ブロックを用いたビジュアルプログラミングができるMakeCodeやMicroPythonを利用可能なので子供のプログラミング&電子工作入門に良さそうと思い購入しました。

mruby/cとは

mruby/cとは、軽量Rubyであるmrubyをさらに組み込み機器向けに軽量化したmrubyの実装とのことです。

www.s-itoc.jp

こちらの記事でWio LTE上で動かしているのを読んでmicro:bit上でも動かせるんじゃないか?と思ったのが今回のきっかけです。
magazine.rubyist.net

ということで早速micro:bit上でmruby/cを動かしてみます。

準備

先ほどのRubyist Magagineの記事を参考に、開発環境にはArduino IDEを利用します。

必要なもの

  • Arduino IDE
  • mruby v2.1.1 (v2.1.2だとエラーになるため)

Arduino IDEmicro:bit v2を使えるようにする

まずはArduino IDEmicro:bit v2を使えるようにするためにsandeepmistry/arduino-nRF5をインストールします。

github.com

Arduinoの環境設定ダイアログを開き、追加のボードマネージャのURLに以下を入力します。
https://sandeepmistry.github.io/arduino-nRF5/package_nRF5_boards_index.json

注: 既に他のURLが入力されている場合、カンマ区切りで後ろに追記してください。

これでツール -> ボード -> Nordic Semiconductor nRF5 boardsという項目の中からBBC micro:bit V2が選べるようになっているはずです。

mruby/cをmicro:bit v2へ移植

次にmruby/cをmicro:bit v2へ移植します。

  • mruby/cのソースを取得
  • HALを実装
  • Arduinoのライブラリにする

mruby/cのソースを取得

次にmruby/cのソースを取得します。

git clone https://github.com/mrubyc/mrubyc.git

今回は何も考えずmasterを利用しました。エラーが出るようならタグが付けられたバージョンを使おうと思ったのですが、幸い特にエラーは起きなかったのでそのままmasterを使いました。

HAL(Hardware Abstraction Layer)の実装

mruby/cにはHALとして以下のものが用意されていました。

  • hal_esp32
  • hal_pic24
  • hal_posix
  • hal_psoc5lp

これらを参照しつつmicro:bit(nRF52833)向けにHALを実装します。

実装手順
  • 既存のsrc/hal_*ディレクトリを削除
  • 既存のsrc/hal_selector.hの修正
  • src/halディレクトリの作成 & hal.h/hal.c/hal.cppの作成

既存のsrc/hal_*ディレクトリを削除

まずsrc/hal_*ディレクトリを削除してしまいます。
(残しておくとArduino IDEでエラーが出たため。原因を調べるのが面倒だったので削除しちゃいました)

既存のsrc/hal_selector.hの修正

次にsrc/hal_selector.hを以下のように修正します。

/*! @file
  <pre>
  Copyright (C) 2016-2020 Kyushu Institute of Technology.
  Copyright (C) 2016-2020 Shimane IT Open-Innovation Center.
  This file is distributed under BSD 3-Clause License.
  </pre>
*/

// 以下をごっそり削りhal/hal.hのincludeだけにする
#include "hal/hal.h"

本来はどのHALを使うのかの判定が入っているのですが、今回はごっそり削ってhal/hal.hを決め打ちしました。

src/halディレクトリの作成 & hal.h/hal.c/hal.cppの作成

次にsrc/halディレクトリを作成し、その中にhal.h/hal.c/hal.cppを作成していきます。

hal.h
/*! @file
  https://github.com/mrubyc/mrubyc/blob/master/src/hal_posix/hal.hを参考に実装
  オリジナルのライセンス表記は以下のとおり

  Copyright (C) 2016-2020 Kyushu Institute of Technology.
  Copyright (C) 2016-2020 Shimane IT Open-Innovation Center.
  This file is distributed under BSD 3-Clause License.
*/
#ifndef MRBC_SRC_HAL_H_
#define MRBC_SRC_HAL_H_

#ifdef __cplusplus
extern "C" {
#endif

/***** Macros ***************************************************************/
#if !defined(MRBC_TICK_UNIT)
#define MRBC_TICK_UNIT_1_MS   1
#define MRBC_TICK_UNIT_2_MS   2
#define MRBC_TICK_UNIT_4_MS   4
#define MRBC_TICK_UNIT_10_MS 10
// You may be able to reduce power consumption if you configure
// MRBC_TICK_UNIT_2_MS or larger.
#define MRBC_TICK_UNIT MRBC_TICK_UNIT_1_MS
// Substantial timeslice value (millisecond) will be
// MRBC_TICK_UNIT * MRBC_TIMESLICE_TICK_COUNT (+ Jitter).
// MRBC_TIMESLICE_TICK_COUNT must be natural number
// (recommended value is from 1 to 10).
#define MRBC_TIMESLICE_TICK_COUNT 10
#endif

#if !defined(MRBC_NO_TIMER)    // use hardware timer.
# define hal_init()        ((void)0)
# define hal_enable_irq()  ((void)0)
# define hal_disable_irq() ((void)0)
# define hal_idle_cpu()    ((void)0)

#else // MRBC_NO_TIMER
# define hal_init()        ((void)0)
# define hal_enable_irq()  ((void)0)
# define hal_disable_irq() ((void)0)
# define hal_idle_cpu()    ((void)0)

#endif


/***** Function prototypes **************************************************/
int hal_write(int fd, const void *buf, int nbytes);
int hal_flush(int fd);

#ifdef __cplusplus
}
#endif
#endif // ifndef MRBC_SRC_HAL_H_

本来はhal_*()達を実装すべきなのですが、今回はとりあえず動かすことが目的なので((void)0)で済ませてます。
動作確認のためにシリアル出力は行いたいのでhal_write()hal_flush()だけは実装します。

hal.c
/*
  https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/src/hal/hal.cを参考に実装
  オリジナルのライセンス表記: https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/LICENSE
*/
#include "hal.h"

int hal_write(int fd, const void *buf, int nbytes)
{
    char* t = (char*)buf;
    char tbuf[2];
    if(nbytes==1){
        tbuf[0]=*t;
        tbuf[1]='\0';
        hal_write_string(tbuf);
        return nbytes;
    }
    hal_write_string(t);
    return nbytes;
}

int hal_flush(int fd) {
    hal_serial_flush();
}

前述のRubyist Magazineの記事を参考に実装してみました。
hal_write_string()hal_serial_flush()はこのあとhal.cppで実装します。

hal.cpp
/*
  https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/src/hal/hal.cppを参考に実装
  オリジナルのライセンス表記: https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/LICENSE

  Copyright (c) 2018, katsuhiko kageyama All rights reserved.
*/
#include <Arduino.h>

extern "C" void hal_write_string(char* text){
    Serial.write(text);
}

extern "C" void hal_serial_flush(char* text){
    Serial.flush();
}

こちらも最低限の実装となってます。

とりあえずこれで最低限動くはずです。

Arduinoのライブラリにする

次にmruby/cをArduinoから使うために先ほど移植したソースをArduinoのライブラリにします。

ここもほぼ前述のRubyist Magazineの記事の通りに進めます。
Arduinoのライブラリディレクトリ(macの場合~/Documents/Arduino/librariesWindowsの場合はC:¥Users¥ユーザー名¥Documents¥Arduino¥librariesなど)にlibmrubycなどという名前でディレクトリを作成します。 そのディレクトリ内に先ほど修正したソース達とlibrary.propertiesというテキストファイルを格納します。

library.propertiesの作成

以下のような内容にします。

name=mruby/c for Micro:Bit v2
version=0.0.1
author=yamamoto-febc
maintainer=yamamoto-febc
sentence=mruby/c implementation for BBC Micro:Bit v2.
paragraph=
category=Communication
url=https://github.com/yamamoto-febc/libmrubyc
architectures=*
includes=mrubyc.h

修正したmruby/cのsrcディレクトリをコピー

次にmruby/cのsrcディレクトリをコピーします。

最終的に以下のようなファイル構成になっているはずです。

(~/Documents/Arduino/など)/libraries/
    ├── library.properties    // 作成したファイル
    └── src                   // mruby/cのsrcからコピーしたもの

これでArduino IDEを再起動するとライブラリとして認識されているはずです。

mruby/cのコード作成〜バイトコード生成〜Arduinoのスケッチ作成

Arduinoの準備が出来たのでいよいよmruby/cのコードを書いてみます。
今回は以下のようにputsするだけです。

puts "hello mruby/c from BBC micro:bit v2!"

これをhello.rbとして作成します。 そしてこのファイルをmrbcコマンドに渡してmruby/cに渡すバイトコードを生成します。

$ mrbc -E -B code hello.rb

これでカレントディレクトリにhello.cが作成されているはずです。
これを後ほどArduinoスケッチに貼り付けます。

TIPS: mrbcコマンドで-e/-E option no longer neededというエラーが出る

おそらくmruby v2.1.2以降を利用しています。
参考: https://github.com/mruby/mruby/commit/48c473a0c4abc67614a00d282d24d18089908449

mruby v2.1.1を利用するようにしてください。

次にArduinoのスケッチを作成します。

/*
  https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/examples/controlLED/controlLED.inoを参考に実装
  オリジナルのライセンス表記: https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/LICENSE
*/
#include <mrubyc.h>

/* ここに先ほど生成したhello.cの内容を貼り付ける */

void setup() {
  delay(100);
  Serial.begin(9600);
  Serial.println("microbit is ready!");

  mrbc_init(mempool, MEMSIZE);
  if(NULL == mrbc_create_task( code, 0 )){
    Serial.println("mrbc_create_task error");
    return;
  }
  Serial.println("--- run mruby script");
  mrbc_run();
}

void loop() {
  delay(1000);

}

スケッチの中に先ほど生成したhello.cの内容をそのまま貼り付けてください。
私の手元の環境では最終的なスケッチは以下のようになりました。

/*
  https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/examples/controlLED/controlLED.inoを参考に実装
  オリジナルのライセンス表記: https://github.com/kishima/libmrubycForWioLTEArduino/blob/master/LICENSE
*/

#include <mrubyc.h>

#include <stdint.h>
const uint8_t code[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x36,0xef,0x0a,0x00,0x00,0x00,0x7a,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x5c,0x30,0x30,
0x30,0x32,0x00,0x00,0x00,0x78,0x00,0x01,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x0c,
0x10,0x01,0x4f,0x02,0x00,0x2e,0x01,0x00,0x01,0x37,0x01,0x67,0x00,0x00,0x00,0x01,
0x00,0x00,0x24,0x68,0x65,0x6c,0x6c,0x6f,0x20,0x6d,0x72,0x75,0x62,0x79,0x2f,0x63,
0x20,0x66,0x72,0x6f,0x6d,0x20,0x42,0x43,0x43,0x20,0x6d,0x69,0x63,0x72,0x6f,0x3a,
0x62,0x69,0x74,0x20,0x76,0x32,0x21,0x00,0x00,0x00,0x01,0x00,0x04,0x70,0x75,0x74,
0x73,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};


#define MEMSIZE (1024*50)
static uint8_t mempool[MEMSIZE];

void setup() {
    // 省略
}

void loop() {
    // 省略
}

実機で動かしてみる

あとはmicro:bitに転送して動かすだけです。 うまくいけばシリアルモニタに文字が表示されるはずです。

f:id:febc_yamamoto:20210220142008p:plain

TIPS: Error: Illegal bytecodeというエラーが出る

hello.cのコピペミスやmrubyのバージョン違いの可能性があります。
最初mrubyの最新版(2.1.2)を使ってたらこのエラーが出ました。
mruby/c側が(記事執筆時点のmasterでは)mruby v2.1.1のバイトコード(RITE0006)を期待してるんですね。
参考: https://github.com/mrubyc/mrubyc/blob/228645971e0005cc743cfa653c99ff2b78ae02a0/src/load.c#L54-L57

終わりに

ということでmicro:bit v2上でmruby/cを動かしてみました。
このままだとシリアル出力しか出来ないのでもうちょっと色々書く必要がありますが、とりあえずmruby/cを使っていくためのスタートラインには立てたんじゃないかなと思います。

参考にした記事/サイト

↓↓この辺はこの記事書いてる時に見つけました。書き始める前に読みたかった。。。

以上です。

Goで書いたWASMでfmt.Println()してからブラウザのコンソールに出力するまでを追う

先日さくらのクラウド向けCLI Usacloudをブラウザ上で動かせるChrome拡張UsaConをリリースしました。

febc-yamamoto.hatenablog.jp

このUsaConを作る際にいろいろ調べたことを備忘をかねて書いておきます。
(もし間違えている箇所などあったらご指摘いただけると嬉しいです。)

今回はfmt.Println()Hello WorldするだけのWASMを題材に、fmt.Println()からWebブラウザで出力されるまでのコードを追ってみます。

今回の環境

  • Go: 1.15.6
  • ブラウザ(動作確認用): Chrome 87

今回の題材アプリ

Hello Worldするだけの単純なアプリをWASMをしてビルドして実行します。

コードは↓↓だけの非常に単純なものです。

package main

import "fmt"

func main() {
    fmt.Println("hello wasm!")
}

あとはこれをビルドしてブラウザから実行可能にします。
実行にはGoに付属しているwasm_exec.htmlwasm_exec.jsを利用します。
この辺は以下のページに手順が書いてますので気になる方は参照してください。

WebAssembly · golang/go Wiki · GitHub

ビルドして実行可能にするまでの詳細はこちら(読み飛ばし可)

GOOSとGOARCHをセットしてビルド

$ GOOS=js GOARCH=wasm go build -o test.wasm main.go

Goに付属しているwasm_exec.htmlwasm_exec.jsをコピー

$ cp `go env GOPATH`/misc/wasm/wasm_exec.html ./
$ cp `go env GOPATH`/misc/wasm/wasm_exec.js ./

適当なWebサーバを起動

ここでは前述のWikiに従いgoexecを利用する形にしています。 既存のWebサーバを使う形やnpmでserveを使ったりする形でもOKです。

# install goexec
$ go get -u github.com/shurcooL/goexec
$ goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'

ブラウザで開く

$ open http://localhost:8080/wasm_exec.html

画面上のRunボタンをクリックするとブラウザのコンソールにhello wasm!を表示されるはずです。

f:id:febc_yamamoto:20201225200858p:plain
RunボタンをクリックしてWASMを実行したところ

fmt.Println()の中を追う

早速fmt.Println()の中身を追ってみます。

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/fmt/print.go#L259-L275

os.Stdout(標準出力)のWrite()を呼んでますね。

ではWASMにした際の標準出力はどういう扱いになっているのでしょうか? まずこの辺をみていきます。

GOOS=js GOARCH=wasmの場合のos.Stdout

os.Stdoutの定義

os.Stdout*os.File型で、以下のように定義されています。

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/os/file.go#L62-L66

syscall.Stdoutは以下のとおりです。

const (
    Stdin  = 0
    Stdout = 1
    Stderr = 2
)

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/syscall_js.go#L115-L119

別段変わったところはないですね。
NewFile()で何か変わったことをしているのでしょうか?
こちらも追ってみます。

os.NewFile()の実装の中身

// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris

func NewFile(fd uintptr, name string) *File {
    kind := kindNewFile
    if nb, err := unix.IsNonblock(int(fd)); err == nil && nb {
        kind = kindNonBlock
    }
    return newFile(fd, name, kind)
}

func newFile(fd uintptr, name string, kind newFileKind) *File {
    fdi := int(fd)

    // ...中略...

    f := &File{&file{
        pfd: poll.FD{
            Sysfd:         fdi,
            IsStream:      true,
            ZeroReadIsEOF: true,
        },
        name:        name,
        stdoutOrErr: fdi == 1 || fdi == 2,
    }}
    // ...中略...

    // poll.FDのInitを呼んできる
    if err := f.pfd.Init("file", pollable); err != nil {
        // ...中略...
    }
}

ソース: https://github.com/golang/go/blob/go1.15.6/src/os/file_unix.go

こちらはビルドタグ(ソース先頭のやつ)が+build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solarisとなっていますので、WASM用に特別なことをしてるわけじゃないですね。
もう少し中の方ではWASM向けに分岐しているのですが、今回は省略します。

次は*os.FileWrite()がどう実装されているか見ていきます。

*os.FileWrite()の実装

*os.FileWrite()

go/src/os/file.goで実装されています。

func (f *File) Write(b []byte) (n int, err error) {

    // ...中略...

    n, e := f.write(b)

    // ...中略...
}

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/os/file.go#L173

write()が呼ばれてるのでそちらを見てみます。

*os.Filewrite()

今度はgo/src/os/file_posix.go

func (f *File) write(b []byte) (n int, err error) {
    // poll.FDのWriteを呼んでいる
    n, err = f.pfd.Write(b)
    // ...中略...
}

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/os/file_posix.go#L48

f.pfd.Write()が呼ばれています。 *os.Filepfdフィールドはos.NewFile()の中でこっそり登場していましたね。
ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/os/file_unix.go#L114-L118

次にpoll.FDWrite()を見てみます。

poll.FDWrite()

// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
    // ...中略...

        // syscall.Writeを呼んでいる
        n, err := ignoringEINTR(func() (int, error) { return syscall.Write(fd.Sysfd, p[nn:max]) })

    // ...中略...

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/internal/poll/fd_unix.go#L267

syscall.Write()が呼ばれています。

GOOS=js GOARCH=wasmの場合のsyscall.Write()

go/src/syscall/fs_js.goで実装されています。

func Write(fd int, b []byte) (int, error) {
    // ...中略...

    // jsに値を渡す
    js.CopyBytesToJS(buf, b)

    // fsCall??
    n, err := fsCall("write", fd, buf, 0, len(b), nil)

    // ...中略...
}

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/fs_js.go#L400-L429

ここでようやくWASMっぽい部分が出てきました。 js.CopyBytesToJS()でバッファに値を入れてfsCall("write", ...)してます。

ちょっと寄り道: GoでビルドしたWASMとJavaScriptの値のやりとりはどうやってるの?

WASM/JavaScript両方からアクセスできるメモリ領域を介して値のやりとりをしています。
参考: WebAssembly.Memory

GoでビルドしたWASMの場合は、memという名前でこのMemoryがエクスポートされています。

f:id:febc_yamamoto:20201225211239p:plain
デバッガでWebAssembly.Instanceのexportsを確認しているところ

先ほど出てきたjs.CopyBytesToJS()はこの共有メモリ領域への書き込みを行なっています。 jsからも読み書きできるところにデータを書いておいてfsCall("write", ...)を呼んでいるということですね。

次にfsCall("write", ...)を追ってみます。

fsCall("write", ...)の実装

fsCallは以下のように実装されています。

func fsCall(name string, args ...interface{}) (js.Value, error) {
    // ...中略...

    // jsFSのCallを呼んでる
    jsFS.Call(name, append(args, f)...)

    // ...中略...
}

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/fs_js.go#L495-L523

jsFSはこちらです。

var jsFS = js.Global().Get("fs")

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/fs_js.go#L20

syscall.jsパッケージのGlobal()経由でGet("fs")してます。

syscal/js

syscall.jsGlobal()は以下のように定義されています。

func Global() Value {
    return valueGlobal
}

var (
    // valueGlobalは106行目で定義されている
    valueGlobal    = predefValue(5, typeFlagObject)

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/js/js.go#L144-L146

syscall/jsValue型ですね。ValueGet()は以下のように実装されています。

func (v Value) Get(p string) Value {
    // ...中略...

    r := makeValue(valueGet(v.ref, p))

    // ...中略...
}

func valueGet(v ref, p string) ref

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/js/js.go#L297-L304

最終的にvalueGet()を呼んでいます。
funcのボディがないですが、これはasmで実装されています。

TEXT ·valueGet(SB), NOSPLIT, $0
  CallImport
  RET

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/src/syscall/js/js_js.s#L15-L17

CallImportというやつがいますね。これは最終的にWebAssemblyにインポートしているfuncを呼ぶようになっているみたいです。 (この辺はまだちゃんと追えていません…)

インポートしてるfuncというのは、JavaScriptでWebAssemblyのインスタンスを作る時に使うWebAssembly.instantiateStreaming()の引数で渡すやつですね。
参考: MDN: WebAssembly.instantiateStreaming()

GoでビルドしたWASMの場合は、wasm_exec.jsの中で定義されているものを利用することになります。

    global.Go = class {
        constructor() {
            // ...中略...
            this.importObject = {
                go: {
                    // func valueGet(v ref, p string) ref
                    "syscall/js.valueGet": (sp) => {
                        const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
                        sp = this._inst.exports.getsp();
                        storeValue(sp + 32, result);
                    },

                    // func copyBytesToJS(dst ref, src []byte) (int, bool)
                    "syscall/js.copyBytesToJS": (sp) => {
                        const dst = loadValue(sp + 8);
                        const src = loadSlice(sp + 16);
                        if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
                            this.mem.setUint8(sp + 48, 0);
                            return;
                        }
                        const toCopy = src.subarray(0, dst.length);
                        dst.set(toCopy);
                        setInt64(sp + 40, toCopy.length);
                        this.mem.setUint8(sp + 48, 1);
                    },
                }
            };
        }
        // ...中略...

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/misc/wasm/wasm_exec.js#L248-L468

先ほど出てきたcopyBytesToJSもここで定義されていますね。 ということで最終的にjs上で定義されたfuncを呼ぶということがわかりました。

fsCall("write", ...)の実装(再び)

ということで改めてfsCall("write", ...)の実装を見てみます。

func fsCall(name string, args ...interface{}) (js.Value, error) {
    // ...中略...

    // jsFS.Call("write", ...)という引数になっている
    jsFS.Call(name, append(args, f)...)

    // ...中略...
}

var jsFS = js.Global().Get("fs")

js.Global().Get("fs")はjs上で定義されたvalueGet()を呼んでいました。 ここでjs上のglobal.jsを取得しています。取得したglobal.jsに対してCall("write", ...)してます。

これによりjs上のglobal.fswrite()を呼ぶようになっています。

ゴール: js上でconsole.log()を呼ぶ部分

global.fsには何が入ってる?

ブラウザ上で実行している場合は前述のwasm_exec.jsにより以下のように定義されています。

    if (!global.fs) {
        let outputBuf = "";
        global.fs = {
            constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
            writeSync(fd, buf) {
                outputBuf += decoder.decode(buf);
                const nl = outputBuf.lastIndexOf("\n");
                if (nl != -1) {
                    console.log(outputBuf.substr(0, nl));
                    outputBuf = outputBuf.substr(nl + 1);
                }
                return buf.length;
            },
            write(fd, buf, offset, length, position, callback) {
                if (offset !== 0 || length !== buf.length || position !== null) {
                    callback(enosys());
                    return;
                }
                const n = this.writeSync(fd, buf);
                callback(null, n);
            },
            chmod(path, mode, callback) { callback(enosys()); },
            chown(path, uid, gid, callback) { callback(enosys()); },
            close(fd, callback) { callback(enosys()); },
            fchmod(fd, mode, callback) { callback(enosys()); },
            fchown(fd, uid, gid, callback) { callback(enosys()); },
            fstat(fd, callback) { callback(enosys()); },
            fsync(fd, callback) { callback(null); },
            ftruncate(fd, length, callback) { callback(enosys()); },
            lchown(path, uid, gid, callback) { callback(enosys()); },
            link(path, link, callback) { callback(enosys()); },
            lstat(path, callback) { callback(enosys()); },
            mkdir(path, perm, callback) { callback(enosys()); },
            open(path, flags, mode, callback) { callback(enosys()); },
            read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
            readdir(path, callback) { callback(enosys()); },
            readlink(path, callback) { callback(enosys()); },
            rename(from, to, callback) { callback(enosys()); },
            rmdir(path, callback) { callback(enosys()); },
            stat(path, callback) { callback(enosys()); },
            symlink(path, link, callback) { callback(enosys()); },
            truncate(path, length, callback) { callback(enosys()); },
            unlink(path, callback) { callback(enosys()); },
            utimes(path, atime, mtime, callback) { callback(enosys()); },
        };
    }

ソース: https://github.com/golang/go/blob/9b955d2d3fcff6a5bc8bce7bafdc4c634a28e95b/misc/wasm/wasm_exec.js#L36-L87

global.fsが未定義だったら最低限の実装で埋めています。 write()は以下の部分です。

            writeSync(fd, buf) {
                outputBuf += decoder.decode(buf);
                const nl = outputBuf.lastIndexOf("\n");
                if (nl != -1) {
                    console.log(outputBuf.substr(0, nl));
                    outputBuf = outputBuf.substr(nl + 1);
                }
                return buf.length;
            },
            write(fd, buf, offset, length, position, callback) {
                if (offset !== 0 || length !== buf.length || position !== null) {
                    callback(enosys());
                    return;
                }
                const n = this.writeSync(fd, buf);
                callback(null, n);
            },

write()writeSync()を呼び、最終的にconsole.log()されていますね。

長かったですがこれでGoのfmt.Println()からコンソール出力されるところまでたどり着きました。


まとめ

無理やりまとめると

  • fmt.Println()syscall.Write()を呼ぶ
  • syscall(のGOOS=js GOARCH=wasm向けの実装)やsyscall/jsはjs側と協調して動作するようになっている
  • js側の基本的な実装はwasm_exec.jsで行われている
  • wasm_exec.jsでのglobal.fsの実装は最低限しかない

みたいな感じでしょうか。

ひとまずGoから標準出力に出力することでブラウザのコンソールに出力できることはわかりました。
でも上記の通りglobal.fsは最低限の実装しかないです。CLIの場合は標準入力から読んだり、その他のファイルを開いたりしたいことがあるかと思います。
CLIを動かしたい場合は自分で実装しないといけない部分が結構ありそうです。

次回以降の記事でUsaConでこの辺の問題をどう解決していったかを取り上げたいと思います。

今回は以上です。

Usacloud v1.0 - WASM対応や全ゾーン一括操作など様々な改善を含むメジャーバージョンアップ

f:id:febc_yamamoto:20201221102556p:plain
UsaConのメイン画面

はじめに

さくらのクラウド向けCLIUsacloudのメジャーバージョンアップとなるv1.0.0をリリースしました。

github.com

Usacloud v1.0.0ではWebAssemblyに対応しました。このリリースに合わせブラウザ上で手軽にUsacloudを実行できるようにするChrome拡張UsaConもリリースしました。

chrome.google.com

この記事ではUsacloud v1.0とUsaConを紹介していきます。

Usacloud v1.0

さくらのクラウド向けCLIとして2017年3月に初版となるv0.0.1がリリースされました。  

初版のリリースが行われて以降、ご利用いただいた方々からのフィードバックを反映したりさくらのクラウド自体の進化に対応したりと地道にアップデートを重ねてきました。

f:id:febc_yamamoto:20201221103456p:plain
Usacloud 初版(v0.0.1)のリリース

これまでの累計ダウンロード数はGitHub ReleasesやDocker Hubを合計すると延べ10,000を超えました。

Usacloud v0系で不満だった点

私自身日常の作業にUsacloudを利用していたのですが、数年利用している中でいくつか不満を感じるようになってきていました。

例えば、

  • フラグ指定する際に指定する位置に気をつける必要がある
  • 複数のゾーンに跨がる処理の実装が困難
  • bash以外のシェル補完が行えない

というような点です。

例えば1点目は--zone=is1aのような指定をしたい場合、以下のように指定するしないといけないということです。

  • OKな例: usacloud --zone=tk1a server list
  • NGな例: usacloud server list --zone=tk1a

指定忘れに気付いたらヒストリーから呼び出して所定の位置にカーソルを移動して、、というのが地味にストレスでした。
(option+←などのキーボードショートカットやctrl+aで先頭に戻ってから右へ移動とかでもOKなんですがそれもめんどくさい)

これらの問題を解決するには依存ライブラリの切り替えやアップグレードが必要で、Usacloud側も大幅に修正する必要がありましたので メジャーバージョンアップして対応することにしました。

Usacloud v1.0系での改善や主な新機能

前述の細かな不満の解消や新機能の追加などを行なっています。

  • フラグ位置を気にしなくてよくなった
  • 全ゾーン一括操作対応
  • 操作対象リソースの指定にID or 名称 or タグを利用可能に
  • シェル補完でbash/zsh/fish/powershellをサポート(github.com/spf13/cobraのおかげ)
  • いわゆるDry Run機能(libsacloudのFakeドライバーというやつを利用)
  • ヘルプ表示の改善(候補値の表示やパラメータ例の追加など)
  • WebAssembly対応

他にも細かな改善がたくさん行われています。この中からいくつかピックアップして紹介します。

全ゾーン一括操作

--zoneフラグにallを指定することで全ゾーン一括操作が可能となりました。

# 全ゾーンのサーバを一覧表示
$ usacloud server list --zone=all

+------+--------------+---------+------+-----+--------+-----------+----------------+----------------+--------------+
| Zone |      ID      |  Name   | Tags | CPU | Memory | IPAddress | Upstream(Mbps) | InstanceStatus | InstanceHost |
+------+--------------+---------+------+-----+--------+-----------+----------------+----------------+--------------+
| tk1a | 100000000008 | example | -    | 1   | 1      | -         | -              | -              | -            |
| tk1b | 100000000011 | example | -    | 1   | 1      | -         | -              | -              | -            |
| is1a | 100000000012 | example | -    | 1   | 1      | -         | -              | -              | -            |
| is1b | 100000000010 | example | -    | 1   | 1      | -         | -              | -              | -            |
| tk1v | 100000000009 | example | -    | 1   | 1      | -         | -              | -              | -            |
+------+--------------+---------+------+-----+--------+-----------+----------------+----------------+--------------+

# 全ゾーンのサーバのうち、名称にexampleを含むサーバをシャットダウン
$ usacloud server shutdown --zone=all example

# 全ゾーンにサーバ作成
$ usacloud server create --name example ... --zone=all

参照系だけでなく、リソース作成や更新、電源操作なども一括で行えます。

操作対象リソースの指定にID or 名称 or タグを利用可能に

Usacloudはusacloud リソース コマンド 操作対象リソースのような書式となっています。
この中の操作対象リソースの部分はv0系だとID or Nameだったのですが、v1からはタグも利用可能になりました。

# 全ゾーンのサーバのうち、env=stagingというタグがついているサーバを一括シャットダウン
$ usacloud server shutdown "env=staging"

いわゆるDry Run機能

libsacloudにはFakeドライバーという機能があり、さくらのクラウドAPI呼び出しをローカルで擬似的に行えるようになっています。 主にテスト用途での利用を想定したものです。

# dry-run
$ usacloud server create --name example --fake

# 作成したリソースのデータを永続化しておきたい場合は--fake-storeを併用する
$ usacloud server create --name example --fake --fake-store ~/.usacloud/fake_store.json

Fakeドライバーはバックエンドストレージとしてインメモリ/ローカルファイルを選べるようになっており、ローカルファイルを用いればデータの永続化も行えるようになっています。
また、Terraformのさくらのクラウド向けプロバイダーPrometheusのさくらのクラウド向けExporterでもFakeドライバーが利用可能で、 例えばTerraformで作ったリソースをPrometheusで監視しつつUsacloudでリソースのCRUDや電源操作の動作確認、といったことを実際のリソースを作成することなく行えます。

RESTコマンドの追加

上級者向けではあるのですが個人的にお気に入りの機能なので紹介しておきます。

docs.usacloud.jp

さくらのクラウドAPIをUsacloudやAPIライブラリlibsacloudの抽象化を通さずに直接叩くためのコマンドです。

# 特定サーバの電源をOFFにする: https://developer.sakura.ad.jp/cloud/api/1.1/server/#delete_server_serverid_power
$ usacloud rest -X DELETE -d '{"Force":true}' https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1/server/$ID/power 

# APIルートURLは省略可能
$ usacloud rest -X DELETE -d '{"Force":true}' /server/$ID/power 

# パラメータはファイルからでもOK
$ usacloud rest -X DELETE -d parameter.json /server/$ID/power

curlを直接使えば良い、という感はありますが、APIキーやゾーンの指定は結構面倒なのでなかなか便利です。

WebAssembly(WASM)対応

GOOS=js GOARCH=wasm go buildでビルドできるようになりました。 実行するにはグルーコードが必要ですがnodejs上やブラウザ上でもUsacloudを動かせるようになりました。

# nodejs上でwasmを実行
$(go env GOROOT)/misc/wasm/go_js_wasm_exec usacloud.wasm -v

1.0.0 js/wasm, build c0ceb3ae

せっかくWebAssemblyに対応しましたのでブラウザ上でも動かせるように次で紹介するChrome拡張UsaConもUsacloud v1.0と同時リリースしました。

さくらのクラウドのコントロールパネル上でUsacloudを実行できるChrome拡張UsaCon

f:id:febc_yamamoto:20201222102242p:plain

chrome.google.com

利用方法についてはこちらのドキュメントを参照ください。 docs.usacloud.jp

UsaConは@wasmer/wasm-terminalというコンポーネントを用いて前述のUsacloud v1.0 WASM版を動かせるようにしたものです。 github.com

@wasmer/wasm-terminalXterm.jsComlinkなどを用いた、WASIという規格に対応したWASMをブラウザ上で実行できるコンポーネントなのですが、 これをGoで作成したWASMを実行できるように改修して利用しています。

(この辺の実装の詳細については機会があればどこかに書いておこうと思っています。)

今はストレージもエディタも付属していないのですが、今後BrowserFSみたいなやつでバックエンドをオブジェクトストレージにしてみたり、vim.wasmみたいにエディタを動かせるようにしたりすると面白いんじゃないかなと思っています。(出来るかはまだわかりませんが)

なお、UsaConはAPIキーの保存にCredential Management APIPasswordCredentialを利用していますのでChromeのみのサポートとなっています。 Firefox/Safariのサポートは今のところ出来ていません。Edgeはもしかしたら大丈夫かもしれませんが開発環境が手元にないので対応していません。

UsaConスクリーンショット

f:id:febc_yamamoto:20201221175149p:plain
UsaConでサーバ作成したところ

f:id:febc_yamamoto:20201221175225p:plain
UsaConでサーバの電源をONにしたところ

Usacloud v1.0 / UsaConの注意点

Usacloud v1.0では過去のバージョンと互換性のない変更がいくつか行われています。

例えば(APIで取得できる)全種類のリソースのサマリーを表示するusacloud summaryコマンドやデータベースアプライアンスのログを表示するusacloud database logsコマンドなどの廃止、パラメータ名の変更(--nw-mask-len => --netmask)などです。 Usacloud v0系からアップグレードされる方は前述の--fakeモードなどでDryRunして動作確認してみることをお勧めします。

その他にもいくつか破壊的な変更点があります。網羅的ではありませんがアップグレードガイドを用意していますのでこちらもご一読ください。

docs.usacloud.jp

まとめ

  • 様々な改善が行われたUsacloud v1.0をリリースしました
  • Usacloud v1.0をブラウザ上で動かすためのChrome拡張UsaConをリリースしました
  • Usacloud v1.0にはv0系と互換性のない変更も含まれるためアップグレードの場合は注意が必要

ということで是非ご利用ください。

以上です。

sakuracloud_exporter v0.13 − ショートメッセージサービス対応/GitHub Container Registryへの移行

さくらのクラウド向けPrometheus Exporterであるsakuracloud_exporterのv0.13をリリースしました。

github.com

主な変更点は以下2つです。

  • ショートメッセージサービスのサポート
  • GitHub Container Registryでのイメージ配布の開始

ショートメッセージサービスのサポート

ショートメッセージサービスとは

先月(2020年9月)に「さくらのクラウド ショートメッセージサービス(SMS)」が提供開始されました。

cloud-news.sakura.ad.jp

携帯電話番号をAPIまたはコントロールパネルから指定すると6桁の番号をスマートフォンや携帯電話に送信することができるサービスです。
2要素認証で使うよくあるアレですね。

こんな感じのメッセージが送られてきます。

f:id:febc_yamamoto:20201002162528j:plain

6桁の数値はさくらのクラウド側で生成、またはAPI( or コンパネ)から指定することができます。

sakuracloud_exporterでのショートメッセージサービスのサポート

sakuracloud_exporterではショートメッセージサービスのサポートとして以下2つのメトリクスを追加しました。

  • sakuracloud_esme_info : ESME自体の情報の参照用、常に1を返す
  • sakuracloud_esme_message_count: 処理したメッセージ送信リクエストの数

それぞれ以下のような値となります。

# HELP sakuracloud_esme_info A metric with a constant '1' value labeled by ESME information
# TYPE sakuracloud_esme_info gauge
sakuracloud_esme_info{description="description",id="123456789012",name="example",tags=",tag1,tag2,"} 1
# HELP sakuracloud_esme_message_count A count of messages handled by ESME
# TYPE sakuracloud_esme_message_count gauge
sakuracloud_esme_message_count{id="123456789012",name="example",status="All"} 4
sakuracloud_esme_message_count{id="123456789012",name="example",status="Accepted"} 2
sakuracloud_esme_message_count{id="123456789012",name="example",status="Delivered"} 2

sakuracloud_esme_message_countの方にはラベルとしてstatusという項目を設けています。
2要素認証SMSのAPIでは各メッセージごとにステータスを持っていますのでそれを集計してます。

// ショートメッセージサービスAPIレスポンスの例(ログ取得API)
{
  "ESME": {
    "logs": [
      {
        "messageId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "status": "Delivered", <-- この値ごとに集計している
        "otp": "123456",
        "destination": "819012345678",
        "sentAt": "...",
        "doneAt": "...",
        "retryCount": 0
      },
      ...
    ]
  }
}

メッセージ全体の件数はstatusがAllのやつを見ればOKです。

GitHub Container Registryでのイメージ配布の開始

v0.13.1からDocker Hubでのイメージ配布に加えてGitHub Container RegistryでもDockerイメージの配布を行うようになりました。

github.com

当面は両方で配布しますが、将来的にはGitHub Container Registryに1本化するつもりです。

使い方は従来のイメージ指定部分にghcr.ioを付け加えるだけです。

# DockerHubのイメージを利用する場合(従来)
$ docker run docker run <options> sacloud/sakuracloud_exporter

# GitHub Container Registryを利用する場合
$ docker run <options> ghcr.io/sacloud/sakuracloud_exporter 

既にドキュメント類もGitHub Container Registryを利用する方法に書き換えていますので、今からsakuracloud_exporterを利用し始める方はそちらをご利用ください。

余談: ESMEってなんの略?

今回追加されたショートメッセージサービスについてですが、さくらのクラウドAPI上ではesmeという名前になってます。

f:id:febc_yamamoto:20201002165823p:plain

なんの略だろうと調べてみたら、どうもExternal Short Message Entityの略っぽいですね。

k-tai.watch.impress.co.jp

なるほど。。勉強になりました。


ということで今回は以上です。是非ご利用ください。

Terraform+GitHub Actionsでコミュニティープロバイダーを利用する方法について

さくらのクラウド向けプロバイダーなどのTerraformのコミュニティプロバイダーをGitHub Actionsで利用する際に独自のGitHub Actionsを作成しているケースを見かけました。

ak1ra24.hatenablog.com

qiita.com

私が最近よく使っているhashicorp/setup-terraform+terraform.d配下にプロバイダーのバイナリーを置くという方法だとアクションを自作しなくても済むので今回紹介しておきます。

hashicorp/setup-terraform+terraform.d配下にプロバイダーのバイナリーを置く方法

こちらのリポジトリさくらのクラウドプロバイダーを利用する例を公開しました。

github.com

Terraformで利用するtfファイル郡+GitHub Actionsとしてhashicorp/setup-terraformを利用する構成になっています。 (注: この例ではstateの扱いを手抜きしてます。本番環境で利用する際はTerraform Cloudを使うなどの対応が必要です)

hashicorp/setup-terraform

HashiCorp社が公開しているGitHub Actionsです。 前述の記事ではhashicorp/terraform-github-actionsが参照されていましたが、こちらの後継がhashcorp/setup-terraformです。

www.terraform.io

こちらの記事に新旧比較がありました。

tech.medpeer.co.jp

hashicorp/setup-terraformを利用すれば基本的なTerraformでの操作は行えそうですね。

コミュニティプロバイダーの配置

コミュニティプロバイダーは~/.terraform.d/plugins/配下以外にもいくつか有効な置き場所があります。

www.terraform.io

特にワーキングディレクトリのterraform.d配下に置く方法は

  • tfファイル類と同じリポジトリ内に配置できる -> コードとプロバイダーのバージョン管理が容易
  • プロバイダーのダウンロードが行えないような環境(Air-Gap)でも利用できる

といった利点があります。Terraform Cloudなどでコミュニティプロバイダーを利用する場合もこの方法を利用しますね。

febc-yamamoto.hatenablog.jp

この2つを組み合わせることでGitHub Actionsでコミュニティープロバイダーを利用可能となります。

実行例: https://github.com/yamamoto-febc/terraform-github-action-test/runs/709137993?check_suite_focus=true

おまけ: Terraform v0.13以降では

febc-yamamoto.hatenablog.jp

こちらの記事のようにコミュニティプロバイダーがTerraform Registryに登録されていればプロバイダーのバイナリーをterraform.d配下に置く必要もなくなります。 この辺も楽になりそうですね。

終わりに

ということでTerraform+GitHub Actionsでコミュニティープロバイダーを利用する方法について紹介しました。
コミュニティプロバイダーでも割と簡易な手順で利用できますので是非お試しください。

以上です。

Terraform v0.13開発版でプロバイダーの自動インストール/更新を試す

今年の1月にTerraform Registryでプロバイダー(3rd含む)を配布できるようにするというアナウンスがありました。

www.hashicorp.com

ベータテストへの参加を募集していたので申し込んでおいたのですが、本日(2020/5/15)参加できるようになったとの連絡をいただいたので早速プロバイダーの公開&自動インストールを試しました。

Terraform Registryでのプロバイダーの配布

従来はterraform initを実行してもgithub.com/terraform-providers配下のプロバイダーのみが自動インストールされるだけで、コミュニティが開発しているプロバイダーは~/.terraform.d/plugins/配下に格納しておくなど手動でのインストール作業が必要でした。

これがTerraform Registryにあらかじめプロバイダーの登録を行っておくことで自動でインストール/更新が可能となりました。

試しにさくらのクラウド向けプロバイダーをTerraformRegistryで公開してみました。

f:id:febc_yamamoto:20200515100233p:plain

registry.terraform.io

(ベータテスト中は上記のプロバイダー自体が予告なく削除される可能性もあります)

プロバイダーのインストール/更新

公開したプロバイダーのインストール/更新を試してみます。

Terraform v0.13開発版のビルド

試すにはTerraform v0.13の開発版が必要です。今はビルド済みのバイナリが提供されていないため、GitHubからソースをクローンしてビルドする必要があります。 Goの開発環境を用意して以下でビルドします。

$ git clone https://github.com/hashicorp/terraform.git ; cd terraform
$ make dev

これで$GOPATH/bin/terraformが作成されます。

tfファイルの準備

以下のようなブロックをtfファイルに記述する必要があります。

terraform {
  required_providers {
    sakuracloud = {
      source = "sacloud/sakuracloud"
      version = "~> 2"
    }
  }
}

terraform initの実行

あとはterraform initを実行するだけです。

f:id:febc_yamamoto:20200515101128p:plain

インストールされましたね! インストールされたプラグインはカレントディレクトリ配下の.terraform/plugins配下のディレクトリ内に格納されます。

f:id:febc_yamamoto:20200515101353p:plain

これは便利ですね! 3rd プロバイダーは更新が面倒なために一度インストールしたらなかなか更新してもらえないという問題があったのですが、 これで更新も行えるようになるため非常に楽になると思います。 (もちろんバージョンをピンすることもできます)

ということで正式リリースがとても楽しみです!!

プロバイダー開発者向け: Terraform Registryでの配布までの作業

ここからはプロバイダー開発者向けにTerraform Registryでプロバイダーを配布するまでに必要な作業をまとめておきます。

全体的には以下のような作業が必要となります。

  • HashiCorp社にベータテストへの参加を申し込む(こちらの記事を参照)
  • 受付メールがきたら以下をHashiCorp社の担当者さんに伝える
    • ベータテストに参加する人のGitHubのユーザー名(Terraform Registryへのログインに使うアカウント)
    • 署名用GPG公開鍵
  • (HashiCorpさん側でTerraform Registryでのベータテストを有効にしてもらう)
  • プロバイダーのビルド/署名(GoReleaserのテンプレートが提供される)
  • (初回のみ) Terraform Registry上からプロバイダーの登録

Note: これ以外にもプロバイダーのリポジトリ内にドキュメントを指定のフォーマットで準備しておく必要があります。
プロバイダーのテンプレートなどを参考に準備しておきましょう。

これらはベータテスト参加時に提供されるドキュメント(多分一般公開されてるやつ)に書いてありますのでそちらに従って作業していけば大丈夫です。

Terraform Registry上での公開は以下のような感じです。

まず、ベータテストに参加すると右上のPublishメニュー内にProviderというメニューが生えます。

f:id:febc_yamamoto:20200515102956p:plain

これを選ぶと次にGitHub上のオーガニゼーションを選択する画面が出てきます。
プロバイダーのリポジトリがあるオーガニゼーションを選択しましょう。

f:id:febc_yamamoto:20200515103035p:plain

オーガニゼーションを選択すると、公開するプロバイダーのリポジトリを選択する画面になります。

f:id:febc_yamamoto:20200515103140p:plain

あとはPublishボタンを押すだけです。 規約に同意したらチェックを入れてPublishボタンを押せば公開されます。

f:id:febc_yamamoto:20200515103220p:plain

ドキュメントもterraform.ioのように綺麗に表示されてますね。

f:id:febc_yamamoto:20200515103347p:plain

なお、古くからプロバイダーを書いている場合、ドキュメントの手直しが少々必要かもしれません。 具体的には各リソースのドキュメント(.md)の先頭にサブカテゴリー(サイドバー)の定義を書いていく必要があります。 この辺はTerraformのドキュメントを参照の上適切に直しておきましょう。


ということでTerraform v0.13の機能を先取りで試してみました。

従来の3rdプロバイダーは更新が面倒なために、古いバージョンを使っていること起因の問題がよく発生していました。 これを解消できますのでかなり期待しています。

v0.13のリリースが待ち遠しいですね!

以上です。