SOPSを試してみる
クレデンシャルなどの情報もGitで管理したい…!! とくに GitOps などを採用しているとそう感じるケースも多いと思います。(僕はよく思っていました) そんなときの解決方法の一つであるSOPSを試してみました。
↓の本で紹介されていて知りました。ツール自体はかなり前からあるみたい
動作確認した環境は macOS Catalina 10.15.5
です。
Install SOPS
まずは sops をインストールします。
brew install sops
$ sops -v sops 3.5.0 (latest)
暗号化の対象になるファイルを作成
今回は簡単なユーザー名とパスワードが入ったファイルを想定します。
$ mkdir config $ cd config
config/dev.yaml
user: dev-user pass: dev-pass
暗号化のための鍵を作成
参考: Google: google_kms_secret - Terraform by HashiCorp
- 簡単化のために
- 3環境用のモジュール化は見逃してください…(devだけ動かします)
- remote state は見逃してください…
$ cd .. $ mkdir terraform $ cd terraform
terraform/kms_dev.tf
resource "google_kms_key_ring" "key_ring" { name = "sops-key-ring" location = "asia-northeast1-b" } resource "google_kms_crypto_key" "my_crypto_key" { name = "sops-crypto-key" key_ring = google_kms_key_ring.my_key_ring.self_link }
プロバイダ用のファイルも作成
terraform/provider.tf
provider "google" { project = "kaito2" region = "asia-northeast1" } // cloud kms を有効化 resource "google_project_service" "kms" { service = "cloudkms.googleapis.com" }
$ terraform init $ terraform plan ... Plan: 2 to add, 0 to change, 0 to destroy. ... $ terraform apply ... Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
$ gcloud kms keyrings list --location asia-northeast1 NAME projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring $ gcloud kms keys list --keyring sops-key-ring --location asia-northeast1 NAME PURPOSE ALGORITHM PROTECTION_LEVEL LABELS PRIMARY_ID PRIMARY_STATE projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key ENCRYPT_DECRYPT GOOGLE_SYMMETRIC_ENCRYPTION SOFTWARE 1 ENABLED
ちゃんと鍵が作成されていることが確認できました。
対象ファイルを暗号化
鍵を指定せずに実行すると怒られます。
$ cd ../config $ sops -e dev.yaml config file not found and no keys provided through command line options
GCP の場合は --gcp-kms
オプションにリソースIDを渡す必要がある(or SOPS_GCP_KMS_IDS
という名前の環境変数に設定する)。
リソースIDとは: Object hierarchy | Cloud KMS Documentation | Google Cloud
コマンドを以下のように変更 (何も指定しないと標準出力に暗号化されたファイルの内容が出力される。)
$ sops -e --gcp-kms projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key dev.yaml user: ENC[AES256_GCM,data:8Hug2XT/PP8=,iv:X6/VCucFyTtqaspP20OOKbBm43AuFvVCN+mA7uReKTw=,tag:GTTIklpZw+kfQK+eamw1bQ==,type:str] pass: ENC[AES256_GCM,data:Fw5s9NJVxY8=,iv:JTkKRLiSKa7i2o+Ek20Dq6+YUpxdUWPm2halpUtzi6o=,tag:4l57RVbGH1oaKdO+4zE80A==,type:str] sops: kms: [] gcp_kms: - resource_id: projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key created_at: '2020-07-13T11:42:45Z' enc: CiQAIyu+rA1vIggweC/+b2fWXzFVDlRYL9utDh9znmBKOoip8VcSSQCqguh30vH6TCl/mhT+AgucZ2sIRJwthwXk+M1GIzOIDvFoXCUHY1VvVyfzaSXCKhkw8boV4EW8QDqYEDyX1n6NVR5kIiSxSBQ= azure_kv: [] lastmodified: '2020-07-13T11:42:46Z' mac: ENC[AES256_GCM,data:BXAavE6OhzKSfdmRE5cQmvyEKga0pnnSOAQLYmnjdadlRTV/TuDhSlriYNenEFXw4+RCHurL2xZfY3cOoqwuct6lZz2ATm81yzqjtymuH63xFG80DqjJRgKFQ+wBuggLPasKyWq8PHUQEr/0CEVZ/u9LgVO3CoJS5gWNISLzrow=,iv:KnaYCHB8r+clN1iVn0yCJnAKfI3r7h/H+sSCji++/Ic=,tag:/OzY47i++dfBO3VkdBiaUQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.5.0
コマンドごとに --gcp-kms
オプションを指定するのも面倒なので .sops.yaml
という設定ファイルを設置することで sops がそれを読み込んでよしなにしてくれる。
See: mozilla/sops
config/.sops.yaml
creation_rules: - path_regex: \.dev\.yaml$ gcp_kms: projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key
コンフィグファイルを設定したので --gcp-kms
オプションなしでも暗号化できるようになりました。
$ sops -e dev.yaml user: ENC[AES256_GCM,data:cxkJBKyiQ4c=,iv:m00uQzik0OBw0tf5es4j1mZH+TM2JxynjifOhkW+ZHE=,tag:Jxtj4cfRe+TIdz3Vp9lw7w==,type:str] pass: ENC[AES256_GCM,data:JUcITsSaKqc=,iv:cbNXopbCJPGtFbSkwn0TAXUKN0ojSjcMQrQJ5DpFOWo=,tag:i4/ey1aJh99PtZovpHU6Ww==,type:str] sops: kms: [] gcp_kms: - resource_id: projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key created_at: '2020-07-13T11:48:46Z' enc: CiQAIyu+rN+NkKTu3H/riwZ4NsNWh5SjmWiZQbp13qyaj8vPfdgSSQCqguh3JyE5RlKHVWKUkJ/cVRL4jNyKnIAMc6MUMZdeXURJ+I2tFuXRZRVbJpHKTf1CV4K0sKPdJyQwVvjdzbJAA2CrOZgdupw= azure_kv: [] lastmodified: '2020-07-13T11:48:47Z' mac: ENC[AES256_GCM,data:/I+KswiR7VsJYDZJMwbUn1M/ygbLmbIdCuUVKR9uaZ+SFSudpU0zqX0mLg9R9jz5fB8t7JkcAPkXK5QiWJywvnE5YjtU3IsjrydNb7lEa630tqB0pEP8J4co+dGh9MzvyDQsldvwrREx9Ln8B3RfdyPaufYv7RfVqwBbWkgIYv0=,iv:g2jiRLD0wJYcj1OmhOzqDxOu5DWCcPTml9ROniyfXtQ=,tag:xXemqikO8oz8N8tlPUM+6A==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.5.0
SOPS はデフォルトですべての値を暗号化しますが、例えば user
の値は暗号化したくないなどの要件がある場合はオプションによって実現できます。
See: mozilla/sops
config/.sops.yaml
を以下のように書き直します
creation_rules: - path_regex: dev\.yaml encrypted_regex: pass gcp_kms: projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key
再度暗号化を実行すると、 user
の値は暗号化されておらず、pass
の値は暗号化されていることが確認できます。
$ sops -e dev.yaml user: dev-user pass: ENC[AES256_GCM,data:nPihbY9Qe+o=,iv:naSvswd7v1Kdnq7VFfj2A3KT3SRV6HtLp2lTHFro3Ks=,tag:dtRf4pW0grYJqWBoNzuS7g==,type:str] sops: kms: [] gcp_kms: - resource_id: projects/kaito2/locations/asia-northeast1/keyRings/sops-key-ring/cryptoKeys/sops-crypto-key created_at: '2020-07-13T11:53:47Z' enc: CiQAIyu+rFdLYVJKYgTiwH9L2sGwet7ugxWIlUPj39YhyyRR5ZISSQCqguh3cc4CB2tIC8KUhCa+5SXIw7HTpx1NP/FVBRIsXdscXHMnwn0FEgx2uH8MBrLGyv+Opb+WnomXWBZoBO+xQdDNOx5uvoI= azure_kv: [] lastmodified: '2020-07-13T11:53:48Z' mac: ENC[AES256_GCM,data:liFOktDxSb/phDHyha8ollNe6fXlZkQYfhxiBPaKZJDuUmDBBw6d7DrQIF/lg1b4FBundJ5l9SW/m5PK58yeLNbzTqTwl/ACXH69M4cE+GQZqi4HRYdMa5hfe7VTwYn+TbKL3DHZFavPWl/Nih6by5PL8ooJds4Z7+Ey28Z5M0A=,iv:0HM9a2wrxJHV9ZALxQYoc5s6Kd2miADVwax8hAig29k=,tag:ABAxVKdOI/tJEt6JIvwIXA==,type:str] pgp: [] encrypted_regex: pass version: 3.5.0
-i
オプションを追加するとファイルを直接暗号化して置き換えます。
$ sops -e -i dev.yaml # dev.yaml が書き換わっている
復号する場合は以下のコマンドを実行します。
sops -d -i dev.yaml
なので CI などでは config/
ディレクトリに移動し、上記のコマンドを実行すれば、必要な鍵を利用する権限があるかチェックされます。そして、権限がある場合は復号が実行されます。
実際にデプロイする流れはまた次回書きます。
所感
決して機能がリッチなわけでは無いですが、シンプルで挙動もわかりやすく、使い勝手のいいツールだと感じました。
carlpett/terraform-provider-sops なども便利そうなので試してみたいです… 特に、コンフィグファイルの可読性を落とさない点と、複数のクラウドの差異を吸収してくれる点は個人的に良いと思います。
よく使われている同じ目的のツールとしては bitnami-labs/sealed-secrets があるようなので、そちらとも比較してみたいです。
2週間休みだったので興味がある本を読み散らかした。
5月後半の2週間お休みだったので、自分を甘やかして「自分の興味のある本を片っ端から読み散らかして良い」という期間にしました。
まったくもって有益ではないですが、読書記録として感想を書いておこうと思います。
ガッツリ読んだ本
思考する機械コンピュータ
- 作者:ダニエル ヒリス
- 発売日: 2014/06/03
- メディア: 文庫
論理回路からAIまで、コンピュータというコンセプトの全体感を楽しみながらつかめる良書だと思います。
各章に出てくる事例も面白いものが多く、知的好奇心が刺激されまくりでした。大学時代の自分に勧めたい一冊。
コンピュータシステムの理論と実装
コンピュータシステムの理論と実装 ―モダンなコンピュータの作り方
- 作者:Noam Nisan,Shimon Schocken
- 発売日: 2015/03/25
- メディア: 単行本(ソフトカバー)
nand2tetrisでおなじみの本ですね。 NANDとDFF回路を手持ちにOS(と本書で定義されるソフトウェア)を作っていくという流れです。 各章ではどう実装するか、ではなくどういった機能を実装すべきかという仕様(と実装のヒント)が示されます。
最適化などはもちろん考慮されておらず、最低限の機能を対象としていますがそれでもこの内容を1冊で学べるので個人的にはとても良い本だおともいました。
コンパイラの章は僕の知識不足で実装しきれなかったので、この機会に勉強してリベンジしたいです。おすすめの本などがありましたら教えて下さい!!
オペレーティングシステムの仕組み
- 作者:河野 健二
- 発売日: 2007/10/01
- メディア: 単行本
ひとつ下の 作って理解するOS を読みはじめたところ、「OSなんもわかってないじゃん…」という気持ちになったので購入して通読しました。 比喩によって説明して「結局どういうこと?」みたいな部分がなかったのが個人的にとても良いと思った点です。
「メモリとは、1バイトのデータを格納できる箱が並んだようなものであり、それぞれの箱にはアドレス(address)という通し番号のようなものがついている」というイメージで説明されてきたはずである。これはメモリに対する素朴なイメージでしかない。
そのレベルで教わってきていたのでとても助かりました笑
作って理解するOS
- 作者:林 高勲
- 発売日: 2019/09/26
- メディア: 単行本(ソフトカバー)
突発的にOSに興味が出たので読み始めた本。まだ8割ほどしか読めていないです(重い…)。
全体として、前半はコンピュータのしくみ・ハードウェアの基礎・CPU命令の仕様 と続き、後半でいざOS実装!!という流れです。
前半は座学っぽくなっているので若干読み進めるのに根気がいりますが、復習としてとても良かったです。 ちなみに、開幕は2進数の説明からはじまります。2進数知らない状態からOSを実装できる状態までもっていくってすごすぎでは…。
今は後半の途中なのですが、全てアセンブリによる実装なので噛み砕いていくのに時間がかかります。 ただその分挙動の解釈に間違いがうまれにくいかなと思いました。
完走がんばります〜
手を出し始めた本
コンピュータの構成と設計
パタヘネ。
コンピュータ何もわからなくなったので復習し始めました。 まだ1章ですが、読んでて楽しいのでちょくちょく読み返していきます。
- 作者:ジョン・L. ヘネシー,デイビッド・A. パターソン
- 発売日: 2014/12/06
- メディア: 単行本
計算機プログラムの構造と解釈
- 作者:エイブルソン,ハロルド,サスマン,ジュリー,サスマン,ジェラルド・ジェイ
- 発売日: 2014/05/17
- メディア: 大型本
実際には非公式日本語訳のほうがわかりやすいとのことだったので下のPDFを読んでいます。 N回挫折しているので今回は大学時代の友人を巻き込んで毎週読み合わせをしています。 ありがたい。
正直 やっと2章までたどり着いた程度なので感想的なものは書けませんが、数学的な能力のほうが不足しているせいで演習問題や例題の理解にビビるほど時間がかかります…。みんなで頑張ろうな!!
感想
普段の業務では直接的には活きなさそうだけど興味があった分野の本を読み漁りました。 友人とのSlackで僕が理解した内容をひたすらつぶやき続けるチャンネルを作ってブツブツ言いながら2週間を過ごしました(リアクションしてくれたみんなありがとう…)。 これで僕もつよつよエンジニアに近づいた!! という感覚は皆無ですが、こういう勉強はこれからも時間を見つけて続けていきたいなと思います。
Firebase hosting + Cloud Run の機能を試していくよ!!
Firebase hosting + Cloud Run で Microservices のL7ロードバランシングができるみたいだったので試してみました!!
勢いでMicroserviceのサンプルAPIを作る
goa とかでやるとパッとできるよ(趣味)
本編
ツールインストール
$ npm install -g firebase-tools $ firebase login
Hosting
Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング | Firebase
以下を参考にリライトする Hosting 動作を構成する | Firebase
firebase.json
の”rewrites”
を以下のように記述するとロードバランシングできます。
最低限のファイル
{ "hosting": { "public": "", "rewrites": [ { "source": "/get-hello**", "run": { "serviceId": "goa-microservice-sample", "region": "us-central1" } } ] } }
$ firebase deploy # hosting のみ選択
以下のように /get-hello
へアクセスすると goa-microservice-sample
の /get-hello
パスが呼び出される。
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" "https://<FIREBASE_DOMAIN>/get-hello?name=hoge"
ちなみに httpstat
で計測してみると…
$ httpstat "https://<FIREBASE_DOMAIN>/get-hello?name=hoge" -H "Authorization: Bearer $(gcloud auth print-identity-token)" ... DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer [ 5ms | 5ms | 20ms | 6ms | 1ms ] | | | | | namelookup:5ms | | | | connect:10ms | | | pretransfer:30ms | | starttransfer:36ms | total:37ms
※ キャッシュにヒットした際のレスポンスのサンプルなのでパフォーマンスの参考にはしないでください。
キャッシュの動作は確認していないので以下のドキュメンとを読む。
キャッシュ動作の管理 | Firebase HTTP キャッシュ | Web Fundamentals | Google Developers
まとめ
Firebase Hosting + Cloud Run で動的なコンテンツをホスティングできることが確認できた。 Firebase Hosting を利用することで 自動的に CDN が用いられる。(デフォルトでは動的コンテンツには適用されていないとドキュメントには記載されていたが、キャッシュされていた…) キャッシュや認証の仕様を確認していないので引き続き調査していきたい。
goaを使ってAPIサーバをサクッと作る
概要
目的
JSONを返すREST APIサーバをサクッと作りたいという要求がちょくちょくあります。
そのたびにサーバの骨組みを作ってリクエストのバリデーションをして…みたいな作業をするのに嫌気がさしたのでJSON APIのクッキーカッターを作ろうと思い立ちました。
目的は上記のとおりなので「こっちのほうが楽なんだがw」といったものがあれば是非教えて下さい!!
なぜgoaを選んだか
まずgoaとはなにかなんですが以下に公式Docを引用します ref. https://github.com/goadesign/goa
Goa takes a different approach to building services by making it possible to describe thedesignof the service API using a simple Go DSL. Goa uses the description to generate specialized service helper code, client code and documentation.
以下が主な選定理由です。
Swaggerが生成されるのが結構いいなと思っています。(普通は逆かもしれませんが) Swaggerを起点にしてJMeterなどの負荷試験ツールのコードも生成したいという野望があります。
Prerequisites
Install goa
goa
コマンドをインストールします。
グローバルを汚したくないという方は適宜moduleを使ってください
$ go get -u goa.design/goa/v3/cmd/goa
作っていく
Design file を書く
プロジェクトを作り、moduleを初期化します。
$ mkdir -p my-project/design $ cd my-project/ $ export GO111MODULE=on $ go mod init # edit design/design.go
後半でDockerも使うのでDocker環境も適宜用意してください。
Design file というインタフェースを定義するファイルを記述します。 今回はリクエストをそのまま返すechoサーバを実装します。 パラメータは
name
: URLのパスとして受け取る (required)age
: クエリストリングで受け取る (required)
です。 POSTでリクエストボディを受け取ったり、デフォルト値を設定したり、フォーマットを指定したりなど便利な機能はたくさんありますが今回は全体の流れを説明するために割愛します。(今後書いていきたい)
DSLの文法自体も詳しくは解説しませんがなんとなく雰囲気で読めると思います。(押し付け)
詳しくは公式のリファレンスを参照してください。v2
とv3
でかなり書き方が違うのでどちらのバージョンかをしっかり確認してください。
dsl - GoDoc
意外とサンプルが転がっていないので(特に日本語サイトはない)公式のExampleも参照するといいと思います。 GitHub - goadesign/examples: Examples for goa showing specific capabilities
design/desing.go
package design import . "goa.design/goa/v3/dsl" // API describes the global properties of the API server. var _ = API("echo", func() { Title("Echo Service") Description("This is HTTP echo service") Server("echo-server", func() { Host("localhost", func() { URI("http://0.0.0.0:8088") }) }) }) // Service describes a service var _ = Service("echo-service", func() { Description("Echo your request") // Method describes a service method (endpoint) Method("echo-get", func() { // define request payload Payload(func() { // Attribute describes an object field Attribute("name", String, "Your name") Attribute("age", Int, "Your age") // Both attributes must be provided when invoking "add" Required("name", "age") }) // define response data type Result(String) // HTTP describes the HTTP transport mapping HTTP(func() { // Requests to the service consist of HTTP GET requests GET("/name/{name}") Param("age") // Responses use a "200 OK" HTTP status // The result is encoded in the response body Response(StatusOK) }) }) })
コードを生成する
Design file をもとにサーバの枠となるファイルを生成します。
# goa gen <MODULE_NAME> $ goa gen github.com/kaito2/my-project/design gen/echo_service/client.go gen/echo_service/endpoints.go gen/echo_service/service.go gen/http/cli/echo_server/cli.go gen/http/echo_service/client/cli.go gen/http/echo_service/client/client.go gen/http/echo_service/client/encode_decode.go gen/http/echo_service/client/paths.go gen/http/echo_service/client/types.go gen/http/echo_service/server/encode_decode.go gen/http/echo_service/server/paths.go gen/http/echo_service/server/server.go gen/http/echo_service/server/types.go gen/http/openapi.json gen/http/openapi.yaml # goa example <MODULE_NAME> $ goa example github.com/kaito2/my-project/design cmd/echo_server-cli/http.go cmd/echo_server-cli/main.go cmd/echo_server/http.go cmd/echo_server/main.go echo_service.go $ tree . . ├── cmd │ ├── echo_server │ │ ├── http.go │ │ └── main.go │ └── echo_server-cli │ ├── http.go │ └── main.go ├── design │ └── design.go ├── echo_service.go ├── gen │ ├── echo_service │ │ ├── client.go │ │ ├── endpoints.go │ │ └── service.go │ └── http │ ├── cli │ │ └── echo_server │ │ └── cli.go │ ├── echo_service │ │ ├── client │ │ │ ├── cli.go │ │ │ ├── client.go │ │ │ ├── encode_decode.go │ │ │ ├── paths.go │ │ │ └── types.go │ │ └── server │ │ ├── encode_decode.go │ │ ├── paths.go │ │ ├── server.go │ │ └── types.go │ ├── openapi.json │ └── openapi.yaml ├── go.mod └── go.sum
ロジックを書く
リクエストを処理するために編集するファイルはプロジェクトルートディレクトリにある echo_server.go
のみです。
echo_server.go
package echo import ( "context" "log" echoservice "github.com/kaito2/my-project/gen/echo_service" ) // echo-service service example implementation. // The example methods log the requests and return zero values. type echoServicesrvc struct { logger *log.Logger } // NewEchoService returns the echo-service service implementation. func NewEchoService(logger *log.Logger) echoservice.Service { return &echoServicesrvc{logger} } // EchoGet implements echo-get. func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) { s.logger.Print("echoService.echo-get") return }
gRPCでスタブを生成した事がある人は見慣れたコードかもしれません。
このEchoGet()
関数にロジックを記述します。
以下のように記述することでリクエストのパラメータを取得できます。
... // EchoGet implements echo-get. func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) { s.logger.Println("echoService.echo-get") s.logger.Printf("Request name: %s", p.Name) s.logger.Printf("Request age : %d", p.Age) return fmt.Sprintf("Your name: %s, Your age: %d", p.Name, p.Age), nil } ...
実行する
実際にサーバを起動してみます。
$ go build ./cmd/echo_server $ ./echo_server [echo] 21:19:42 HTTP "EchoGet" mounted on GET /add/{name} [echo] 21:19:42 HTTP server listening on "localhost:8088"
別のターミナルからアクセスすると…
$ curl "http://localhost:8088/name/kaito2?age=13" "Your name: kaito2, Your age: 13"
無事動作しました。
ちなみに Required
に指定したパラメータを記述しないと400
が返ります。
$ curl "http://localhost:8088/add/kaito2" {"name":"missing_field","id":"XXXXXXX","message":"\"age\" is missing from query string; invalid value \"\" for \"age\", must be a integer","temporary":false,"timeout":false,"fault":false}
上で書いたクライアントも生成されているので利用します。
$ go build ./cmd/echo_server-cli/ $ ./echo_server-cli --help ./echo_server-cli is a command line client for the echo API. Usage: ./echo_server-cli [-host HOST][-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags] -host HOST: server host (localhost). valid values: localhost -url URL: specify service URL overriding host URL (http://localhost:8080) -timeout: maximum number of seconds to wait for response (30) -verbose|-v: print request and response details (false) Commands: echo-service echo-get Additional help: ./echo_server-cli SERVICE [ENDPOINT] --help Example: ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796 $ ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796 "Your name: Quam dolores., Your age: 8576636692946696796"
デフォルトでsampleの値が用意されているので変な値になっていますが(笑)
クライントで動作確認もできます(curl
でいい気もするが)
Dockerizeする
せっかく(?)なのでDockerizeしましょう。
Dockerfile
# Dockerfile References: https://docs.docker.com/engine/reference/builder/ FROM golang:1.11.12 as builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/echo_server FROM alpine:3.10.1 WORKDIR /app EXPOSE 8080 COPY --from=builder /app/main . CMD ["./main"]
プロダクションで alpine ってどうなんだろう…結構使われてるのかな…
実行してみます。
$ docker login $ docker build . -t my-project-image $ docker run --rm -p 8088:8088 my-project-image [echo] 12:48:25 HTTP "EchoGet" mounted on GET /name/{name} [echo] 12:48:25 HTTP server listening on "0.0.0.0:8088"
いい感じに実行できました!!
まとめ
という流れで簡単にサーバができました!! これをCIでビルドしてCloud Runにデプロイできるようにして…と夢が広がります!!
DSLに関しては少し癖があり、色々動作検証をしてみたのでまた記事にしたいと思います。
dockerコマンド無しでコンテナを動かしてみる
以下のブログを参考にコンテナ周りの技術についての学習も兼ねてDockerコマンドなしでコンテナを動かす。
ref. Container Runtimes Part 2: Anatomy of a Low-Level Container Runtime - Ian Lewis
Dockerコマンドを使わずにコンテナを走らせる。(ファイルシステムの用意では使う)
(後始末が面倒なのでVirtualboxやクラウドサービスのVMなどを推奨)
0. 環境
- Google Compute Engine
- Machine Type:
f1-micro
- OS:
Ubuntu 18.04 LTS
- Machine Type:
- Cloud Shellからアクセス
1. ファイルシステムのセットアップ
まずはファイルシステムをセットアップする。
(早速Dockerコマンドという感じですが、ファイルの雛形を用意するためでコンテナの実行には使わないのでご容赦ください。)
今回はbusybox
のDockerイメージからファイルとディレクトリを抽出する。
$ sudo su # (1) $ CID=$(docker create busybox) # (2) $ ROOTFS=$(mktemp -d) # (3) $ docker export $CID | tar -xf - -C $ROOTFS
(1) 補足
docker create
は書込み可能なコンテナレイヤを作成する。- つまり「コンテナはは作成するが runはしない」状態
- 通常の
docker container ls
では作成したコンテナは作成されないが、-a
オプションで出力できる。 - コンテナIDを標準出力に表示する。
Example
$ docker create busybox 64aad11d274cec50b6e03f4422d50e01bfec2d894692934cd6965afd693a7c49 $ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 64aad11d274c busybox "sh" 8 seconds ago Created silly_swartz
(2) 補足
ref. mktemp(1): create temporary file/directory - Linux man page
- 一時ファイルまたはディレクトリを作成するコマンド
-d
オプションなのでディレクトリが作成される。- 引数で
X
を3個以上渡すとその文字数分ランダム文字列のファイルまたはディレクトリが作成される。 - 指定しない場合は
/tmp/tmp.XXXXXXXXXX
になる。
Example
$ mktemp /tmp/tmp.ZTZHxN7V7B $ mktemp -d /tmp/tmp.Svu9ewej2o $ mktemp XXXX t5rk
(3) 補足
ref. Dockerイメージのexportとimport( save, load ) - Qiita
docker export <CONTAINER_ID>
はコンテナをtarファイルにする。tar -xf - -C $ROOTFS
にリダイレクトしてtarファイルを展開-x
: eXtract(展開)-f
: fileから展開(デフォルトはテープデバイスらしい)-C <DIR>
:<DIR>
に移動してから実行
2. コントロールグループを作成
$ apt install cgroup-tools # (4) $ UUID=$(uuidgen) # (5) $ cgcreate -g cpu,memory:$UUID # (6) $ cgset -r memory.limit_in_bytes=100000000 $UUID $ cgset -r cpu.shares=512 $UUID # (7) $ cgset -r cpu.cfs_period_us=1000000 $UUID $ cgset -r cpu.cfs_quota_us=2000000 $UUID
(4) 補足
ref. uuidgen(1) - Linux manual page
- UUIDを生成する
- UUIDは結構奥が深いらしいが、今回はメインじゃないので割愛。(Universally unique identifier - Wikipedia)
(5) 補足
refs.
そもそも cgroup
とは
ref. 第1章 コントロールグループについて (cgroup) - Red Hat Customer Portal
cgroup(control group)を用いることでCPU時間、システムメモリ、ネットワーク帯域などのリソースをシステムで実行中のユーザ定義タスクグループ(プロセス)に割り当てることができる。
コマンドについて
cgcreate
でコントロールグループ作成後、以下のコマンドで確認できる。
$ lscgroup | grep $UUID cpu,cpuacct:/*86aec293-c8c7-47f4-8158-817d978570ea* memory:/*86aec293-c8c7-47f4-8158-817d978570ea*
(6) 補足
ref. cgroupを使ってCPUとメモリの割り当てを制限する - 偏った言語信者の垂れ流し
(5)
で作成したコントロールグループの制限を設定する。
以下のコマンドで確認できる。
# cgset で設定した値になっている $ cat /sys/fs/cgroup/cpu/$UUID/cpu.shares 512 $ cat /sys/fs/cgroup/memory/$UUID/memory.limit_in_bytes 99999744
- Ubuntuでは
/sys/fs/cgroup
以下にcgroupの情報が格納される cpu.shares
はスケジューリングの優先度の値limit_in_bytes
はそのままメモリのバイト単位の制限
(7) 補足
ref. リソース管理ガイド - Red Hat Customer Portal
cpu.cfs_quota_us
- cgroupによるCPUリソースの再割り当てが行われる間隔(μs だが、ここでは
us
と表記されている) 1,000 < cpu.cfs_quota_us < 1,000,000
- cgroupによるCPUリソースの再割り当てが行われる間隔(μs だが、ここでは
cpu.cfs_period_us
- cgroup内のすべてのタスクが
cpu.cfs_quota_us
間に利用できるCPU時間(μs) - 特定のタスクが使い切った場合はcgroup内の他のタスクはスロットリングされ、次の期間まで実行ができない
- Ex) cgroupが1秒あたり0.2秒間単一ののCPUにアクセスするためには以下のように設定する
-
cpu.cfs_period_us = 1,000,000
-
cpu.cfs_period_us = 200,000
-
- cgroup内のすべてのタスクが
CFS (Completely Fair Scheduler)とは
ref. Linux カーネル 2.6 Completely Fair Scheduler の内側
CFS の背後にある主な概念は、タスクに与えるプロセッサー時間のバランス (公平性) を維持するためのものです。つまり、それぞれのプロセスには公平にプロセッサー時間が与えられるようにしなければなりません。
3. コンテナ内でコマンドを実行
1.
2.
で作成したファイルシステムとコントロールグループでコンテナを作成し、その中でコマンドを実行する。
# (8) $ cgexec -g cpu,memory:$UUID \ unshare -uinpUrf --mount-proc \ sh -c "/bin/hostname $UUID && chroot $ROOTFS /bin/sh" # (9) / # echo "Hello from in a container" Hello from in a container / # touch hoge.txt / # exit
(8) 補足
cgexec
- ref. cgexec(1): run task in given control groups - Linux man page
cgexec [-g <controllers>:<path>] [—sticky] command [arguments]
-g
オプションでコントロールグループを指定command
はunshare
以降を呼び出し
unshare
- refs.
unshare [options] [program [arguments]]
- オプション
-uinpUrf
-u
: UTS namespace を共有しない- システム識別子である(
nodename
,domainname
を独立させる)
- システム識別子である(
-n
: network namespace を共有しない- 独立した仮想ネットワークを構築する
-i
: IPC(Inter-Process Communication:プロセス間通信) namespace を共有しない- 異なるIPC namespace の共有メモリやセマフォにアクセスできないようにする
-p
: PID(Process ID) namespace を共有しない- 別空間のPIDと重複を許す
-U
: user namespace を共有しない- 別空間の
UID/GID
の重複を許す
- 別空間の
-r (--map-root-user)
: 新しいNameSpaceのrootユーザを起動したときのユーザIDに対応させる-f (—fork)
:unshare
で実行するプログラムを現在のプロセスの子ではなく、unshare
の子プロセスとしてForkする--mount-proc
: 実行直前にprocファイルシステムをmountpoint(デフォルトは/proc
)にマウントする- PID namespaceを新たに作成するのに役立つ
- オプション
/bin/hostname
- hostnameを変更する
- linux でのデフォルトは
localhost.localdomain
らしい
chroot
- ref. chroot(1) - Linux manual page
- ルートディレクトリを変更して
COMMAND
を実行 chroot [OPTION] NEWROOT [COMMAND [ARG]…]
実行後にマウントしていた$ROOTFS
以下を確認するとhoge.txt
がある。
$ ls $ROOTFS bin dev etc hoge.txt home proc root sys tmp usr var
(一応)クリーンアップ
$ cgdelete -r -g cpu,memory:$UUID $ rm -r $ROOTFS
DNS再入門
ScrapBoxのメモを整形して貼ります。 なので僕が理解が薄い部分を調べたりメモしたり引用したりしているだけなので体系的な知識は得られないかと思います。
本編
Google Cloud DNS を使って独自ドメインを取得ししつつDNSについて学んでいきます。
Google Cloud DNSでIPアドレスとドメイン名を紐付ける - Qiita
まずはコレにしたがってやっていく
Cloud DNS 登録
事前にしておく
お名前.com 等でドメインを取得
UIとかいろいろ変わっていくと思うので公式サイトを参照されたし
(お名前ドットコム)
DNSゾーン
DNSゾーンとは
あんまり詳しく知らなかった。
ref. DNSサーバとゾーン
一つのネームサーバが管理する範囲をDNSゾーンっていうみたい
サブドメインを移譲したらそのサブドメインは別のDNSゾーンになる みたいな?
ref. @IT > Master of IP Network > DNS Tips > ゾーンとは
ゾーンとは、ネームサーバがドメインを管理する範囲です
DNSゾーン作成
ネットワークサービス > Cloud DNS
に移動
「DNSゾーンの作成
」> 入力 > 作成
NSレコードとSOAレコードがデフォルトで作成されています
ぼく:「NSレコード? SOAレコード??」
NSレコード
ざっくりいうと
管理を委託しているDNSサーバさんの名前が書いてあるんだな~
らしい
ref. NSレコードとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
それっぽく書いてあるスライド: https://jprs.jp/tech/material/iw2012-lunch-L3-01.pdf
SOAレコード
Start Of Authority recordの略らしい
権威の開始を示す
ref. SOAレコードとは何ですか。|よくあるご質問|法人向けクラウドサービスのbit-drive
ゾーン(管理する範囲)に関する情報が書いてあるんだな~
ref. SOAレコードとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典SOAレコードの1番目に定義されるns1.example.com.は、そのドメインのプライマリDNSサーバを指定します。
通常、そのゾーンのNSレコードで定義されたホスト名のいずれか1台と一致するかと思います。
ref. DNS - SOAレコードの最初の部分の意味がわかりません。|teratail
Aレコードの追加
AレコードはIPアドレスとドメイン名を対応させるレコードです。
これはわかりやすい
www.example.com => 123.123.123.123
みたいな。
レジストラの登録
Cloud DNS で登録したドメインのDNSサーバを参照されるように上のレジストラのネームサーバに登録する必要がある
レジストリとは、世界中で使われている「.com」や「.net」「.jp」などのトップレベルドメイン毎に1つのみ存在する一番上位の機関をいいます。
レジストラ
レジストラはドメインを登録する事業者をいいます。
ref. レジストリとレジストラ|ドメインの基礎知識|名づけてねっと
- data
- ns-cloud-d1.googledomains.com.
- ns-cloud-d2.googledomains.com.
- ns-cloud-d3.googledomains.com.
- ns-cloud-d4.googledomains.com.
これらのns サーバーをお名前.com等のレジストラに登録する。
ネームサーバーの変更
>他のネームサーバーを利用
で更新可能
移譲完了
GAEのレイテンシがつらいという話
TL;DR
GAEは…
- (Google App Engine)のレイテンシが思っていたより大きい
- Keepaliveヘッダーを無視する
- API サーバーとして利用するにはあまり向いていなさそう(感想)
経緯
既存のシステムの一部をAPIとして切り出そうという作業をしていました。 そこで目をつけたのがGCP(Google Cloud Platform)のGAE(Google App Engine)です。(楽だし) 他のシステムから呼ばれることもあってレスポンスタイムの要求がかなりシビアなので計測しておくことにしました。 計測対象のアプリ自体はただの echo server です。
環境
region: asia-northeast1 environment: standard language: go
計測
ab
コマンドを用いて計測しました。
$ ab -c 1 -n 100 <URL> ... Concurrency Level: 1 Time taken for tests: 25.657 seconds Complete requests: 100 Failed requests: 0 Total transferred: 31412 bytes HTML transferred: 4100 bytes Requests per second: 3.90 [#/sec] (mean) Time per request: 256.571 [ms] (mean) Time per request: 256.571 [ms] (mean, across all concurrent requests) Transfer rate: 1.20 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 163 198 16.5 198 250 Processing: 49 58 11.5 54 150 Waiting: 47 58 11.6 53 150 Total: 219 256 21.0 257 360 Percentage of the requests served within a certain time (ms) 50% 257 66% 262 75% 270 80% 271 90% 281 95% 289 98% 311 99% 360 100% 360 (longest request)
300ms 程度のレスポンスタイムとなりました。
さらに httpstat
コマンドを使って内訳を見てみると。
$ httpstat <URL> ... DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer [ 4ms | 61ms | 148ms | 49ms | 0ms ] | | | | | namelookup:4ms | | | | connect:65ms | | | pretransfer:213ms | | starttransfer:262ms | total:262ms
TLS Handshake
がかなりの時間を占めていました。
他のシステムからの利用であることもありぱっと思いつくのは、Keep-Alive なので試してみます。
$ ab -c 1 -n 100 -k <URL> ... Concurrency Level: 1 Time taken for tests: 6.330 seconds Complete requests: 100 Failed requests: 0 Keep-Alive requests: 100 Total transferred: 33804 bytes HTML transferred: 4100 bytes Requests per second: 15.80 [#/sec] (mean) Time per request: 63.302 [ms] (mean) Time per request: 63.302 [ms] (mean, across all concurrent requests) Transfer rate: 5.21 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 2 20.0 0 200 Processing: 51 61 5.2 60 81 Waiting: 51 61 5.2 60 81 Total: 51 63 20.5 60 260 Percentage of the requests served within a certain time (ms) 50% 60 66% 61 75% 63 80% 64 90% 68 95% 76 98% 81 99% 260 100% 260 (longest request)
いい感じ? httpstat
で確認してみると
$ httpstat -k <URL> ... DNS Lookup TCP Connection Server Processing Content Transfer [ 4ms | 57ms | 60ms | 0ms ] | | | | namelookup:4ms | | | connect:61ms | | starttransfer:276ms | total:276ms
なんか数値が壊れている…
本当に Keep-Alive が効いているのか確認してみます。
$ curl -s -I -k <URL> HTTP/2 400 content-type: text/plain; charset=utf-8 x-cloud-trace-context: XXXXXXXXXXXXXX content-length: 30 date: Sun, 19 May 2019 13:54:14 GMT server: Google Frontend alt-svc: quic=":443"; ma=2592000; v="46,44,43,39"
Connection: keep-alive
がない…
ぐぐってみると…
The following headers are removed from the request: ... Keep-Alive ...
ref. Request Headers and Responses
そういう話か…
検証はしてないですが、ab
, httpstat
コマンドは-k
オプションを付けると 「Keep-Alive」が効いている前提で計測しているだけで実際にKeep-Aliveが効いているかの確認はしていないみたいです。
ちなみに Apache JmeterではしっかりコネクションタイムがKeep-Aliveを有効にしても計測されていました。
コンテナの思想的にもKeep-Aliveを無視するというのはあっている気がするが、これに気づくのにかなり時間を使ってしまったので頭の片隅においておきたいです。
レイテンシが99%ileで360になるのはつらいと思うんだが、みんなどうやって解決しているのか気になる…
おまけ
計測の中でGCEを立てて httpstat
で計測をした結果があったので載せます。
DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer [ 4ms | 1ms | 10ms | 5ms | 0ms ] | | | | | namelookup:4ms | | | | connect:5ms | | | pretransfer:15ms | | starttransfer:20ms | total:20ms
当たり前のことなんですが内部ネットワークが爆速で一気にすべてGCPに移行したい気持ちになりました。(そうはいかない。)