ほわいとぼーど

ぷろぐらまのメモ帳

hashicorp/go-pluginを試しかけたメモ

GolangでPlugin機構を使ってみたくなった。
Golangは1binaryが利点の1つだと思っているけど、
本体とPluginを分離することによりPlugin開発速度と本体の堅牢さの両立を
維持したいケースもあると思います。

例えばmackerelio/mackerel-agent-pluginsなんかは良い例。
他にPlugin機構を持っているケースとしてHashicorpプロダクトが思い浮かんだので
ちょっと調べたところ hashicorp/go-pluginというライブラリを見つけたので触ってみた。
go-pluginはTerraformで使われている。

ちなみにHashicorpのPluginに関する話としてmitchellさんの動画がある。
2016 Kickoff - Hashicorp & Go Plugin Architecture
今年の2月のものだけど、これまでのHashicorpのPluginに対する開発歴史や今後も語られていて面白い。
今のTerraformがv5で、今後はよりSecureかつユーザフレンドりな要素を入れたv6を作って
VaultでもユーザがPluginを作れるようにしたいそう。

ということで、hashicorp/go-plugin/exampleを試してみる。
このexampleは単体で動くようになっているので、ビルドして実行すれば動く

$ go build main.go
$ ./main
Hello!

内部的にはmain()の中から再度mainバイナリをpluginとして起動すると
mainPlugin()がPlugin側として動作するものらしい。

しかしこれだとPluginの恩恵が感じられないので本体とPluginが分離した状態で動かしたい。
もっとも簡単にやるならmain.goを複製してplugin.goを作成し、
mainPlugin()をmain()にして不要なimport文を削れば動く・・・のだが
それでは意味がないので試行錯誤した。
なお、完全に理解したとは言い難い(特に言語的に)のでご容赦ください。

最終的にした構成はmain.go plugin1.go plugin/plugin.go plugin/greeter_plugin.go

plugin/plugin.go

package plugin

import (
    "os/exec"

    "github.com/hashicorp/go-plugin"
)

var handshakeConfig = plugin.HandshakeConfig{
    ProtocolVersion:  1,
    MagicCookieKey:   "BASIC_PLUGIN",
    MagicCookieValue: "hello",
}

func NewClient(cmd string) (c *plugin.Client) {
    return plugin.NewClient(&plugin.ClientConfig{
        HandshakeConfig: handshakeConfig,
        Plugins:         pluginMap,
        Cmd:             exec.Command(cmd, "plugin"),
    })
}

// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
    "greeter": &GreeterPlugin{},
}

func Serve(g Greeter) {
    plugin.Serve(&plugin.ServeConfig{
        HandshakeConfig: handshakeConfig,
        Plugins:         PluginMap(g),
    })
}

func PluginMap(g Greeter) map[string]plugin.Plugin {
    return map[string]plugin.Plugin{
        "greeter": &GreeterPlugin{PluginFunc: g},
    }
}

本体とPlugin双方で使うHandshake、pluginMapの部分をまとめている。
NewClientのexec.Commandの部分はPluginパスを起動元から受けて実行する形にした。
本体側の入力に応じて実行するPluginを変えることもできるし、
hashicorp/go-pluginにDiscoveryの仕組みも入っているので色々できそう。
Serve()はPlugin側で定義したものを受け取ってセットする。
元はここがPlugin用に定義してるものを直接読んでいたために、最後まで一番ハマった。
雑に直した感あってベターなのかよくわからない。

plugin/greeter_plugin.go

package plugin

import (
    "net/rpc"

    "github.com/hashicorp/go-plugin"
)

// Greeter is the interface that we're exposing as a plugin.
type Greeter interface {
    Greet() string
}

type GreeterPlugin struct{
    PluginFunc Greeter
}

func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
    return &GreeterRPCServer{Impl: p.PluginFunc}, nil
}

func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
    return &GreeterRPC{client: c}, nil
}

// Here is an implementation that talks over RPC
type GreeterRPC struct{ client *rpc.Client }

func (g *GreeterRPC) Greet() string {
    var resp string
    err := g.client.Call("Plugin.Greet", new(interface{}), &resp)
    if err != nil {
        // You usually want your interfaces to return errors. If they don't,
        // there isn't much other choice here.
        panic(err)
    }

    return resp
}

// Here is the RPC server that GreeterRPC talks to, conforming to
// the requirements of net/rpc
type GreeterRPCServer struct {
    // This is the real implementation
    Impl Greeter
}

func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error {
    *resp = s.Impl.Greet()
    return nil
}

Greeter interfaceに関わる部分を集約。
Plugin側で定義したコマンドを受け取るためにGreeterPluginに要素生やして
GreeterRPCServerに渡している。
本来はPluginとのI/F定義が集約されるので一番大事なのだが、
まだ理解しきってないので今後の課題です・・・

plugin1.go

package main

import (
    "github.com/a3no/go-plugin-example/plugin"
)

func main() {
    plugin.Serve(new(GreeterHello))
}


// Here is a real implementation of Greeter
type GreeterHello struct{}

func (GreeterHello) Greet() string { return "Hello!Hello!!" }

plugin起動時にGreeter interfaceの実体を渡している。

main.go

package main

import (
    "fmt"
    "io/ioutil"
    "log"

    "github.com/a3no/go-plugin-example/plugin"
)

func main() {
    // We don't want to see the plugin logs.
    log.SetOutput(ioutil.Discard)

    // We're a host! Start by launching the plugin process.
    client := plugin.NewClient("./plugin1")
    defer client.Kill()

    // Connect via RPC
    rpcClient, err := client.Client()
    if err != nil {
        log.Fatal(err)
    }

    // Request the plugin
    raw, err := rpcClient.Dispense("greeter")
    if err != nil {
        log.Fatal(err)
    }

    // We should have a Greeter now! This feels like a normal interface
    // implementation but is in fact over an RPC connection.
    greeter := raw.(plugin.Greeter)
    fmt.Println(greeter.Greet())
}

ラップしたNewClientを呼ぶように変えたくらいで元のmain()と大差なし

これでそれぞれビルドして動かせば動く

$ go get github.com/a3no/go-plugin-example/plugin
$ go build plugin1.go
$ go run main.go
Hello!Hello!!

なんとか分割したもののPluginとのI/Fを自在に定義できるようにならないと
柔軟には使えないと思うのでもうちょいどうにかしたい。

雑にAWSリソースにboto3でタグつけてみたのだが

既存のAWSリソースに管理のためにタグ付け直すことになり
それ用のスクリプトを適当に書いたのだが、
なんでリソース毎にAPIが全然違うのか、という気分になったのでメモる。
ちなみにboto3です。

EC2

read ec2 tag

client = boto3.client('ec2', "ap-northeast-1")
resp = client.describe_instances()
for resv in resp['Reservations']:
    for inst in resv['Instances']
        print(inst['Tags'])

write ec2 tag

client = boto3.client('ec2', "ap-northeast-1")
resp = client.create_tags(
    Resources=[ec2id],
    Tags=[
        { 'Key': 'Env', 'Value': 'production' }
    ]
)

取得はInstanceと一緒に出来るので比較的楽。
むしろReservationsって何って感じだが今回は割愛。
付与は専用だがこれも比較的素直

RDS, ElastiCache

read rds tag

client = boto3.client('rds', "ap-northeast-1")
resp = client.describe_db_instances()
for rds in resp['DBInstances']:
    resp2 = client.list_tags_for_resource(
        ResourceName="arn:aws:rds:ap-northeast-1:accountid:db:" + rds['DBInstanceIdentifier']
    )
    print(resp2['TagList'])

write rds tag

client = boto3.client('rds', "ap-northeast-1")
resp = client.add_tags_to_resource(
    ResourceName="arn:aws:rds:ap-northeast-1:accountid:db:" + dbidentifier,
    Tags=[
        { 'Key': 'Env', 'Value': 'production' }
    ]
)

Tag操作は取得も別メソッド。かつ、リソースの指定方法がARN。

ELB

read elb tag

client = boto3.client('elb', "ap-northeast-1")
resp = client.describe_load_balancers()
for elb in resp['LoadBalancerDescriptions']
    resp2 = client.describe_tags(
        LoadBalancerNames=[elb['LoadBalancerName']]
    )
    for tagdesc in resp2['TagDescriptions']:
        if tagdesc['LoadBalancerName'] == elb['LoadBalancerName']:
            print(tagdesc['Tags'])

write elb tag

client = boto3.client('elb', "ap-northeast-1")
resp = client.add_tags(
    LoadBalancerNames=[elb_name],
    Tags=[
        { 'Key': 'Env', 'Value': 'production' }
    ]
)

取得する方はタグも複数形で帰ってくる。
ではと、複数ELBを指定して一気にとって後で突き合わせればと思ったのだが、
一度に指定できる数が20までと怒られたので結局1個ずつ処理した。
Documentにも上限書いてないのだが・・・
Listタイプのメソッドは結果がページングになっていることはあるが、
入力も分割必要なのは結構ダルい。

付与する方はほぼEC2と一緒。

Kinesis

read kinesis tag

client = boto3.client('kinesis', "ap-northeast-1")
resp = client.list_streams()
for stream_name in resp['StreamNames']:
    resp2 = client.list_tags_for_stream(StreamName=stream_name)
    print(resp2['Tags'])

write kinesis tag

client = boto3.client('kinesis', "ap-northeast-1")
resp = client.add_tags_to_stream(
    StreamName=stream_name,
    Tags={ 'Env': 'production' }
)

一見、RDSに近い(ARNではないが)ように見えるが、
付与する際のTagsの形状が異なることがお分かりになるだろうか。
あと、付与時に複数に連続処理していると、
必ず10個連続実行の後にRateLimitExceptionになったのでtime.sleep挟むようにした。
他のAPIでは待たされることはあってもエラーとなることは無かったので何でコレだけ・・・

Redshift

read redshift tag

client = boto3.client('redshift', "ap-northeast-1")
resp = client.describe_clusters()
for cluster in resp['Clusters']:
    print(cluster['Tags'])

write redshift tag

client = boto3.client('redshift', "ap-northeast-1")
resp = client.create_tags(
    ResourceName="arn:aws:redshift:ap-northeast-1:accountid:cluster:" + cluster_name,
    Tags=[
        { 'Key': 'Env', 'Value': 'production' }
    ]
)

取得は一緒にとれる。付与はARNタイプ

S3

read s3 tag

client = boto3.client('s3', "ap-northeast-1")
resp = client.list_buckets()
for bucket in resp['Buckets']:
    tags = []
    try:
        resp = client.get_bucket_tagging(Bucket=bucket['Name'])
        tags = resp['TagSet']
    except:
        pass
    print(tags)

write s3 tag

client = boto3.client('s3', "ap-northeast-1")
resp = client.put_bucket_tagging(
    Bucket=bucket_name,
    Tagging={
        'TagSet': [
            { 'Key': 'Env', 'Value': 'production' }
        ]
    }
)

意味不明ぽいがTagにアクセスして存在しなかった場合Exceptionになるので
仕方なしにこの形になった・・・
boto3Document一通り眺めたけどすぐには他に思いつかなかず。
ちなみに他のプロパティも同様の挙動をしました。

付与する方も少し階層が他と異なる。

感想

もちろん、処理方法は1種類しかないわけじゃないので違う書き方もできるし、
見落としてる部分もあるかもしれない。
とりあえず各リソースの一覧+タグを拾ってきて、それを更新するのを雑に書いてみた結果です。
ただboto3になったのここ1年くらいだったと思うので思いのほかバラついてるなぁと思った次第。

開発環境をDockerにした話

若干釣りタイトルっぽい。
どの開発環境かという話だけど、自分の場合、

  • インフラ用途のちょっとしたPythonスクリプトの開発
  • Terraformをローカルから実行する
  • 開発したAnsibleのRoleをローカルで試す
  • golangの開発をローカルで行う

みたいなのをこれまではVagrantVirtualbox上で行ってきた。
主な理由として、

  • 家のPCはWindowsLinux開発をローカルで実施しにくい
  • 会社のMacでもローカルを汚したくない
  • 家と会社の環境を共通化したい

みたいなのがある。
TerraformやgolangMacローカルにも入れてたのだけど、
BrewCaskでの更新が危うく、やはりローカル直は管理ダルイよねと。
Vagrantでも回っていたんだけ思考。

でDockerでやることにしたんだけど、 やるだけならぶっちゃけ簡単で
Docker Toolboxさえ入れておけば、あとは
python:2.7.11-slim」とか「golang:latest」
とか引っ張ってきてほぼ終了なのだが、
利便性のためにいくつかのツールを同梱するようにした。

  • direnv
  • peco
  • ghq

direnvはAWS複数アカウント切替のため。
direnv allowの結果を保持するのでユーザホームもマウントしている。
peco+ghqはレポジトリ管理をgopath方式にしていて便利に使うため。
$GOPATHをマウントする。捗る。

あと一番やりたかったのはAnsibleで1.9系と2.x系の移行期をDockerで切り抜けたいのだが、
実はココはまだうまくいってない部分がある。

そんなわけで雑なDockerfileを量産体制に入りつつ、
余勢を駆ってあわよくばアプリもどうにかしていきたいと画策しているところ。
Mac for Dockerはまだ試してません。

Docker Toolboxで時刻を合わせる話

Docker ToolboxでDocker使っていて時刻がずれて
直し方をなんだかんだで何度か調べているのでメモっておく。
(自分が欲しい方法が一発で検索されない)

ググると何種類か方法が出てくるのだが自分が直したい場面はそんな致命的じゃなくて
スリープ直後に時間がずれてAWS APIとか叩くと怒られた時くらいなので  

docker-machine ssh default "sudo ntpd -q -p 1.jp.pool.ntp.org"

Docker Toolboxの裏側のboot2docker vmssh経由でntpdコマンドを実行する。
これをMacはalias、Windowsはシェルで置いておいて、
怒られたら一旦containerから出て(もしくは別窓開いて)実行する。

元コマンドは次を参考にしました。
MacでDockerを動かす際に気をつけること

Windosw環境のDocker Toolboxでshared foldersのpermission設定する話

最近、VagrantをDockerに置き換え始めてるのだけど、 以前*1と同じ問題が発生したので対応した。

Docker Toolboxも裏側はVirtualbox + boot2dockerなので基本は一緒なのだが、
Docker Toolxoxだと今のところ設定でどうにかする方法は用意されてなさそうなので、
(前回のmount_optionはあくまでVagrantの機能実装)
作成されたVMを権限変えてマウントしなおす。

docker-machine ssh default "sudo umount c/Users"
docker-machine ssh default "sudo mount -t vboxsf -o uid=0,gid=0,fmode=0666,dmode=0777 c/Users /c/Users"

この作業はVMをcreateしたりstop-startする度にやる必要があって、
さすがに面倒だったのでクジラの絵を出してるシェルを適当にハックした。

C:\Program Files\Docker Toolbox\start.sh

この中でVMが存在するかチェックしてcreateする部分と、
VMのstatusをチェックしてstartする部分があるので

if [ $VM_EXISTS_CODE -eq 1 ]; then
  "${DOCKER_MACHINE}" rm -f "${VM}" &> /dev/null || :
  rm -rf ~/.docker/machine/machines/"${VM}"

(中略)

  "${DOCKER_MACHINE}" create -d virtualbox "${VM}"
fi

VM_STATUS="$(${DOCKER_MACHINE} status ${VM} 2>&1)"
if [ "${VM_STATUS}" != "Running" ]; then
  "${DOCKER_MACHINE}" start "${VM}"
  yes | "${DOCKER_MACHINE}" regenerate-certs "${VM}"
fi

この2箇所の後ろに2行ずつ追加すればよい。

  "${DOCKER_MACHINE}" ssh "${VM}" "sudo umount /c/Users"
  "${DOCKER_MACHINE}" ssh "${VM}" "sudo mount -t vboxsf -o uid=0,gid=0,fmode=0666,dmode=0777 c/Users /c/Users"

これで次に起動したりする際には権限を変えてマウントされる。
なお、これはdefault VMの設定なので、自分でcreateした場合には自分で対処する必要がある。
あとDocker Toolboxを更新するときにリセットされそうな気もする。

参考: Dockerにホストのフォルダをマウントしたい!

2段階認証+Windows10+SourceTree+Github

先日、「AWSとGitHubに2段階認証の導入」という記事を見かけたのと
同僚が2段階認証を啓蒙していたので設定することにした。
MFA用のアプリはGoogleAuthenticatorをこれまで使っていたのだけれど、Authyに変更した。
多数並んだ時にアイコン付きで非常にわかりやすそうで、
更にバックアップ面でも効果がありそう。

入れ方はココを参照した。

  • 若干UIが変わってる部分があったが流れを読めばさほど問題ない
  • GoogleChromeExtensionと連携する際にアプリ側で受け入れる設定をする箇所があった
  • GoogleChromeExtensionがアプリ側で設定した更新の一部がうまく反映されないような・・・
    • 入れなおしたら反映されたのでバージョンとかあるかも、、Authyの公式から順に入れよう
  • 登録内容は暗号化されてサーバに保管されるので携帯交換が起きてしまってもAuthyから復旧できるらしい

Githubを2段階認証にするとhttps方式でアクセスしていた人はそのままではダメで、
アクセストークンを設定する必要があるそうなのだが、
ここはオーソドックスにSSH認証に変更する。
更に複数アカウントの人の場合はssh_config等で工夫する必要があり、
Linux的には普通だが自分の場合はWindowsだったので少し手間取った。

Windows10、SourceTreeを利用した設定をメモしておく。

基本的には次の通り
Windows+SourceTree+SSHに関してはこれ

  • msysgit導入
  • SourceTree設定で「システムGitを使用」
  • SourceTree設定で「PuTTY/Plink」ではなく「OpenSSH」を設定する
  • C:\Users\username.ssh\config にssh_config設定
    • C:\Program Files\Git\etc\ssh\ssh_config でもよいかもしれないが権限変更が必要だったので回避

何でOpenSSH設定でいけるのかちゃんと調査してないが、
msysgitを導入すると使えるようになった気がする。
というのはSourceTreeの「ターミナル」からsshコマンドを実行できているので。

2015年振り返り

適当メモ

雑感

SIerから事業会社に転職して1年以上が経ちました。
転職自体はおおむね満足しています。
やりたい事がやれる、というと若干語弊があるのですが、
不足しているものはいくらでもあって気づいた人が手を上げればやれる、という感じでしょうか。
インフラ的ポジションになってこちらも1年強ですがやりたいことは尽きないです。
前に戻りたい要素は無いですね。
自分の技量面の不足は圧倒的に感じます。

仕事の内容で見ると前半は仕組みを作ろうと少しずつやってたのですが
後半は運用雑務に追われてしまったように思います。
夏からチームメンバーが増えたので自分が運用保守している間に
なにかしら新しいものを作ってもらうことが出来るようにはなってきました。
2016年はもっと柔軟な構成をとれるように土台の仕組みづくりしていきたいです。

ざっくりやったこと

  • AnsibleでSetup
  • AWSもAnsibleで
  • mackerelで監視
  • fluetdでログバックアップ(S3)とログ可視化(Kibana)
  • AlertもERRORログSlackへ
  • hubot+Slackでデプロイ、Setup、一部状態管理等

AWSPython、Ansible、Hubotばかりやっている印象でした。
同僚も「最近nodeJSばかり書いてる」と言ってた時期がありましたが自分はもっと・・・

AnsibleをSlackから実行するようになって未来感あるかと思ったけど
結果使う人が限定されかけていて負債化しかけているような。
作ったもの全般的にそうですね、普及施策が不足orやり方が悪いです。

スケールという意味で伸び悩み

プルリクはmackerel-agent-pluginsにいくつか出来たけど思ったより出来なかったです。

勉強会参加

2/13 第8回elasticsearch勉強会
3/12 道玄坂LT祭り第2回(統計、機械学習、データ抽出)
4/15 SmartNews Tech Night Vol.2 ~クラウド事業者に就職する以外の インフラエンジニアの生きる道~
5/26 mackerel meetup #4
6/3 norikra meetup #4
6/26 IPROS Tech Meetup!インフラ系SaaSカジュアルトー
7/2 サポートエンジニアNight
7/26 JTF
8/19 rebuild meetup
8/20-22 YAPC
9/17 mackerel meetup #5
10/9 次世代クラウド勉強会 Docker、DevOpsを取り巻くマイクロサービスのコンセプト
10/10 渋谷Java
10/18 次世代Webカンファレンス (ustream)
12/14 Workflow Hacks #1
12/15 Embulk meetup #2

今年は運よく1度発表させてもらえました。皆様には非常に感謝しています。
コミュ障なんで外部勉強会行っても知り合い増えない(懇親会参加しない)ですが、
発表する立場になると話しかけてもらえるという利点はありました。
開発力が無いので共有できるとしたら運用とかになるのでしょうが、
現状は同僚にもうまく共有できてないので悩ましいです。

勉強会はモチベーションアップか現場の温度感を感じに行くことが多いのですが
今年はそういった意味では面白いのが多かったですね。
rebuildからYAPCの盛り上がりが個人的にも大きかったです。
podcastもっと流行りそうですね。rebuild supportersお勧めです。
自分はrebuild.fm, mosaic.fm, admins.bar 聞いてます。

全般的に参加の方向性は完全にインフラ側に寄りました。

社内勉強会

http://takezoe.hatenablog.com/entry/2015/12/26/163807

毎週開催してもらえて頭の下がる思いです。
岡本さんの回がYAPCで見れなかったのが残念。

社内では1度発表させたもらったのと、
エンジニアチームで主催している社外勉強会の一環でAnsibleのハンズオンをしました。
教えるための資料作りは知識が体系化されるのでいいですね。
しかし代償に精神の疲弊が半端ないす。

買った本

  • WEB+DB PRESS 85
  • Webエンジニアが知っておきたいインフラの基本
  • 実践ドメイン駆動設計
  • WEBエンジニアの教科書
  • WEB+DB PRESS 88
  • たのしいインフラの歩き方
  • Nginxリファレンス

相変わらずの積読・・・
情報はネットで仕入れまくってるとはいえ、
どこかで体系的には抑えておきたいものです。

その他

婚活ェ・・・
プライベートは全然だけど仕事が好転したので今後に期待したいです。
Griffon隣にあるんで一人飲みも覚えるべきか。

映画

あんま見てない、、、
後はベイマックスを友人宅で見たか。
Amazon Fire TV Stickを買ったのでもっと家に篭りそうです。

ゲーム

最近は携帯携帯でしかやってなくて、
携帯のゲームは時間を気にしすぎるのでそれも止めました。
FF7remakeを果たしてどうするか

2016年はもっと時間が効率的に使えるようになれるといいなぁ。