ツールを作ろうと思うモチベーションと、作る流れ

以下は完全に自分の独断と偏見で書いており、こうすべき!と押し付けるものでは一切ないことを明記しておきます。また、ツールって言ってるのはコマンドラインツールが主です。

モチベーション

なぜツールを作ろうと思うのか

何かやるのに手数が多い

オペレーションの工数が多くてそれが人間の手作業であるとき、往々にしてミスは発生する。 ヒューマンエラーを極力なくすために、自動化できるところは自動化すべきである。 操作がコード化されたら、その振る舞いに対してテストを書いて検証することもできる。

例えば Auto Scaling Group で管理された + ELB に吊るされた Elasticsearch のノード入れ替えは、以下の手順で行う必要があった(インスタンス起動時に Elasticsearch が立ち上がること + EC2 Discovery を仮定)。

  1. 退役させるノードを1台選ぶ(対象ノード)
  2. 対象ノードを ELB から外す
  3. Draining が終わって完全に外れるまで待つ
  4. (Elasticsearch 1系のみ) 対象ノードの _shutdown API を叩いて、shard が別のノードに退避するのを確認
  5. 対象ノードを Terminate
  6. ASG により新しいノードが追加、shard が配置されるのをされるのを確認
  7. 残りのノードに対して 1. から繰り返す

これを手作業でやると、

  • リクエストを受け付けているのに、ELB から外さずクラスタから外してしまう or Terminate して 503 を返す
  • 複数台一気に Terminate してしまって shard が複製ごと吹っ飛ぶ

ような事故が起こりえる。

AWS も Elasticsearch も API が提供されているので、構築から状態確認まですべてプログラマブルに行える。 こういうのはワンコマンドで完結するよう自動化すべきである。

なお、上記の Elasticsearch ノード入換を自動化するツールは esnctl である。

実行コマンド or API リクエストが複雑

AWS の操作は awscli 経由でできるわけだけど、あれは API の薄いラッパーなので人間にはわかりづらい。 ヒューマンフレンドリーに入力値を与えたい、ヒューマンフレンドリーかつ行指向的に出力したい…

AWS はまだ awscli があるからなんとかなるかもしれないけど、他の SaaS だとコマンドラインツールがなくて SDK しか提供されてない、あるいは SDK すらないことが往々にしてある。 そういう場合は curl その他で頑張ろうとせずに、ツールを書いたほうが楽な気がする。 認証周りが面倒なので。

…とはいえ UNIX 哲学的には、awscli の出力を JSON にした上で jq などのツールをパイプで繋いでいくのが正しい。 でも結局入力がどうにもならないのと、awscli 自体 Python で少々もたつくというのがあって、小さなツールを書くという発想に至ってしまうのであった…。

既存のものが使えないか探す

すでに適切なソリューションがあるのに一から自作するのは無駄なので、まずは既存のプロダクトを徹底的に調査する。 GoogleGitHub で関係ありそうなワードを並べて検索している。 自分の Chrome はアドレスバーで「gh hogehoge」と入力すれば、GitHub で検索できるようにしてある (e.g., gh hogehoge -> https://github.com/search?utf8=%E2%9C%93&q=hogehoge )*1

f:id:dtan4:20170709174052p:plain

関係ありそうなリポジトリがヒットしたら、

  • README
  • 実装言語
  • コード

を読んで、実際に使えるかどうか判断する。 Go などで実装されていてバイナリが配布されていれば、実行が楽なので良い。 そうでなくとも Docker image が配布されていれば、手元の環境によらず実行できるのでよい*2

良さそうなら実際に手元へダウンロードして実行し、挙動を確認する。

あとは

  • コミット履歴
  • “Last commit” の日付
  • スター数
  • 作者

を見る。 特に複数リポジトリヒットしたときは要チェック。

コミット数がある程度積まれており、なおかつ Last commit が直近であれば、継続的にメンテナンスされてるということで順位が上がる。 スター数が多ければ、それだけポピュラーであるかつ各所で使われてる可能性が高いので良い。 あとは作者が(信用できると思う)企業や人、以前使って良かった別プロダクトの作者であれば、多少信用度は上がる。 中身が大切なので参考程度ではあるが。

以上調べてそれでもそのまま使えるものが無ければ、自分でツールを作ることになる。

OSS として作る

自社のビジネスロジックと密結合したものならともかく、大抵のツールは既存のポピュラーなサービス (e.g., AWS, GCP) や OSS (e.g, Kubernetes, Terraform) の上に成り立つものである(自分が作るツールの場合)。 つまり、自分たちだけが使うシステムというよりは、他人・他社も基盤技術として使うソフトウェアの使い勝手を良くするものであることが多い。

自分たちの困っていることは、同じ OSS を使う他の人も困ってるんじゃないか?と思う*3。 なので、いまこの問題を解決するツールを作れば自分たちだけでなく他の人も幸せになれる。 そうして世界の何処かにいる他の人に使ってもらえたら、自分たちの気づかなかったフィードバックやバグ修正をもらえるかもしれない。 あとは公開しておけば、そのまま自社や自分自身のブランディングにもなりえる。

というわけで、ビジネスロジックや機密情報を含まない、公開して会社的に問題ないものは OSS として作るようにしている。 もちろん今の所属先ではこれが認められている。

リポジトリオーナーを自分にするのか会社にするのか

前述したように今はそのへん自由にやってるけど、基準として

  • 自分が一人でサクッと作る -> 自分
  • チームのプロジェクトでやるとかで、自社メンバー複数人で作る -> 会社
    • 社員間でコードレビューを行うような場合

でやってる。 実際はほとんど前者、つまり自分オーナーで公開させてもらってる。

このへん企業によってはルールが決まってそう。

ツール名

プロダクトに名前をつける時に気をつけたいこと - Qiita

ググラビリティ大切。 一方でキラキラしすぎるのも良くない。

言語

適材適所ではあるけど、基本 Go で書いている。 2年前までは Ruby 使ってたけど、2016年に入ったあたりから徐々に乗り換えた。

  • ロスコンパイルできる
    • 社の開発環境は Mac に統一されてるので、それよりは OSS 公開するときのメリットが強い
  • ワンバイナリに収まるので、Homebrew 等での配布が楽
    • 社内向け private tap が存在しており、インストール & アップデートはそれ経由でやる
    • private gem 時代もあったけど、rbenv 下の Ruby バージョンそれぞれに入れないとカレントディレクトリによっては使えない事案が多発した
  • 実行速度
  • 標準ライブラリ・外部ライブラリともに豊富
    • メジャーなサービスなら大抵 SDK が用意されている

という点で選択している。

(ほとんどそんなこと無いけど)SDK がなかったり、あと DSL 自作するような場合は Ruby など他の言語も選ぶ。

作るにあたって

UNIX 哲学を意識してやる。 一つのことをうまくやる、他のプログラムと協調して動くようにする。 例えば世の中には awk, cut, tr, sed, … といった出力加工の便利ツールがたくさんあるので、変にフィルタを実装せずプレーンな出力のみ提供するようにする。

ドキュメントはちゃんと書く。 OSS として作るならもちろんそうだし、社内向けツールも実際のユーザやあとから手を入れる人のことを考えてちゃんと書く。

業務としてやる場合はデッドラインが存在するので、そこは細かいところにこだわり過ぎないようバランス取るようにする。 品質と実装スピードを両方高レベルで担保できるよう腕を磨くのが大切。

そういう意味でも、必要最小限の機能を実装した「小さなツール」を心がけるのは大切。

プロモーション

ツール作っても実際使われなければ無駄なだけなので、特に他人に使ってもらう場合プロモーションはちゃんとする。 社内ツールなら適切な場所(チャットや GitHub Issue)で広報する、OSS ならブログを書く。

同時に、フィードバックもちゃんと受けられるようにしておく。

まとまりがないけど以上。


なぜこんなもの書こうと思ったのか

よく自分の仕事内容の一部として「自動化や開発支援のツールを書いてます」って紹介するけど、どういうモチベーションでツール作ろうと思うのか & どうやっているのかというのをまとめたくなったので書いた。 ろくに文章がまとまってない気がする…。

ところで、こんな文章書いてる時間があるのなら

コードを書けという話ですね。

去年就職してからプライベートでコード書く時間が逓減したけど、最近また週末にもコード書くようになってきたから細々でも維持したい…

おわりに

リポジトリの新鮮度をよくチェックすると言いながら、自作の放置されたプロダクトの存在に心を痛めるのでした。

*1:query: https://github.com/search?utf8=%E2%9C%93&q=%s

*2:Haskell で実装されたツールとかこうなってると個人的には嬉しい。環境構築が面倒なので

*3:Terraforming や Kubernetes 系ツールはこれがでかい

ghrls: GitHub Tags / Releases を手元からシュッと確認するツールを作った

GitHub に push されたタグの一覧、またタグに紐付いた Releases の情報をワンコマンドでシュッと表示するツール ghrls を作りました。

github.com

背景

自分は何かアプリケーション・ライブラリを作るときに依存ライブラリのバージョンを細かく気にする質で、たいてい Semantic Versioning でいう patch version レベルで指定しています (e.g. ~> 1.6.10, ~1.6.10)。 そういうわけで新しい依存を加えるときはまず最新バージョンがいくつか確認しているのですが、そのために毎回

  1. 該当リポジトリをブラウザで開く
  2. コミット数と並んでいる 30 Releases をクリックして開く
  3. 先頭に記載されたバージョン番号を確認する

というステップを踏んでいて、さすがに面倒になっていました。

f:id:dtan4:20170213223606p:plain

Releases に上がっている tarball の URL を確認するには、さらに「タグをクリックして開く」「tarball を右クリックで “Copy Link Address"」が加わりやってられない。

というわけで面倒くさいが高じた結果、手元でサクッと確認したいということになり ghrls を作りました。

使い方

ghrls list <user/name> で、そのリポジトリの Tag / Release 一覧を表示します。

$ ghrls list kubernetes/kubernetes | head
TAG               TYPE           CREATEDAT                        NAME
v1.6.0-alpha.0    TAG
v1.5.3-beta.0     TAG
v1.5.2            TAG+RELEASE    2017-01-12 13:51:15 +0900 JST    v1.5.2
v1.5.2-beta.0     TAG
v1.5.1            TAG+RELEASE    2016-12-14 09:50:36 +0900 JST    v1.5.1
v1.5.1-beta.0     TAG
v1.5.0            TAG+RELEASE    2016-12-13 08:29:43 +0900 JST    v1.5.0
v1.5.0-beta.3     TAG+RELEASE    2016-12-09 06:52:35 +0900 JST    v1.5.0-beta.3
v1.5.0-beta.2     TAG+RELEASE    2016-11-25 07:29:04 +0900 JST    v1.5.0-beta.2

ghrls get <user/name> <tag> で、指定したタグの Release 情報を表示します。 対応するコミットハッシュや作成日、Release にアップロードされた成果物 URL などが表示されます。

$ ghrls get kubernetes/kubernetes v1.5.2
Tag:         v1.5.2
Commit:      08e099554f3c31f6e6f07b448ab3ed78d0520507
Name:        v1.5.2
Author:      saad-ali
CreatedAt:   2017-01-12 13:51:15 +0900 JST
PublishedAt: 2017-01-12 16:25:50 +0900 JST
URL:         https://github.com/kubernetes/kubernetes/releases/tag/v1.5.2
Assets:      https://github.com/kubernetes/kubernetes/releases/download/v1.5.2/kubernetes.tar.gz

See [kubernetes-announce@](https://groups.google.com/forum/#!forum/kubernetes-announce) and [CHANGELOG](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG.md#v152) for details.

SHA256 for `kubernetes.tar.gz`: `67344958325a70348db5c4e35e59f9c3552232cdc34defb8a0a799ed91c671a3`

Additional binary downloads are linked in the [CHANGELOG](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG.md#downloads-for-v152).

各プラットフォーム対応バイナリを上げてある例も:

$ ghrls get dtan4/s3url v1.0.0
Tag:         v1.0.0
Commit:      0ebc096229e46f560827b9e041479abf5cf3823b
Name:
Author:      dtan4
CreatedAt:   2017-02-06 00:59:16 +0900 JST
PublishedAt: 2017-02-06 01:04:06 +0900 JST
URL:         https://github.com/dtan4/s3url/releases/tag/v1.0.0
Artifacts:   https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-darwin-386.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-darwin-386.zip
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-darwin-amd64.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-darwin-amd64.zip
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-linux-386.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-linux-386.zip
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-linux-amd64.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-linux-amd64.zip
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-windows-386.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-windows-386.zip
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-windows-amd64.tar.gz
             https://github.com/dtan4/s3url/releases/download/v1.0.0/s3url-v1.0.0-windows-amd64.zip

インストール

Mac をお使いであれば Homebrew でインストールできます。 Go で書いてワンバイナリ吐くようにしておくと、Homebrew で簡単に配布できて最高。

$ brew tap dtan4/tools
$ brew install ghrls

GitHub Releases で各プラットフォーム対応のバイナリを配布しています。

このまま即使うこともできますが、GitHub API の制限で未ログインユーザは1時間に60回しか API call できないようになっています (参照)。 IP ベースの制限なので、オフィス / 集合住宅からの通信や Homebrew を併用する場合は速攻で上限に引っかかる可能性があります。

なので、心配な方はここから Personal Access Token を発行して、GITHUB_TOKEN 環境変数にセットしておいてください1。これなら1時間5000回まで上限緩和されます。

おわりに

ghrls を作ったおかげで、新しくツールを作るときの初速がだいぶ上がりました。 バグとか Feature Request、Pull Request をぜひいただけると幸いです :pray:


  1. Homebrew の場合も、HOMEBREW_GITHUB_API_TOKEN を設定することで同様の上限緩和が可能です (参照)。

Basic 認証かけるだけのプロキシサーバ Docker image 作った

Web アプリがあってとりあえず Basic 認証かけたいときは前段に Nginx 置けば楽なんだけど、毎回設定ファイル書いたり htpasswd 生成するのが面倒なので、サクッと用意できるよう Docker image を用意しました。

github.com

https://quay.io/repository/dtan4/nginx-basic-auth-proxy

以下のように、環境変数で username, password そしてプロキシ先の URL を指定すれば Basic 認証設定済みの Nginx が起動します。

$ docker run \
    --rm \
    --name nginx-basic-auth-proxy \
    -p 8080:80 \
    -e BASIC_AUTH_USERNAME=username \
    -e BASIC_AUTH_PASSWORD=password \
    -e PROXY_PASS=https://www.google.com \
    -e SERVER_NAME=proxy.dtan4.net \
    quay.io/dtan4/nginx-basic-auth-proxy

Docker Compose や Kubernetes を使って、メインの Web アプリコンテナは外部にポートを公開しない設定で立てた上でその前段に立ててあげるとセキュアで便利かもしれません。

version: '2'
services:
  web:
    image: tutum/hello-world:latest
  nginx:
    image: quay.io/dtan4/nginx-basic-auth-proxy:latest
    ports:
      - 8080:80
    environment:
      - BASIC_AUTH_USERNAME=username
      - BASIC_AUTH_PASSWORD=password
      - PROXY_PASS=http://web/

設定はサクッと書いただけなので、不備があればご指摘いただけると幸いです。

本当はなぜ作ったのか

ちょっと Amazon Elasticsearch Service を使い始めて Kibana も触ってたんですが、アクセス制限は IAM Policy 記述なので IAM リソース単位 or グローバル IP でしかいじれないのが悩みでした。ブラウザレベルで制限かけたかったので前段に何か噛ますか〜というので、とりあえず作ってみた形です。

毎回 Signed URL を発行するようにして Es クラスタドメインを隠すプロキシサーバを CoreOS が書いてたので、一緒に見てます。

github.com

余談でした。

k8stail: Kubernetes の複数 Pod のログをまとめて流し読みできるツールを作った

Kubernetes の特定の namespace にある、全 Pod のログを一括で流し読みできるコマンドラインツール k8stail を作りました。

github.com

取り急ぎこちらスクリーンショットです。

f:id:dtan4:20161118221101p:plain

インストール

Mac をお使いなら Homebrew でインストールできます。

$ brew tap dtan4/dtan4
$ brew install k8stail

その他の OS 用バイナリは GitHub Releases で配布しています。

また、対応している Kubernetes のバージョンは 1.3 以上 です。

使い方

-namespace で namespace を指定すると、その namespace に所属する全ての Pod のログが tail -f の如くリアルタイムで流れます。 -namespace 指定しなかったら default namespace を使います。

1 Pod に複数のコンテナがぶら下がっている場合は、それらもまとめて表示します。 コマンド実行後に Pod が追加された / 作り直されても、それに追従して新しい Pod のログが流れます。

Ctrl-C で止まります。

$ k8stail -namespace awesome-app
Namespace: awesome-app
Labels:
----------
Pod awesome-app-web-4212725599-67vd4 has detected
Pod awesome-app-web-4212725599-6pduy has detected
Pod awesome-app-web-4212725599-lbuny has detected
Pod awesome-app-web-4212725599-mh3g1 has detected
Pod awesome-app-web-4212725599-pvjsm has detected
[awesome-app-web-4212725599-mh3g1][web]  | creating base compositions...
[awesome-app-web-4212725599-zei9h][web]  |    (47.1ms)  CREATE TABLE "schema_migrations" ("version" character varying NOT NULL)
[awesome-app-web-4212725599-zei9h][web]  |    (45.1ms)  CREATE UNIQUE INDEX  "unique_schema_migrations" ON "schema_migrations"  ("version")
[awesome-app-web-4212725599-zei9h][web]  |   ActiveRecord::SchemaMigration Load (1.8ms)  SELECT "schema_migrations".* FROM "schema_migrations"
[awesome-app-web-4212725599-zei9h][web]  | Migrating to CreatePosts (20160218082522)

-timestamps オプションをつけるとタイムスタンプがつきます。

$ k8stail -namespace awesome-app -timestamps
Namespace: awesome-app
Labels:
----------
Pod awesome-app-web-4212725599-67vd4 has detected
Pod awesome-app-web-4212725599-6pduy has detected
Pod awesome-app-web-4212725599-lbuny has detected
Pod awesome-app-web-4212725599-mh3g1 has detected
Pod awesome-app-web-4212725599-pvjsm has detected
[awesome-app-web-4212725599-mh3g1][web] 2016-11-15T10:57:22.178667425Z  | creating base compositions...
[awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309011520Z  |    (47.1ms)  CREATE TABLE "schema_migrations" ("version" character varying NOT NULL)
[awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309053601Z  |    (45.1ms)  CREATE UNIQUE INDEX  "unique_schema_migrations" ON "schema_migrations"  ("version")
[awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463700110Z  |   ActiveRecord::SchemaMigration Load (1.8ms)  SELECT "schema_migrations".* FROM "schema_migrations"
[awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463743373Z  | Migrating to CreatePosts (20160218082522)

また、-labels オプションで Pod についた Label を用いたフィルタリングができます。 下の例では、name: awesome-app-web ラベルが付いた Pod のログのみ表示しています。

$ k8stail -namespace awesome-app -labels name=awesome-app-web
Namespace: awesome-app
Labels:    name=awesome-app-web
----------
Pod awesome-app-web-4212725599-67vd4 has detected
Pod awesome-app-web-4212725599-6pduy has detected
Pod awesome-app-web-4212725599-lbuny has detected
Pod awesome-app-web-4212725599-mh3g1 has detected
Pod awesome-app-web-4212725599-pvjsm has detected
[awesome-app-web-4212725599-mh3g1][web]  | creating base compositions...
[awesome-app-web-4212725599-zei9h][web]  |    (47.1ms)  CREATE TABLE "schema_migrations" ("version" character varying NOT NULL)
[awesome-app-web-4212725599-zei9h][web]  |    (45.1ms)  CREATE UNIQUE INDEX  "unique_schema_migrations" ON "schema_migrations"  ("version")
[awesome-app-web-4212725599-zei9h][web]  |   ActiveRecord::SchemaMigration Load (1.8ms)  SELECT "schema_migrations".* FROM "schema_migrations"
[awesome-app-web-4212725599-zei9h][web]  | Migrating to CreatePosts (20160218082522)

Why

Kubernetes のコマンドラインツール kubectl には、Pod のログを見るためのコマンド kubectl logs があります。 ですが、このコマンドは一つの Pod のログしか見られないため、予め Pod の名前を把握しておく必要があります。 ReplicationController を使って動的にポコポコ Pod を作っている場合は Pod 名にランダムな Suffix がつくので、いちいち kubectl get po で確認しないといけません。 また、大抵は1サービスに対して冗長性や負荷分散の目的で複数 Pod を立てることになります。 その場合は kubectl logs を使っていると「サービス全体の」ログを一度に見ることが困難です。 Pod の数だけターミナルを開くとかになります…

Fluentd で Elasticsearch に飛ばして云々…とか Logentries などの SaaS に飛ばしてフィルタリングする方法もありますが(所属先はもともと後者で全ログを取ってる)、確か Pod 名とかそういうフィールドがなくてフィルタリングが面倒くさそうでした。 あとターミナルからサクッと確認するのが難しいですね。

あと、Deployments / ReplicationController 使ってると Pod spec の設定をミスったときに Pod 作り直しループに突入するのですが、そうなると Pod が速攻で削除されて別名の Pod が立つのでログが全然負えなくて厳しかったです。

所属先では現在積極的に Kubernetes の導入を進めていて、先日リリースしたサービスもバックエンドはマイクロサービス化されて Kubernetes 上で稼働しています。 そういうわけで「サービス全体の」ログを手元で見たい、という需要が高まってきたので今回 k8stail を作りました。

既に johanhaleby/kubetail というのはあって、これは同じ機能をシェルスクリプトで提供しています。 が、Known issues にかかれているように終了時の挙動が微妙なのと途中から Pod が追加された場合も追従できなさそうだったので今回 Go で一から作りました。

おわりに

k8stail の紹介をしました。 さくっと作ったという感じなので詰めが甘い部分ありますが、ちょくちょく改善していく所存です。

最近公式で出た Go の Kubernetes クライアントライブラリ kubernetes/client-go を使ってみたかったので、良い題材になりました。 ただ、Kubernetes でかいので仕方ないんですがビルドにかなり時間食うようになるのが厳しいですね…

s3url: S3 の署名付き URL を一発で発行するコマンドを作った

S3 オブジェクトの署名付き URL を一発で発行できるコマンドラインツール s3url を作ったのでご紹介です。

github.com

下のような感じで、S3 オブジェクトのパスを与えると5分間誰でもアクセスできる URL が即座に発行されるコマンドです。

$ s3url s3://my-bucket/foo.key
https://my-bucket.s3-ap-northeast-1.amazonaws.com/foo.key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA***************************%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20160923T010227Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=****************************************************************

S3 署名付き URL とは、指定したオブジェクトに対する「一定期間有効な」「誰でもダウンロード可能となる」URL のことです。詳しくはドキュメントを御覧ください。

docs.aws.amazon.com

インストール

Mac ユーザであれば Homebrew 経由でインストール可能です。dtan4/dtan4 tap にレシピがあります。

$ brew tap dtan4/dtan4
$ brew install s3url

その他の OS をお使いの方は、GitHub Releases からバイナリをダウンロードしてください。

使い方

引数に S3 オブジェクトの URL を与えることで、一時的に誰でもそのオブジェクトをダウンロード可能になる署名付き URL が表示されます。デフォルトだと5分間有効となっています。5分過ぎるとアクセスできなくなります。

$ s3url s3://my-bucket/foo.key
https://my-bucket.s3-ap-northeast-1.amazonaws.com/foo.key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA***************************%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20160923T010227Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=****************************************************************

オブジェクトのは https://s3:// で始まる URL、または -b, -k オプションでそれぞれ指定することができます。

-d オプションを与えることで、秒単位で有効期間を指定することができます。

# 10分間有効
$ s3url s3://my-bucket/foo.key -d 600

--upload オプションでローカルファイルのパスを指定すれば、そのファイルを S3 へアップロードした後に URL を発行します。自分のマシンにあるファイルを他人へ渡すような場合に、アップロードを他の手段でしなくてよくなるので便利です。

# カレントディレクトリの foo.key を s3://my-bucket/foo.key にアップロードして URL 発行
$ s3url s3://my-bucket/foo.key --upload foo.key

なお、ここで発行された URL は期限付きとは言え誰でもアクセスできる URL です。なので、ファイルの機密性が高い場合は URL をパブリックな場所で公開してはいけません。チャットツールの private channel とかで必要な人だけと共有しましょう。

Why

チームで開発していると、他の人とファイルのやり取りをすることがそれなりにあるでしょう。で、他人に見られても問題ないファイルであればどこで共有しようといいでしょう。しかし、クレデンシャルファイル (e.g. AWS Management Console から落とせる IAM user の Access Key ID, Secret Access Key を書いた CSV) のような他人に見られちゃマズいファイルを共有するには、何らかのセキュアな方法を取らないといけません。

  • AirDrop
    • Mac に限定される、更にいうと Mac のバージョンもある程度限定される
    • Bluetooth で検知できる範囲にいないといけないので、リモート勤務とかだと使えない
  • Dropbox やチャットツールで直接共有
    • 他人にファイルを見られる可能性がある
    • ファイルを消し忘れて、相手が引き続きアクセスできる状態が保たれてセキュリティ上よくない
  • USB メモリ
    • 手間がかかって面倒、使いたいときに限って見つからない

そんな中で S3 と署名付き URL 使うのは便利だと思っています。

  • バケットやオブジェクトに対して細かくアクセスルールを決められる
  • URL なのでどんな環境でもだいたい送れる
  • 署名付き URL は指定した時間経つと無効化される

先述した IAM クレデンシャルの場合は、Ops チームのみ読み書きできるバケットを作成した上でそこに CSV を置き、相手には s3url で発行した URL を private chat で渡す運用にできます。

あとは機能的な why があります。公式ドキュメントや s3url のコードをみたらわかるんですけど、署名付き URL って AWS CLI で生成できないし API も1コールで生成できないんですよね…。Management Console でもいいけど。なので、この辺のやり取りを全部ひっくるめてワンコマンドでできたら便利かなー、というので作られたのが s3url なのです。

おわりに

最近作った s3url の紹介をしました。s3url、自分のチームを中心にそれなりに使われています。

同じ機能を持ちかつ s3url と名のつくツールは既にいくつかあったので例によって他の名前を考えたのですが、いいのが思いつかず…。そこで既存の見てみると、どれも LL 上の実装で言語ランタイムをインストールしないといけないやつでした。これはバイナリ置くだけで実行できるコマンドラインツール作れば、名前が被っていようと他を駆逐できるのでは…という目論見で、あえて s3url と名付けて Go で実装 & プラットフォームごとのバイナリを一括配布する形にしました。

Issue, Pull Request お待ちしています :octocat:

terraform import と Terraforming

先日 Terraform 0.7 がリリースされました。 Terraform 0.7 の目玉機能は、なんと言っても既存リソースの import terraform import ではないでしょうか。全世界の Terraform ユーザが長年待ちわびていた機能がついに搭載されたことになります。 あ、あと terraform.tfstate のバージョンが 1 から 3 に上がったので後方互換性が地味に失われているのも大きいですね…

さて、自分は1年以上前から既存リソースをコード化する手段として Terraforming を開発し今に至るまでメンテナンスしてきました。

github.com

現在ではそれなりの認知をいただき、リソース追加などで Pull Request も多くもらうようになりました。 そんなことをしていたので、既存リソース import の公式対応には注目している、むしろしなければならないような立ち位置となっています。

本記事では、terraform import を試しつつ Terraforming がこの先生きのこれるのかどうかを見ていきたいと思います。

terraform import

概要

terraform import は、既存のリソースを Terraform の管理下に置くための機能です。Terraform 0.7 時点では、tfstate(Terraform がリソース管理状態を把握する JSON)のみ生成できます。人間が書く tf ファイルの生成はできません。

公式ドキュメントはこれ => Import - Terraform by HashiCorp

This is a great way to slowly transition infrastructure to Terraform

とのことです。楽しみですね。

A future version of Terraform will fully generate configuration significantly simplifying this process.

とも言ってるので、いずれは tf の生成にも対応するのでしょう。

対応リソース

公式が提供しているだけあって、リリース時点で数多くのリソースが import 機能に対応しています。逆に言うとすべてのリソースが対応しているわけではありません。

対応リソース一覧 => Import: Resource Importability - Terraform by HashiCorp

上にあるリソースを数えてみたところ *1107種類のリソースが import に対応しています。AWS だけでなく、Azure や DigitalOcean、OpenStack など複数 provider に対応しているのも公式の強みですね。 意外と S3 系のリソースは対応していなかったりします。

実際に使ってみる

適当に EC2 インスタンスを1台立てました(以降、EC2 インスタンスの情報は一部マスキングした状態でお届けします)。

f:id:dtan4:20160818010212p:plain

おもむろに terraform import します。引数に Terraform 上でのリソース名 (tf ファイルの resource につける名前) と import するリソースの ID (ここでは EC2 instance ID) を指定してあげます。 実行すると、カレントディレクトリに terraform.tfstate が生成されます。

$ terraform import aws_instance.great-instance i-96163a09
aws_instance.great-instance: Importing from ID "i-96163a09"...
aws_instance.great-instance: Import complete!
  Imported aws_instance (ID: i-96163a09)
aws_instance.great-instance: Refreshing state... (ID: i-96163a09)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.

$ ls
terraform.tfstate

中身はこんな感じ。当たり前ですがちゃんと tfstate が生成されています。

{
    "version": 3,
    "terraform_version": "0.7.0",
    "serial": 0,
    "lineage": "c1f9c929-52e9-4b4a-897f-6c5be268e505",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.great-instance": {
                    "type": "aws_instance",
                    "primary": {
                        "id": "i-96163a09",
                        "attributes": {
                            "ami": "ami-374db956",
                            "availability_zone": "ap-northeast-1a",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "i-96163a09",
                            "instance_state": "running",
                            "instance_type": "t2.micro",
                            "key_name": "****",
                            "monitoring": "false",
                            "network_interface_id": "eni-********",
                            "private_dns": "ip-172-31-7-232.ap-northeast-1.compute.internal",
                            "private_ip": "172.31.7.232",
                            "public_dns": "ec2-52-196-13-225.ap-northeast-1.compute.amazonaws.com",
                            "public_ip": "52.196.13.225",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_size": "8",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "0",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-********",
                            "tags.%": "1",
                            "tags.Name": "great-instance",
                            "tenancy": "default",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.**********": "sg-********"
                        },
                        "meta": {
                            "schema_version": "1"
                        }
                    },
                    "provider": "aws"
                }
            }
        }
    ]
}

ところで、terraform.tfstate がすでにある状態で terraform import するとどうなるのでしょうか。 もう一台インスタンスを立てて試してみました。

f:id:dtan4:20160818010244p:plain

import はちゃんとできます。

$ envchain dtan4 terraform import aws_instance.awesome-instance i-60103cff
aws_instance.awesome-instance: Importing from ID "i-60103cff"...
aws_instance.awesome-instance: Import complete!
  Imported aws_instance (ID: i-60103cff)
aws_instance.awesome-instance: Refreshing state... (ID: i-60103cff)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.

tfstate もきっちりマージされた状態になっています。serial もインクリメントされているのでこのまま terraform plan を実行しても問題ありません。

{
    "version": 3,
    "terraform_version": "0.7.0",
    "serial": 1,
    "lineage": "c1f9c929-52e9-4b4a-897f-6c5be268e505",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.awesome-instance": {
                    "type": "aws_instance",
                    "primary": {
                        "id": "i-60103cff",
                        "attributes": {
                            "ami": "ami-374db956",
                            "availability_zone": "ap-northeast-1a",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "i-60103cff",
                            "instance_state": "running",
                            "instance_type": "t2.micro",
                            "key_name": "****",
                            "monitoring": "false",
                            "network_interface_id": "eni-********",
                            "private_dns": "ip-172-31-15-63.ap-northeast-1.compute.internal",
                            "private_ip": "172.31.15.63",
                            "public_dns": "ec2-52-198-18-169.ap-northeast-1.compute.amazonaws.com",
                            "public_ip": "52.198.18.169",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_size": "8",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "0",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-********",
                            "tags.%": "1",
                            "tags.Name": "awesome-instance",
                            "tenancy": "default",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.**********": "sg-********"
                        },
                        "meta": {
                            "schema_version": "1"
                        }
                    },
                    "provider": "aws"
                },
                "aws_instance.great-instance": {
                    "type": "aws_instance",
                    "primary": {
                        "id": "i-96163a09",
                        "attributes": {
                            "ami": "ami-374db956",
                            "availability_zone": "ap-northeast-1a",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "i-96163a09",
                            "instance_state": "running",
                            "instance_type": "t2.micro",
                            "key_name": "****",
                            "monitoring": "false",
                            "network_interface_id": "eni-********",
                            "private_dns": "ip-172-31-7-232.ap-northeast-1.compute.internal",
                            "private_ip": "172.31.7.232",
                            "public_dns": "ec2-52-196-13-225.ap-northeast-1.compute.amazonaws.com",
                            "public_ip": "52.196.13.225",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_size": "8",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "0",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-********",
                            "tags.%": "1",
                            "tags.Name": "great-instance",
                            "tenancy": "default",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.**********": "sg-********"
                        },
                        "meta": {
                            "schema_version": "1"
                        }
                    },
                    "provider": "aws"
                }
            }
        }
    ]
}

あとは、この tfstate や Management Console で得られる情報を元に tf ファイルを書き、terraform plan で差分が出なければ既存リソースの import は完了です(今回はそこまでしません…)。

Terraforming との比較

それぞれ機能の比較をしてみました。

terraform import Terraforming
メンテナ HashiCorp @dtan4
対応リソース数 107 37
(↑うち AWS 67 35
AWS 以外の provider Azure, DigitalOcean, Fastly, OpenStack, Triton DNSimple, Datadog
全リソース一括 import x o (resource type 単位)
リソースを指定した import o x
tfstate の import o o
tf の import x o

メンテナ

まず、terraform import は言わずもがな HashiCorp 本家がやっているので安心感があります。Terraform でのリソースパラメータ追加にもリアルタイムで追従できるものと思われます。 たまにあるんですよね。Terraform のバージョンが上がってパラメータ足されたから、Terraforming の生成結果が仕様を満たさなくなることが…。 一方で Terraforming は @dtan4 が(ほぼ)プライベートの時間でメンテナンスをしています。最近 Issue, PR の消化が追いつかなくなってきて危機感を感じています。

リソース数

対応リソース数は歴然とした差があります。特に Terraforming は AWS 以外がめっぽう弱いです。AWS も基本自分が触るリソースを中心に対応しているので、ある程度偏りが出てしまうのは否めません。

AWS 以外の provider

AWS 以外の provider は、terraform import と Terraforming できっかり分かれました。ちなみに DNSimple と Datadog は、会社で使っているので特別に開発したという経緯があります。

全リソース / リソースを指定した import 機能

terraform import は全リソース一括 import ができないのが痛いですね。数台程度ならまだしも、多くの場合だと数十から数百のリソースを一気にコードに落とし込みたいケースになるのではないでしょうか。 awscli と連携すればできなくもなさそうですが…。この辺、Terraforming は普通に全リソースを攫ってくるようになっています。

逆に Terraforming リソース個別の import には対応していません。前々から要望はあったりするのですが、なかなか導入する気になれず…

tf の import

terraform import は tf の生成ができないのも現時点だと難しいですね。tfstate に比べるとまだ書きやすいですが、terraform plan で差分が出ないようきっちり書こうと思うとかなり神経を使います。 Terraforming は既存リソースの import をすべてツールに任せる思想で初めから開発していたので、tf 生成は対応しています。もちろん terraform plan で差分が出ないようチェックも行っています。

というわけで

自分の結論としては、現時点ではまだ Terraforming が生き残れるということです。

安心感とリソース対応数という点では完敗です。しかし、いま実際既存 AWS インフラのリソースを Terraform コードに落とし込みたい場合に使うとなると圧倒的に Terraforming の方がお手軽だといえます(リソースが対応している場合)。 コマンド一発で自分のアカウントが管理するリソースを一括 import する機能と tf の import、この2つが terraform import に実装される日までは Terraforming が使われて続けていくでしょう。使われ続けてほしい。雑なまとめだ!

今後も多くの場面で使ってもらえるよう、Terraforming のメンテナンスは精力的に続けていきます。みなさん引き続きご支援をよろしくお願いいたします。Issue, Pull Request 大歓迎です!

とりあえず Terraforming v0.10 をいい加減今週中に出さないとですね…

*1:Chrome DevTool Console で $('#main-content > div > ul > li').length