1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

ZenHubのPrometheus Exporter作ってみた

記事一覧はこちら

背景・モチベーション

仕事でexporterを書くときにスッと書けるようにという練習がてら。
オンボーディングの中で自分でマイクロサービスをデプロイしてみるというタスクもあり、そこでも使える気がしたので一石二鳥を狙っている。

プロダクトバックログとかの量とかタスクの見積もりとかをaggregateしたりで可視化出来るのは悪くないような気も。GitHubAPIも利用するとLabelでいろんな属性を付けられるので良さげなんだけれども、とりあえず今回はZenHubのAPIのみを使っている。

ちなみにGrafanaのorgの下には、ほとんど手が付けられていないzenhub-exporterがあったりする。

本編

Exporterの書き方はここに書いてある。公式でツールセットだったりも提供してくれていて非常に手厚い。

prometheus.io

exporterのデフォルトのポート番号を選ぶ

Prometheusのexporterは1つのホスト上で複数動くことが充分に考えられるため、デフォルトのポート番号がwikiで整理されている(詳細)。使われていないポート番号を選ぶことを始めた。今回はポート番号9861を利用することにした。実際に広く使われるexporterを作成する場合には、このwikiを編集する必要がある。

github.com

Exporterを実装する上で気をつけたいこと

前述のWriting Exporterというページを参考に一部抜粋してみる。
📝 のマークが付いているのは自分の感想でドキュメントに書いてあった内容ではありません。

Configuration

YAMLがPrometheusの標準的な設定記述なので、もし設定ファイルを必要とする場合にはYAMLを利用すること。

Metrics

Naming

メトリクスの名前は特定のシステムに詳しくない人でも類推できるものする必要がある。そのため、メトリクス名は、通常 haproxy_up のように、exporter名をprefixに付ける必要がある。

メトリクスはsecondsやbyteなどのbase unitsを使用して、Grafanaなどのgraphing toolsで読みやすいようにしておく。同様に、パーセンテージではなくratioを利用する。
📝 これは自分の中でも印象が強く、node_exporterがいつだったかのバージョンアップの時に今までなかった単位がメトリクスのsuffixに足されたのを覚えている。

Metrics名にはexportされる際のラベルを含めるべきではない。ただし、同じデータを複数のメトリクスで異なるラベルを付けてエクスポートしている場合は例外だ。単一のメトリクスにすべてのラベルを付けてエクスポートするとカーディナリティが高くなりすぎる場合にのみこの例外対応が必要になる。
📝 ここでのカーディナリティはRDBでなく時系列DBの用語を指すので注意(fukabori.fm #53

Prometheus のメトリクスとラベル名はsnake_caseになる。
_sum _count _bucket _total といったsuffixはSummaryやHistograms、Countersに使われるため、それら以外であればこれらのsuffixは利用しないこと。_total はcounter用に予約されているのでCOUNTERタイプのメトリクスであれば利用するべきsuffixである。

process_scrape_ というprefixは予約されているので利用しないこと。ただし、これらに独自のprefixを追加した場合には利用可能となる。たとえば、jmx_scrape_duration_secondsは問題ない。

成功リクエストと失敗リクエストの数がある時に、これを公開する一番良い方法は一つのメトリクスではトータルのリクエスト数、もう一つのメトリクスでは失敗したリクエストを扱うこと。 1つのメトリクスに失敗または成功のラベルを付けないように注意する!
同様に、キャッシュのヒットまたはミスについても、1つのメトリクスを総計に、もう1つのメトリクスをヒット数にするのがよい。
📝 ついついメトリクスを分けるのではなくLabelで対応しがちなので特に気をつけたい項目

Labels

ラベルには関してはこちらのドキュメントにおいても言及されています。

prometheus.io

一般的なガイドラインとしてメトリクスのカーディナリティを10以下にするようにして超えるものは一握りとする。
📝 ここではLabelの取りうる値のことについて言っていると自分は解釈しました。保存するデータ量はLabelの取りうる値の掛け算によって決まるので非常にパフォーマンスに影響が出やすい部分のはずです。cardinality-is-keyというブログを参考にしました。

なにか事象が起こるまで存在しない時系列データは扱いが難しくなるため、事前に存在する可能性があるとわかっている時系列データに関しては0などのデフォルト値を書き出しておくこと。

ターゲットラベルと衝突するラベル名は避けることが推奨されている。例えば、region, zone, cluster, availability_zone, az, datacenter, dc, owner, customer, stage, service, environment, envなど。
📝 ターゲットラベルと言っているのはおそらくExporterごとに付与されるラベルのことと思います。例えば、ec2のService Discoveryを利用されている場合にはmetaラベルでAZが取得できるので、それをavailability_zoneにrelabelするのはよくあるかなあと思います。

Read/writeとsend/receiveはラベルとしてではなく、別々のメトリクスとして使用するのが最適です。これは通常、一度に1つだけを気にするからであり、その方が使いやすいからです。

ラベルの付与は慎重に。ラベルが増えると、ユーザがPromQLを作成するときに考慮しなければならないことが増えます。メトリックに関する追加情報は、infoメトリックを介して追加することができます。例えば、kube_pod_infoとかがそれに当たります。

Types

MetricsのTypeは通常CounterかGaugeだけれども、もしデクリメントのあるCounterの場合には誤解を招かないようにGAUGEではなく、UNTYPEDを利用する。

Help Strings

ヘルプをメトリクスそれぞれを付けておく、メトリクスのSourceが分かると良い。

Collector

ExporterのCollectorを実装する場合には、scrapeごとにメトリクスを更新するという方法を取るべきではない。
毎回新しいメトリクスを作成してください。GoではCollect()メソッドでMustNewConstMetric()を使ってメトリクスの作成を行います。
📝 作成されるメトリクスの構造体は変更できないようにフィールドがunexportedになっています。
新しいメトリクスを毎回作成する理由は2つあります。

  • 2つのscrapeが同時に発生する可能性があり、direct instrumentationの場合にはグローバル変数が競合する可能性がある
  • ラベルの値が消失したときにもexportされてしまう

Elasticsearch などの多くのシステムでは、CPU、メモリ、ファイルシステム情報などのマシン・メトリクスが公開されています。Prometheusのエコシステムではnode_exporterがこれらを提供しているので、このようなメトリクスは削除すべきです。

Deployment

それぞれのexporterは1つのinstanceのアプリケーションを監視すべきで、できれば同じマシン上で隣り合っているのが望ましい。

ただし2つの例外があります。

  1. 監視しているアプリケーションの横で実行することが全く無意味な場合
    • 📝 今回作ったzenhub_exporterblackbox_exporterはこのケースに当てはまる
  2. システムのランダムなインスタンスからいくつかの統計情報を引き出し、どのインスタンスと話しているかを気にしない場合

Scheduling

メトリクスは、Prometheus がscrapeするときにのみアプリケーションから引き出されるべきであり、exporterは独自のタイマーに基づいてscrateを実行すべきでない。

公開するメトリクスにタイムスタンプを設定すべきではなく、Prometheusに任せたほうが良い。

メトリクスの取得に1分以上かかるなど、特にコストがかかる場合はキャッシュしても構いわないが、HELP Stringsにその旨を記載する。
Prometheusのデフォルトのscrape timeoutは10秒です。もしexporterがこれを超えることが予想される場合は、ドキュメントの中で明示的にその旨を伝える必要がある。

Failed scrapes

Scrapeに失敗したときの処理はパターン主に2つ。2つ目はexporterのダウンとアプリケーションのダウンの見分けが付く点で優れる。

  1. 5xxエラーを返す
  2. myexporter_up(例:haproxy_up)という変数を用意して、スクレイプが成功したかどうかに応じて0または1の値を持つ

Landing page

http://yourexporter/ にユーザがアクセスした時に見せるシンプルなHTMLを用意して、そこにはexporterの名称と/metrics ページへのリンクを貼るべし。

f:id:jrywm121:20210920213503p:plain

上記踏まえて ZenHub Exporter 作った

上記を踏まえて、今回作ったアプリケーションのリポジトリはこちらです。

github.com

ビルドやリリースのツールもPrometheusが提供していて、haproxy_exporternode_exporterなどのPrometheusのorgで管理されているリポジトリはだいたいpromuというツールを使ってリリースなどが行われているそうです。実際には、promuを利用したCircleCIのOrbが作られており、それを各リポジトリから利用されているようです。ドラフトのリリースを作ってくれます。

今回はそのpromuというツールでビルドやリリースを行うようにしてみました。便利ではありますし、Officialのexporterとリリース形式が同じになるため良いですが、必ずしも利用する必要性が無さそうなところも見えましたので普通にgoreleaserを使っても良いのではと思います。ただしその際には、ldflagsgithub.com/prometheus/common/version.Versionなどにバージョン設定をしないと、--version実行時に歯抜けで表示されるので注意です。

まとめ(雑記?)

OpenMetricsを有効化したけど、どこからUnitを設定するのかよく分からなかった。スペックを読む限りだとTypeやHelpと同じところに表示されそうな気がしてるんだけど、Prometheusのclientライブラリからは読み取れなかった…。

github.com

MongoDB UniversityのM103を修めた

記事一覧はこちら

Table of Contents

背景・モチベーション

8月1日から中途入社して働いている会社ではRDBだけでなく、いわゆるNoSQL(Not Only SQL)であるMongoDBを利用しています。そのうえshardingも利用しているということで、データ指向アプリケーションデザインを7月に読んでいたこともあり、非常に興味を持ちました。

同じチームの方から、MongoDB Universityが良いよというアドバイスを頂き、早速受講してみたのでまとめたメモをブログに残しておこうと思います。

自分が受講したコースは、レクチャー動画を観て、Quizを解き、更にインタラクティブなターミナルをブラウザ上で動かして実際に構築しているという流れで進んでいきます。動画を観るだけではないので、講義内容が右から左に抜けていきづらくなっていると思います。動画は英語ですが、Transcriptがダウンロード出来るようになっているので、最悪DeepLにお願いすることでなんとかなります。

講師の方の中にスターウォーズが好きな人が居て、時々画面の前の僕らに向かってYoung Padawanと語りかけてくるのが面白いです。

M103

f:id:jrywm121:20210830010153p:plain

Chapter 1: The Mongod

What is mongod

mongodはmongodbのメインのdaemon processです。
It is the core server of the database, handling connections, requests, and most importantly, persisting your data.

mongodはReplicaSetやSharded Clusterなど複数サーバでの構成において、各サーバで動作する

Default Configuration

設定ファイルや起動引数で設定を渡すことになるが、渡さない場合にはデフォルトの設定で動く

  • The port mongod listens on will default to 27017.
  • The default dbpath is /data/db.
    • the data files representing your databases, collections, and indexes are stored so that your data persists after mongod stops running
    • The dbpath also stores journaling information (crash logs…)
  • mongod binds to localhost
    • mongodに接続できるクライアントはlocalhostに存在するものだけ
    • リモートクライアントを受け入れるには設定を変更する必要がある
  • Authentication is turned off

当たり前っちゃ当たり前だけど、リモートからのアクセスを許可するときには、authtrue にしましょうね

Configuration file

コマンドラインでオプション渡す事もできるが、設定ファイルを利用することも可能

設定ファイルの形式はYAMLとなる 階層化されてるので、コマンドオプションより格段readabilityが増して分かりやすいぞ

mongod -f "/etc/mongod.conf"

Configuration File Options — MongoDB Manual

File Structure

MongoDB standalone server のときのファイル構造

root@2a9d0458fdcc:/data/db# tree -L 2 .
.
|-- WiredTiger
|-- WiredTiger.lock
|-- WiredTiger.turtle
|-- WiredTiger.wt
|-- WiredTigerHS.wt
|-- _mdb_catalog.wt
|-- collection-0-7411902203291987629.wt
|-- collection-2-7411902203291987629.wt
|-- collection-4-7411902203291987629.wt
|-- diagnostic.data
|   |-- metrics.2021-08-22T08-52-18Z-00000
|   `-- metrics.interim
|-- index-1-7411902203291987629.wt
|-- index-3-7411902203291987629.wt
|-- index-5-7411902203291987629.wt
|-- index-6-7411902203291987629.wt
|-- journal
|   |-- WiredTigerLog.0000000001
|   |-- WiredTigerPreplog.0000000001
|   `-- WiredTigerPreplog.0000000002
|-- mongod.lock
|-- sizeStorer.wt
`-- storage.bson

WiredTiger (storage engine) がクラスタメタデータやWiredTiger固有の設定などの情報をtrackする用途で使われているファイル群が上記

The WiredTiger.lock file acts as a safety. 2つ目のMongoDBプロセスを同時に実行し、このフォルダを指定した場合、ロックファイルはその2つ目のMongoDBプロセスが起動するのを防いでくれます。 mongod.lock も同じような役割を果たす。

ホストマシンが落ちたりcrashしたときには、このlockファイルのせいでmongodが起動できないことがある。

.wt の拡張子を持つファイルは、collectionとindexのデータ。WiredTigerでは、それぞれ別の構造として保存される。

diagnostic.data ディレクトリはMondoDBサポートエンジニアが診断する目的のみのために利用されるもの。 この診断データは、Full Time Data Capture (FTDC) モジュールによって取得される。

journal ディレクトリ内のファイルはそれぞれWiredTigerのjournaling systemの一部。

WiredTigerでは、書き込み操作はメモリにバッファリングされる。60sごとにflashされてデータのチェックポイントが作成される。

また、WiredTigerでは、ディスク上のjournal fileへの書き込みにWAL(Write Ahead Logging)を採用している。journal entryははまずメモリ上にバッファリングされ、その後WiredTigerは50ミリ秒ごとにjournalをディスクに同期します。Each journal file is limited to 100 megabytes of size.

WiredTigerは、データのディスクへの同期にファイルローテーション方式を採用しています。障害発生時には、WiredTigerはjournalを使用してチェックポイント間に発生したデータを回復することができます。

※ 基本的にMongoDBのdata direcrotyは直接編集を行わないこと!

root@2a9d0458fdcc:/tmp# tree .
.
`-- mongodb-27017.sock

/tmp ディレクトリには、socketファイルがありプロセス間通信に利用される。 このファイルは起動時に作成され、MongoDBサーバーにこのポートを所有させる。

Basic Commands

Shell Helperを利用することでメソッドLikeに操作することができる

  • db - database
    • db.<collection>.createIndex 等…
    • db.runCommand() をwrapしてusabilityを高めたもの
  • rs - replicaset
  • sh - sharded

db.commnadHelp(<Command>) でヘルプが見れる

Logging

db.getLogComponents() でlogLevelを確認できる commandやindexなどのcomponentごとにLoglevelが設定できる

-1 は親の指定を引き継ぐこと。下記でいうと、一番上のレベルで”verbosity”: 0 が宣言されていているので0 になる 0 - 5 まで設定することでき、数字が高いほどverboseとなる。0はinfoレベルのみで、1からdebugレベルの出力を行う。

{
    "verbosity" : 0,
    "accessControl" : {
        "verbosity" : -1
    },
    "command" : {
        "verbosity" : -1
    },
    "control" : {
        "verbosity" : -1
    },
    "//": "omitted..."
}

ログレベルの設定

mongo admin --eval '
  db.setLogLevel(0, "index")
'

ログを確認する手段は2つ

# via Mongo Shell
db.adminCommand({ "getLog": "global" })

# via command line
tail -f /data/db/mongod.log

5 Severity Levels Logエントリの2つ目のフィールド

F - Fatal
E - Error
W - Warning
I - Information(Verbosity Level 0)
D - Debug(Verbosity Level 1-5)

Profilers

ログにはコマンドに関するデータも含まれていますが、クエリの最適化を開始するのに十分なデータはありません。

execution statsやクエリで使用されるインデックス、rejected planなどの情報を取得し、遅いoperationをデバッグするためには、ログではなくProfilerを利用する。

Profilerが取得するデータは下記の3つ

  • CRUD operations
  • Administrative operations
  • Configuration operations

データベースレベルでProfilerを有効にできる。 有効にすると、system.profile というcollectionに、CRUD operationのprofiling dataが格納される。

profilingLevelには3つの段階がある

  • 0: profilingがオフになっている
  • 1: slow operationのみprofilingする
  • 2: 全てをprofilingする

デフォルトでは、100ms 以上の操作を「遅い」と判断するが、自分で設定することも可能

> db.getProfilingLevel()
0
> db.setProfilingLevel( 1, { slowms: 50 } )
{ "was": 0, "slowms": 50, "sampleRate": 1, "ok": 1 }
> db.system.profile.find().pretty()

Security

Authentication

  • SCRAM (default)
  • X.509

MongoDB Enterprise Only

Authorization

RBAC

  • Each user has one or more Roles
  • Each Role has one or more Privilleges
  • A Previlleges represents a group of Action and the Resources those actions apply to

Localhost Exception

mongodでauthenticationを有効にしても、デフォルトではMongoDBはユーザを作成しないので手詰まりになってしまいます。

そのため、最初にユーザを作成するまではlocalhostからなら認証なしで操作できるというLocalhost Exceptionが存在します。

必ず最初に管理者権限のあるユーザーを作っておき、Localhost Exceptionがclose後に、ユーザを作成できる状況にしておく必要があります。

$ mongo --host 127.0.0.1:27017
> use admin
> db.createUser({
  user: "root",
  pwd: "root123",
  roles : [ "root" ]
})

Role

Role Structure

Role is composed of

  • Set of Privilleges
    • Actions → Resources
  • Network Authentication Restrictions
    • clientSource
    • serverAddress

Resources

  • Database
  • Collection
  • Set of Collections
  • Cluster
    • ReplicaSet
    • Sharded Cluster

Built-In Roles

Each Database or All Database (two scope)

Only one scope

> use admin
> db.createUser(
  { user: "m103-application-user",
    pwd: "m103-application-pass",
    roles: [ { db: "applicationData", role: "readWrite" } ]
  }
)

Server Tools

root@2a9d0458fdcc:/tmp# find /usr/bin/ -name "mongo*"
/usr/bin/mongodump
/usr/bin/mongod
/usr/bin/mongo
/usr/bin/mongos
/usr/bin/mongoexport
/usr/bin/mongotop
/usr/bin/mongosh
/usr/bin/mongoimport
/usr/bin/mongostat
/usr/bin/mongofiles
/usr/bin/mongorestore

mongostat

mongostat — MongoDB Database Tools

CRUD Operationやメモリ、ネットワークの統計を確認することができる

mongodump / mongorestore

mongodumpはBSONで保存されているCollectionをBSONのまま出力する。 データの変換がないために高速。ディレクトリを指定しないと、カレントディレクトリ配下にdump ディレクトリが作成されてその中に吐かれる。metadataはJSON形式で出力される。

mongorestoreコマンドでcollectionをdumpから作成できる。

mongoexport / mongoimport

単一のファイルにJSONファイルとして吐ける(デフォルトでは標準出力に)。 BSONでなくJSONなのでそんなに速くない。また、metadataのファイルを作らないので、database名やcollection名を指定してあげる必要がある。

# mongoimport --port 27000 --file /dataset/products.json \
-d applicationData -c products -u m103-application-user \
-p m103-application-pass --authenticationDatabase=admin

Chapter 2: Replication

What is ReplicaSet?

同じデータセットを持つmongod プロセスのグループ

ReplicaSetには最大1つのPrimaryが存在する。もしPrimaryがunavailableとなった場合には、新しいPrimaryがSecondaryからvoteのプロセスを経て選出されてサービスを継続する(fail over)。 Replica Set Elections に詳細がある。

全てのメンバがRead operatoinを受け入れられるが、Write operatoinはPrimaryのみ。Secondaryは非同期にPrimaryのデータ更新を受け取り同期する。

Replicationのプロトコルには異なるバージョンがあるが、デフォルトはProtocol Vesion1でRAFTをベースにしたもの。

Rplicationメカニズムの中心となるのが、oplog になる。Primaryノードへの書き込みが成功するたびにoplog がidempotentな形式で記録される。

ReplicaSetのメンバには、Primary/Secondaryの他にarbiterという役割も設定できる。arbiterはデータセットを持たない。なのでPrimaryにもなれない。electionのプロセスで頭数を揃えるためにある役割。分散データシステムの一貫性に大きな問題を引き起こすので利用をあまりおすすめしない。

Failoverには過半数のノードが利用可能であることを必要とする。 ReplicaSetは奇数のノードが必要(最低でも3ノード)

ReplicaSetは最大50メンバーまで利用可能だが、voting memberは7まで。7を超えると時間がかかりすぎてしまうため。

Secondary には特定のプロパティが存在する

  • Hidden Node
    • アプリケーションから隠されたデータのコピーを持つこと
    • レプリケーションプロセスの遅延も設定できる(= Delayed Node)
  • Delayed Node
    • アプリケーションレベルの破損に対して、コールドバックアップファイルに頼らずに回復できるようにする目的

Initiate ReplicaSet

Replicationするための設定 replicationを設定すると、clientのauthenticationを行われるようになる

openssl rand -base64 741 > /var/mongodb/pki/m103-keyfile
chmod 400 /var/mongodb/pki/m103-keyfile
# add lines for replications
security:
  keyFile: /var/mongodb/pki/m103-keyfile
replication:
  replSetName: m103-example

Replicationを開始するためのコマンド

rs.initiate() # このコマンドを発行したノードがPrimaryになる
rs.isMaster() # どれが Primary になっているか確認

rs.stepDown() # 意図的にSecondaryをPrimaryに昇格させる

Secondaryを参加させるには、rs.add() を利用する必要がある

mongo --port 27003 -u m103-admin -p m103-pass —-authenticationDatabase admin
rs.add("localhost:27001")
rs.add("localhost:27002")
rs.status() # 確認

Replication Configuration

  • JSON Objectで表現される設定
  • mongo shellから手作業で設定することも可能
    • helper method: rs.initiate(), rs.add(), etc…

Replica Set Configuration — MongoDB Manual

{
  "_id": "replicaSetName",
  "version": X,
  "members": [
    {
      "_id": 1,
      "host": "mongo.example.com:28017",
      "arbiterOnly": false,
      "hidden": false,
      "priority": 1,
      "secondaryDelaySecs": 3600,
    }
  ]
}

Replication Command

  • rs.status()
    • ReplicaSetの総合的な情報を出力してくれる
    • Heartbeatも確認できる
  • rs.hello()
    • ノードのRoleを表示する
    • rs.status() よりシンプル
    • 以前は、rs.isMaster() という名称だったがdeprecatedになっている
  • db.serverStatus()[‘repl’]
    • rs.isMaster() に含まれていないrbidというのが出る
  • rs.printReplicationInfo()
    • oplogに関する情報

Local DB

ReplicaSetの構成を組んでいる場合に local DBの中身は下記のように複数ある standaloneの場合には、startup.logのみ存在する local DBに直接書いたデータはReplicationされない

殆どはサーバ内部で利用している情報 oplog.rsは、レプリケーションカニズムの中心で、レプリケートされているすべてのステートメントを追跡するoplogコレクション

oplog.rsコレクションには、知っておくべきことが幾つか

  • capped collection
    • サイズが制限されているコレクションのこと
  • デフォルトではoplog.rsコレクションは、空きディスクの5%を占める(デカい)、もちろん設定で指定することも出来る
  • oplogは短時間ですぐ増大する(Fear not, young Padawan.)
    • oplogのサイズが埋まったら、古いログから上書きされていく
  • oplogのサイズはどう影響するか?
    • 例えば、Secondaryのノードの接続が途切れてしまった場合、そのノードは Recoveryモードになり同期できてた操作から直近までの操作を一気に書き込んで追いつきます。しかしながら、既に同期できてた操作がoplogに残っていなかった場合に追いつけずエラーになってしまう
> use local
> show collections

me
oplog.rs
replset.election
replset.minvalid
startup.log
system.replset
system.rollback.id

Reconfiguraion Replicaset

例えば、4ノードになっていたのでvote出来るnodeを奇数にしつつ、hiddenにしてしまうのケースの場合には…

これはDBを止めることなく反映することが可能

cfg = rs.conf()
cfg.members[3].votes = 0
cfg.members[3].hidden = true
cfg.members[3].priority = 0
rs.reconfig(cfg)  // updating

rs.conf() // confirm

Reads and Writes on ReplicaSet

Secondaryのノードに対してmongo shellを起動してデータを読み込もうとしてみます。ちなみに ReplicaSetの名称を指定すると自動的にPrimaryにつなぎにいくので注意が必要になる。

Secondaryのノードではこのままだとコマンドを実行できません。 MongoDBはconsistencyを重視しているため、Secondaryから読み込む場合には明示的に伝える必要がある。それが、rs.secondaryOk になる。

consistencyを担保するために書き込みはPrimaryにしか出来ないようになっている。 ReplicaSetが過半数のノードに到達できなくなると、レプリカセットの残りのノードはすべてSecondaryになる。たとえ、残ったのがPrimaryのノードだったとしても。

Failover and Election

Primaryが利用できなくなる理由

一般的にはメンテナンスがそう 例えばローリングアップグレード(e.g. v3.4→ v3.6)

  • SecondaryのMongoDBプロセスを停止し、新しいDBのバージョンで戻ってきます(1台ずつ)
  • 最後に Primary でrs.stepDown() で利用して安全にelectionを開始します
  • electionが完了すると最後の古いバージョンのMongoDBプロセスはSecondaryになる
  • そして同じようにプロセスを新しいバージョンで起動すれば全て完了!かつ可用性も損なわない

Electionの仕組み どのSecondaryがPrimaryに立候補するかという点に関してはロジックが存在する

  • Priorityが全ノード同じ値
    • その場合は最新データを持っているノードが立候補する
    • そのノード自身が自分に投票を行う
    • 2ノードが名乗り出たとしても、投票権を持っているのが奇数ノードであれば問題ないが、偶数ノードだった場合には同点となる可能性があり、もし同点になった場合はelectionをやり直すことになる(=処理がストップする)
  • Priorityがノードごとに違う場合
    • Priorityが高いほど、Primaryになる可能性が高くなる
    • ノードをPrimaryにしたくない場合には、Priorityを0にする
      • Primaryになる資格のないノードのことをpassive nodeと言うらしい

前述したが、ReplicaSetの過半数のノードがダウンしたときには、たとえPrimaryだとしても疎通できなくなる

Write concern

write concern はdeveloperが、write operationに追加できるAckknowledgement(確認)の仕組み ACKのレベルが高いほどDurability(耐久性)が増す

書き込みが成功したことを確認するReplicaSetのメンバが多ければ多いほど、障害が発生したときに永続化が継続する可能性が高い Durabilityを高めようとすると、各ノードから書き込み確認の応答を貰う必要があるので待ちが必要になります。

Write Concern level

  • 0: クライアントは確認応答を待たないので、ノードの接続に成功したかどうかを確認するだけ
  • 1: デフォルトの値。クライアントはPrimaryからの確認応答を待つ
  • => 2 : Primaryと1つ以上のSecondary、例えばlevelが3のときには1つのPrimaryと2つのSecondaryから確認応答を待つ

majority というキーワードを利用することができる。これはReplicaSetのサイズが変わってもいちいち変えなくても良い。levelはメンバの数を2で割って切り上げた値となる。

Write Concernはsharded clusterも対応している。

MongoDBには更に2つのWrite Concernオプションがある。

  • wtimeout: クライアントが操作に失敗したと判断するまでの最大時間。重要なのは書き込みが無かったことになるわけではなく要求した耐久性を満たさなかったということ
  • j: journalの意。各ReplicaSetのメンバが書き込みを受け取ってジャーナルファイルにコミットしないと確認応答を返せない。これをtrue にすることでディスクに書き込まれることまで保証できる。false のときにはメモリに保存する所までの確認。

MongoDB 3.2.6以降は、Write concernが過半数になるとデフォルトでj がtrueになる。

Write concernはクライアントのアプリケーションから指定するっぽい。

Read Concern

起きうる不味いシチュエーション

  • クライアントアプリケーションがdocumentをinsertする
  • そのdocumentがSecondaryにReplicateされる前に、クライアントアプリケーションがReadする
  • 突然Primaryが壊れる
  • ReadしたdocumentはまだSecondaryにReplicateされてない
  • 古いPrimaryがオンラインに戻ったとき、同期プロセスの中でそのデータはRollbackして存在していないことになります
    • 📝ここで古いと行っているのはFailoverが起こるため、復帰したときにはSecondaryになっているからと思います

このシチュエーションを許容できない要件のアプリケーションの場合に困ります。 そんなときに役に立つのがRead Concernである。

Read Concernで指定された数のReplicaSetメンバに書き込まれたと認められたデータのみが返されるようになる。

Read Concern Level

  • Local: Primaryを読み取るときのデフォルトの設定。クラスタ内の最新のデータを返す。Failover時のデータの安全性が保証されない。
  • Available: Secondaryを読み取るときのデフォルトの設定。ReplicaSetのときはLocalと同じで、Sharded Clusterのときに挙動が変わる
  • Majority: 過半数のReplicaSetメンバに書き込まれたことが確認されたデータのみを返す。Durabilityと Fastの中間だが古いデータを返すこともある
  • Linearizeable: read your own writeを提供する。常に新しいデータを返すが、読み取り操作が遅かったり、制限がある。

どのようなRead Concern Levelにするかは、”Fast”, “Safe”, “Latest” の観点で考えると良さそう LocalやAvailableはSecondaryの読み取りに関しては最新が返ってくるとは限らない

Read Preference

読み込みのoperationをルーティングする設定

  • primary(default): Primaryのノードからしか読み取らない
  • primaryPreffered: PrimaryがUnavailableになったときにはSecondaryから読み取る
  • secondry: Secondaryのノードからしか読み取らない
  • secondaryPreffered: Secondaryが全てがUnavailableになったときにはPrimaryから読み取る
  • nearest: 地理的に一番近いところ

Chapter 3: Sharding

What is Sharding?

https://docs.mongodb.com/manual/images/sharded-cluster-production-architecture.bakedsvg.svg

画像引用元

MongoDB sharded clustershard , mongos , config servers で構成されている。

  • shard: 物理的に分散されたcollectionを保存する。replica set としてデプロイされる。
  • mongos: shardへクエリをルーティングする。
  • config servers: shardのmetadataを保存する。

When to sharding?

いつShardingが必要になるのかを考えてみましょう

まず最初にVerticcal scaleが経済的に可能かを確認する。特定されたボトルネックにリソースを増やすことでダウンタイムなしにパフォーマンスが向上する。

しかしながら、経済的な理由や非常に困難なポイントにぶち当たりスケールアップが難しくなります。

もう一つ考慮すべき点は、運用業務への影響です。 データセットの量が10テラバイト級になるとバックアップやリストアに時間がかかる。ディスクのサイズを増やすとインデックスのサイズも増えることになり、多くのRAMが必要になる。

一般的には、個々のサーバーには2~5テラバイトのデータを格納することが望ましいとされている それ以上になると時間がかかりすぎてしまう。

最後に、下記のようなケースでもshardingは有用

  • 並列化可能なSingle Thread Operation
  • 地理的に分散したデータ → zone sharding
    • 特定の地域に保存する必要があるデータ(M121でしっかり理解出来るやつ)
    • ↑のようなデータを取得するクライアント

Sharding Architecuture

https://youtu.be/6cCL4-3gF8o

動画がとても分かりやすかった

mongosconfig server からクエリされたデータがどのshardにあるかを取得し、ルーティングする。各shardに含まれる情報は時間とともに変化する可能性があるのでとても重要。mongos は頻繁にconfig server にアクセスする。

なぜ変化するかというと、各シャードのデータ量が均等になるように分散させるため。

sharded clusterはprimary shardという概念も存在する。各データベースにはprimary shardが割り当てられ、そのデータベースのshard化されていないcollectionは全てそのshardに残る。 primary shardには他にも幾つか役割がある。一つは、aggregation commandのMERGE操作によるもの。

shard key以外を条件にしたクエリの場合、例えば動画の例だとサッカー選手の名前でshardを分けているが、年齢に関するクエリを受け取った場合には全てのshardにクエリを送信する mongos もしくはcluster内でランダムに選択されたshardで、それぞれの結果を収集しソートなどを行います。これをSHARD_MERGE ステージと呼ぶ。

Setting Up a Sharded Cluster

config server の実態はMongoDBのReplicaSetです ただし、.sharding.clusterRole: configsvr を設定ファイルに追加する必要がある

.security.keyFile をレクチャーでは利用しているが、本番環境においてはX509証明書を利用することになる

既存の単一のReplicaSetをsharding構成にRolling Upgradeするには下記の手順を取る

  • CSRS (= Config Server Replica Set) を起動させる
  • mongos を起動させる
    • データを保存する必要が無いため設定ファイルでdbpath プロパティが無い
    • mongosconfig server で作成したユーザを継承する
    • config server の向き先を設定する必要がある
  • 既存のReplicaSetのnodeの設定を変更して再起動する
    • .sharding.clusterRole: shardsvrを設定にいれる
    • Secondaryをそれぞれshutdownする
    • PrimaryでstepDownを行い、他のnodeにPrimaryの役割を引き渡した後にshutdownする
  • mongos からshardを追加する
    • sh.addShard( "rs1/mongodb0.example.net:27018" )
    • ReplicaSetの中の1つのノードを指定するだけでPrimaryを認識可能

参考:

Configuration File Options — MongoDB Manual

ConfigDB

MongoDBが内部的に使うものなので、ユーザ側でデータの書き込みは行わないが有用な情報を読み取ることが出来る

use config

# databaseが幾つにpartitioningされているか
db.databases.find().pretty()

# collectionのshardkeyに関する情報
db.collections.find().pretty()

# shardとして利用しているReplicaSetの情報
db.shards.find().pretty()

# chunkがshardkeyのどの範囲を持っているかとどのshardに属するか
db.chunks.find().pretty()

# mongosの情報
db.mongos.find().pretty()

Shard Keys

sharded collectionのデータを分割して、cluster内のshardに分割するために使用する、indexed field(s) をshard keyと言う

chunkはshard keyを使ってdocumentを分けた論理的なグループのこと shard keyとして選択したフィールドの値によって、各chunkのinclusive lower boundとexclusive upper boundが決まる(e.g. 1 <= x < 6)

新しいdocumentをcollectionに書き込むたびに、mongosルータはどのshardにそのdocumentのkey valueに対応するchunkがあるかを確認し、そのshardのみにdocumentを送る。 つまり、挿入されるdocument全てにshard keyの項目が必要になる。

shardingを行う手順

  • databaseのshardingを有効に: sh.enableSharding("m103")
  • indexを作成: db.products.createIndex( { "sku": 1 } )
  • shard keyを選択: sh.shardCollection( "m103.products", { "sku": 1 } )
    • 📝 shard keyはimmutableとLecture動画で言われているが、shard fieldがimmutableな_id で無い限り、refineCollectionShardKey で更新することができる

Picking a Good Shard Key

What makes a Good Shard Key?

  • High Cardinality
    • Cardinalityが高ければChunkが増えshardの数も増えるのでクラスタの成長を妨げることがない
    • 例えばbool値をkeyにした場合には、上限が2chunkになりshardも2つまでになってしまいます
  • Low Frequency
    • frequencyはデータの中でuniqueな値が発生する頻度を表す
    • 例えばアメリカの州をshard keyにしたとして、90%の確率で「New York」のdocumentが挿入される場合、書き込みの90%以上が1つのシャードに行くことになる = HotSpot!
  • Non-Monotonically Change
    • 単調に変化する値(e.g. タイムスタンプやID)は、chunkが下限と上限を持つ性質上、相性が悪い
    • Monotonicなshard keyを分散させるためには後述のhashed shard key を利用する必要がある

shard keyの選定でもう一つ重要なことはread isolation です。よく実行するクエリに対応しているかどうかを検討する必要がある

shard keyを条件にしたクエリであれば、多くの場合は1つのshardにアクセスするだけで済むが、そうでなければ全てのshardにアクセスするscatter gather な操作になってしまい時間がかかる

shard keyを選ぶときに注意する点

  • 1度shardingしたcollectionはunsharding出来ない
  • 1度shardingしたcollectionはshard keyを更新できない(条件によっては可能)
  • shard keyのvalueは更新できない(これも可能と全セクションで言及があった)

Hashed Shard Keys

hashed shard keyを利用する場合、MongoDBはshard keyに指定しているField Valueのhash値を使ってどのchunkにdocumentを置くか決めることになる。

実際に保管するデータがhash値になるわけではなく、shard keyのベースとなるindex自体がhash値で保存される。データをより均等に分散させることができる。 前述したようにMonotonicな値でもちゃんと分散されるようになる。

hashed shard key の欠点

  • shard key fieldに対する範囲のクエリは単一のshardではなく複数shardに投げられることになる
  • データを地理的なグループに分離できない
  • 利用できるのは単一のField Shard Keyのみで、配列や複合インデックスはサポートしていないし、hash化されているのでsortも速くない

Chunks

Chunkはdocumentsの論理的なグループである config server が保持する最も重要な情報の1つはchunkとshardのマッピングである

chunkが作成されてからの流れは下記のような形

  • collectionをshardingした瞬間に、1つの初期chunkを定義する
    • この初期chunkは$minKeyから$maxKeyまでの範囲にある
  • 時間とともに初期chunkを複数のchunkに分割してshard間でデータが均等になるようにする

shard内のchunk数を決める要素はshard keyのcardinalityの他にchunk sizeがある デフォルトのChunk Sizeは64MB 設定によって1MB <= ChunkSize <= 1024MB の幅で変更することが可能 Chunk Sizeは稼働中に変更することが可能だが、新しいデータを入ってこないとmongosはアクションを起こさないのですぐにはchunk数に反映されない可能性がある

Jumbo chunkという概念がある 新しいdocumentの90%が同じshard keyを持っていたりすると、定義されたchunk sizeよりも大きくなる可能性が高い。そうなった場合は、Jumbo chunkとしてマークされる。Jumbo chunkは大きくて動かせないという判断をされる。 これを避けるためにも、Shard KeyのFrequencyの考慮は大事だ

Balancing

config server のReplicaSetのPrimaryで実行されているbalancer プロセスがshard間でchunkを移動させてデータを均等にしている

balancer は1ラウンドで複数のMigrationを並行に行うことができるが、1つのノードが複数のMigrationプロセスに一度に参加することは出来ない

balancerはデータの移動だけではなく、必要に応じてchunkの分割を行う

balancer は特にユーザの入力や指示を必要としないが、開始停止や設定の変更など行うこともできる

Targeted Queries vs Scatter Gather

Targeted Queryは全てのshardにクエリを飛ばさなくて済むクエリ

combound indexを利用したshard keyを設定することで、scatter gatherなクエリを避けることができる

# Shard Key
{ "sku": 1, "type": 1, "name": 1 }

# Targetable Queries
db.products.find( { "sku": … } )
db.products.find( { "sku": …, "type": … } )
db.products.find( { "sku": …, "type": …, "name": … } )
db.products.find( { "type": …, "name": …, "sku": … } )

# Scatter Gather 
db.products.find( "type": … )
db.products.find( "name": … )

db.products.find({"sku" : 1000000749 }).explain() のようにexplainでクエリがtargetedになっているかを確認できる

  • winningPlan.stage の値がSINGLE_SHARD になっているか
  • inputStage の値がIXSCAN (index scan)になっているか

Targeted Queryにはshard keyが必須になることが大事!

まとめ

ちゃんとモチベーションを維持しつつ進められるような仕組みになっていた。
次はM201のPerformanceをやりたいところだけど、Datadogも触りたいので一旦後回しにしちゃいそう。
この記事なんと2万字になっててビビる。

Terraformの基礎を学んだ

記事一覧はこちら

Table of Contents

背景・モチベーション

8月から中途入社した会社ではInfrastructureの管理にTerraformを利用しています。業務において自分が利用したことがあるのは、AWSのCloudFormationとGCPのdeployment-managerのみでTerraformはプライベートな利用でとどまっていました。

雰囲気で利用している状況から抜け出すために、1つ1つの概念をちゃんと学習していくことにしました。幸い、HashiCorpのドキュメントはとても充実していたため公式のドキュメントをベースに学習を進めることが出来ました。

参考文献

learn.hashicorp.com

www.terraform.io

Terraform

TerraformはTerraform CoreTerraform Plugins で構成されている。
図は Perform CRUD Operations with Providers | Terraform - HashiCorp Learn から引用させていただきました。

https://learn.hashicorp.com/img/terraform/providers/core-plugins-api.png

  1. Terraform Core 設定を読み込んで、resource dependency graphを構築する
  2. Terraform Plugins (providers and provisioners) は Terraform Core とそれぞれのターゲットとなるAPIの橋渡しを行います

Terraformは設定をHCLというHuman ReadableなHashiCorpの言語で記述する。ただ、Machine ReadableなJSON Syntaxも提供している。
詳細なスペックはthe HCL native syntax specificationに定義してある。

Resources

Define Infrastructure with Terraform Resources | Terraform - HashiCorp Learn

resource blockはTerraform構成において1つ以上のinfrastructure objectを表す。resource blockではresource typeとnameを宣言する。
typeとnameは、resource_type.resource_name という形式のresource identifier(ID)を形成する。下記の例だと、aws_instance.web となる。resource IDは workspace内で一意でなければいけない。

resourceはargumentとattribute、meta-argumentを持つ。

  • Arguments
    • 特定のリソースの設定を行うための引数
    • 多くのArgumentsはリソース固有のもの
    • 必須なものとOptionalなものがあり、必須なArgumentsを指定しない場合にTerraformはエラーを返す
  • Attributes
    • 存在するResourceで公開される値
    • Resourceのattributeへの参照は、resource_type.resource_name.attribute_name という形式になる
    • 設定を指定するArgumentsと異なり、AttributesはクラウドプロバイダやAPIによってassignされる
  • Meta-arguments
    • Resourceの振る舞いを変更するもの
    • 例えば、count というMeta-Argumentsを利用することで複数のResourceを作成することが出来る(他にも、depends_on, for_each , provider , lifecycle がある)
    • Terraform自体の機能なので、ResourceやProviderに固有のものではない
resource "aws_instance" "web" {
  // 以下3つはArguments
  ami                    = "ami-a0cfeed8"
  instance_type          = "t2.micro"
  user_data              = file("init-script.sh")  // file() function
}

Input Variables

Customize Terraform Configuration with Variables | Terraform - HashiCorp Learn Input Variables - Configuration Language - Terraform by HashiCorp

📝 variables.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

Input variables は、end userが設定をカスタマイズするために値を定義できるようにすることでより、Terraformの設定をより柔軟にする

Declare variables

variablesはImmutableで、Terraformが動くときに変更はない
variable <変数名> { ... } 句で宣言し、3つのOptionalな引数をとる。 全部設定しておくことを推奨。

  • Description: A short description to document the purpose of the variable.
  • Type: The type of data contained in the variable.
  • Default: The default value.

Default値を設定しない場合には、Terraformが設定をapplyする前にassignする必要がある。variableの値はリテラル値でなくてはいけないので、式とかは🙅

Typeがサポートしているkeywordは、 string, number, boolの3種類。
type constructorsを利用することで collectionのような複雑な型も宣言できる: list(<TYPE>), set(<TYPE>), map(<TYPE>), object({<ATTR_NAME> = <TYPE>, …>}), tuple([<TYPE>, …])
any keywordは任意の値がacceptableであることを示す

variable "public_subnet_count" {
  description = "Number of public subnets."
  type        = number
  default     = 2
}

variable "public_subnet_cidr_blocks" {
  description = "Available cidr blocks for public subnets."
  type        = list(string)
  default     = [
    "10.0.1.0/24",
    "10.0.2.0/24",
    "10.0.3.0/24",
  ]
}

variable "resource_tags" {
  description = "Tags to set for all resources"
  type        = map(string)
  default     = {
    project     = "project-alpha",
    environment = "dev"
  }
}

module "vpc" {
    source  = "terraform-aws-modules/vpc/aws"
    version = "2.44.0"
    public_subnets  = slice(var.public_subnet_cidr_blocks, 0, var.public_subnet_count)
    tags = var.resource_tags
    // …
}

Assign Values

varialeにdefault値を持たせなかったときはterraform apply 時にpromptで聞かれる。ただし、promptはエラーを誘発しやすい。

Assgin Valuesの方法(くわしくはここ

  • 変数を定義したファイル (.tfvars)
    • Terraformはカレントディレクトリのterraform.tfvarsに完全一致するファイルもしくは*.auto.tfvarsに部分一致するファイルを 自動的に全て読み込む
    • 上記に一致しないファイル名でも、-var-file flagで渡すことができるが、ファイルの拡張子は .tfvars or .tfvars.jsonである必要がある
  • Command Line
    • terraformコマンドの-var optionで指定することが可能
    • example: terraform apply -var='image_id_list=["ami-abc123","ami-def456"]' -var="instance_type=t2.micro"
  • 環境変数
    • Terraformは自プロセスに保持する TF_VAR_ から始まる環境変数を読み込む、そのprefixの後には変数名が続く
    • example: export TF_VAR_image_id=ami-abc123
  • Terraform Cloudの場合はworkspace variables

Terraformは以下の順序で変数をロードし、後のソースが前のソースよりも優先される
環境変数の優先度が低いことに注意

  1. 環境変数
  2. terraform.tfvars
  3. terraform.tfvars.json
  4. Any *.auto.tfvars or *.auto.tfvars.json (ファイル名の辞書順)
  5. Any -var and -var-file options on the command line (渡された順番)

Reference Values

variableを参照するときは var.<variable_name>
文字列補間でも参照することができる 例: name = "web-sg-${var.resource_tags["project"]}-${var.resource_tags["environment”]}”

また、Variablesをvalidateすることも可能で、variable ブロックの中で、validation フィールドを持つことができる。 regexall() 関数は正規表現をとり文字列をテスト一致した文字列のリストを返すので、これを使ってcondition に一致すればOKで一致しなければ error_message が出力される

variable "resource_tags" {
  description = "Tags to set for all resources"
  type        = map(string)
  default     = {
    project     = "my-project",
    environment = "dev"
  }

  validation {
    condition     = length(var.resource_tags["project"]) <= 16 && length(regexall("[^a-zA-Z0-9-]", var.resource_tags["project"])) == 0
    error_message = "The project tag must be no more than 16 characters, and only contain letters, numbers, and hyphens."
  }
}

Output Values

Output Data from Terraform | Terraform - HashiCorp Learn Output Values - Configuration Language - Terraform by HashiCorp

📝 outputs.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

output はTerraform Moduleの戻り値とも言えるようなもので、用途としては主に下記の3つとなる。

  • Child ModuleがParent Moduleにリソース属性のサブセットを公開するため
  • Root Moduleでは、terraform applyを実行した後に特定の値を表示するため
  • remote stateを利用している場合、root moduleのoutputsはterraform_remote_state data source経由(後述)で他の設定からアクセスできる。

Terraformのstateにoutputはロードされ、terraform output commandでクエリすることができる

  • 特定のものをクエリするには、terraform output lb_url と名前を引数に渡してあげるとよい
  • -raw フラグを使うと、stringに対してdouble quoteが付かなくなる

output blockにはsensitive フィールドを付与できる

  • plan , apply , or destroy のときの表示は <sensitive> となり見れない
  • terraform.tfstate ファイルには平文で入っているし、terraform output ではredactされないことに注意すること

terraform output -json のようにoptionを付けることで、json形式で出力できる。machine-readable format for automationなのでtoolから使うとき便利だよね

output "db_password" {
  value       = aws_db_instance.db.password
  description = "The password for logging in to the database."
  sensitive   = true
}

output "instance_ip_addr" {
  value       = aws_instance.server.private_ip
  description = "The private IP address of the main server instance."

  depends_on = [
    // このIPアドレスが実際に使用される前に、セキュリティグループのルールが作成されていなければ、サービスに到達できない
    aws_security_group_rule.local_access,
  ]
}

Locals

Simplify Terraform Configuration with Locals | Terraform - HashiCorp Learn

Terraformのlocals は設定の中で参照できる名前付きの値です。local valueを利用することで、繰り返しを避けTerraformの設定をsimpleに保つことが出来る。また、値をhard-codingするのではなく、意味のある名前付けを使うことで、より読みやすく出来る。

locals {
  name_suffix = "${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
}

Dependency Lock File

Lock and Upgrade Provider Versions | Terraform - HashiCorp Learn Dependency Lock File (.terraform.lock.hcl) - Configuration Language - Terraform by HashiCorp

📝 versions.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

Terraformプロバイダは、TerraformとターゲットAPIの間で通信を行い、リソースを管理する。ターゲットAPIが変更されたり、機能が追加されたりすると、プロバイダーのメンテナはプロバイダーを更新してバージョンを上げる。
設定でプロバイダのバージョンを管理するには、以下の2つを行う。

  1. Providerのバージョン指定/制限をterraform blockで行う
  2. dependency lock fileを利用する

terraform init すると、Terraformはcurrent directoryに.terraform.lock.hcl ファイルを作成する。Terraformが、across your team and in ephemeral remote execution environmentsで同じプロバイダバージョンを使用するためにVCSに含める必要がある。

terraform init -upgrade で、すべてのプロバイダーを、設定であらかじめ設定されたバージョン制約内で一貫した最新バージョンにアップグレードします。

lockファイルで確認できるのは、実際に利用しているバージョンと、この選択を行うときの制約(constraint)。また、チェックサム検証を行うので、hashes というフィールドでハッシュを保持する。zh: はzip hashを意味する、これはレガシー。h1: は現在推奨されているhash scheme

ここで、providerのversion constraintsにも触れておく。詳細はここにある。

A module intended to be used as the root of a configuration, 互換性のない新バージョンへの誤ったアップグレードを避けるために、動作することを意図したプロバイダの最大バージョンも指定する必要がある。演算子 ~> は、特定のマイナー・リリース内のパッチ・リリースのみを許可するための便利な略記法です = 📝パッチバージョンしか上がらない?

terraform {
  required_providers {
    mycloud = {
      source  = "hashicorp/aws"
      version = "~> 1.0.4"
    }
  }
}

多くの設定で再利用しようとするモジュールの場合! たとえそのモジュールが特定の新しいバージョンと互換性がないことがわかっていても、~> (または他の最大バージョン制約)を使用しないでください。そうすることでエラーを防ぐことができる場合もありますが、多くの場合、モジュールのユーザーが定期的なアップグレードを行う際に多くのモジュールを同時に更新することを余儀なくされます。最小バージョンを指定し、既知の非互換性を文書化し、最大バージョンをルートモジュールに管理させます

Module

Modules Overview | Terraform - HashiCorp Learn

TerraformでInfrastructureを管理していくと、どんどん複雑な設定になる。 設定ファイルの理解や操作が難しくなり、類似した設定のブロックが増えて重複が生まれる。 プロジェクトやチーム間で構成の一部を共有したいと思っても容易ではなく、切り貼りするだけではメンテナンス困難に。

What are modules for?

前述の問題をModuleは解決してくれる

  • Organize configuration
    • 設定の関連する部分をまとめておくことで、設定の理解や更新を容易にする
  • Encapsulate configuration
    • カプセル化することが出来るため、設定の一部の変更をしたときに意図しない変更を防ぐことが出来る
  • Re-use configuration
    • 自分自身、他のメンバー、moduleを公開している Terraform practitionersによって書かれた設定を再利用できる
  • Provide consistency and ensure best practices
    • 設定に一貫性を持たせることで、全ての構成にベストプラクティスが適用されることを保証できる

What is a Terraform module?

Terraform Moduleは、1つのディレクトリにあるTerraform設定ファイルのSetです。 1つのディレクトリに1つ以上の.tf ファイルを配置しただけのシンプルな設定でもModuleになる。このようなディレクトリから直接Terraformコマンドを実行した場合には、それはroot moduleと見なされる。

Calling Modules

設定ではmodule blockを使って他のディレクトリにあるmoduleを呼び出すことができる。Terraformはmodule blockに遭遇すると、そのmoduleの設定ファイルをloadして処理する。

他の設定から呼び出されたmoduleは、その設定のchild moduleと呼ばれることがある。

moduleはローカルのファイルシステム、リモートソースのどちらからも読み込むことができる。 TerraformはTerraform Registry, GitHubなどの様々なリモートソースをサポートしている。

Build and Use a Local Module

GitHub - hashicorp/learn-terraform-modules-create

静的ウェブサイトホスティングのためのAWS S3バケットを管理する例

典型的なchild moduleの構成

.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
  • LICENSE はTerraformは利用せず公開するときに必要になるもの
  • README.md はTerraformでは利用しないが、Terraform Registryなどで使用される
  • main.tf はModuleの構成のmain setが含まれる
  • variable.tf で定義した変数は、Module利用者側からはmodule blockのArgumentsとして設定される
  • outputs.tf では、Moduleの出力を定義する。moduleで定義されたInfrastructureに関する情報を他の設定に渡すために利用される。

ローカルのModuleをInstallする場合には、terraform get コマンドを発行する

State

State - Terraform by HashiCorp

Purpose

Stateの目的は大きく4つ

  • Mapping to the Real World
  • Metadata
  • Performance
  • (Syncing)

Mapping to the Real World

State is a necessary requirement for Terraform to function. Terraformは設定をreal worldにmapするために、なんらかのデータベースを必要とする。 例えば、"aws_instance" "foo" という宣言がある場合、Terraformはmapを利用してインスタンス i-abcd1234 がそのリソースで表現されていることを知ることができる。

AWSのようなプロバイダの場合、Terraformは理論的にはAWSタグのようなものを使うことができる。Terraformの初期のプロトタイプでは、実際にステートファイルがなく、この方法を使っていた。 しかし、すぐに問題が発生。全てのproviderがタグをサポートしているわけではなかったのだ。

Terraform expects that each remote object is bound to only one resource instance, which is normally guaranteed by Terraform being responsible for creating the objects and recording their identities in the state.

Metadata

Alongside the mappings between resources and remote objects, Terraform must also track metadata such as resource dependencies.

Terraformは通常、依存関係の順序を決定するためにConfigurationを使用します。 しかし、TerraformのConfigurationからリソースを削除する場合、Terraformはそのリソースを削除する方法を知っている必要があります

Terraformは、設定にないリソースにマッピングが存在することを確認し、plan to destoryすることができます しかし、リソースのconfigurationが存在しなくなったため、configurationだけでは依存関係の順序を判断することができません

正しい動作を保証するために、Terraformは最新の依存関係のセットのコピーをstate内に保持します。これでTerraformは、configurationから1つまたは複数のアイテムを削除しても、ステートから正しい破壊順序を決定することができます。

Performance

Terraform stores a cache of the attribute values for all resources in the state. 📝 例えば、EC2におけるPrivateIPとかもそう

When running a terraform plan, Terraformは希望の構成に到達するために必要な変更を効果的に判断するために、リソースの現在の状態を知る必要がある

小規模のインフラの場合、Terraformはプロバイダーに問い合わせて、すべてのリソースから最新の属性を同期することができます。これはTerraformのデフォルトの動作で、planとapplyのたびに、Terraformは状態のすべてのリソースを同期します。

大規模なインフラでは、すべてのリソースへの問い合わせは時間がかかりすぎます。APIのレート制限が掛かる可能性も高い。 Terraformの大規模なユーザーは、この問題を回避するために-refresh=falseフラグと-targetフラグを多用する。これらのシナリオでは、キャッシュされた状態がrecord of truthとして扱われます。

The terraform_remote_state Data Source

The terraform_remote_state Data Source - Terraform by HashiCorp

The terraform_remote_state data source retrieves the root module output values from some other Terraform configuration, using the latest state snapshot from the remote backend.

データを共有する上で、root moduleのoutputは便利!だけど欠点もある。ユーザはstate snapshot全体にアクセスする必要があるが、その中にはSensitiveな情報も含まれることがある。 可能であれば、remote state経由ではなく、別の場所に明示的に公開することを推奨!

構成間で明示的にデータを共有するためには、以下のような様々なプロバイダーのmanaged resource typeとdata sourcesのペアを使用することができる

terraform_remote_stateではなく、別の明示的なconfiguration storeを使用することの主な利点は、コンピュートインスタンス内の設定管理やスケジューラーシステムなど、Terraform以外のシステムでもデータを読み取ることができる可能性があることです

Backend: State Storage and Locking

Backends: State Storage and Locking - Terraform by HashiCorp Backend Overview - Configuration Language - Terraform by HashiCorp

Backends are responsible for storing state and providing an API for state locking. State locking is optional.

State Storage

Backends determine where state is stored. 例えば、 local(default) backendは、ディスク上のローカル JSON ファイルに状態を保存します

When using a non-local backend, Terraform will not persist the state anywhere on disk except in the case of a non-recoverable error where writing the state to the backend failed.

sensitive valueがstateに含まれている場合、remote backendを使用することで、そのstateがディスクに永続化されることなくTerraformを使用することができる。

バックエンドへの状態の永続化にエラーが発生した場合、Terraformはstateをlocalに書き込みます。これはデータ損失を防ぐためです。この場合、エラーが解決したらエンドユーザーが手動でリモートバックエンドに状態をプッシュする必要がある。

Manual State Pull/Push

You can still manually retrieve the state from the remote state using the terraform state pull command. This will load your remote state and output it to stdout.

You can also manually write state with terraform state push. これは非常に危険なので、可能であれば避けてください。これはリモートの状態を上書きします。

Backend Types

Backend Overview - Configuration Language - Terraform by HashiCorp

Terraform's backends are divided into two main types, according to how they handle state and operations:

  • Enhanced backends can both store state and perform operations. There are only two enhanced backends: local and remote.
    • local : The local backend stores state on the local filesystem, locks that state using system APIs, and performs operations locally.
    • remote : The remote backend stores Terraform state and may be used to run operations in Terraform Cloud.
  • Standard backends only store state, and rely on the local backend for performing operations.
    • s3 (with locking via DynamoDB): Stores the state as a given key in a given bucket on Amazon S3. This backend also supports state locking and consistency checking via Dynamo DB.
    • 他にも、gcs とかpg (postgres)とかある

Backends Configuration

Backend Configuration - Configuration Language - Terraform by HashiCorp

Backends are configured with a nested backend block within the top-level terraform block:

terraform {
  backend "remote" {
    organization = "example_corp"

    workspaces {
      name = "my-app-prod"
    }
  }
}

State Locking

State: Locking - Terraform by HashiCorp

If supported by your backend, Terraform will lock your state for all operations that could write state. This prevents others from acquiring the lock and potentially corrupting your state.

Data Sources

Query Data Sources | Terraform - HashiCorp Learn

Terraformはdata sourcesを利用して、disk image IDなどcloud provider APIからの情報や、他のTerraformワークスペースのoutputsを通して残りの環境情報を取得する

下記の例のaws_availability_zones data sourceは、AWS providerの一部。resources blockと同じように、data source blocksも引数を取る。この場合、state Argumentsは現在利用可能なものに限定する。

data sourcesのattributeを参照する場合には、 data.<NAME>.<ATTRIBUTE> という形式をとる。

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "primary" {
  availability_zone = data.aws_availability_zones.available.names[0]

  // ...
}

他のTerraformワークスペースのoutputを参照するためには、terraform_remote_state を使うことが出来る。

data "terraform_remote_state" "vpc" {
  backend = "remote"

  config = {
    organization = "hashicorp"
    name = "vpc-prod"
  }
}

provider "aws" {
  region = data.terraform_remote_state.vpc.outputs.aws_region
}

Resource Targeting

Target resources | Terraform - HashiCorp Learn

Command: plan - Terraform by HashiCorp

通常のTerraformワークフローでは、plan全体を一度に適用します。ネットワーク障害、upstream cloud platformの問題、Terraformまたはそのproviderのバグが原因で、Terraformの状態がリソースと同期しなくなった場合など、planの一部のみを適用したい場合があります。

You can use the -target option to focus Terraform's attention on only a subset of resources.

depends_on Meta-Arguments

Create Resource Dependencies | Terraform - HashiCorp Learn

ほとんどの場合、Terraformは与えられた設定を元に依存関係を推論し、正しい順序で作成・削除されるようにする。しかし、場合によってTerraformが依存関係を推論できないことがあるので、depends_on 引数で明示的な依存関係を作る必要がある。

aws_eip resourceは、EC2インスタンスにElastic IPを割り当てて関連付けするもの。Elastic IPが作成される前にEC2インスタンスが存在する必要がある。 以下は暗黙的な依存関係の例である。

resource "aws_instance" "example_a" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
}

resource "aws_eip" "ip" {
    vpc = true
    instance = aws_instance.example_a.id // ここの参照から依存グラフをTerraformで作成する
}

明示的に依存関係を宣言しなくてはいけないケースもある。 EC2インスタンス上で、S3バケットの使用を期待するアプリケーションが実行されているとする。この依存関係はアプリケーションがもたらすものなので、Terraformからは見えない。

depends_on Argumentsは任意のresourcesもしくはmodule blockで受け入れられる。これを使って明示的な依存関係を構築できる。

resource "aws_s3_bucket" "example" {
  acl    = "private"
}

resource "aws_instance" "example_c" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"

  depends_on = [aws_s3_bucket.example]
}

module "example_sqs_queue" {
  source  = "terraform-aws-modules/sqs/aws"
  version = "2.1.0"

  depends_on = [aws_s3_bucket.example, aws_instance.example_c]
}

Other

  • 似たようなresourceを、countfor_eachで管理することでMaintenabilityを高める
  • Functions を使って動的な設定を作成したり
    • templatefile() 関数を使ってUserdataを動的なものにしたり
    • lookup() を使ってmap[region]ami のhashmapからamiを探し出したり
  • 3項演算子を利用して動的な設定を構成したり(Expression

まとめ

Remote StateやLockファイル、dataのクエリ、Moduleなどプライベートな利用では中々踏み込まないところを公式のドキュメントを通して認識できた。
これで多少は既存のコードとかがちゃんと読めるようになっていると良いな!次はMongoDBについて勉強しよう。

お休み1ヶ月振り返り

記事一覧はこちら

なぜ1ヶ月お休み?

7月末で3年3ヶ月勤めていた会社を退職することになったためです。7月1日が最終出社日で、以降はお休みを頂いておりました。8月1日から新しい会社で働くことになるため、お休みが約1ヶ月ある状態だったということになります。

この1ヶ月でやったこと

このようなご時世でなければ、6年間住んでいた北海道や5年間住んでいた新潟とかに一人旅して、昔食べていたラーメンとかアイス食べたり酒飲んだりしたかったのですが叶わず…。仏教関連の本読んでいたので奈良行くのも有りだったなあ。

どこにも行けないなら勉強しちゃうぞ!と息巻いていましたが、やろうとしていることの4分の1も出来ないとは…(絶望)。

読書

本当はこの時間があるときにElegant Puzzleとか、英語でしか読めない本を読むぞと思っていましたが触る余裕がありませんでしたね。
後、貞本エヴァもこの期間で読もうと思ってたのに読めなかった。Twitter眺めている時間をここに投資すれば良かったよ…。

データ指向アプリケーションデザイン

www.oreilly.co.jp

きっかけとしては、最終出社日の少し前にfukabori.fmというPodcastを知り、ずっと聴いていました。t_wadaさんのデザインパターンの回を最初に聴いたのですが、あまりに面白くて遡って結構聴いていました。
1年前ほどに、こばさんがゲストとして出演していたNewSQL回がこれまた面白く、その話の中でデータ指向アプリケーションデザインという書籍について触れられていたので、この期間に読みました。
自分が今まで読んだ本の中で一番ボリュームがあるであろう600ページ超えでして、読む前に心が折れて一瞬枕にしそうだったのですが。何とか踏みとどまりました。

自分は基礎的な部分に関してもちゃんとは理解していない箇所が多かったため、第一部から面白かったように感じます。
3章でLSMツリーやBツリーについての説明があったのですが、最初Bashのkey-Valueストアの実装から始まる説明は非常にとっつきやすくて良かったです。また、4章ではエンコーディングについての説明がありますが、Protocol BuffersやThriftに疎かったため(特にThriftはUberGatewayの設定で利用されていたというイメージしか無かった)、非常に勉強になりました。

第2部はデータを分散をさせることによるConsistencyに関する問題とかをどう解決していくかという所が非常に面白かったです。
マルチリーダーやリーダーレスのDBには馴染みが無かったので、理解しきれていない部分も多い。衝突を解決するためのCRDTというMisreading Chatで聴いた内容が出てきて、点と点がつながった感覚になった。
あと8章は勉強していたゆるーく勉強していた分散システムの知識が凄く分かりやすくまとまっていて有り難かった。

なにはともあれ再読は必要!

Site Reliability Workbook

www.oreilly.com

ずっと読もう読もうと思いつつ読めていなかった書籍。e34.fmで良いぞという話が出ていたので、ちゃんと読もうと思って読みました。
自分がDevOpsへの理解が浅かったこともありますが、第1章で語られていたDevOpsとSREの関係性というのは、自分の頭の中を整理する上でとても良かったです。書籍の中でも触れられていますが、class SRE implements devops というのは自分の中でしっくりきたものはありました。

読んでて面白かった章は、第2章のimplemeting SLOsですね。サンプルのシステムアーキテクチャをベースにどのようなSLIが考えられるかあったり、SLO/エラーバジェットポリシーを作成する手順などが記載されており、これをベースにSLOの運用を始めていけると確信できるような内容になっていると思いました。
後は、第5章Alerting on SLOsも良かったです。SLOのアラート設定をシンプルなアプローチから始めていき、それをprecisionやrecallといった指標を元に評価し、burn rateなどを用いて改善していくのは読み進めやすかったですね。
そして飛んで、第20章の SRE Team Lifecycles は、SRE組織をmatureにしていくためのロードマップが示されているのですが、SREを始めるときのイメージが運用チームがSREに転換するというパターンしか自分の頭には無かったので、いくつかパターンがあることに驚きを覚えました。
これも、First SRE Bookと合わせて再読必須だなと思いました。

スイッチ!

www.hayakawa-online.co.jp

これは、お休み入る前にkindleの半額セールで購入したものです。
人間の感情的な部分を「象」に、そして理性的な面を「象使い」に例えて、「象使いに方向を教える」「象にやる気を与える」「道筋を定める」の3つのステップから行動を変えるというのを説明した本。事例と、それを裏付ける実験を紹介していく形式になっている。納得感はあるけど実践できるのか?みたいなところはある。1つ確実なのは、この休みが始まる前に読めば良かったってこと。
この他にも早川の本で買ったものがあるので読めたらなと思っている。

あ、後ブクログがちゃんと使えてないので、進捗が入力できるorkindleなら勝手に記録してくれるものが良いなあと思って代わりを探しています。良いのがあれば教えて下さい。個人的にはgoodreadsってどうなのかなと思っていたり…。

趣味

ちょっと前にもブログに書いたんですが、Travis JapanとINIというボーイズグループを細々と追っているので、その辺の動画やWEB RADIOをずっと観てしまいました。

44smkn.hatenadiary.com

IMAGE NATIONというトラジャの全国ツアーの最終日に配信がありましたが…最高でしたね。去年のライブも配信で観ることが出来たのですが、今年は特に歌がより進化している気がしました。INTERACTIONALもっとちゃんと観たかったなあ。円盤化を何卒…何卒…!
INIのデビューも待ち遠しい!みんな言っていますが、Wanna Oneのenergiticみたいな曲がデビュー曲だと嬉しいな!

分散システムの勉強(中途半端)

ここは中々苦戦しました。deeeetさんのブログ記事を読んで、UCSCの2021年の分散システムの講義動画を観ていましたが、シンプルに「僕のリスニング能力低すぎ…」となり辛みでした。

第10回くらいまでは観たのですが、ちゃんと頭に入っているかがかなり微妙で、結局は講義PDFも存在する、ケンブリッジ大学の分散システムの講義に乗り換えて勉強しています。お休み終わる前までにRaftにたどり着けないので一生合意できない人間になりつつある。
ケンブリッジ大学の授業の第3回leap secondsの話で日光東照宮の🙈 が出てきてビビりました。

また、分散システムと言えば、タネンバウム先生のDistribureted Systemsだと思います。一応、そちらを読み進めています。ですが、「僕、英語読むの遅すぎ…」となり心が挫けそうです。まだ第2章のARCHITECTUREの途中です。
第3版は日本語訳が出ていないですし、第2版は絶版とのことなので英語を読むしかない。僕には英語が分からぬ。この前、TOEICで750点取れたのは奇跡だったのか、それとも750点はこのレベルなのかどっちなの。

手動かしたもの

自作OS本

book.mynavi.jp

ブートローダーの部分までしかやっていないので、実質自作ブートローダー本状態だ。
みかんOSは「未完」というところから名付けられていますが、始まってもいない。始める前のWSL周りでハマって時間を結構使ってしまった感がある。
土台は作ったのでちょっとずつ進められると良いよね…そう…ちょっとずつね。

React/GatsbyJSに入門

新卒で入った会社にいた頃に趣味でAngularを触っていた。その頃の最新はv4だったように思います。mozaic.fmのep24でlacoさんの話を聞いて面白そうと思って、当時既にReact全盛だったと思うのですがAngularを触り始めていました。
業務でJSとSassを書いてたころも有りましたが、ReactやVueのようなライブラリを使った開発は行っていなかったので、この期間に触るかと思ってSSGと共に少し触っていました。
今度ブログ書こうと思います。まだローカルで動かしている段階でCloudflareとかにデプロイ出来ていないので。

式場探し

何気に時間が溶けていったもの。この期間に始めて良かった。

ブライダルフェアなるものが、あんなに一回で時間と精神を消費するものだとは思わなかった。なにも知らなかった頃は1回1時間くらいなのかなあというぼんやりした印象を持っていたが、実際は4時間前後かかるものだった。
2箇所の式場を回ったけど、それぞれの項目をスプレッドシートに書き出して比較検討するのは楽しくもあり疲れもしますね。

来年も新型コロナウイルスの影響を引きずるということも考えられるので、キャンセルポリシーとかもしっかり確認しつつ動かなきゃだ。
結婚式挙げた人たちマジで凄い。

新しいノートテイキングツール

Notionを使っていたのだけれど、僕はこの高機能を活かしきれていないと思っていました。後ローカルに保存できない。
もう少しシンプルでパフォーマンスの良いもの、かつローカルで利用できるものにしたいなと思っていました。というのも、仕事だとオンラインにアップロードするものはどうしても使いづらいというのがモチベーションでした。
そんなときに、Rebuild.fmの#308 を聴き、宮川さんが今はcraftというツールを利用しているということで気になり、1ヶ月使っていました。Macのネイティブのアプリが自然で良いのと、グループ化の機能がいい感じです。ローカル保存も上手く動くことを確認しました。WebはまだBetaでかつMaciPhoneのアプリでしか出てないので、自分のユースケースにはぴったり合いますが他の人にとっては微妙かもです。
難点としては、機能のドキュメントがあまり無いためにリリースノートを遡ることになる点と、画像を貼ると全体的にもっさりする点です。

結局のところ、課金してしまって継続利用しようかなと意気込んでいたのですが、自分のAppleIDで買ったので会社で使うためにはもう一回買わなきゃでは…?と戦々恐々としているところです。
(今の所ログインはAppleIDのみがサポートされていたはずのため…)

まとめ

リラックスできた1ヶ月だったのではなかろうか。専業主夫チックな生活も新鮮で良かったな。
料理する頻度はこの1ヶ月は確実に多かったので、今後の生活に響いてくるのは、なんだかんだそういう部分なのかもしれない。効率上がってたら良いなあ。計測しておけば良かった。以上!

WSL1でみかん本を進める上で躓いたところ

背景・モチベーション

低レイヤを理解したいというのは、ソフトウェアエンジニアをやっていると、どうしても沸き起こってしまう感情なのではと思ったりします。
自分は工学部出身ですが、コンピュータサイエンスを専門とした学科ではなかったので電子回路とかを除くとC言語くらいしか情報系の授業は受けておらず、OSの仕組みとかは全然知りません。
7月末に退職し8月から新しい会社で働くことになるのですが、有給だったりで約1ヶ月間のお休みができたので、この機会に、DMMセールのときに購入し積んでいたみかん本をやるぞと思い立ちました。

WSL1を使うに至った経緯

2015年に買ってずっと眠らせていたSurface3を引っ張り出し、Windowsのバージョンを最新化してWSL2をセットアップしようとしました。
指定された手順を行っていきましたが、特にエラーなど起こることなく完了しました。ubuntuを起動しようとしたときに、 Installation failed with error 0x80070003 or error 0x80370102 というエラーメッセージが出力され起動に失敗しました。

ドキュメントを確認したところ、下記のように記載がありました

  • Please make sure that virtualization is enabled inside of your computer's BIOS
  • WSL2 requires that your CPU supports the Second Level Address Translation (SLAT) feature, which was introduced in Intel Nehalem processors (Intel Core 1st Generation) and AMD Opteron.

「これからBIOSを学ぶってときにBIOSをいじるんか…」と思いつつ、ここを見ながらBIOSの画面に飛んだ。が、Virtualizationの項目は特にない。調べたところSurfaceでは元々Virtualizationは有効になっているらしい。
「タスクマネージャ」の「パフォーマンス」タブの「仮想化」の欄を見たら、ちゃんと有効になっていた。

ではHyper-Vの要件を満たしていない…?と思い確認してみる。powershellSysteminfo.exeを実行してみた。Hyper-Vの要件を満たしているかは一番最後に出てきますが、全てが「はい」になっていることが確認できた。

WSLのGitHubのIssueRedditを見つつ色々やりましたが駄目でした。Hyper-VはWSL2利用時には明示的に入れなくても良いと書いてありましたが、Windowsのイベントログには「Hyper-Vがインストールされていないため、仮想マシンの作成に失敗しました」と出力されていたので、Hyper-Vもインストールしましたが同じエラーが出てしまい八方塞がりになってしまいました。

もうこれはWSL1で動かすしかないと諦めて、WSL1で進めていくことにしました。

WSL1で躓いたところ

USBに書き込んでそこから起動させる方式ではなく、QEMUで起動させる方式を自分は取りました

ディスクイメージをマウント出来ない

BOOTX64.EFIのファイルを作成し、ディスクイメージに書き込むために mnt ディレクトリを作成しマウントさせようとしましたが下記のようにエラーとなりました。

$ sudo mount -o loop disk.img mnt
mount: ./mnt: mount failed: Operation not permitted.

こればWSL1と2でシステムコールファイルシステムの扱い方が違うからなのかな(参考)と思いつつ調べていたところ、GithubのIssueとして既にどなたかが挙げてくださっていました。とてもありがたい…。

github.com

mtoolsを利用することで、wsl1でもディスクイメージにファイルの書き込みが出来るようです。Issueの中ではubuntuに元々入っているということでしたが、自分が動かしているWSL1のUbuntu 20.04には入っていなかったので apt 経由でインストールしました。
下記のような手順でディスクイメージにファイルをコピーすることができました!

$ sudo apt install mtools
$ mmd -i disk.img ::/EFI
$ mmd -i disk.img ::/EFI/BOOT
$ mcopy -i disk.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI

$HOME/osbook/devenv/make_image.sh の中身のマウント部分の処理も上記のように変更しておくと、後々もいい感じです。

仮想マシンで生成されたファイルの中身が見れない

P61のメモリマップを出力した後にファイルを確認するときの手段についてです。上に関連した話になっています。
run_qemu.shを動かしたディレクトリにdisk.imgが作成されていますが、マウントして観ることはできないのでmtoolsを利用します。

# ファイルの存在確認
mdir -i disk.img ::/ 

# ファイルをコピーして確認する
mcopy -i disk.img ::/memmap ./memmap
less memmap

番外編: QEMUが動かない

これはWSL1であること由来ではありません。なんらかの理由でansibleの実行で一部上手くいかないと起こる事象のようです。
qemu-system-x86_64 を実行すると、Unable to init server: Could not connect: Connection refusedというエラーメッセージが出力されてしまいました。XLaunchが起動していることは確認済の状態です。

下記のように作業すると解決しました(参考
DISPLAY という環境変数はXサーバが利用するもののようです(参考
xhostはXサーバへの接続許可リストに追加するコマンドっぽいのですが、Xサーバの仕組み周りを自分が理解していないのでよく分からないまま利用している状態です。普段使いはWindowsではないので、あまり深追いはしないつもりです。

$ sudo apt install x11-xserver-utils
$ export DISPLAY=:0
$ xhost +

やっとこさ進める状態になったので、ちょっとずつ進めていこう

s3selectを実行するGo製CLI を作ってみての気づきとか

背景・モチベーション

ALBのアクセスログとかをクエリしたいときがあります。例えばELB5xxエラーが発生していて、そのリクエストの詳細を知りたい時などです。しかも複数ファイルに対して、一括でクエリを投げられるとなお嬉しい。
1日待てばAthenaに放り込まれるが待っていられないぞーという自分がいたりします。

そこで、自分の要件に沿ったs3selectCLIを勉強がてら作ってみました。作る上で参考にしていたのは、GitHub CLIAWS LoadBalancer ControllerCopilot CLIあたりです。

気づきをこの記事でまとめて、後々の自分に役立ててもらおうと思います。

リポジトリはこちらになります。

github.com

CLIについて

cobra を使う前提です

エラーハンドリングについて

cobraSilenceErrorsSilenceUsagetrueにして、cmd.ExecuteC() の呼び出し元でSentinel Error方式を利用しエラーの種類によって出力を分岐させるのが良さそう。エラーによっては Usageを出力したりしなかったりとか諸々パターンが有ると思うので、cobraの設定でハンドリングするのが難しいからかもしれない。
CLIであれば、Error interfaceを実装した独自エラー構造体を作りerrors.Asを使って処理を分岐するといったところまで作り込まなくても良さそう。

func NewCmdRoot(f *cli.Factory, version, buildDate string) *cobra.Command {
    cmd := &cobra.Command{
        SilenceErrors: true,
        SilenceUsage:  true,
    }
}

func main() {
    if _, err := rootCmd.ExecuteC(); err != nil {
        if errors.Is(err, terminal.InterruptErr) {
            fmt.Fprint(stderr, "\n")
        }
    }
}

CLIで共通利用する設定はひとまとめにする

ターミナルの入出力設定やCLIが独自の設定ファイルを用意する場合には、cmd.AddCommandの引数に渡す *cobra.command を生成するコンストラクタに共通の設定を定義した構造体を渡してあげるようにするとスッキリする。

type Factory struct {
    In     io.ReadCloser
    Out    io.Writer
    ErrOut io.Writer

    Config     func() (config.Config, error)
    Logger     *zap.Logger
    Executable string
}

func main() {
    cliFactory := cli.NewFactory(buildVersion, logger)
    rootCmd := root.NewCmdRoot(cliFactory, buildVersion, buildDate)
    // ...
}

テストについて

外部APIを利用するテストにはgomockを使う

外部API、今回の文脈ではaws-sdk-goを利用して叩くAWSAPIをテストする場合についてです。スタブを用意しても良いのかもしれませんが、間接出力も合わせて検証したいのでモックを利用します。モック/スタブの定義は テストダブル - Wikipedia を参考にしています。

Goにおけるモックライブラリでよく使われていそうなのは、golang/mockもしくはstretchr/testifymockパッケージという印象があります。今回、自分は前者を利用しました。
自分で定義したinterfaceに、aws-sdk-goで定義されているインターフェース(下記のコードで言うところのs3iface.S3API)をEmbeddingすることで、mockgenが外部APIのモックコードも自動生成してくれます。

//go:generate mockgen -destination=../../../mocks/aws/services/mock_s3.go -package=mock_services github.com/44smkn/s3select/pkg/aws/services S3
type S3 interface {
    s3iface.S3API

    // wrapper to ListObjectsV2PagesWithContext, which aggregates paged results into list.
    ListObjectsV2AsList(ctx context.Context, input *s3.ListObjectsV2Input) ([]*s3.Object, error)
}

設定ファイル読み込み関数はテストはStubを用意するのも一つの手

テスト対象がメソッドかつ間接出力も検証したいという場合にはモックが必要になるかなと思いますが、テスト対象が関数かつ間接出力の検証も必須でない場合にはStubで十分というのがあります。設定ファイルの読み込み処理なんかは、もちろんメソッドに落とし込むことも出来るかと思いますが関数にしがちかと。
テスト対象から呼び出される関数は変数として宣言しておきます。

var ReadConfigFile = func(filename string) ([]byte, error) { ... }

// テスト関数から defer で呼び出す
func stubConfig(t *testing.T, configContents string) func() {
    t.Helper()
    original := config.ReadConfigFile
    config.ReadConfigFile = func(filename string) ([]byte, error) { ... }
    return func() {
        config.ReadConfigFile = original
    }
}

go testするときに -race flagつけるべし

Race Conditionを検出するフラグがgo testにあることを僕は知らなかったです。オライリーから出てるGo言語による並行処理のAppendixに記載ありましたが見逃していました。Go1.1からほぼ全てのgoコマンドに追加されたもので、検出された際には標準エラー出力にログを出力するようになっています。これをCIに組み込むのは有用なので、GitHub Actionで定義したテストにはこの引数を指定しています。

blog.golang.org

ビルドタスクについて

Goで書いても良いんじゃない?

Goではビルドするときに、go build の引数である -ldflagsというgo tool linkコマンドのオプションに渡されるフラグで、-X importpath.name=valueと指定してバージョンはdateを埋め込むことが多いと思います。後はWindowsの場合には、拡張子でexeを付けるとか。
自分はそのようなタスクをシェルスクリプトorMakefileで書くものと思っていましたが、GitHub CLIがgoでビルドタスクを書いてMakefileから呼び出すようにしていました。Windowsでも動きますし有用かもしれません。

.PHONY: bin/gh$(EXE)
bin/gh$(EXE): script/build
   @script/build $@

script/build: script/build.go
  GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o $@ $<

goreleaserが便利

goreleaserは有名なので知ってはいましたが、今回始めて使いました。GitHubでのリリースが抽象化されていてあっという間にやりたいことが出来ました。GitHub Actionsとの組み合わせも容易でした。リリースノートもコマンドのオプションで渡せますしね。
自分はやっていないですが、nfpmを利用して、Linuxのパッケージマネージャからも扱えるように出来たりするのも良い点ではと思います。

goreleaser.com

雑多

awssdk利用する上でHandlerは理解しておいたほうが良さそう

今までちょこちょこ aws-sdk-go 使ってきたけど理解が足りなかったところで、aws-sdk-go/aws/request.Handlerにぶら下がるHanlderListをhttpリクエストの対応するフェーズで呼び出しています。

例えば、CloudTrailのレコードにUserAgentが記録されますが、そこにツール名を入れられると後で確認しやすそうです。その場合には、下記のようにHandlerを利用側から追加することが出来ます。
また、各リクエスト毎にもこの設定は可能で、かつHandlerを逆に削除することも可能です。リクエストの可変長引数に request.Optionを渡せるのですが、このOptionの型が func(*Request)なので割と好き勝手に出来てしまいます。

// ハンドラーの追加
func NewCloud(cfg CloudConfig) (Cloud, error) {
    sess := session.Must(session.NewSession(aws.NewConfig().WithRegion(cfg.Region)))
    sess.Handlers.Build.PushFrontNamed(awsrequest.NamedHandler{
        Name: fmt.Sprintf("%s/user-agent", appName),
        Fn:   awsrequest.MakeAddToUserAgentHandler(appName, build.Version),
    })
}

// ハンドラーの削除
func hoge() {
    resp, err := s.cloud.S3().SelectObjectContentWithContext(ctx, input, func(r *awsrequest.Request) {
        r.Handlers.Send.RemoveByName(awsclient.LogHTTPResponseHeaderHandler.Name)
    })
}

renovate入れとく

renovateは依存モジュールのバージョンを最新に保ってくれるためのサービスです。Githubリポジトリにはここから導入することが出来ます。依存しているモジュールがバージョンアップしたら、Pull Requestを発行してくれます。

アップデート後にgo mod tidyを実行するように設定ファイルにpostUpdateOptionsを宣言しておくと良いです。

{
  "postUpdateOptions": [
    "gomodTidy"
  ]
}

docs.renovatebot.com

VSCodeデバッグを行う上でちゃんと launch.json 使う

めっちゃ当たり前!なんだけど出来ていなかった。リクエストを受け付けるアプリケーションやシンプルなものだと、なんとなくでデバッグ出来てしまっていた。
CLIとかだと引数によって挙動が大きく変わるので、launch.jsonargsとかによって複数設定を管理しておくと楽ということに気づけた。

code.visualstudio.com

感想

作ってみたアプリケーション自体は、テストのカバレッジがまだまだ低かったりエラーハンドリングまだまだだったりするので、勉強がてらちまちま書いていこう
以上!

CDK for TerraformがGoをサポートしたので試してみた

記事一覧はこちら

背景・モチベーション

CDKには元々興味がありました!CloudFormationでのyaml記述はやはり辛いものがあるし、プログラミング言語を利用することでコード補完によるサポートを得られたり、抽象化やvalidateを入れたり出来るのではと期待があります。
CDK for Terraformのv0.0.4がリリースされ、Goのサポート(experimental)を開始したことをこの記事から知り、良い機会なのかなと思い試してみました。

参考文献

CDK For Terraform とは

ここにあるとおり、プログラミング言語でインフラストラクチャを記述したいというDeveloperの要望に応える形で、AWS CDKのチームと協力し、AWS CDKの2つの主要なコンポーネントを活用してTerraform用のCDKをサポートし始めたとのことです。

AWS CDKに関しては、こちらが分かりやすいです。
AWS CDKはCloudFormationテンプレートをjson形式で吐くのに対し、Terraform用CDKはTerraformの構成ファイルをjson形式で吐きます。元々、HCLだけでなくjsonもサポートしているのはあんまり意識したことなかったなあと思ったり。

CDKは多言語をサポートしてます。そこにはjsiiというライブラリとかが絡んできていて、掘るのが面白そうなのですがそれはまた別の記事にしようと思います。

Getting Started

リポジトリはこちら

github.com

※ Terraformはインストール済の前提

実装の初期段階まで

$ brew install cdktf
$ mkdir cdk-for-terraform-sample

# 雛形の作成
$ cdktf init --template="go" --local

# moduleの宣言が自動生成されているので修正する
$ vi go.mod 

プロジェクトルートにある cdktf.json に利用するTerraform ProviderとModuleを宣言します
例えば、AWSのProviderとEKSのModuleを利用するのであれば下記のように宣言する

{
    "language": "go",
    "app": "go run main.go",
    "codeMakerOutput": "generated",
    "terraformProviders": [
        "hashicorp/aws@~> 3.40.0"
    ],
    "terraformModules": [
        {
            "name": "aws-eks-module",
            "source": "terraform-aws-modules/eks/aws",
            "version": "~> 16.0"
        }
    ]
}

下記のコマンドを実行すると、上で設定した内容を元にコードを自動生成します
codeMakerOutput で設定したディレクトリに生成したコードが配置されます

$ cdktf get
⠇ downloading and generating modules and providers...  # めっちゃ時間かかりますし、メモリを6-7GB食います

Goのコードで実装

main.go に宣言された、NewMyStack 関数にてリソースの定義を行っていきます。
基本的な関数の構造としては下記のような形で、第一引数と第二引数は各関数で共通で、第三引数は各リソースごとのinputのパラメータを持った構造体を渡してあげるイメージ。殆どが参照渡しになるので、適宜jsiiパッケージのヘルパー関数を利用する。

vpc := aws.NewCamelCaseResourceName(stack cdktf.TerraformStack, id *string, config *aws.CamelCaseResourceNameConfig)

例えば、簡単なAWSのスタックを作成するとなると下記のようになると思います。

func NewMyStack(scope constructs.Construct, id string) cdktf.TerraformStack {
    stack := cdktf.NewTerraformStack(scope, &id)

    aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
        Region: jsii.String(region),
    })

    vpc := aws.NewVpc(stack, jsii.String("isucon_vpc"), &aws.VpcConfig{
        CidrBlock: jsii.String("172.16.0.0/16"),
        Tags: &map[string]*string{
            "Name": jsii.String("isucon-training"),
        },
    })

    igw := aws.NewInternetGateway(stack, jsii.String("isucon_vpc_igw"), &aws.InternetGatewayConfig{
        VpcId: vpc.Id(),
    })

    subnet := aws.NewSubnet(stack, jsii.String("isucon9_subnet"), &aws.SubnetConfig{
        VpcId:            vpc.Id(),
        CidrBlock:        jsii.String("172.16.10.0/24"),
        AvailabilityZone: jsii.String("ap-northeast-1d"),
        Tags: &map[string]*string{
            "Name": jsii.String("isucon-training"),
        },
        DependsOn: &[]cdktf.ITerraformDependable{
            igw,
        },
    })

    // 他にもリソースを作成(詳しくはリポジトリをご覧ください)

    return stack
}

デプロイしてみる

下記のコマンド実行時に cdktf synth も実行されて、cdktf.outディレクトリにjson形式でterraformの構成ファイルが生成されます。
注意点としては、cdktfのv0.4.0の時点では cdktf destroy時にエラーが発生します(Issue)。

# terrafrom planとほぼ同じような出力が得られる
$ cdktf diff
Stack: cdk-for-terraform-sample
Resources
 + AWS_DEFAULT_ROUTE_TA isucon9_subnet_rout aws_default_route_table.isucon9_subnet_
   BLE                  e_table             route_table
 + AWS_EC2_MANAGED_PREF default_prefix      aws_ec2_managed_prefix_list.default_pre
   IX_LIST                                  fix
 + AWS_EIP              isucon9_qualify_ins aws_eip.isucon9_qualify_instance_eip
                        tance_eip
 + AWS_INSTANCE         isucon9_qualify_ins aws_instance.isucon9_qualify_instance
                        tance
 + AWS_KEY_PAIR         developer_keypair   aws_key_pair.developer_keypair
 + AWS_ROUTE_TABLE_ASSO isucon9_subnet_rout aws_route_table_association.isucon9_sub
   CIATION              e_table_d           net_route_table_d
 ~ AWS_SECURITY_GROUP   isucon9_qualify_ins aws_security_group.isucon9_qualify_inst
                        tance_sg            ance_sg
 + AWS_SUBNET           isucon9_subnet      aws_subnet.isucon9_subnet

Diff: 7 to create, 1 to update, 0 to delete.

# デプロイ
$ cdktf deploy

# お片付け(エラーが発生します)
$ cdktf destroy
ypeError: Cannot read property 'startsWith' of undefined

使ってみての所感

文字列をポインタで渡さなくてはいけない関係上、jsii.String関数を毎回使うのでスッキリ見えないのがあまり良くないかもというのはあります。
また、この段階だからだと思うのですが、Terraformのresourceやmoduleを元にコード生成する過程でかなり時間とリソースを食います。これはネックになりそうです。
規模感が大きくならないと、中々恩恵は受けづらいのかなと思う面もありました。
(でもyaml書くより楽しい…)
以上!