Eng (なりたい)

はやく エンジニア になりたい

SOPSを試してみる

クレデンシャルなどの情報もGitで管理したい…!! とくに GitOps などを採用しているとそう感じるケースも多いと思います。(僕はよく思っていました) そんなときの解決方法の一つであるSOPSを試してみました。

↓の本で紹介されていて知りました。ツール自体はかなり前からあるみたい

www.oreilly.co.jp

動作確認した環境は 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週間お休みだったので、自分を甘やかして「自分の興味のある本を片っ端から読み散らかして良い」という期間にしました。

まったくもって有益ではないですが、読書記録として感想を書いておこうと思います。

ガッツリ読んだ本

思考する機械コンピュータ

論理回路からAIまで、コンピュータというコンセプトの全体感を楽しみながらつかめる良書だと思います。

各章に出てくる事例も面白いものが多く、知的好奇心が刺激されまくりでした。大学時代の自分に勧めたい一冊。

コンピュータシステムの理論と実装

nand2tetrisでおなじみの本ですね。 NANDとDFF回路を手持ちにOS(と本書で定義されるソフトウェア)を作っていくという流れです。 各章ではどう実装するか、ではなくどういった機能を実装すべきかという仕様(と実装のヒント)が示されます。

最適化などはもちろん考慮されておらず、最低限の機能を対象としていますがそれでもこの内容を1冊で学べるので個人的にはとても良い本だおともいました。

コンパイラの章は僕の知識不足で実装しきれなかったので、この機会に勉強してリベンジしたいです。おすすめの本などがありましたら教えて下さい!!

オペレーティングシステムの仕組み

ひとつ下の 作って理解するOS を読みはじめたところ、「OSなんもわかってないじゃん…」という気持ちになったので購入して通読しました。 比喩によって説明して「結局どういうこと?」みたいな部分がなかったのが個人的にとても良いと思った点です。

「メモリとは、1バイトのデータを格納できる箱が並んだようなものであり、それぞれの箱にはアドレス(address)という通し番号のようなものがついている」というイメージで説明されてきたはずである。これはメモリに対する素朴なイメージでしかない。

そのレベルで教わってきていたのでとても助かりました笑

作って理解するOS

作って理解するOS  x86系コンピュータを動かす理論と実装

作って理解するOS x86系コンピュータを動かす理論と実装

  • 作者:林 高勲
  • 発売日: 2019/09/26
  • メディア: 単行本(ソフトカバー)

突発的にOSに興味が出たので読み始めた本。まだ8割ほどしか読めていないです(重い…)。

全体として、前半はコンピュータのしくみ・ハードウェアの基礎・CPU命令の仕様 と続き、後半でいざOS実装!!という流れです。

前半は座学っぽくなっているので若干読み進めるのに根気がいりますが、復習としてとても良かったです。 ちなみに、開幕は2進数の説明からはじまります。2進数知らない状態からOSを実装できる状態までもっていくってすごすぎでは…。

今は後半の途中なのですが、全てアセンブリによる実装なので噛み砕いていくのに時間がかかります。 ただその分挙動の解釈に間違いがうまれにくいかなと思いました。

完走がんばります〜

手を出し始めた本

コンピュータの構成と設計

パタヘネ。

コンピュータ何もわからなくなったので復習し始めました。 まだ1章ですが、読んでて楽しいのでちょくちょく読み返していきます。

計算機プログラムの構造と解釈

実際には非公式日本語訳のほうがわかりやすいとのことだったので下のPDFを読んでいます。 N回挫折しているので今回は大学時代の友人を巻き込んで毎週読み合わせをしています。 ありがたい。

github.com

正直 やっと2章までたどり着いた程度なので感想的なものは書けませんが、数学的な能力のほうが不足しているせいで演習問題や例題の理解にビビるほど時間がかかります…。みんなで頑張ろうな!!

感想

普段の業務では直接的には活きなさそうだけど興味があった分野の本を読み漁りました。 友人とのSlackで僕が理解した内容をひたすらつぶやき続けるチャンネルを作ってブツブツ言いながら2週間を過ごしました(リアクションしてくれたみんなありがとう…)。 これで僕もつよつよエンジニアに近づいた!! という感覚は皆無ですが、こういう勉強はこれからも時間を見つけて続けていきたいなと思います。

Firebase hosting + Cloud Run の機能を試していくよ!!

Firebase hosting + Cloud Run で Microservices のL7ロードバランシングができるみたいだったので試してみました!!

勢いでMicroserviceのサンプルAPIを作る

goa とかでやるとパッとできるよ(趣味)

goa.design

本編

ツールインストール

$ 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.

以下が主な選定理由です。

  • golangベースのDSLで書きやすい(※個人の感想です)
  • DSLによってサーバの枠組みが生成される
    • リクエストのvalidationもしてくれる(少し癖があるが)
    • リクエストの構造体を受け取って処理するロジックのみを書くだけ
  • DSLからSwaggerが生成される
  • クライアント用のコードも生成される(後述)

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の文法自体も詳しくは解説しませんがなんとなく雰囲気で読めると思います。(押し付け)

詳しくは公式のリファレンスを参照してください。v2v3でかなり書き方が違うのでどちらのバージョンかをしっかり確認してください。 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"

いい感じに実行できました!!

まとめ

  • goaでAPIサーバをつくる
    • Design file を記述
    • Design file をもとにコード生成
    • リクエスト処理のロジックを記述
  • Dockerで実行

という流れで簡単にサーバができました!! これを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
  • 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) 補足

ref. http://docs.docker.jp/engine/reference/commandline/create.html create — Docker-docs-ja 17.06.Beta ドキュメント

  • 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

(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
  • 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

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) 補足

実行後にマウントしていた$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ゾーンとは

あんまり詳しく知らなかった。

Image

ref. DNSサーバとゾーン
一つのネームサーバが管理する範囲をDNSゾーンっていうみたい
サブドメインを移譲したらそのサブドメインは別のDNSゾーンになる みたいな?

Image
ref. @IT > Master of IP Network > DNS Tips > ゾーンとは

ゾーンとは、ネームサーバがドメインを管理する範囲です

DNSゾーン作成

ネットワークサービス > Cloud DNS に移動
DNSゾーンの作成」> 入力 > 作成

Image

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に移行したい気持ちになりました。(そうはいかない。)