読者です 読者をやめる 読者になる 読者になる

ほわいとぼーど

ぷろぐらまのメモ帳

Terraform 0.6.16 で格闘したことのメモ

Terraform

Terraformの現バージョン(0.6.16)で相対した悩ましい問題のメモ。
暫定対処したが根本解決はしてない?。次のバージョンでは一部しそう。

aws_route_tableのrouteを削除しても適用されない

差分も出ないし実行もされない。
Issueが出ている -> Inline routes not removed from aws_route_table. #7632
routeをやめてaws_routeで全て書くようにしたら大丈夫になったような?。

aws_route_tableのrouteが変更してないのにplan上は差分が出る

planで差分が出るがapplyでは何も実行されない。
差分と言ってもA.1->A.2みたいな同じrouteを違う名前で付け直してるみたいな表示。
routeが複数(もしくはgateway使った時)の時しか起きないかも。
Issueが出てPR mergeされた? -> aws_route_table: Inconsistency when using gateway_id / nat_gateway_id #6551
routeをやめてaws_routeで全て書くようにしたら差分表示されなくなった。

複数のaws_security_groupで相互にingressできない

sg-Aのinboudにsg-B、sg-Bのinboundにsg-Aを設定したい。
Circle とか怒られる。
aws_security_group_ruleで全て書くようにしたら作成できた。

Accountをまたいだaws_security_group_ruleでplan上は差分が出る

Accountをまたいだ設定の時にAccount付とAccount無しを比較しているように見える。
Issue-> Terraform EC2 security group ingress rule flapping #6135
対策なし (-/+と出るのが救い)

aws_security_groupとaws_security_group_ruleを同時に使うとおかしくなる

これは問題というよりそういう仕様で注意書きがある -> NOTE
同じリソースにそれぞれアクセスして片方が上書くのでtfstateと合わなくなる。
aws_route_table/routeとaws_routeもそう。
ingressとegressは別のリソース判定っぽそう?

aws_iam_policy_attachmentで共通policyをattachmentする際には要注意

users/roles/groupsとリストになっているのを見れば気づくかもしれないが、
共通のpolicyを新しく作成したIAM Roleにattachしようとして、
rolesに単独で設定すると1回目は想定通り実行されるが、
tfstateを管理している場合に2回目に実行した時に他のIAM Roleがdettachされる。
この挙動はAWS Consoleやaws cli等で操作した時とは感覚的に異なるので注意が必要。

aws_iam_policy_attachmentのusers/roles/groupsには
紐付けられている全IAMが書かれている必要がある。
自分の意識では「IAM Role に IAM Role PolicyをAttach」する想定だったため、
その時作成したIAM Roleだけを書いて実行した。
(実際に実行したのは同僚でデバッグしたのも同僚・・・)
1回目の実行時は特に問題ないが、結果できるtfstateには全IAMが記載される。
この状態でもう一度(aws_iam_policy_attachmentにIAM Roleを1つだけ記載)
plan実行すると、記載した以外のIAMは前回との差分として抽出され、
apply実行すると、実際に他は外れて1個だけの状態になってしまう。

Terraform的にはIAMにpolicyをAttachするのではなく、policyにIAMをAttachするということ
なお、新しいリソースが追加される予定になっている模様
provider/aws: Add per user, role and group policy attachment (supersedes #5816) #6858

おしまい

Terraformは非常に開発速度の速いプロダクトなので、
これらは現バージョンのみの問題かもしれません。
なので説明はだいぶ雑になってますが、
自分の感覚的には非常に扱いづらい問題だったのでメモっておきました。
次のバージョンではもっとスッキリ書けると良いなぁ。

lambda-uploader (VirtualBox shared folder on Windows)で「Exception: virtualenv returned unsuccessfully」

VirtualBox VirtualEnv

Windows10上で、Docker Toolboxでlambda-uploader(virtualenv)を動かしたらエラーが出たので調べたメモ。

$lambda-uploader --no-upload -VV
中略

DEBUG:lambda_uploader.package:Virtualenv stderr: Traceback (most recent call last):
  File "/usr/local/bin/virtualenv", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/site-packages/virtualenv.py", line 708, in main
    symlink=options.symlink)
  File "/usr/local/lib/python2.7/site-packages/virtualenv.py", line 921, in create_environment
    site_packages=site_packages, clear=clear, symlink=symlink))
  File "/usr/local/lib/python2.7/site-packages/virtualenv.py", line 1353, in install_python
    os.symlink(py_executable_base, full_pth)
OSError: [Errno 71] Protocol error

 Unexpected error. Please report this traceback.
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/shell.py", line 172, in main
    _execute(args)
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/shell.py", line 73, in _execute
    venv, cfg.ignore, extra_files)
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/package.py", line 47, in build_package
    pkg.build(ignore)
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/package.py", line 68, in build
    self.install_dependencies()
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/package.py", line 138, in install_dependencies
    self._build_new_virtualenv()
  File "/usr/local/lib/python2.7/site-packages/lambda_uploader/package.py", line 171, in _build_new_virtualenv
    raise Exception('virtualenv returned unsuccessfully')
Exception: virtualenv returned unsuccessfully

こんな感じのエラーが出るわけだが、ずばり以下の記事のとおりだった。
「vagrant環境でtoxしたらハマった件」

VirtualBoxのshared folderの問題で、
VagrantでもDocker Toolboxでも起きたし、
試しにshared folderではない別の場所にコピーして実行したら問題なく動いた。

Infrastructure as Code [2016/07/07 Thu.]を聴いてきた話

勉強会 DevOps

Recruit Technologies Open Lab #03 テーマ:Infrastructure as Codeに参加して来ました。
自分にとってのDevOpsInfrastructure as Codeは3年前くらいから始まっていて、
特に「はじめるDevOps」「JTF2013」への参加が印象に強いので、
Naoyaさん、ryuzeeさん、mizzyさんといったスピーカー陣・時期・内容からいっても
まさに振り返りのつもりで参加しました。
主催のmizzyさんも振り返りと言ってらした気がする。

資料

Naoya Itoさん 「Infrastructure as Code

ryuzeeさん 「Infrastructure as Codeと企業文化」

mizzyさん 「Infrastructure as Code のこれまでとこれから」

songmuさん 「運用・監視もコード化する。開発者が監視まで考える方法論」

katchangさん 「プロビジョニングツールはMakeで決まりだろ」

感想とか

NaoyaさんによるInfrastructure as Codeの歴史から。
初めは自動化から始まって徐々にコードのバージョン管理・ビルド再現性・テストと
アプリケーションと同じ流れで来てると。
それもサーバプロビジョニングだけでなくクラウドインフラ全体に対して。
Naoyaさん的にはこのままモデリングも・・・と思ったがイマイチ来てないとか。

自分もここ1年で書いたAnsibleが組織的には負債になってしまっているので
よい方法があったら知りたいが、ryuzeeさんの組織論にもヒントがありそうだった。
Dockerが来たことで違うStepに来てるのかなという気もする。
プロビジョニング層の複雑さを違う形で解決できそう。
この辺はkatchangさんの話にもつながる。

ryuzeeさんの組織論。
DevOps云々の前に組織文化大事ですよ。
バージョン管理やテストもしてないのでDevOpsどころじゃないよと。
大きく組織構造3パターン

  1. 開発部門とインフラ部門が独立、サービスに紐づかない
  2. 部門はそのままだがサービス毎にチームを組む
  3. サービスでワンチーム

基本的には1->2->3となるにつれ変化に強くなる半面、
サービス毎に品質のバラつき等の問題もおこりうる。
チャレンジする場合は小さく初めて必ず成功させようとのこと。

自分もインフラ担当1人なのに無理して1のパターンぽくなってボトルネックになってしまったので、
どういう感じに動くと3のパターンになれるのかというのは気になるところ。
組織文化・・・

mizzyさん
手法や分類を非常に丁寧に説明されていたので資料参照。
今後でいうとコンテナの影響によりプロビジョニング部分のツールの重要性は相対的に低下して
サーバ管理というよりはプロセス管理になっていくと。
マイクロサービス化によりテストやモニタリングが重要に。
自立制御のような仕組みが重要になっていくのでは。動的平衡

Serverspec以降の(テスト界隈の?)進展がないと仰ってて実際その辺は気になりました。
元々はリファクタのために作成されたはずですが、
インフラのモデリングが進まないのも一旦なのか、
それとToolもまだ少ない(Terraform一択)ですよね、という話をされてて割と同感。

さわのぼりさんのpodcast聴かなくちゃ。

songmuさん
監視やログは標準化した方が良いby「マイクロサービスアーキテクチャ
やっていくしかない
監視は継続的なテスト(先にやるか後にやるか)
欲しいapi生やしてモニタリングできる
アプリケーションエンジニアはコード書くの得意なので見たい項目を取ろう

見たい人が書いて改善してくの良いよなぁと思ってるけどどうやって推進すればいいのか難しい。
組織論の3のパターンだとこういうの進みそう。
Toolの説明してるように見えてチームの精神みたいなのも垣間見れる発表でした。

katchangさん
makeでプロビジョニングする。エラーも即中断して便利。
一般のプロビジョニングツールは宣言型アプローチ
実は冪等ではない(記述を削除しても反映されない)

ネタっぽい流れでありながらも考察は的確で
Dockerfileで実は一周回ってきた感あるし同感、みたいな話だった。
シンプルなのがいいよねっていう話なのかも。
そういえば誰かもイージーよりシンプルという話をしてた気がする・・・

まとめ

おっさん的振り返りとして参加したが学びも非常にあって良かった。

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

Golang

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 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にした話

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 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を動かす際に気をつけること