出力を入力へ

プログラミングに関する自分が考えた事を中心にまとめます

2023年のふりかえりと2024年の抱負

2023年のふりかえり

できたこと

実用性を意識した個人開発 & 腕力で実装する

もともとの目的である、課題を解決する実装をやりきる点については満足とは言えないが、ひたすら実装することは今まで以上に取り組めた。 特に、成果をOSSとして公開するところまでもっていけた(最低限利用できる状況にすることができた)のはよかった。

ec2id AWS EC2インスタンスのNameタグからインスタンスIDを取得するツール。

github.com

tfcvars Terraform Cloudのworkspaceに変数を登録する、または変数を取得するツール。

github.com

tflint-ruleset-formatter tflintのプラグイン、terraform fmtでは検知されないフォーマットの問題を検知する。

github.com

awsresq AWS上のリソース一覧を取得する

github.com

ghpr GitHubの特定条件に合致するプルリク一覧を取得する

github.com

一方で、すべてがcliツールと偏っていることには気になっている。 これは実用性を意識した結果、まず自分が困っているところに開発リソースを振り分けた結果なのでしょうがない。一方で、別の目標であるデータと仲良くなることが上手くいかなかった原因でもある(詳細後述)。

また、開発したソフトウェアはすべて自身がオーナーとなるソフトウェアであり、社外のOSSに対するコントリビュートという意味ではほとんどできなかった。2023年にGo言語の開発を重視したのもterraform-provider-awsで未実装なさまざまな機能を実装したいというのも理由の1つだったが、結局terraform-provider-awsにコードでコントリビュートはできなかった。この原因の1つに個人開発KPIの設定がある(後述)。

全体的に見れば個人開発にある程度のリソースを投入してそれなりの成果を得られたものの、上記の見直しも含めて、今後どのような開発に取り組むかはもう少しバランスを取りたい。

できなかったこと

細かなアウトプット

ブログ記事のアウトプットはもっと時間を掛けるつもりが、あまり上手くいかなかった。 このメインブログはこのふりかえり記事を含めて5件、会社ブログ1件、Qiitaは3件、Zennは12件だった。特に今年の後半は一切書けなくなったので、もっと気軽に書ける場所をとサブブログを立ち上げて、20件書くことができた。 2022年までに比べると件数は増えたのでよかったが、あまり計画的に書けていないし、開発の知見をまとめるという意味でもあまり上手くいっていない。目標として立てる前よりは書いている、というのを上手くいったと言うのはあやしいところ。それとも高望みだろうか。

昨年もそうだったが、これを書こうと思っていたネタを書けずに下書きで終わらせているのはよろしくない。アドベントカレンダーネタも結局検討だけして書かずに終わってしまった。 ソースコードだけでなくブログも書ききることを意識したい。

データと仲良くなる

構造・非構造に限らずデータの取り扱いにもっと取り組む想定が期待していたよりはできなかった。 書籍としては検索システム

や実践Redis入門

を読んだり、実務でもRedisやMySQLのアップデート、MLOps基盤の構築、といった隣接領域に取り組むことはできた(elasticsearchは来年になりそう)が、時間の制約などもあり、結局今まで通りインフラエンジニアとして基盤の取り扱いだけで終わってしまった。業務で扱う以上はしょうがない面もある。 そういう意味でもやはり個人開発でwebサービスを立ち上げて、そこでデータの扱いに取り組むべきだった。

その他気になっていること

個人開発KPI

個人開発を継続する上で、わかりやすい指標がGitHub のContributionグラフだったので、毎日草を生やすことを当面の目標に取り組んだ。 これ自体は比較的上手くいった。しかし業務でのコントリビューションを除外するためにPrivate Contributionsをオフに、Activity overviewをオンにした結果、自分のリポジトリであってもプライベートリポジトリに対するコミットは優先度が落ちることになった。この結果がOSSのcliツールばかり開発することを後押ししてしまった。

GitHub Contributionsの設定が上記2つしかなく、特定の企業向けコミットだけを除外することができなかったのでしょうがなかったが、これはKPIの設定が不適切だった。 このあたりは計測方法を変更して、より自分が開発したいことが開発できるように修正したい。

Generative AIの活用

ChatGPTやGitHub Copilotなど、AI活用が盛り上がった1年だったが、自分は上手く使えていない。 もちろん上記ツールは利用しており、苦手なフロントエンド/TypeScriptまわりを書くときや、英語文書を見直すとき、似たようなコードを何度も書くときにはそれぞれ活用している。しかし世の中の盛り上りほどは使えていない。 これは世の中が過度な期待の中にいるとも言えるが、自分の場合はまだ活用の余地があるし、世の中の上手い使い方を取り込めていないという感触が強い。

このあたり自分の老害感が強くなっていると危惧している。 生成AIが今後ますます重要になることは間違いないので、最低限でもキャッチアップは続けたいし、新しい技術は積極的に利用していきたい。

職場での振舞い

2023年は会社でリーダーシップやマネジメントの面で頑張るよりは、個人の成果を重視することを意識してきた。 そしてそれは一定効果はあったと思う。個人開発で成果は出せたし、業務においても多くのアウトプットを出すことができた。

しかし、会社における組織的な取り組み・改善は上手くいかなかった。これは自分が積極的にアクションを取らなかったので当然といえば当然。 しょうがない面はあるが、結果的に見れば優先度を下げた弊害はあったように思える。ここは見直す必要がある。 また、リーダーシップとマネジメントの両方の優先度を落とすのはよろしくなかった。マネジメントに費す時間を減らすことは会社との合意事項ではあったが、リーダーシップを発揮すべき場面であまり率先して行動を取れなかったのはよろしくなかった。 このあたりは改善したい。

インプット

勉強会に参加したり、技術書を読んだり、他者ブログを読んだりする量は意識的に減らした。特に勉強会への参加はあまり行わなかった。 これは勉強会に参加する時間を個人開発の時間に当てたかったこと、技術書を読んでよりよい方法や考え方を学ぶよりも今持っている手段で対処して前に進む方法を優先したかったことが理由としてある。

悪くはなかったが、勉強会に参加しなかった時間がそのまま個人開発に取り組めたわけではないし、技術書もなんだかんだ参照していた。 なので意識的にインプットを絞る必要はなさそう。今後もインプットは継続して質の良いアウトプットが出せるようにしたい。

健康

健康が最重要なのは言うまでもない事だし、ありがたい事に大きな怪我や病気になることはなかった。しかし理想的な状態でもなかった。 運動は継続できたが、5-6月に運動量を減らしたらあっという間に体力が落ちたことにびっくりした。やはり何よりも継続することを意識したい。

そして運動以上に睡眠が上手く取れなかった。もともと睡眠障害持ちで上手くいっていなかったが、この1年は(も?)良くなかった。唯一の改善点はアクティビティトラッカーとしてvivosmart5を装着するようになったことで、これで自分の休憩の取り方が下手だと再認識できた。 それを踏まえての改善はまだまだ上手くいっていないが、もっと上手に休めるようになりたい。

2024年の抱負

2024年は意識することを絞って取り組みたい。

行動と思考をアウトプットする

アウトプットを継続することは今年も課題だが、特に「行動」と「思考」をアウトプットすることを意識する。 1年をふりかえってみて記録が全然足りていないと感じたので、後からふりかえって自分のアクティビティを簡単にでもトレースできるようにしたい。

行動のアプトプットは単に何をしたか。 ツールやサービスをリリースしたでもよいし、調べたことでも試したことでも何でもよい。 大抵の場合は1日何もしていませんでした、なんて日は存在しないので書き易いはず(その記事に価値があるかは別として)。

思考のアウトプットは何を考えたか。 書籍やブログ、勉強会等に参加したら何かしら感想を持つと思うのでそれを書けばよいし、ツールやサービスをリリースしたら何を解決したくてリリースしたのか、または機能を実装したらどうしてそのような設計にしたのかなど、無限に思考・判断が含まれているはず。これを言語化する。

アウトカムベースで個人開発に取り組む

2023年は今までに比べてよい個人開発ができたと思う。一方で「実用性を意識した」という点が上手くいっていないケースが多くあることを感じた。 それを強く感じたのがGitHub ContributionsをKPIとして利用しており、開発したいことに着手できない、あまり必要のないものに時間を割いているとき。これは個人開発KPIにも記載したが計測方法がよろしくなかった。 この方法はあくまでアウトプットを計測しており、自分の課題を解決できているかを計測できていなかった。

これはまさにビルドトラップにはまっていると感じた。 もちろん個人開発なので手段が目的になってもいいし、継続することが最重要なのでアウトプットをKPIに設定することも間違いではないと思う。しかし、自分のやりたい事を考えれば、課題を解決できているかやアウトカムを達成できているかをもっと重視したい。

go-tfeで単体テスト

go-tfe概要

go-tfeはTerraform Cloud (およびTerraform Enterprise)のためのGo SDK。 go-tfeを利用することで、Terraform Cloudの各種APIを利用したツールを実装することができる。

github.com

go-tfeのテスト

go-tfeを利用したツールを実装するときに単体テストをどうするかが悩ましいところ。 go-tfeには単体テストに相当するものがなく(?)、すべてE2Eテストになっている。 また、Terraform Cloud APIをやりとりするclientはinterfaceが定義されていないので、mockを生成してテストに利用することができない。

github.com

// Client is the Terraform Enterprise API client. It provides the basic
// connectivity and configuration for accessing the TFE API
type Client struct {
    baseURL           *url.URL
...
}

リクエスト先のbaseURLを書き換えることはできるので、Terraform Cloud自体のmockを生成すればテストの実装が可能になるかもしれないがこれは厳しい。 terraformもTerraform Cloudも機能追加が活発なので、そのAPIを利用するならテストの担保はしておきたい。 できれば単体テストとしてテストを実装したいので、いきなりE2Eテストは避けたい。

mockの利用

go-tfe自身は利用していないが、go-tfeの各種サブ機能に関するmockコードは生成されている。 GoMockを用いてmockが生成されているので、go-tfeを利用したアプリを実装する場合はこのmockを利用して単体テストを書くとよさそう。

ただし、前述の通りclient自体にはinterfaceは定義されておらず、mockコードにもclientに関するコードは存在しない。 このため、go-tfeを用いたツールを実装するときにはclientを引数などには含めず、サブ機能ごとのインスタンス(こちらはinterfaceが定義されている)に依存した実装にするとよさそう。

例えば、workspace名とVariables APIを引数としてVariableListを返すmyFuncの単体テストは以下のようになる。

package test

// func myFunc(ctx context.Context, workspace string, variables tfe.Variables)

import (
    "context"
    "testing"

    "github.com/golang/mock/gomock"
    tfe "github.com/hashicorp/go-tfe"
    "github.com/hashicorp/go-tfe/mocks"
)

func TestListVariables(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockVariables := mocks.NewMockVariables(ctrl)
    ctx := context.TODO()

    var items []*tfe.Variable
    mockVariables.EXPECT().
        List(ctx, "w-test-no-vars-workspace", nil).
        Return(&tfe.VariableList{
            Items: items,
        }, nil).
        AnyTimes()

    variableList, err := myFunc(ctx, "w-test-no-vars-workspace", mockVariables)

    if err != nil {
        t.Errorf("expect no error, got error: %v", err)
    }
    if len(variableList.Items) != 0 {
        t.Errorf("expect no variables, got variables: %v", variableList.Items)
    }
}

Emacs環境のアップデート

自分のメインの開発環境(エディタ)としてはEmacsを利用し続けている。部分的にはVSCodeやIntelliJなどを使いつつもEmacsメインであることは10年以上変わっていない。 とはいえ、最近はあまりelispまわりの整備が行えておらず、良い開発環境にはなっていなかった。 さすがにこのままではよろしくないし、かといってVSCode等への完全移行もできそうにないのでEmacs環境を整備した。

バージョンアップデート

今まではUbuntu 22.04のデフォルトである27.1を利用していた。その前も基本的にはOSのデフォルト提供されているバージョンを変えてはおらず、最新のバージョンを積極的には採用していなかった。 また、macOSなどUbuntu以外の環境でもUbuntuで利用できるバージョンを合わせていた。 これであまり不自由していなかったが、ふとEmacs User Suervey 2022を見たときに結構みんな新しいバージョンを利用しているのだなと知った。 slackのemacs-jpでも比較的新しいバージョンについて話題にしていることが多く、実は新しいバージョンに追従した方がよいのかも、と考えるようになった。 新しいバージョンを導入しても大きなトラブルが発生する頻度が少なく、それでいて新機能を試せるのであればメリットの方が大きいので。また、自分の手元でも確認してみたいと思うことは多々あったので。

あまり手間を掛けるつもりはなかったが簡単に導入できるならとEmacsを28系にアップデートすることにした。 Ubuntu環境は ppa:kelleyk/emascを、macOS環境はhomebrew caskをそれぞれ利用してインストールした。 これによりUbuntuでは28.1をmacOSでは28.2にアップートできた。 どちらも特に導入の手間を掛けずに実現することができた。

まだ最新バージョンを利用することのメリットや機能検証なんかはできていないが、これからそういった情報も継続収集できればよいかなと思う。

leafの導入

パッケージマネージャとしてel-getを利用していたが、autoloadまわりが上手くいっていないケースがあった。 このせいでいくつかのパッケージが利用できていない(または手動でロードする必要がある)状態で、これが今までEmacsを使っている中で一番の課題だった。 適切な設定を行えばおそらくel-getのままでも解決できるとは思うが、せっかくなのでとパッケージマネージャごと変更することにした。

use-packageだとelpa/melpa以外からのインストールができないということで leafを導入することにした。 2023年1月時点で、el-getを直接利用するよりも(国内の)ユーザは多そうで参考となる設定が公開されているところも地味に助かるところ。

qiita.com

まだ全部のパッケージをleafで置き換えられてはいないが順調に更新できており、今まで動作しなかったパッケージも動作するようになったのでかなり助かっている。 合わせてパッケージの移行や設定見直しなども行っているのでもう少し時間が掛かりそうだが、少しずつでも開発環境が便利になっていくのは非常に体験が良い。

init-loaderの継続

init-loaderを導入したときは、どちらかというと .emacsファイル1つでは管理しにくいからinit-loaderを利用してファイル分割した方がよい、という考えの人が多かった印象。 自分もその考えに賛成で積極的に分割していきたいと思いinit-loaderを導入していた。

最近ではあまり分割し過ぎない方がよい、またはinit-loaderを利用する必要はないという考えもあるようで、今回の見直しでinit-loaderをどう扱うか再検討した。 再検討してみたが、やはり自分は目的に応じてファイルが分かれている方が好みだなと思い init-loaderの利用は継続することにした。 init-loaderがあるとEmacsの起動が遅くなるという話もあり、高速起動できた方が魅力的ではあるとは思いつつも、自分にはあまり恩恵はないくファイル分割を実現できた方がよいと考えた。 ただし、それでも分割し過ぎの面はあると思うので過度な分割には注意していきたい。

また、他の人はinit-loaderの利用を止めてしまったのかなと公開している人の.emacsファイルを確認してみたら、まだまだ使っている人も多いみたい。 あまり心配せず、自分の好みを優先させてよさそう。

フォント

自分はフォントにあまりこだわりがなく、以前Emacs環境を整備したときにたまたまRictyが流行っていたのでRictyを採用していた。 その後Rictyフォントの生成はansible化していたので環境移行時にもセットアップの手間を感じず使い続けてきた。

今回環境の見直しを行う中で、Rictyのサイト上にて他のフォントを推奨する案内があることに気が付いた。 前述の通りあまり困ってはいなかったが、他のフォントを推奨するのであれば変更しようということで見直すことにした。

rictyfonts.github.io

いろいろ確認してみたところ白源がよさそうということでこれを利用してみることにした。 1週間ほど使い続けているが、特に違和感なく利用できるのでこのまま継続する予定。

github.com

custom-set-variable対応

いつからか、勝手に init.elファイルに追記・更新されるようになってしまった。 自分は .emacs.dをそのままリポジトリ管理しているのでファイルが勝手に更新されるとうれしくない。

これを制御して書き込みを完全に止めることは難しいらしく(なんだそりゃ)、別のファイルに追記させてそれを読み捨てるという方法がよく採用されるらしい。 あまり良い方法には思えないが、手軽に他の対策を取ることもできないのでこの方針を採用する。

qiita.com

まとめ

おそらく Emacs環境の大幅見直しをしたのは7-8年ぶりくらい。 まだ大きな変更と言えるほどの変更は入っていないが、それでも自分にとって現状把握と全体の見通しを改善できた。 最近のIDEのように表示をリッチにすることは考えていないが、こまかな改善は継続的に取り入れていきたい。

github.com

EC2インスタンスIDを取得するec2idをリリースした

AWS EC2インスタンスのインスタンスIDをNameタグから逆引きするCLIツールec2idを作ってリリースした。

github.com

インスタンスIDを取得したい

AWS EC2インスタンスを一意に識別する情報はインスタンスIDであり、AWS SSMセッションズマネージャーでログインするときなどでインスタンスIDを指定する必要がある。 例えば、aws cliを用いてSSMセッションズマネージャーで接続するには以下の通り。

$ aws ssm start-session --target <instance-id>

docs.aws.amazon.com

このようにインスタンスIDを指定する上で、インスタンスIDを取得する必要がある。 インスタンスIDはユーザがEC2に割り当てることはできず、EC2インスタンスを構築する度にランダムに決定する。このため、EC2インスタンスを頻繁に再構築している場合、事前に値を記録する運用も難しい。 インスタンスIDを利用する直前に動的に取得する仕組みが必要である。

インスタンスIDの取得方法

マネージメントコンソールから取得する

一番直感的なのはWebのマネージメントコンソールから取得する方法である。

マネージメントコンソール画面からインスタンスIDを参照する

一方で、EC2インスタンスIDを取得するために、わざわざマネージメントコンソール画面を開きたくない。 また、大量のEC2インスタンスIDが稼動している場合に目当てのインスタンスを探すのは手間だし間違いが生じやすい。 より簡単にインスタンスIDを取得する方法が必要である。

aws cliを利用する

aws cliでSSM接続するためにインスタンスIDが必要なのであれば、同じくaws cliでインスタンスIDを取得できれば話は早い。 当然aws cliにはそのためのサブコマンドが提供されており、describe-instancesを利用することでインスタンス情報一覧を取得できる。

awscli.amazonaws.com

一方で、この方法ではquery/filterオプションを覚えるのが面倒という問題がある。 EC2インスタンスが1台しか存在していないのであれば問題ないが、複数インスタンスが稼動している中で目的のインスタンスを取得するには、queryおよびfilterオプションを指定する必要がある。 query/filterで、インスタンス名sampleのインスタンスIDおよびその起動時刻を取得するコマンドが以下の通り(以下の例では該当するインスタンスが2台ある場合)。

$ aws ec2 describe-instances --filters Name=instance-state-name,Values=running Name=tag:Name,Values=sample --query 'Reservations[*].Instances[*].[LaunchTime, InstanceId]'
[
    [
        [
            "2022-12-31T10:02:33+00:00",
            "i-0691a69ff0914bae1"
        ]
    ],
    [
        [
            "2022-12-31T11:24:57+00:00",
            "i-0acd9f178c934caea"
        ]
    ]
]

このように、これだけでもquery/filterはそれなりに複雑で覚えておける気がしない。 特に同名インスタンスがある場合には出力結果を人の目でパースする(いわゆる目grepする)か、追加のsortフィルタ等を加えて適切なインスタンスを選ぶ必要がある。

ec2id

こういった課題を解決するためのcliツールとしてec2idを開発・リリースした。

github.com

専用CLIの実装

やりたい事はインスタンス名からインスタンスIDを取得すること。なのでその用途に特化したCLIがあればよい。 無駄なオプションを実装せず単目的に利用することを想定することで、シンプルなツールを実現した。

これを利用することで簡単にインスタンスIDを取得できる。

# 引数を指定しなければ全インスタンスから最後に起動したインスタンスのIDを取得する
$ ec2id
i-0691a69ff0914bae1

# 引数を指定すればそのインスタンス名の中から最後に起動したインスタンスのIDを取得する
$ ec2id sample
i-0acd9f178c934caea

go言語による実装

利用時に依存パッケージの問題を発生させたくないのでシングルバイナリで利用できることが望ましい。 Linux(Ubuntu) / Intel mac / m1 macで利用することは見えているので、マルチOSサポートが容易が言語にしたい。 そういった事を考えるとgo言語が適切だと判断した。

わざわざgo言語で実装しなくてもシェルスクリプトなどでも実装可能かもしれないが、フィルタやクエリが複雑になりメンテが大変になりそうだった。 また、別のツール用にgo言語のCLIを作りたいとも考えていたので、go言語のナレッジ蓄積をしたかった。 go言語の技術検証用にシンプルな仕様のツールがあると都合が良く、ec2idはこの条件に合っていた。

作ってみて

検証時に思った通り、テストまわりが難しく感じた。 AWS APIをモックにする必要があり、インタフェースやモックコードの生成などもからむので、多少複雑にはなった。 これはgo言語に慣れれば解決するのか、今のところ感触はあまりよろしくない。

シングルバイナリやマルチOSサポートが容易なのは大きなメリットと感じる。 このあたりリリースまわりが楽なのは嬉しい。

2022年のふりかえりと2023年の抱負

2022年のふりかえり

目標に対してはあまり上手くいった年ではなかった。

できたこと

テスト

ものすごく上手くいったわけではないが、いろいろ取り組むことができた。 IaCに対しては静的解析やPolicy as Codeに、ソフトウェアに対しては特にgo言語を中心に、システムとしてはE2Eテストを試すことができた。 1つ1つはまだ入門レベルではあるが、やっとひと通りそろってきた。

健康増進

技術まわりとは関係ないけれど、健康増進に向けて運動習慣を身に付けることができた。 テニスは再開(これは正確には2021年だが)して1年以上継続することができた。 それ以上に、zwiftを始めてかなりのめり込むことができた。最初は20分がやっとだったのに、今では60分を倍近いパワーで軽く走れるようになった。このおかげで減量にもなったし体力がついた。 リモートワークで運動機会がなく危機的だった状況は脱することができたので、これは2023年も継続したい。むしろやり過ぎに注意した方がよいくらいか。

できなかったこと

個人成果の追求

これは良い面と悪い面でできなかった。

良い面とは結局組織をリードする取り組みは止めなかったということ。 これは社内のエンジニア組織およびインフラチームの拡大を通して、自分がEMおよびリードエンジニアとしての動く必要があった。また、より良いプロダクト開発のためにも、あえて組織まわりの課題を無視する・手を出さないという選択肢は自分には取れなかった。 ある程度割り切ったところもあるが、当初考えていたよりも組織寄りの行動が多かったかなと思う。

悪い面は単に個人開発に投入する時間が減ってしまったこと。 業務においても他の人の開発をフォローする時間は多かったし、プライベートでも技術以外に時間を使うことが多かった。1番の課題は個人開発のモチベーションが夏頃にゼロになってしまったこと。結局秋には自然と回復したものの、個人開発をここまで止めたことがなかったので軽い危機感があったがそれでもモチベーションが出ない状況だった。 興味に合わせて取り組む技術を選び過ぎて継続も完結もできなかったし、結局開発を楽しめたかというと疑問。このあたり、自分がどう開発と向き合いたいのか考えるか、それとも何も考えずに興味のままに取り組むか選択する必要がある。

成果のアウトプット

アウトプットもあまり満足できるものではない。

  • 登壇: 0件
  • 個人ブログ: 5件
  • Qiita+Zenn:10件
  • 会社ブログ:1件

登壇は増やしていくつもりが、結局機会を得ることができなかった。 ブログ投稿数は2022年と同水準で、これも増やしていくつもりがまったく増えなかった。実は下書きはいくつかあり、これを公開までもっていけなかったことが大きい。 勢いで書き切ること、計画的に書くことが必要。

xR技術

2021年始めに少し手を出しただけで、ほとんど取り組めなかった。特に動画まわりは何もできなかった。 会社ではこのあたりの技術が着実に前に進んでいて面白いフェーズにあり、自分はインフラエンジニアとして1歩引いた位置で取り組めずにいる。このもったいない状況はどうにか解消したい。

低レイヤ技術

低レイヤもほとんど取り組めなかった。 ECS活用は拡大しているものの、k8sはまったく触れられておらず、GPUやLinux Kernel/ディストリビューションもほとんど触れていない。多少トラブル対応しただけか。

このあたり、もったいないので手を出したい気持はある一方で、今自分がやりたい領域ではないとも感じている。必要に迫られれば対応すると思うので、どうにか機会を捻出したいところ。

2023年の抱負

2023年は好奇心と実用性をテーマにする。

2022年は目標は意識しつつも、興味を持ったものに手を出すことはできた。しかし、興味だけでは開発を継続することはできなかったし、当初定めたゴールまで辿りつくことはできなかった。 2023年は興味に応じていろんな技術に手を出すことは継続しつつも、それを自分の役に立つこと・実用性を考えて継続することを意識する。

実用性を意識した個人開発

テストもxRも低レイヤも、興味だけでは成果につながらなかったしアウトプットにたどり着けなかった。 これではどこまで開発を進めても知的好奇心を満たすことしかなかったし、ある程度取り組むと満足して手が止まってしまう。

スタートは興味でよいので、その後は実用性を意識して開発を継続するようにする。 もちろん業務やOSSコミュニティに価値を提供できればそれが一番だが、少なくとも自分の役に立つ開発になることを意識したい。

細かなアウトプット

2022年は変にアウトプットに拘ってしまった。新規性が、正確性が、充実度が、と考えて結局記事を公開できないことも多かった。 2023年はもっと細かくアウトプットする。他の人の二番煎じになって、箇条書きでも、検証不足な点があってもよい。アウトプットしない状況より小さくアウトプットすることを意識していく。

社外登壇となると難しいところだが、やはり他人とネタ被りを恐れるくらいなら考えずにCfP応募できるようにしたい。LTならそれこそ考え過ぎないでよいはず。 どちらかというと日頃のアウトプットを積み重ねて社外登壇にもっていけるようにしたい。

腕力で実装する

個人開発もアウトプットも、当初のゴールまでやり切れずに止まったことが大きな課題。 開発やアウトプットに着手したとき、始めた勢いである程度の成果にたどり着けるかがポイントだと感じており、それができればある程度継続もできる。

これは書籍「事業をエンジニアリングする技術者たち」で技術的負債の返却には腕力が必要という言葉で表現されており、これが技術的負債の返却以外にも通用すると感じた。 かっとなって開発を始めた・ブログ記事を書き始めたときに、ある程度の形になるまでもっていけるかは腕力が重要。この腕力を鍛えていきたいし、自分の腕力では解決できない課題はきちんと分割して取り組みたい。

データと仲良くなる

2023年は今まで以上にデータの扱いが重要になりそう。 いわゆるRDBだけでなくNoSQLだったり全文検索だったり、ログだったり画像だったり。

データストアの使い分けや取り扱いを上手くできるようになりたいし、システムをきちんと運用できるようになりたい。 また、システムだけ理解してデータの扱いを別のエンジニアにまかせるだけでなく、自分でもデータ操作できるようになりたい。

GoでSDK呼び出しをモックできるコードを書く

AWS SDK for GoなどのSDKを利用する場合のユニットテストの書き方およびプロダクトコードの実装方法が難しい。 AWS公式には Unit Testing with the AWS SDK for Go V2 にて解説があり、モックで差し替えられるようにAPIのインタフェースを利用してテストで差し替えることができるように推奨している。 一方で、このケースにおいてプロダクトコード側をどのように実装すればよいかわからず、またテストも複雑でどのようにテストを増やせばよいかわからなかった。

いろいろと検討して自分の中である程度納得できたのでそのまとめ。とはいえわからないことも沢山あるのでまだまだ検証が必要。 ベースとなるコードは Unit Testing with the AWS SDK for Go V2 から、検討結果のコードはこちらのリポジトリにある。

プロダクトコードの実装

インタフェースの暗黙的な実装

一番最小限の方法。 Goのインタフェースは暗黙的に実装される (implemented implicitly)ので、オブジェクトを代入する変数の型として S3GetObjectAPI 型を宣言すればよい。 これにより変数 apiS3GetObjectAPI 型 (実態は s3.Client)になり、GetObject 関数を呼び出すことができる。また、テスト時にはAWSドキュメント記載の通りモックで差し替えればよい。

func CallGetObjectFromS3Direct() {
    var bucket = "sample-bucket"
    var key = "sample-object-key"

    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        log.Fatal(err)
    }
    var api S3GetObjectAPI = s3.NewFromConfig(cfg)

    objectString, err := GetObjectFromS3(context.TODO(), api, bucket, key)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(objectString))
}

ただし、これでは GetObjectFromS3 関数を呼び出す側はawsに強く依存したままだし、S3GetObjectAPI 型のインスタンスを生成するときにその実装は s3.clientであることを知らないといけない。 単体テスト時にSDK依存部分をモックで差し替えることが目的で、SDKに依存することを隠蔽したいわけではないのでこれで十分かもしれないが、インタフェースで実装は分離できていない。

コンストラクタによる生成の分離

AWS SDKに強く依存すること、インスタンス生成に関する知識が求められることが問題であれば、コンストラクタを使えばよい。これによりインスタンス生成に関する知識を分離できる。 S3GetObjectAPI インタフェース型のインスタンスを実装するための構造体として S3GetObject を定義し、そのコンストラクタとして NewS3GetObject を定義する。 これによりGetObjectFromS3 を呼び出す側もAWS SDKに依存せず、apiの生成に関する知識にも依存しなくなった。

type S3GetObject struct {
    Client S3GetObjectAPI
}

func NewS3GetObject() (*S3GetObject, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return nil, err
    }
    client := s3.NewFromConfig(cfg)

    return &S3GetObject{
        Client: client,
    }, nil
}

func CallGetObjectFromS3Interface() {
    var bucket = "sample-bucket"
    var key = "sample-object-key"

    api, err := NewS3GetObject()
    if err != nil {
        log.Fatal(err)
    }

    objectString, err := GetObjectFromS3(context.TODO(), api.Client, bucket, key)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(objectString))
}

ただし、ユニットテストでAWS APIをモックしたいだけなら過剰に見える。 また、呼び出し側が api.Client を渡す必要があるのも違和感がある。どうすればよいかはわからなかった。

モックを用いたテストの改善

モック定義の分離

Goのテーブルテスト手法 を考えると、オリジナルのテストケースごとにモック関数を定義する方法はわかりにくいし差分を意識することが大変。 モック関数の定義はテーブルの外で行い、各テストケースはこれを参照するだけにしたい。

func TestGetObjectFromS3(t *testing.T) {
    mockClient := func(t *testing.T) S3GetObjectAPI {
        return mockGetObjectAPI(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
            t.Helper()
            if params.Bucket == nil {
                t.Fatal("expect bucket to not be nil")
            }
            if e, a := "fooBucket", *params.Bucket; e != a {
                t.Errorf("expect %v, got %v", e, a)
            }
            if params.Key == nil {
                t.Fatal("expect key to not be nil")
            }
            if e, a := "barKey", *params.Key; e != a {
                return nil, errors.New("NoSuchKey")
            }

            return &s3.GetObjectOutput{
                Body: ioutil.NopCloser(bytes.NewReader([]byte("this is the body foo bar baz"))),
            }, nil
        })
    }

    cases := []struct {
        name string
        client func(t *testing.T) S3GetObjectAPI
        bucket string
        key string
        expect []byte
    }{
        {
            name: "return content",
            client: mockClient,
            bucket: "fooBucket",
            key:    "barKey",
            expect: []byte("this is the body foo bar baz"),
        },
...

これで、複数のテストケースを追加した場合でも見通しがよくなる。また、テストケースにテスト名(name)を加えることでよりテスト内容をわかりやすくできる。 ただし、テストケースごとに期待する挙動が異なるのでテストケース中に定義した方がよい場合もありそう。上記の例だと比較的汎用的な動作をするモックを作成しているのでテーブル外定義でよさそう。

モック用の構造体の定義

S3GetObjectAPIはインタフェース型として定義されている一方で、mockGetObjectAPIは関数型として定義されている。 個人的にはこの対応関係がわかりにくかった。プロダクトコードとテストコードで対応関係が取れていて欲しい。 (ただし、この方法には課題がある。詳細後述)

type MockGetObject struct {
    MockGetObjectAPI func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}

func (m *MockGetObject) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
    return m.MockGetObjectAPI(ctx, params, optFns...)
}

func TestGetObjectFromS3(t *testing.T) {
    mockClient := func(t *testing.T) S3GetObjectAPI {
        return &MockGetObject{
            MockGetObjectAPI: func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
                t.Helper()
...

ポイントとしては2つ。

1つ目はMockGetObjectのレシーバとしてGetObject関数を定義しているところ。この関数内部ではテスト用の MockGetObjectAPIを呼び出しており、MockGetObjectAPIをテスト時にモックで差し替えることでテストに応じた動作を実装することができる。 ただし、テスト時にGetObjectをモックとして直接差し替える方法があればもっとシンプルになる。このように段階を踏む必要が本当にあるのか疑問。

2つ目はMockt生成時に*testing.T を引数とした関数を介しているところ。 テストテーブルにて直接 &MockGetObject{...} を返さず *testing.Tを引数の関数を返すようにしている。これにより MockGetObjectAPI のモック実装時にテストのヘルパー関数を利用できる。 ヘルパー関数が不要であれば直接mockGetObjectのインスタンスを返した方がよいだろうか。

まだよくわかっていないところ

テストで繰り返し定義が必要

MockGetObject構造体定義、GetObject関数定義およびインスタンス生成時のMockGetObjectAPI実装時の合計3回、GetObjectに関する引数を持った型定義が必要。 これが非常に煩雑で面倒に見える。 今回はGetObjectだけだからよいが、GetObject以外の関数もモックが必要になると大量の定義が必要になる。 これをどうにかならないか、もっときれいな実装がないか疑問。 このあたりを解決するのがgomockなどの自動生成ツールなのか、という疑問もありそれは別途検証する。

プロダクトコードとテストコードの対応関係

このブログ記事を書きながら考えを整理していたら、S3GetObject 構造体と MockGetObject 構造体は全然対応できていないことに気が付いた。 S3GetObject 構造体のフィールドとして Clientを利用しているところがたぶん上手く理解できていない。

参考

デスクトップ環境をUbuntu22.04にアップデートした

日頃利用するデスクトップ開発環境としてUbuntuを利用しているが、これをUbuntu 20.04→Ubuntu22.04にアップデートした。

旧環境の課題

原因不明の動作不具合がいくつかあり、これを解消したかった。

音声入出力の不安定

映像出力は問題ないが、音声入出力が不安定という問題を抱えていた。

音声入力はボリューム設定が度々リセットされて自分の設定より小さくなった。多くの場合はサスペンド&解除するとリセットされ、たまに画面ロックしただけでリセットされることもあった。このせいでwebミーティングのマイク音声が小さくて自分の声が届いていないことが多々あり、ミーティング開始時に毎回音声入力ボリュームをチェックする必要があり面倒だった。

また、音声出力も不安定だった。音楽をかけていると急にボリュームが小さくなったりノイズが載ったりしていた。断線等を疑ったが結局GPUがおかしいのでは?という結論で改善できていなかった。音声出力の問題はUbuntu 18.04のときから発生していたのでOS設定が原因ではなくハードウェア的な問題だろうと(症状からしてそう)、GPU交換待ちだった。ただし、GPUの高騰のせいでなかなか交換を試せないでいた。

さらに、firefoxなど一部アプリが指定のスピーカーを利用してくれないという問題があった。システム設定を無視して別のスピーカーを利用してしまう。これを回避する方法がわからなかったのでfirefoxではweb会議や動画再生ができないという制約があった。

起動時のNFSマウントエラー

デスクトップ起動時にNASをNFSマウントする設定だったが、これが上手くいかない問題があった。

起動処理で/etc/fstabの設定を反映してくれず、毎回手動で sudo mount -aする必要があり面倒だった。また、libvirtまわりでもNFSマウントしている設定があり、これが原因でデスクトップ起動時にVMも起動してくれないとうい問題を抱えていた。 こちらはOSをUbuntu20.04にアップデートしてから発生したのでOS設定まわり(特にsystemdのブート順序まわり)を疑っていたが結局解決できなかった。OSクリーンインストールすれば解決するだろう、くらいの感覚だった。

フルディスク暗号化

既存のシステムディスクは暗号化を適用していなかった。

デスクトップ環境であり自宅外に持ち出す機会はないものの、暗号化できていないという事実には気になっていた。主要なデータはクラウド管理・バックアップしているが、個人情報の塊なので。

ハードウェアアップデート

以下のハードウェアについて更新した。

GPU更新

音声入出力のトラブルのうち、少なくとも出力についてはGPUのトラブルだろうと推測できていたのでGPUを更新した。

今まではGTX1660Tiを利用していて、パフォーマンス的には特に困ってはいなかった(ゲームをするわけでも機械学習するわけでもないので)。ただし、blender等さわったりする機会はあるし、RTXレイトレーシングAPIを利用できる環境は欲しかった。

RTX4000シリーズの発売直前ではあるし、RTX3000シリーズのハイエンド帯は価格が下落傾向ではあるが、エントリー帯であれば価格改訂や値下げが進むことはないだろうと判断してRTX3050を購入した。 RTX3050であれば現行GPUとほぼ同程度のパフォーマンスを出すことができるので自分の用途で困ることはない。 将来的にはGPUのミドルエンド・ハイエンドへのアップデートも考慮しつつ、このGPUは別サーバ等でも利用することを想定する。

SSD更新

OS更新時にはシステムストレージごと交換して新規インストールすることにしている。このため現在システム用ストレージに利用しているSSD(NVMe M.2)も更新する。

従来はSamsung SSD 970 EVO 250GBを利用しており、パフォーマンス的には満足していたものの容量不足を感じていた。

新規ストレージとしてはWD Blue SN570 1TB を選択した。 細かなパフォーマンス比較はしていないが、WD BlueはHDDとしてはよく利用しているしNVMe M.2 SSDとしても利用してみたかった。また、SN570が比較的新しくリリースされた製品なので試してみようと考えた。価格も特に高価とうわけではなかった。

OSインストール

OSインストールまわりのトラブルについて。

インストールメディアの準備

USBメモリを利用してOSをインストールするため、インストールメディアを作成して利用した。

今回はddコマンドを利用して作成したのだが、手順ミスなのかたまたまデータ書き込みエラーが発生したのか、作成したインストールメディアでインストーラを起動できなかった。 結局そのときはWindows PCにRufusを使ってインストールメディアを作成して対処した。

ddでもよいが、Ubuntuヘルプ または Ubuntuチュートリアルの通り Startup Disk Creator を使って作成してもよかった。

また、できればPXEブート環境を構築してUSBメモリなしにOSセットアップを可能にしたい。ただし、これを実現するには開発用ネットワークを分離する必要がありハードルが高い。

フルディスク暗号化

OSの新規インストールと合わせてフルディスク暗号化オプションを有効にする。

手順は Ubuntu22.04フルディスク暗号化を有効にする などが参考になるが、特にLVMレイアウトを変更しないので考慮する点はない。

問題はリカバリーキーの扱いで、生成されたリカバリーキーはデフォルトでは永続化されない。このため、インストール直後の再起動前もしくはインストールセットアップ時にリカバリーキーを永続化する設定が必要だった。この対応をしないとリカバリーキーは失われてしまう。 リカバリーキーを保存することなく削除してしまったら、リカバリーキーの失効と再発行を行う必要がある。また、LUKSは今までまともに使ったことがないので理解を深める必要がある。

ソフトウェアインストール

OSインストール後のセットアップまりのトラブルについて。

Extension Manager および Sound Input & Output Device Chooser

音声まわりのトラブルに対処するために、入出力デバイスを細かく制御できる必要があった。 これを実現するために Sound Input & Output Device Chooser を、GNOME Shell Extension Manager経由でインストールする。詳細については以下の記事を参照。

gihyo.jp

これにより、音声入出力デバイスの制御が可能となった。ただし、初手で Sound Input & Output Device Chooser をセットアップしたのでUbuntu22.04環境で本当に必要だったのかわからない。OS更新やGPU更新で問題は解消できていた可能性もあったので、Sound Input & Output Device Chooser をインストールする前に各種音声まわりのトラブルが発生しないか確認しておくべきだった。 Sound Input & Output Device Chooser 自体は便利に利用できているので、これを継続利用すること自体は問題なし。あとはExtension Managerまわりもまだ理解できていないので理解を深めておきたい。

NFSマウントセットアップ

OSクリーンインストールにより、起動時にNFSマウント失敗する問題も解消される想定だったが、そもそもNFSマウントに失敗するようになった。

原因等不明だが、対策としてはマウントオプションとして mountvers=4 を付与することで解消した。これにより、mountコマンド経由でも、/etc/fstab 経由でもマウント/自動マウントするようになった。

NFSバージョン詳細やマウントオプション詳細はあまり理解できていないので、詳細確認が必要。

deb版 firefox

snap版firefoxは不具合もあるのであまり使わない方がよいと事前から聞いてはいたが、実際に上手く動作しない問題があった。1passwordのアドオン拡張で Ctrl+. のショートカットが動作せず、パスワード補完にマウス操作が必要になるという問題があった。 これはさすがに許容できないと、以下の記事を参考にdeb版のfirefoxを導入した。

yuzu441.hateblo.jp

これにより無事1passwordアドオンも動作するようになった。ただし、カーソルが入力欄にあたっているとまだ動作しないという問題は残っており、完全解決には至っていない。

また、firefox以外も基本的にはsnap版を利用しないという方針をとっている。slackなどのアプリも上手く動作しないという事象に遭遇しており(Ubuntu20.04時、詳細は忘れた)、そういったトラブルの可能性を抱えてまでsnap版を利用するメリットを感じていない。

vim error

vi/view/lessなどのコマンドを利用するとき、 Failed to source defaults.vim のエラーが発生するというトラブルに遭遇した。 この不具合は既知のもので、いくつかのフォーラムにて報告されている。

bugs.launchpad.net

bugzilla.redhat.com

対策としては、 touch ~/.exrc にて .exrcファイルを作成した。これによりエラーは発生せずに動作するようになった。