#builderscon tokyo 2019で「自動作曲入門」について話した
自分のトーク
デモはこちらです: musik
社内勉強会でやったトーク (『Scalaで自動作曲の練習』を社内勉強会で話した - Sexually Knowing) をベースにしています。 自分は資料を使い回して登壇することに強い抵抗を感じるので心苦しくあったのですが、社内勉強会から飛び出して広いところでぜひ話したいという思いがある一方で、ここから進んだ話をスクラッチから立てるのは今は厳しいという評価もあったので、折衷案としてトークの内容はほぼそのままに、きちんと動くデモを作りなんなら登壇するその場でメロディをつけるくらいのライブ感を出すくらいはやれたら自分も納得できそうだということでproposalを出し、トークする場をいただきました。
当日はWeb Audioを使ったデモはうまくいったのですが、MIDIキーボードを挿してメロディをつけるところはうまくいかなかったのでそれだけ心残りです。 Oscillator nodeのゲイン調整をしておらず音が大きすぎたため、メロディが鳴っているはずだけどほとんど聞こえないということが起きていました。ちゃんとGain nodeを繋げたらよさそう。
正直、目新しくも高度でもない話なので失望されないかドキドキしていましたが「おもしろかった」「やってみようかと思った」といった感想をいただけて登壇冥利につきます。
トークを聞いた方・このブログを読んだ方はぜひaereal/musikをforkしておもしろコンポーザを作ってください!!!
聞いたトーク
当日朝までデモを作るなどした結果、時間も体力も尽きてあまり聞けなかったのが悔やまれます。
Open SKT: メルペイ開発の裏側 - builderscon tokyo 2019
メルペイのオンボーディング内容をベースに公開できる内容にしたということで、まずオンボーディングプロセスが充実していることに驚きました。
個人的には分散トランザクションを伴う決済システムを開発しているので、わかる〜〜〜と思いながら、やっぱりある程度泥臭くなるよね、と安心しました。
コンパイラをつくってみよう - builderscon tokyo 2019
DQNEOさんによるコンパイラを1から作るライブコーディング。
Goのscannerなども使わず、コアの部分はほぼスクラッチから書いていくかんじで他の言語でも実践できそうなつくりなのが真似しやすくて親切設計でした。
会場から「そこtypoしています」とかやりとりがあったり、ライブ感があってめちゃくちゃ楽しかった。
Ruby (off|with) the Rails - builderscon tokyo 2019
僕はこのトークを「Railsに乗る = 使わされるだけではなく、ツールとして使う対象にする」「守破離」という趣旨だと理解しました。
普段、Railsを使わない立場 (業務で書いた経験はある) なので「大変そうだなー ActiveRecordが向いていないところもけっこうあるよなー」とわりと対岸の火事気味に聞いていましたが、一方、ライブラリ・フレームワークを使う上で一般的な話だとも思います。
つまりライブラリの事情などを抜きにして、責任の分離など抽象度の高い設計の作業を行い、それを実装へ落とし込む際に設計で果たしたい分離や凝集を壊しそうなライブラリの機能 *1 はこれとこれがありますね、というような思考を辿っていくもので、Railsユーザー以外でも得るものが多い内容でした。
Building, and Upkeeping Super Kamiokande - builderscon tokyo 2019
事前から楽しみだったし実際にめちゃくちゃおもしろかったトーク。
実はこのトークを聞くまでスーパーカミオカンデが何なのかよくわかっていなかったので、そういった点でもdiscover something newが果たされました。
基礎物理学の世界の遠大な観測対象であっても、個々は素朴な技術 (センサー) と素朴な役割のソフトウェアシステムでできあがるというのがおもしろい。ただそのスケールがめちゃくちゃに大きいというのが興奮します。
11月に一般公開があるみたいなので申し込みます。いやーよかった。
北千住
事前の公式ブログで北千住駅の案内で脅かされていたり、足立区はなかなかラディカル *2 な土地だよと聞いて、トラブルに巻き込まれずに済むのか内心ヒヤヒヤしていましたが、無事どころかずいぶん楽しいできごとが多くて好きになりました。かなりホーム感がありました。
駅の近くにクラフトビールのお店がいろいろあったり、雑なところからそこそこのところまで飲食に困らなかったり、なにより荒川が近いのが良い。
川で飲んだりすることはけっこう奇行じみていると思われがちだけど、実質立食パーティみたいなもので、席に囚われず歩きまわっていろんな人と話せるというとても合理的なかたちでかなり好き。 懇親会が終わったあと20人くらいで荒川に移動したけど、飛び込みで20人も入れるお店はなかなか無いと思うし、見つかっても近くの席以外の人と話すのはなかなか難しいと思う。 出入り自由で、飛び入り参加したければ任意でお酒や食べ物を持ち寄るだけでよく実質プリペイドなのも気軽。
他人との距離、たしかにという感じ。居酒屋だと近すぎて疲れるけど、川だと自由に歩いて離れられるから気が楽、とかある。あとは、クラブとか行くとうるさいから距離近くなるとかある気がする。 かくれた次元 - hitode909の日記
鴨川でビール飲んでた.15人くらい来てくれた.
コンビニでクリスマスケーキ買って川で立ってケーキ持ってたら雪降ってきて最高の誕生日みたいな感じだった.前回は寒すぎてすぐ店に入ってしまったけど今回は暗くなるまで川にいられて自由に歩きまわって会話できてよかったと思う. ■ - hitode909の日記
荒川はとても広く地元の石狩川を彷彿とさせるスケールで、懐しい顔ぶれと話しているシチュエーションも手伝ってやけに楽しくちょっと懐かしい感じがした。
Google App Engine Standard EnvironmentにScalaで書いたWebアプリケーションをデプロイ
してみた: GitHub - aereal/gae-scala
GAE SE (Google App Engine Standard Environment) のいわゆる2nd generationと呼ばれるgVisorで構築された世代でJava 8/11が使える。
GAE SE (Google App Engine Standard Environment) ではgVisor上で実行されるランタイムとして新たにJava 8/11が選べる。
※当初Java 8を含めて2nd gen.としていたが公式ドキュメントによるとJava 8はgVisorで仮想化されているが、世代としてはJava 11のみが2nd gen.だったので訂正します。
Second generation runtimes are: Python 3.7, Java 11, Node 8, Node 10 PHP 7.2, PHP 7.3, Ruby 2.5, Go 1.11, and Go 1.12.
The App Engine Standard Environment | App Engine Documentation | Google Cloud
かつ、Serveletに対応しているJVM言語なら基本なんでも動くっぽく、公式のサンプルではKotolinの例もあった
https://github.com/GoogleCloudPlatform/getting-started-java/tree/master/appengine-standard-java8
古いJava 7ランタイムとの詳しい違いは以下:
The App Engine Java 8 runtime, which is based on OpenJDK 8, supports all of the existing features available in the current Java 7 runtime, which is based on OpenJDK 7, but with the following upgrades and enhancements:
Java 8 Runtime Environment | App Engine standard environment for Java 8 | Google Cloud
- Doesn't impose a security manager as the Java 7 runtime does, which means your code won't be restricted by Java permissions issues.
- Supports all the standard public Java libraries.
- Uses Jetty 9
- Supports the Java Servlet 3.1 and Java Servlet 2.5 specifications.
- Supports all Google Cloud-based APIs accessible from the Google Cloud Client Library for Java.
で、Scalaも動きそうだったので試した。
やったことは:
- Scalaなのでmvnではなくsbtを使いたいのでsbt-appengineを入れる
- GAEと関係ないけどScala 2.13にしたかったのでScalatra 2.7 RC1にした
- ドキュメントに書いてある依存ライブラリのバージョンだと古くて2.13に対応していなかったりするので適宜上げたりした
- Homebrewで入れたApp Engine SDK for Javaのどこにパスを遠したらいいかわからなかったけど `libexec` が正解だった
元気に動いています: https://gae-scala-247510.appspot.com
Serveletに対応していないとだめなのでPlayは動かなさそう。
こういう時はScalaでもWSGI/Rack的なやつがはやく策定・普及するといいですね〜〜って思う。
#builderscon 2019で「自動作曲入門」というトークをします
8月29日の前夜祭から始まるbuilderscon tokyo 2019で「自動作曲入門」というトークをします。
以前、このブログで紹介したScalaで実装してみたものを発展させたものをベースにしながら「そもそも作曲という行為をソフトウェアエンジニアの視点で再解釈するとどうなるのか」といった話から始める予定なので「むずかしそう」と思っている方でも楽しめる・むしろそんな方にこそ楽しんでほしいトークになると思います!
あと、おそらく音がなります。
僕のトークは8月31日の11:30から1204 セミナー会議室です。
そんなbuilderscon tokyo 2019のチケットは7月22日までの販売だそうなのでまだの方はいますぐカモンジョイナス!
SVGから複数の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
RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
comma-delimited lists of elements. The full form is "# element"
indicating at leastand at most elements, each separated by a
single comma (",") and optional whitespace (OWS).
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のはなし。
- Cloud Firestoreというものがベータ提供されている
- Datastoreの後継という位置付け
- DatastoreはGAEとかで良く見るあれ
- Datastoreの後継という位置付け
- Cloud Firestoreは既にDatastoreを使っているプロジェクトだと勝手に使えないという罠がある
で、ドキュメントを見たり上記ブログを見るとこういった状況になった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
使い方を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行で追加
GitHubのリポジトリはこちら: GitHub - aereal/cdk-mackerel-container-agent: experimental: AWS-CDK library for mackerel-container-agent
mackerel-container-agentを5行くらいで追加
先日、mackerel-container-agentがベータリリースされましたね。めでたい。
早速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, })
……と、こういうかんじです。 TaskDefinition
の compatibility
や networkMode
を見てよしなに 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年目。主に仕事について。
大きな仕事と新しい仕事に取り組んだ一年だったと思う。
今年前半はHTTPS化という大きい仕事を担当した。やりがいはあったけれど正直に言えば重荷が降りたという気持ちが大きい。
言葉を選ばずに言えば、2018年にこれをやるというのは問題の難しさを踏まえても遅かったなと思う。
ので、これからは自分のソフトウェアエンジニアリング力を大局的な意思決定へフィードバックできるように振る舞っていきたい。
まあいろいろ思うところはありつつ、自分が関わった仕事をふんだんに知ってもらう機会に恵まれ、インタビューなどしていただいたりもした。
その他、新しい仕事としてチームマネジメントに力を入れはじめた。
具体的に変わったことというと、中長期的な計画について考える時間が増えた。
数年後どんなチームにしたいか考えて、そのために半年〜1年後の人員計画を考えたり。
数年後はてなブログというサービスはどう成長するだろうからこういうアーキテクチャになっていくだろう、それに備えてこう改修させたい、などなど。
やるべきこと・やりたいことがとにかく多すぎて自分だけでどうにかしていくのもままならないので、自分がどうなりたいか・どうしたいかを踏まえて手段を考えた結果、自然にここに辿り着いた。
今でもマネジメントを自分がやるなんて大変なことだと思っているけれど、チームメンバーを通した先にあるはてなブログというサービスを良くしていきたいというゴールを見据えると自然と動けている。
逆にこういうかんじだから、マネジメントそのものを目的に働いていくというのはまだ想像がつかない。
新しい仕事なので慣れない点も多く、心身が付いてこないこともあり、同僚との関わり方を見直したりもした。
とにかく関わっているはてなブログを良くすることを第一に考えて動いたら自然と成果と道筋が見えてきているので、いい環境に身を置けているなと思う。
もちろん順風満帆ではないけれど。
とはいえサービスを成長させていくという仕事は外から見えてわかるレベルに達するのは難しいし、ソフトウェアエンジニアリングに限界を見てマネジメントに逃げるような姿勢をとるのはどちらの分野にも失礼だから、どっちも全力でやる。
特に来年はサービス開発という文脈に頼りすぎないソフトウェアエンジニアリングの面でちゃんと成果を出していきたい。戦略的にOSS開発をやっていきたい。