Posts for: #2024/09

キャリア談義

たまたまだけど、お昼にキャリアセミナーの動画をみて、夜にキャリア相談にのった一日だった。

キャリアセミナーと転職

あんちぽサロン に参加されているメンバーがキャリアセミナーで話されている動画を紹介されていたのでみた。その方も5回転職していて、私と似たようなキャリア形成をされてきたようにみえた。そのせいもあって価値観も共感できるところが多かった。私もそうだが、一定回数以上の転職をする人は自身の好きなことに価値を見出す人が多いのかな?とその動画をみていて思った。転職について私自身の考えを考察してみる。

私の考える転職で得られるメリットは次になる。

  • どんな組織にも良いところと悪いところがあるとわかる
  • 多様な人間関係や価値観に触れるきっかけになる
  • 新しい知識やスキルを身につけるきっかけになる
  • コンフォートゾーンを抜け出すことで変化に対応するスキルが身につく
  • 年収をあげやすい

一方でデメリットもある。

  • 10年とか時間のかかる知識やスキルを身につけられない
  • 1度転職すると、その後も転職を繰り返しがちになる
  • 転職後の変化に対応するストレスが大きい
  • 転職が嫌なことからの逃げ道になってしまう
  • 一定数を超えると長期のキャリア形成を望む組織に信頼されない

いまの時代、転職におけるもっとも大きなメリットは変化に対応するスキルが身につくことだと個人的には思う。誰もが慣れた環境や人間関係、同じ業態・業種のお仕事をずっと続けていると、それが当たり前になってずっと続くような錯覚をしてしまう。そして、実際には3年、5年、10年と世の中は変わっていって普段しているお仕事がなくなっていく。個人と比べて組織はずっと変化に疎い。同じ組織にいると世の中の変化に鈍感になる傾向はあると思う。しかし、転職はリスクのある判断で一概に奨められるものでもない。とくに年齢が高くなると自身が求める待遇と先方から求められるスキルセットのマッチングが難しくなっていく。そしてマッチングで失敗するとキャリアにおけるダメージも大きい。私は20代の頃から転職してきたからリスクも折り込み済みで働いてきたが、過去を振り返るとよかった転職とわるかった転職は五分五分だと思う。そして40代になって転職先がなくなって自分で会社を経営するしかなくなった。その選択に後悔はないが、あまり他人へ奨められるようなキャリアではない。キャリアや働き方を大きく左右する転機の1つとして、若いときに転職するかどうかがあるように思う。

キャリア相談

夜にコミュニティの仲のよいメンバーと飲みに行った。急に誘いがきたからなんとなくそういう系の話しかな?という予測はついていた。そのメンバーはスタートアップで働いていて、あまりうまくいっていないという話しを以前から聞いていた。どうやら創業者が体調不良で会社を清算するらしい。事業を他社に売却するものの、従業員はどうなるか分からないらしい。黒字化していない事業だからあまりよい値段がつくとも思えないことから従業員までは雇わないのではないかと推測する。予定通り事務手続きが進めば今月末に会社がなくなるらしい。

そこで個人事業主でやっていくか、転職するかの相談にのっていた。まだ30代の若い方なのでどうしてもやりたいことがあるなら自分でやるのも1つの手だけど、個人事業主はいつでもなれるから転職して経験を積んだ方がよいとお勧めした。一番起業の成功率の高い年齢は、40代半ば にもあるように、起業して成功する確率が高いのは統計的に40代になる。当たり前の話しだけど、経験のある方がうまく経営したり失敗を避ける上での知恵が働くのだと推測する。うちの会社が受託開発できるのも世の中にシニアエンジニアは少なく、30代で身につけたスキルの貯金で食いつないでいるといえる。

あと貯金が減っていくのは精神的によくないという実体験を話した。起業するなら3年ぐらいは収入がなくても生活できる貯金を用意した方がよいとアドバイスした。生活費がなくなると目先のことしか考えられなくなって判断を誤るというのは「貧すれば鈍する」と昔からの格言にある。脱サラした経営者がキャッシュフローをみてないといった失敗談もたまにみかけるが、想定外のことが起こっても資金繰りに困らないだけの十分な余力として3年ぐらいの生活費をみた方がよいと考えている。

集中力の正体

昨日は20時半頃にお仕事を終えて帰った。スーパーで買いものして、家に帰って晩ご飯を作って食べて、休んだらオフィスへ戻るつもりがそのまま寝てしまった。

メッセージ作成

年一ゲストとして出演している terapyon channel100回記念公開収録 があった。いつもお世話になっているので私は参加できないがカンパ枠で応援していた。昨日てらださんから音声メッセージを作ってほしいと依頼され、本当は昨日の夜に作ろうと思っていたものの、疲れて寝てしまったので朝起きてから慌ててオフィスへ行って作成した。ubuntu の apt でインストールできるツールとして audacity を使って録音した。簡単に使えた。お祝いのメッセージを文章に書き出して、マイクテストも含めて最終的には7回撮り直した。文章を書いてから、話してみると、流れが悪かったり、語呂が悪かったり、途中で詰まったり、かんでしまったりと、なにか気になるところをみつけて、文章を推敲して録音しながら1時間ほどやってた。録音して聞き直すと、いかに自分の話し方が下手であるかがよくわかる。話し方を練習しないとうまくならないのだろうな。

みなとのもりの運動

前回の所感 。14時頃に作業を終えてストレッチまで1時間ほど時間が空いた。ふと運動してからストレッチへ行くとよさそうだと思い立ってジョギングと縄跳びをしてきた。前回が7月24日なので1ヶ月半ほど運動していなかった。1kmほどジョギングしてから縄跳びを15分間した。以前と比較したら軽めに流したにも関わらず、久しぶりに運動したら体力が落ちていてかなりバテた。習慣的に運動していないとすぐに衰える。久しぶりに運動できたので、また再開するきっかけになるかもしれない。

ストレッチ

今日の開脚幅も開始前148cmで、ストレッチ後152cmとあまり数値は出なかった。直前に運動して行ったのでよく伸びるかな?と予測したものの、測ってみるとそうでもなかった。それでも運動した後にストレッチを受けるのはカラダによさそうにも思える。ここ1ヶ月半ほど運動できていないものの、体重は67kg台を維持していて体脂肪率や筋肉量はあまり変わっていない。トレーナーさんとそういう話をしていたら、筋肉は落ちにくくつきにくいから1ヶ月ではそんなもんとのこと。

プログラミングの集中力と生産性への考察

ストレッチを受けているときにトレーナーさんと話していて思うことがあったのでまとめておく。いまの開発フェーズは開発者としてプロジェクトに参加している。平均すると1日10時間ぐらいお仕事していて、早く帰る日もあるから遅い日は日付が変わるぐらいまではコードを書いていることがある。家に帰ると休んでしまうからオフィスで晩ご飯を食べることも多い。なぜプログラミングに集中していると運動ができないのか?を考察してみた。

  • 他の余分なことを排除すると集中力が増す

いまやっている開発のお仕事は1日で完了するような簡単な開発ではない。1年半開発しているので残っている課題は厄介で複雑な問題への対応であることが多い。新規機能を追加するときも既存機能や仕様や依存関係を考慮しないといけない。そうすると2-3日かけて課題を解決することが多い。そのときに他の余分なことを排除した方が脳のリソースを課題解決に割り当てられる。通勤しているときも、寝ているときも、ご飯を食べているときも取り組んでいる課題のことを四六時中考えている。他のことに脳のリソースを割くと課題解決の品質が下がる可能性が高い。ずっと考え続けることが複雑な問題の課題解決に重要となる。

  • システム開発とはタイムアタック競技である

システム開発のプロジェクトマネジメントにおいてもっとも重要な概念はタイムボックスだと私は考えている。適度な期間 (うちは2週間) を設け、モノゴトに取り組む最初と最後を作ること。認知心理学の研究からも記憶の仕組みからも期間の最初と最後がもっとも学習効果が高いことを示唆している。このことは私の経験則においてもシステム開発で当てはまる。作業期間が適切でないと人間はだらだらしてしまう (パーキンソンの法則) 。プログラミングにおいて動くのは当たり前で、動いた上でいかに品質をあげられるかが腕の見せ所になる。エラー処理を適切に制御できているか、他人がコードを読んでも理解しやすく保守しやすいか、将来の拡張性を考慮して設計されているか、など品質をよくするための取り組みには答えがない。仕事ができる・できないを分ける行動の1つとして、答えのない問いにどれだけ準備できるか、考え続けられるかと言い換えられるかもしれない。答えがないからいくらでも品質をあげるための施策がある。しかし、現実の業務には期限があるため、期限内に最善の品質を目指すことになる。そのため、タイムボックスでシステム開発をマネジメントする限り、いつもタイムアタック競技をしているのと同義になるから時間が足りない。

プログラミングはいつも妥協を強いられる。完璧で最高のシステムなどありえない。その時々で開発途中における最善の動くものをスナップショットとしてバージョン管理しているに過ぎない。この妥協するレベルが私にとってはマネージャーと開発者という役割で大きく異なるのだろうと思う。マネージャーとしての遊撃なら先送りできても、開発者としては多少の負荷があっても解決してしまう。自身の基準を満たす働き方に違いがあるのではないかと思う。言語化してみると、この考え方はプログラミングに限らず、そのときに取り組む対象によって何にでも応用できそうに思う。

ldap スキーマの情報を返す web api の実装

一昨日昨日 と ldap スキーマの parser について調査して実装した。最終的には構文解析した値をそのまますべて返すわけではなく、実際に ui や内部の処理で必要な情報のみを使いやすい構造にして web api として実装する。マージリクエストを作成してメンバーにコードレビューをしてもらっていて、スキーマ情報だけでは足りない仕様に1つ気付いた。たとえば、次の cn という属性の定義には SUP (super の略語で親の定義から派生していることを表す) として name が定義されている。

attributetype ( 2.5.4.41 NAME 'name'
       EQUALITY caseIgnoreMatch
       SUBSTR caseIgnoreSubstringsMatch
       SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )

attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' )
       DESC 'RFC2256: common name(s) for which the entity is known by'
       SUP name )

cn の定義には SYNTAX は定義されていないが、これは name の定義にある SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 (Directory String のこと) の oid を継承するという仕様になっている。自分で SUP を調べて継承元の情報を取得するのは面倒なので構文解析した後に SUP が指定されていれば Equality, Ordering, SubStr, Syntax を親から取得するように改善した。こういうところは自前で parser を実装しているので自分たちの都合にあわせてカスタマイズしやすい。

3日もかかってしまったが、これで ldap スキーマを返す web api の実装を完了できた。いまの開発フェーズにおける、私が機能拡張する issue はこれで完了にしようと考えている。来週からはテストや qa 向けの作業にのみ注力していこうと思う。本番導入前に少しでも運用トラブルの懸念を減らす、もしくは品質をあげるための取り組みをあと3週間やってからしっかり検証をしていく。ここ1ヶ月ほど深夜までコードを書いていることが多かった。終わってみれば、面倒で厄介な issue も大半を fix した。当たり前の話だけど、四六時中ずっとコードを書いていたら気付いたら成果として積み上がってきた。マネージャーを移管したことでその分の労力を開発にまわせているのもある。

go で parser を実装してみた

昨日の ldap スキーマ parser 実装 の続き。parser generator は使わず自分で parser を実装してみることにした。go の標準ライブラリにある text/scanner を使って字句解析をしたら、次のカスタマイズを行うことでそれっぽく token に分解できた。

s.Mode ^= scanner.ScanFloats
s.IsIdentRune = func(ch rune, i int) bool {
	return ch == '.' ||
		unicode.IsLetter(ch) ||
		unicode.IsDigit(ch)
}

しかし、text/scanner はあくまで go のソースコードを字句解析するためのツールになる。ldap スキーマの字句解析も一応は意図したように分割できたが 'objectClass' のようなシングルクォートで文字列を囲むような構文を go のソースコードでは記述できないため、エラーをチェックすると invalid char literal として必ずエラーになってしまう。これでは字句解析のエラーチェックをできなくなってしまうため text/scanner の利用は断念した。

そこで bufio#Scanner を使って字句解析を実装した。字句解析を行うツールを lexer または tokenizer と呼ぶ。go ではこのツールを scanner と呼ぶ慣習になってるようにみえる。分割する token をカスタマイズするには SplitFunc を定義する。ldap スキーマの字句解析は次のように定義できた。

const (
	singleQuote byte = '\''
)

func tokenize(
	data []byte, atEOF bool,
) (
	advance int, token []byte, err error,
) {
	advance, token, err = bufio.ScanWords(data, atEOF)
	if err != nil {
		return
	}
	if len(token) > 0 && token[0] == singleQuote {
		since := 0
		if data[0] != singleQuote {
			since = bytes.IndexByte(data, singleQuote)
		}
		i := bytes.IndexByte(data[since+1:], singleQuote)
		if i == -1 {
			return 0, data, bufio.ErrFinalToken
		}
		pos := since + i + 2
		return pos, data[since:pos], nil
	}
	return
}

func NewScanner(src string) *bufio.Scanner {
	s := bufio.NewScanner(strings.NewReader(src))
	s.Split(tokenize)
	return s
}

次のような ldap スキーマの文字列は、

( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )

次のように token として分割される。

(
2.5.4.0
NAME
'objectClass'
DESC
'RFC4512: object classes of the entity'
EQUALITY
objectIdentifierMatch
SYNTAX
1.3.6.1.4.1.1466.115.121.1.38
)

あとはこれらの token から構文解析を行う parser を実装するだけ。AttributeTypeDescription の文法は次になる。

AttributeTypeDescription = "(" whsp
      numericoid whsp              ; AttributeType identifier
    [ "NAME" qdescrs ]             ; name used in AttributeType
    [ "DESC" qdstring ]            ; description
    [ "OBSOLETE" whsp ]
    [ "SUP" woid ]                 ; derived from this other
                                   ; AttributeType
    [ "EQUALITY" woid              ; Matching Rule name
    [ "ORDERING" woid              ; Matching Rule name
    [ "SUBSTR" woid ]              ; Matching Rule name
    [ "SYNTAX" whsp noidlen whsp ] ; Syntax OID
    [ "SINGLE-VALUE" whsp ]        ; default multi-valued
    [ "COLLECTIVE" whsp ]          ; default not collective
    [ "NO-USER-MODIFICATION" whsp ]; default user modifiable
    [ "USAGE" whsp AttributeUsage ]; default userApplications
    whsp ")"

AttributeUsage =
    "userApplications"     /
    "directoryOperation"   /
    "distributedOperation" / ; DSA-shared
    "dSAOperation"          ; DSA-specific, value depends on server

字句解析する scanner と組み合わせて実装した parser は次のようになる。

func ParseAttributeTypeDescription(
	src string,
) (*AttributeTypeDescription, error) {
	var err error
	d := &AttributeTypeDescription{}
	s := NewScanner(src)
	for s.Scan() {
		token := s.Text()
		switch token {
		case leftParenthesis:
			if s.Scan() {
				d.OID = OID{Value: s.Text()}
			}
		case rightParenthesis:
			// do nothing
		case "NAME":
			if s.Scan() {
				d.Name = ParseNAME(s)
			}
		case "DESC":
			if s.Scan() {
				d.Desc = Unquote(s.Text())
			}
		case "OBSOLETE":
			d.Obsolete = true
		case "SUP":
			if s.Scan() {
				d.Sup = NewWOID(s.Text())
			}
		case "EQUALITY":
			if s.Scan() {
				d.Equality = NewWOID(s.Text())
			}
		case "ORDERING":
			if s.Scan() {
				d.Ordering = NewWOID(s.Text())
			}
		case "SUBSTR":
			if s.Scan() {
				d.SubStr = NewWOID(s.Text())
			}
		case "SYNTAX":
			if s.Scan() {
				d.Syntax, err = ParseNOIDLen(s.Text())
				if err != nil {
					return nil, fmt.Errorf("failed to parse SYNTAX: %w", err)
				}
			}
		case "SINGLE-VALUE":
			d.SingleValue = true
		case "COLLECTIVE":
			d.Collective = true
		case "NO-USER-MODIFICATION":
			d.NoUserModification = true
		case "USAGE":
			if s.Scan() {
				usage := GetAttributeTypeUsage(s.Text())
				d.Usage = &usage
			}
		default:
			if strings.HasPrefix(token, "X-") {
				if s.Scan() {
					if d.Extension == nil {
						d.Extension = make(map[string][]string)
					}
					d.Extension[token] = ParseQuotedStrings(s)
				}
				break
			}
			slog.Warn("unsupported", "token", token, "oid", d.OID, "name", d.Name)
		}
	}
	if err := s.Err(); err != nil {
		return nil, fmt.Errorf("failed to scan: %w", err)
	}
	return d, nil
}

昨日の調査で go 製の parser generator をあまりみつけられなかったことに気付いた。その理由を理解できた。go で初めて parser を実装してみて簡単に実装できることに気付いた。標準ライブラリにある text/scanner や bufio#Scanner を使うと簡単に字句解析できるため、自分で parser を実装する労力が小さい。したがって parser generator を使って実装するよりも、自分で parser を実装することを選ぶ開発者が多いのではないかと推測する。

openldap スキーマと parser generator

この開発フェーズではあまり機能拡張を行わない方針としているが、できればやっておいた方がよい機能拡張の1つに openldap サーバーから ldap スキーマを取得する処理がある。ちょうどいま若いメンバーに実装してもらっている大きな機能のバリデーションにも ldap スキーマの情報があると便利そうなので私が対応することに決めた。

openldap サーバーで管理している ldap スキーマの定義は openldap サーバーの Schema Specificationrfc 4512 の2つをみると仕様がわかる。たとえば、次のように AttributeTypeDescription というスキーマの定義は Augmented Backus–Naur form (abnf) という記法で定義されている。rfc などのネットワークプロトコルの世界では abnf のフォーマットで仕様を説明することが多いらしい。

AttributeTypeDescription = "(" whsp
      numericoid whsp              ; AttributeType identifier
    [ "NAME" qdescrs ]             ; name used in AttributeType
    [ "DESC" qdstring ]            ; description
    [ "OBSOLETE" whsp ]
    [ "SUP" woid ]                 ; derived from this other
                                   ; AttributeType
    [ "EQUALITY" woid              ; Matching Rule name
    [ "ORDERING" woid              ; Matching Rule name
    [ "SUBSTR" woid ]              ; Matching Rule name
    [ "SYNTAX" whsp noidlen whsp ] ; Syntax OID
    [ "SINGLE-VALUE" whsp ]        ; default multi-valued
    [ "COLLECTIVE" whsp ]          ; default not collective
    [ "NO-USER-MODIFICATION" whsp ]; default user modifiable
    [ "USAGE" whsp AttributeUsage ]; default userApplications
    whsp ")"

AttributeUsage =
    "userApplications"     /
    "directoryOperation"   /
    "distributedOperation" / ; DSA-shared
    "dSAOperation"          ; DSA-specific, value depends on server

openldap サーバーに対して ldap スキーマを取得する方法は How can I fetch schema information from the server? の faq に書いてある。search base に対してサブスキーマサブエントリを返す dn を取得する。openldap サーバーの場合 ldap の root ツリーに対応するのは cn=Subschema がデフォルトとなる。

$ ldapsearch -H ldap://localhost -x -LLL -b dc=example,dc=com -s base subschemaSubentry
dn: dc=example,dc=com
subschemaSubentry: cn=Subschema

この dn にスキーマ情報の属性が格納されているのでそれらを取得する。このスキーマ情報は operational attributes として管理されているので + という記号が operational attributes 属性群をまとめて取得するキーワードになっている。

$ ldapsearch -x -LLL -b cn=Subschema -s base '(objectClass=subschema)' +

これでスキーマ情報を取得できる。先に書いた AttributeTypeDescription の実際のスキーマ情報は次のような内容になる。こういったスキーマのテキスト情報をたくさん取得できる。

( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )

go の parser generator 調査

この手のものは abnf から parser をコード生成するのが一般的なやり方かな?と、まずは go で使えそうな parser generator について調べてみた。意外と go 製の parser generator はみつからなかった。私が発見できたのは次の3つぐらい。

最後の antlr は java 製のツールだけれども、go のソースコードを出力できるので go 実装の parser をコード生成できるという意図で対象としている。goyacc は abnf の文法を yacc に変換しないといけない。yacc で実装した ldap スキーマの parser を公開しているのもみかけたが、yacc は違うなと思って除外した。antlr も abnf から専用の文法に変換しないといけないから不採用にした。

それで消去法的に bnf (ebnf) を扱える gocc で parser をコード生成できないかを調査してみた。しかし、半日ほど bnf を書いて実際にコード生成してみて不採用とした。ebnf の options 記法として [...] に gocc が対応していないようにみえた。この記法がないと ldap スキーマの文法定義が煩雑になる。issue のコメント をみると、lexer は対応したが、parser は対応していないといったコメントがある。コード生成しようとするとエラーになるので未対応なのかもしれない。bnf でこの options 記法相当のものを自分で文法定義すると、1つ2つなら簡単に変換できるが、数個の組み合わせがあると途端に文法の複雑さが大きくなるように思える。abnf から bnf に変換する過程で文法が複雑になってしまうとその後の保守ができなくなる懸念が生じるので断念した。

今回の gocc の採用は却下したが、軽く触ってみて gocc 自体の感触はよかった。シンプルな bnf で表現できるものであれば機会があれば採用してもよいと思える。gocc example をみればわかるが、bnf を書きながら単体テストのコードを実行して動作検証を小さく簡潔にできる。これは parser のコード生成の開発サイクルは速くできそうに感じた。こういう小さく単体で動くツールは好み。

最終的な結論としては parser generator は使わず、自前で parser を実装することを決めた。

case-insensitive という古くて厄介な問題

casemap によるリファクタリング

ldap プロトコル (v3) は rfc 2251 で定義されていて属性へのアクセスは case-insensitive (大文字小文字を区別しない) に扱うという仕様になっている。

ldap サーバーへの問い合わせや検索は case-insensitive となるため、ldap エントリーを扱う他システムの処理もすべて case-insensitive にしないと整合性が取れなくて混乱する。これは利用者だけでなく開発者も同様であり、一部が case-insensitive なのに、一部を case-sensitive (大文字小文字を区別する) にすると、比較処理がマッチしたりしなかったりという混乱を生じる。そういう面倒くさい issue が過去にいくつか散見されてきた。この問題は場当たり的な修正ではなく、本質的な対応が求められた。

結論としては、すべてを case-insensitive に扱う必要があって、そのためにキーを case-insensitive として扱う map like なデータ構造を実装した。オリジナルのキー情報も保持しつつ、case-insensitive にアクセスできるよう小文字変換しておいた名前変換テーブルを内部にもつ。最初からこういったデータ構造を定義しておけば開発時に混乱は起きないが、こういうものは後になってわかってくる。うちは1年半経ってからこの問題の本質を理解した。そのため、関連する処理のインターフェースを見直して置き換えることになった。こういうリファクタリングは時間がかかる割に成果も地味なので労力に対して評価されないことが多い。私もやっていてあまり楽しくはないが、こういうところをきっちり作ると品質に寄与するのを経験的に知っている。

type CaseMap struct {
	keyValue map[string][]string
	nameConv map[string]string
}

func (m *CaseMap) String() string {
	return fmt.Sprintf("%v", m.keyValue)
}

func (m *CaseMap) Values(name string) []string {
	origName, ok := m.nameConv[strings.ToLower(name)]
	if !ok {
		return nil
	}
	values, ok := m.keyValue[origName]
	if !ok {
		return nil
	}
	return values
}

func (m *CaseMap) Get(name string) ([]string, bool) {
	origName, ok := m.nameConv[strings.ToLower(name)]
	if !ok {
		return nil, false
	}
	values, ok := m.keyValue[origName]
	return values, ok
}

func (m *CaseMap) Set(name string, values []string) {
	m.keyValue[name] = values
	m.nameConv[strings.ToLower(name)] = name
}

func (m *CaseMap) Len() int {
	return len(m.keyValue)
}

func (m *CaseMap) Iter() iter.Seq2[string, []string] {
	return func(yield func(string, []string) bool) {
		for k, v := range m.keyValue {
			if !yield(k, v) {
				return
			}
		}
	}
}

type CaseName struct {
	Original  string
	LowerName string
}

func (m *CaseMap) IterWithLower() iter.Seq2[CaseName, []string] {
	return func(yield func(CaseName, []string) bool) {
		for lowerName, origName := range m.nameConv {
			name := CaseName{
				Original:  origName,
				LowerName: lowerName,
			}
			if !yield(name, m.keyValue[origName]) {
				return
			}
		}
	}
}

func (m *CaseMap) ToMap() map[string][]string {
	return m.keyValue
}

func New(length int) *CaseMap {
	return &CaseMap{
		keyValue: make(map[string][]string, length),
		nameConv: make(map[string]string, length),
	}
}

func NewFrom(attr map[string][]string) *CaseMap {
	m := New(len(attr))
	for k, v := range attr {
		m.Set(k, v)
	}
	return m
}

開発が進捗することの作用

昨日おこなった調査 の成果もあって、いろいろ他の開発作業も進捗して、今日はいくつか小さい issue を fix できて1日の成果としては満足できる内容になった。それで夕方に洗濯したり、晩ご飯を作ったり、家事をやる余裕があったり、さらにオフィスへ戻って作業をしてから外出してくることもできた。ここ数日にはなかった充実した1日だったように実感した。いまの生活に満足できなかったりモチベーションが上がらなかったりする背景の1つとして、溜まっている残タスクへのストレスがある。このストレスを解消するのは残タスクを fix することがもっとも効果的である。

結論はやる気がなくてもやるしかない。少しずつでも問題を解決していくしかない。

古い文章だが、いまでも覚えていてたまに引用する Joel on Software の格言の1つに 射撃しつつ前進 がある。いま自分が置かれている状況はまさにこれに相当するものなので地道な積み重ねをしていくしかない。そういうときもあって人生はちょうどよいのだろうと思う。

ぱっとしない休日

9月に入ってしまった。8月は開発者の生活を思い出すための試行錯誤の月だった。よくもわるくもかな。

キャンセル料の支払い

この前の金曜日は しみん福祉スポーツセンター の体育館を借りてバドミントンを行う予定だった。木曜日は戻ってこれない可能性 があったし、前日の天気予報では金曜日の夕方が神戸に台風が来る予報になっていた。そこで木曜日の夜に参加者も少なかったしキャンセルすることに決めた。キャンセルは1週間前でないとできないため、これは自然災害だから仕方がないと体育館のキャンセル料金3,000円を支払うことにした。直接、しみん福祉スポーツセンターの窓口でないと支払いできない。窓口へ行って paypay で支払いしてきた。しみん福祉スポーツセンターの体育館を借りるのは値段が高いので参加者数が増えてから借りる運用を変えようと思う。

X-Forwarded-For ヘッダーの制御

先日の作業の続き 。本当は土曜日にやろうと思っていて、全然やる気がなくて、なにもやらないよりはちょっとでもやった方がよいかと、今日やり始めたら集中できて2-3時間で調査と対応を完了した。もともと構築しているリバースプロキシとしての nginx には次のヘッダーを扱う設定が追加されていた。

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;

追加で remote_addr を置き換える設定を入れてみれば、リバースプロキシ経由でリクエストを受ける go の http Request が参照する RemoteAddr の値も置き換わるのかな?と検証してみた。

set_real_ip_from 172.29.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
proxy_set_header REMOTE_ADDR $remote_addr;

結論としては RemoteAddr の値は置き換わらなかった。nginx の real_ip_header モジュールは nginx 環境における remote_addr の値を変更する設定であり、転送するときにパケットの値を置き換えるものではないようにみえる。そこで api サーバー側で X-Forwarded-For ヘッダーを参照するのが正しい対応方法だと理解できた。このヘッダーを参照することは一般的な用途に思えるので調べたら echo の IP Address のドキュメントにその設定方法やユーティリティの扱いなどが書いてあってすぐに参照できることもわかった。

X-Forwarded-For ヘッダーはクライアントが任意で偽装できる。デフォルトでは内部ネットワークから転送されたヘッダーの値のみを信頼するように設定されている。それが次の IPExtractor の設定になる。デフォルトの制約を変更することもできるが、コンテナで運用するとすべての通信はコンテナネットワークの gateway からリクエストされているようにみえるのでアクセス制御という側面ではこの設定そのものにあまり意味はない。

e := echo.New()
e.IPExtractor = echo.ExtractIPFromXFFHeader()

X-Forwarded-For ヘッダーの値を実際に参照するときは c.Request().RemoteAddr ではなく c.RealIP() を使う。api サーバーはこのぐらいの変更で実際のクライアントから転送されてきた ip アドレスを知ることができた。