SVGから複数のfaviconを出力したり開発環境用に色を変えたりする

  • faviconやらapple-touch-iconやらいろいろ必要なアイコンが多い問題
  • 開発環境と本番でfaviconを区別して事故を防ぎたい問題

……などの話題がアイコン界隈にはあります。

SVGを使ってどちらも解決してみよう! のコーナーです。

必要なアイコンを生成する

icon-genというnpmパッケージを使うとSVGからicoやらpngが生成できます。便利。

こういうかんじ:

      const results = await icongen(variant.src, destPath, {
        favicon: {
          sizes: [180, 192],
        },
      })

開発環境ごとにfaviconを変える

↑でSVGからico/pngを生成するグッズを手に入れたので、ソースのSVGを環境ごとに変えればよさそう。

こういうかんじ:

const generateVariantSource = (variant) => {
  const { fillColor, src: dest } = variant;
  if (fillColor === undefined) {
    throw new Error('fillColor is empty');
  }
  const buf = Buffer.from(readFileSync(variants.live.src)); // assumed buffer
  const content = buf.toString().replace(/fill="#000000"/, `fill="${fillColor}"`);
  writeFileSync(dest, content);
};

replace(/fill="#000000"/, `fill="${fillColor}"`) は色を変えるハイテクなコードです。

BuildKitによるレイヤキャッシュのtargetは変数 (ENV, ARG) を展開してくれない

feature request: allow variables in the `RUN --mount=type=bind` values · Issue #815 · moby/buildkit

RUN --mount=type=cache,target=${APP_DIR}/pkg/cache go get -v と書いても `${APP_DIR}` という名前のディレクトリが作られるだけです! びっくり!

✘╹◡╹✘ < docker run --rm api:builder ls
${APP_DIR}
Makefile
api
go.mod
go.sum

HTTP関連のRFCで現れる `N#token` はカンマ区切りのリストを表す

1#header-token みたいなのは「少なくとも1個以上のheader-tokenがカンマ区切りのリストとして現れる」と読める。

出典:

A #rule extension to the ABNF rules of [RFC5234] is used to improve
readability in the definitions of some header field values.

A construct "#" is defined, similar to "*", for defining
comma-delimited lists of elements. The full form is "#element"
indicating at least and at most elements, each separated by a
single comma (",") and optional whitespace (OWS).

RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing

aws-xray-sdk-nodeを使う時はexperimentalを入れよう、じゃないと最近のnpmパッケージとの組み合わせだと動かないぞ

タイトルがすべてです。

AWS X-RayをNodeアプリケーションに組み込むaws-xray-sdk-nodeというパッケージがありますが、これを使う時はlatest (何もバージョン指定しないとこれ) ではなくexperimentalを入れると良いです。
yarn add -D aws-xray-sdk@experimental こういうかんじ。

なぜかというとlatestだと使っているライブラリの問題で親segmentを見つけられず、Webアプリケーションのコントローラ内で発行されたHTTPリクエストがsubsegmentとして回収されないのでアプリケーショントレースとしてほとんど意味をなさないためです。

あらゆるケースで親segmentを見つけられないわけではなくautomatic modeでかつasync/awaitを使っている場合に限られます。
が、最近のアプリケーションおよびnpmパッケージはasync/awaitを使っていることが多いですし、明示的にsegmentを渡さずとも勝手にSDKが切ってくれるautomatic modeを無効にすることは現実的ではないので、多くのユースケースで問題になります。

特にREADMEに書かれていませんがissueでさらっと言及されています。

問題はautomatic modeで使っているcontinuation-local-storageというパッケージがasync/awaitに対応していないことです。experimentalではこれにパッチを当てたバージョンを使っているのでsubsegmentが回収される、ということのよう。
continuation-local-storageはいわゆるスレッドローカル変数にあたるものをNodeに提供するものですが、async/await関数のセマンティクスに対応できていないようです。

aws-xray-sdk-goを使う時はhttp.Client.Getとか使ってはダメで必ずcontext.Contextを渡さないといけない

aws-xray-sdk-goというAWS X-Rayでトレースを記録する便利グッズがある。

これはoutgoing HTTP requestも記録できるのだけれども、ある時を機会にトレースの記録に失敗するようになって試行錯誤したけど今日、IQ200になってすべてを理解した。
結論はタイトルの通りで、以下は詳細。

具体的にはこういうエラー・警告が出ていた:

[00] 2019-05-16T15:50:34+09:00 [Error] Suppressing AWS X-Ray context missing panic: failed to begin subsegment named 'example.com': segment cannot be found.
[00] 2019-05-16T15:50:35+09:00 [Warn] failed to record HTTP transaction: segment cannot be found.

つまり親のsegmentが取れていないということ。

HTTP APIなので xray.Handler を使ってsegmentを作っているはずだけど……。
コードはREADMEにある以下のようなかんじ:

func main() {
  http.Handle("/", xray.Handler(xray.NewFixedSegmentNamer("myApp"), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello!"))
  })))
  http.ListenAndServe(":8000", nil)
}

aws-xray-sdk-goはcontextを使ってrecorderというオブジェクトを持ち回しおり、このrecorderというオブジェクトがsegmentを保持している。

トレースできなかったコードはこういうかんじ:

	resp, err := c.httpClient.Get(endpointURL.String())

このメソッドにcontextは渡しているけれど、HTTPリクエストを送る時に渡していないのが原因
どうしたらいいかというとhttp.Requestオブジェクトを作ってWithContext()を呼ぶとうまくいった。

	req, err := http.NewRequest(http.MethodGet, endpointURL.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("[BUG] failed to build request: %s", err)
	}
	resp, err := c.httpClient.Do(req.WithContext(ctx))

いや〜〜〜〜〜〜〜大変。

気付くのに時間がかかったポイントとしては、以前のリビジョンではうまくいっていたのでHTTPリクエストを送るコードより設定やライブラリのバージョンに関心が寄っていたこと、他にトレースを発行するコードがなかったので全体に問題があるのかHTTPリクエスト送信に問題があるのか切り分けがむずかしかったこと、があげられそう。
手でsubsegmentを作ってうまくいくか見れば、トレースが取れてxray.Clientの使い方がおかしいと気付けたかもしれない。

ちなみになぜ以前はうまくいっていたかというと、その当時呼んでいたメソッドはWithContextを呼んでおり、最新のアプリではhttp.Client.Getを呼ぶ実装になっていたため……。

#gcp Datastoreを使っているプロジェクトでCloud Firestoreを使うには

Firebaseのはなし。

で、ドキュメントを見たり上記ブログを見るとこういった状況になったGCPプロジェクトでは詰みのように見えるが、結論からいうとあとからなんとかなる

GCPのコンソールからDatastoreのダッシュボードへ移動するとFirestoreへ「アップグレードしますか?」というボタンがあるのでこれを押せば移行できるのでプロジェクトを作り直さなくとも良い。
ただし、データが空の時に限るようなので、既にデータがあったらやはり作り直さないといけないと思う。

dockerの.envファイルむずかしい

docker-compose/dockerで使える.envファイルを、docker-compose/docker以外からも使えるようにしたい。 ちょっとしたスクリプトの実行時に source .env すれば環境変数が設定されるような体験がほしい。

が、実際には一工夫いる。

dockerの .env ファイルは K=V という形式を厳密に守らないといけない。 ので export EK=EV みたいに書くとinvalidとみなされてdocker-compose/docker実行時に正しく設定されない。

一方、.envファイルとしてvalidなフォーマットだと export がないので source しても実行時のプロセスで環境変数は設定されない。

ので、けっきょくこうした:

|sh| eval "$(cat .env | ruby -anlpe '$ = %|export | + $')" ||<

@aereal/go-dsn: TypeScriptでgo-sql-driverのDSNを組み立てるNPMパッケージを作った

go-sql-driverのDSN (Data Source Name) をオブジェクトから生成するライブラリを書きました。

yarn add @aereal/go-dsn

github.com

使い方をsynopsisから引用します:

import { formatDSN } from "@aereal/go-dsn"

formatDSN({
  dbName: "test-db",
  passwd: "mypasswd",
  user: "root",
})
// => "root:mypasswd@/test-db"

便利。

AWS RDSへ接続するようなGoで書いたアプリをAWS CDKでECSにデプロイする際に使うと便利です。

import { Ec2TaskDefinition } from "@aws-cdk/aws-ecs";
import { DatabaseCluster } from "@aws-cdk/aws-rds";

const taskDef = new Ec2TaskDefinition(this, "TaskDefinition", {});

const dbCluster = DatabaseCluster.import(
  this,
  "DatabaseCluster",
  databaseClusterProps
);

const dsn = formatDSN({
  addr: dbCluster.clusterEndpoint.socketAddress,
  charset: "utf8mb4",
  collation: "utf8mb4_bin",
  dbName: "app",
  user: "root",
});

const appContainer = taskDef.addContainer("app", {
  environment: {
    DSN: dsn,
  },
  // ...
);

たいへん便利! どうぞご利用ください。

cdk-mackerel-container-agent: ECSのServiceにmackerel-container-agentを5行で追加

www.npmjs.com

GitHubリポジトリはこちら: GitHub - aereal/cdk-mackerel-container-agent: experimental: AWS-CDK library for mackerel-container-agent

mackerel-container-agentを5行くらいで追加

先日、mackerel-container-agentがベータリリースされましたね。めでたい。

mackerel.io

早速AWS ECSで使ってみたのですが、やや設定が煩雑な印象もあります (ベータなのでフィードバックしたら改善・検討してもらえるかもしれない)。 参考: コンテナを監視する - Mackerel ヘルプ

特に MACKEREL_CONTAINER_PLATFORM は使う側からすると自明な選択なので自動化したい! 構成管理の中にロジックを含められて、かつパッケージとして再配布できるとなるとAWS CDKだよね、ということで作った次第です。 使い方はsynopsisに書いた通りで:

import { addMackerelContainerAgent } from "@aereal/cdk-mackerel-container-agent"
import { Ec2TaskDefinition } from "@aws-cdk/aws-ecs"
import { Stack } from "@aws-cdk/cdk"

const stack = new Stack()
const taskDefinition = new Ec2TaskDefinition(stack, "TaskDefinition", {})

addMackerelContainerAgent({
  apiKey: 'keep-my-secret',
  taskDefinition,
})

……と、こういうかんじです。 TaskDefinitioncompatibilitynetworkMode を見てよしなに MACKEREL_CONTAINER_PLATFORM も設定します。便利。

注意点としてmackerel-container-agentはベータ版、AWS CDKはdeveloper previewということでこのライブラリもいきなり破壊的変更が入る可能性があります。 現時点でmackerel-container-agentやAWS CDKを利用している方はリスクを承知の上でのことと思いますが、念のため。

ちなみにAWS CDKとは The AWS Cloud Development Kit (AWS CDK) is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. というもので、CloudFormationの高水準かつプログラマブルなインターフェースを提供するライブラリといった趣です。 JavaやTypeScriptで提供されているためIDEの恩恵を受けやすいことだけではなく、IAMやSecurity Groupなどセキュリティに関係する変更が含まれる場合は別途diffを出力し、ユーザが明示的に同意する入力をしないとデプロイしないといった、CloudFormationにはまだない改善点も含んだ便利なツールキットでもあります。

現在developer previewですが、GAに向けて開発が続いているので広く利用できる日も近いのではないでしょうか。

どうぞご利用ください!

どうぞご利用ください!

GraphQL: gqlgenで指定されたFieldに応じてresolverの実装を変える

GraphQLのいいところとして、クライアントがリクエストごとに欲しいフィールドを明示するので、サーバ側はリクエスト毎にレスポンス返却を最適化しやすい、という点が挙げられると思います。

gqlgenというGoのGraphQLライブラリがあり、これを使って実際に要求されるFieldに応じて、レスポンスの作成 (gqlgenではresolverという) を最適化するにはどうしたらいいか?
結論から言うと、 `graphql.ResolverContext` にクライアントが求めるFieldが入っているので、それを見たらよいです。

gqlgenでの実装例ですが、だいたいのライブラリはこういうかんじだろうと思います。ScalaのSangriaが似たAPIを供えていることは確認しています。


たとえばデータストアにはMarkdownはてな記法で書かれた本分を保存しておき、APIの応答としてはそれを展開したHTML断片を返却したい、というケース。
Markdownはてな記法の展開はそこそこコストがかかりますし、そもそも本分データは他のカラムに比べてデータ量が多く、クエリ数が増えるとDBとアプリケーションの間の転送量も馬鹿にならないので節約したいです。

gqlgenでは `graphql.ResolverContext.Field.Selections` にクライアントが求めるFieldが渡されるので、これの名前 (`Name`) の一致を見て分岐させることができます

schema.graphql:

type Query {
  article(slug: String!): Article
}

type Article {
  title: String!
  slug: String!
  formattedBody: String!
}

resolver.go:

import (
	"context"

	"github.com/99designs/gqlgen/graphql"
	"github.com/vektah/gqlparser/ast"
)

func (r *queryResolver) Article(ctx context.Context, slug string) (*Article, error) {
	var (
		article *Article
		err error
	)
	if shouldLoadBody(graphql.GetResolverContext(ctx)) {
		article, err = r.resolveArticleWithBody(ctx, slug)
		if err != nil {
			return nil, err
		}
	} else {
		article, err = r.resolveArticle(ctx, slug)
		if err != nil {
			return nil, err
		}
	}

	return article, nil
}

func shouldLoadBody(resCtx *graphql.ResolverContext) bool {
	for _, sel := range resCtx.Field.Selections {
		switch sel := sel.(type) {
		case *ast.Field:
			if sel.Name == "formattedBody" {
				return true
			}
		}
	}
	return false
}

`shouldLoadBody` が肝です。 `resolveArticleWithBody` / `resolveArticle` は省略しています。

`Selections` は `Selection` というインターフェースのスライスになっているので、構造体へキャストして `Name` を取り出します。
この例だと、 `formattedBody` が計算を要するFieldなので、これが含まれている場合のみbodyを引き、かつ展開する処理をする `resolveArticleWithBody` を呼び、そうでなければ展開を省略した `resolveArticle` を呼びます。

resolverのメソッドの返り値は構造体である必要があるので `Article` は必要とされるFieldすべての和である必要があります。
そのため `resolveArticle` は常に空の `formattedBody` を返すことになり、ちょっとドキドキしますが、そこはGraphQLのレイヤで参照されないことが担保されます。


その気になればDBから引くカラムをSelectionsに限定することもできそうですが、少なくともGoのORMの表現力だとコストばかりかかって大変そうなので、パフォーマンスに著しい影響がありそうなところだけピンポイントで使うのがよさそうです。

2018年

もう7年目。主に仕事について。
大きな仕事と新しい仕事に取り組んだ一年だったと思う。

this.aereal.org

今年前半はHTTPS化という大きい仕事を担当した。やりがいはあったけれど正直に言えば重荷が降りたという気持ちが大きい。

言葉を選ばずに言えば、2018年にこれをやるというのは問題の難しさを踏まえても遅かったなと思う。
ので、これからは自分のソフトウェアエンジニアリング力を大局的な意思決定へフィードバックできるように振る舞っていきたい。

geek-out.jp

まあいろいろ思うところはありつつ、自分が関わった仕事をふんだんに知ってもらう機会に恵まれ、インタビューなどしていただいたりもした。

this.aereal.org

その他、新しい仕事としてチームマネジメントに力を入れはじめた。

具体的に変わったことというと、中長期的な計画について考える時間が増えた。
数年後どんなチームにしたいか考えて、そのために半年〜1年後の人員計画を考えたり。
数年後はてなブログというサービスはどう成長するだろうからこういうアーキテクチャになっていくだろう、それに備えてこう改修させたい、などなど。

やるべきこと・やりたいことがとにかく多すぎて自分だけでどうにかしていくのもままならないので、自分がどうなりたいか・どうしたいかを踏まえて手段を考えた結果、自然にここに辿り着いた。

今でもマネジメントを自分がやるなんて大変なことだと思っているけれど、チームメンバーを通した先にあるはてなブログというサービスを良くしていきたいというゴールを見据えると自然と動けている。

逆にこういうかんじだから、マネジメントそのものを目的に働いていくというのはまだ想像がつかない。

新しい仕事なので慣れない点も多く、心身が付いてこないこともあり、同僚との関わり方を見直したりもした。

this.aereal.org

とにかく関わっているはてなブログを良くすることを第一に考えて動いたら自然と成果と道筋が見えてきているので、いい環境に身を置けているなと思う。
もちろん順風満帆ではないけれど。

とはいえサービスを成長させていくという仕事は外から見えてわかるレベルに達するのは難しいし、ソフトウェアエンジニアリングに限界を見てマネジメントに逃げるような姿勢をとるのはどちらの分野にも失礼だから、どっちも全力でやる。
特に来年はサービス開発という文脈に頼りすぎないソフトウェアエンジニアリングの面でちゃんと成果を出していきたい。戦略的にOSS開発をやっていきたい。

go-http-replay: VCRみたいに実際のHTTPレスポンスを保存してテストで再利用できるライブラリ

を作りました。

github.com

外部HTTPリクエストをスタブするTest::WWW::StubというPerlのモジュールや、スタブするレスポンスをあらかじめ記録しておいた実際のレスポンスから再現するVCRというRubyのライブラリなどにインスパイアされました。

使い方はこういうかんじ:

import (
    "net/http"
    "testing"

    httpreplay "github.com/aereal/go-http-replay"
)

func Test_http_lib(t *testing.T) {
    httpClient := &http.Client{
        Transport: httpreplay.NewReplayOrFetchTransport("./testdata", http.DefaultClient),
    }
    // httpClient will behave like the client that created from NewReplayTransport but DO actual request if local cache is missing.
}

ローカルにキャッシュが無い初回は実際にリクエストしそれを保存する、次回以降は保存されたレスポンスを再現するので実際にリクエストしない、といった振る舞いをします。

net/httpのAPIしか使っていないdependencies freeな実装なところがアピールしたいポイントです。

どうぞご利用ください!!!

Ruby: 非ASCII文字列がパーセントエンコードされていないかもしれないURLもがんばってパースする

normalized_url =
  begin
    URI.parse(url)
  rescue URI::InvalidURIError
    URI.parse(url.gsub(/\p{^ASCII}/) {|s| URI.encode_www_form_component(s) })
  end

URL中の非ASCII文字列をパーセントエンコードしてない (例: http://example.com/?q=姉) と `URI::InvalidURIError` が投げられるので、それを補足しUnicodeプロパティの否定を使ってエンコードしなおして再度 `URI.parse` するというもの。

雑なスクリプト中で使う用途なのでかなり雑だけど、これでだいたいうまくいったので満足。

TheSchwartzの失敗したjobとかerrorがいつどのように消えていくのか

この記事ははてなエンジニアAdvent Calendar 2018の16日目の記事です。
昨日はid:akiymによるOpenSSLはどこにいるでした。

地味にOpenSSL系のモジュールはリポジトリのセットアップ時にハマりがちで、かつたまにバージョンアップでビルド引数の渡し方が変わったりする曲者です。が、Crypt::OpenSSL::Guessはナイスモジュールですね。
あとbut sometimes consistency is not a bad thing eitherという下の句(?)があるのは知りませんでした。id:akiymはキャリア的には一応後輩(?)ですがPerlについては学ぶことばかりです。


16日目はid:aerealがお届けします。
普段はアプリケーションエンジニアですが、最近は社内カメラマンを拝命する機会をいただくことがあり、エンジニアインターン2018麻婆豆腐なしにどっしり構えられるエンジニア生きることとコードを書くことが同じエンジニアのインタビューの撮影に携わりました。
特にインタビュー二作は既にご一読いただけたでしょうか? 手前味噌ですがありのまま度の高い率直な内容で楽しんでいただけるかと思いますので、もしまだご覧いただけてない方がいらっしゃればぜひ。

さて、はてなブログをはじめとするいくつかのはてなのサービスではジョブキューにTheSchwartzというPerlMySQLを使ったミドルウェアを利用しています。

TheSchwartzの失敗したjobやerrorがいつ消えるのか、そのうち消えるといった認識だったけど、どれくらい経つと消えるのか、どういうタイミングなのか、詳しく知らないなーと思い調べました。

exitstatus

消しているところ: https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz/Job.pm#L162

  • set_exit_statusが呼ばれたとき:
    • ランダムに
    • exitstatus.delete_after < 現在時刻のレコードが
    • ……deleteされる
  • set_exit_statusが呼ばれるのは?
    • TheSchwartz::Jobを継承した子クラスで keep_exit_status_for を再実装してtruthyな値を返して、かつ completed/_failedメソッドが呼ばれたとき
    • = ジョブの実行が終了したとき

error

消しているところ: https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz/Job.pm#L126

  • add_failureが呼ばれたとき:
    • 発生時刻 (error_time) が一定時間より古いとき
      • 一定時間: 現在時刻 - maxage
      • maxageのデフォルトは7時間
    • ……deleteされる
  • add_failureが呼ばれるのは?
    • _failedが呼ばれたとき
    • = ジョブが失敗したとき

おわり

アドベントカレンダーは毎日1つ穴を開けるとお菓子が出てきたりするもので、そこから転じたブログ記事のアドベントカレンダーはもともとささやかなtipsを連続で紹介するといった風情の取り組みだったといいます。
本日の記事はアドベントカレンダーの起源に迫るような小粒でもぴりりと辛い、そんな記事を目指しましたがいかがでしたか?

明日はid:Windymeltです。

Dockerの--link/Docker Composeのlinksはdeprecatedだった、気になる脱出先は? 年収は?

知らなかった……。

Warning: >The --link flag is a legacy feature of Docker. It may eventually be removed. Unless you absolutely need to continue using it, we recommend that you use user-defined networks to facilitate communication between two containers instead of using --link. One feature that user-defined networks do not support that you can do with --link is sharing environmental variables between containers. However, you can use other mechanisms such as volumes to share environment variables between containers in a more controlled way.

https://docs.docker.com/compose/compose-file/#links

どうすればいいかというと↑に書いてあるようにカスタムネットワークを追加して、それに参照しあいたいコンテナをそれに参加させたらよい。

before

---

version: '3'
services:
  api:
    build: .
    ports:
      - '8000'
  front:
    build: .
    ports:
      - '3000:3000'
    links:
      - api

after

---
version: '3'
services:
  api:
    build: .
    expose:
      - '8000'
    networks:
      - local_dev
  front:
    build: .
    ports:
      - '3000:3000'
    depends_on:
      - api
    environment:
      GRAPHQL_ENDPOINT: 'http://api:8000/api/graphql'
    networks:
      - local_dev
networks:
  local_dev:

おわり

意外とさっくり移行できて助かった。