Go の Web サーバで Response Body と Request Body をロギングする
TL;DR
- こんな感じの middleware を実装すれば Request Body も Response Body もロギングできるようになる
// loggingWriter http.ResponseWriter の wrapper type loggingWriter struct { http.ResponseWriter multi io.Writer } // Write はmultiwriter に書き込むことで response body を記録 func (w *loggingWriter) Write(b []byte) (int, error) { return w.multi.Write(b) } // extraLog なんかいい感じの構造体に突っ込んどく // Log は極力 Json 化しておいたほうがいい (入門 監視 等の書籍に書いてある) type extraLog struct { RequestBody interface{} `json:"requestBody"` ResponseBody interface{} `json:"responseBody"` } // loggerMiddleware negroni 等を利用した middleware の実装 func loggerMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { var logBuf bytes.Buffer multiWriter := io.MultiWriter(w, &logBuf) rspWriter := &loggingWriter{w, multiWriter} // request body のロギング reqBody, err := ioutil.ReadAll(r.Body) if err != nil { log.Warnf("Failed to read request body: %s\n", err) } r.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // 次のハンドラでも使えるように // loggingWriter を渡してあげる next(rspWriter, r) // あとはこれをいい感じのロギングライブラリで表示してあげるだけ extra := extraLog{ RequestBody: string(reqBody), ResponseBody: logBuf.String(), } }
背景
最近、仕事で Go を書くことが多く、最近は API Server の実装を行っていることが多いです。
その中で、サーバログとして Request Body と Response Body を出力することがあったので備忘録的にまとめておきます (セキュリティ的な観点もあるので、Request Body と Response Body をログ出力するのはあまり良しとは言えませんが...)
Request Body は割とすんなり扱えたのですが、Response Body を Middleware を挟んでログ出力するのは少し工夫が必要だったので、まとめておきます
やっていること
http.ResponseWriter
の Interface は、 以下のような定義になっています、
type ResponseWriter Interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int) }
上記で作成した例は、Writeを Overwrite する形で、ログ出力用の MultiWriter
に書き込むようにしています
また、middlewareで r.Body
を読み取ってしまうと、次の Handler に渡せなくなってしまうため、ioutil.NopCloser
を利用しています
Istio on GKEでハマったこと
備忘録として書き留めます。コメントありましたらよろしくお願いいたします。
TL;DR
- Terraformで、default nodeを削除してクラスタを立てると0 nodeになる
- promsd (defaultのprometheus) はいくつかexporterがなく、かつReconcile設定が入っているのでConfigmapを書き換えられない
- kialiやjaegerを入れる場合は、IstioのHelm Chartからの差分を手動でManifestに設定してapplyする必要がある
Istio on GKEとは?
Istio on GKEは、Google Kubernetes EngineでIstio有効にするためのアドオンです。これを利用することによって、わざわざクラスタにIstioをinstallしなくても、Istioが有効なクラスタが立ち上がります
通常Istioを入れる場合はHelm Chartを利用してInstallする必要があります
ただ、Helmで入れない場合、いくつかハマりどころがあったのでそれを紹介したいと思います
default nodeを利用しないとPodが立ち上がらない
基本、GKEの構成管理はTerraformを利用することが多いのだが、ここで一つトラップがあった。
現在、柔軟なリソース管理のために、いくつかリソースの異なるNodeを利用してPodのスケジューリングを行うことができる。
その際、デフォルトのnodepoolを使わずにクラスタを立てる方法があり、terraformだと以下のように記述ができる
しかし、この方法でClusterを作成すると、後から作ったNodeが0Nodeとなって、Podがスケジューリングできない現象に遭遇した。エラーメッセージは、 Increase maximum size limit for autoscaling in one or more node pools that have autoscaling enabled.
と表示される
この問題は、Default node poolを使ってクラスタを立てることで解決した。後からNode poolを足していくことは問題なさそう
promsdの設定が書き換えられない
Istio on GKEでIstioを入れた場合、デフォルトでpromsd(混乱を割けるためにpromsdという名前になったらしい https://cloud.google.com/istio/docs/istio-on-gke/release-notes )
ただし、このpromsdはトラップがあって、Helmで入るPrometheusと設定が異なっているうえに、
labels: addonmanager.kubernetes.io/mode: Reconcile
が設定されたconfigmapで、後からPrometheusの設定を変更したり、exporterを追加したりすることができなかった
解決法としては、Istio on GKEの公式にも書かれている方法での独自PrometheusのInstallで落ち着いた
監視系の追加Install
KialiやJaegerといった監視や可視化のComponentsはIstio on GKEでは入っていない。 これらを入れるには、IstioのHelm Chartから上記Adaptersの差分を取り出してきて、手動でmanifestの作成を行う必要がある
どんな感じで設定するかというと、例えば、Kialiを入れたい場合、
curl -L https://git.io/getLatestIstio | ISTIO_VERSION=${CHART_ISTIO_VERSION} sh - helm template --set kiali.enabled=false --namespace istio-system istio-${CHART_ISTIO_VERSION}/install/kubernetes/helm/istio > off.yaml helm template --set kiali.enabled=true --namespace istio-system istio-${CHART_ISTIO_VERSION}/install/kubernetes/helm/istio > on.yaml diff -u off.yaml on.yaml > kiali.yaml
といった感じで、Installする場合としない場合のyamlのdiffを取って、それらを手動で整形していくという形になる。これは運用事故りそうだなと感じてはいるが、現状はこの方法が一番ベストなはず
Grafana+PrometheusでKubernetesのリソース監視(Helmを利用した環境設定について)
TL;DR
- PrometheusもGrafanaも,helmを利用することで簡単にInstallできる
- Helmを利用したInstallスクリプトまとめました
- GrafanaのDashboardはTerraformによる管理や,DashboardのJson化ができるので,監視体制やDashboardを Infrastructure as Code として管理できる
Helmを利用したInstallについて
PrometheusもGrafanaも,k8sの監視ツールとしてはかなり有名なので,名前を聞いたことのある人も多いと思います 今回は,備忘録も兼ねてHelmを利用したk8sへの導入をまとめたいと思います.
helmを利用したInstallについては,こちらを参考にしました
なお,本記事は,クラスター及びクライアントでのHelmのセットアップが完了しているものとします
PrometheusのInstall
まずは,helmのvaluesの確認です.
こちらにHelmで設定ができる項目が入っていますが,実際のYamlでの記法を確認するのであれば,
helm inspect values stable/prometheus > prometheus_values.yaml
を実行して,デフォルトのYamlを確認してみてください.
自分が設定しているvalues.yaml
は以下のとおりです
## Define serviceAccount names for components. Defaults to component's fully qualified name. ## serviceAccounts: alertmanager: create: false server: persistentVolume: ## Prometheus server data Persistent Volume size ## size: 10Gi ## Use a StatefulSet if replicaCount needs to be greater than 1 (see below) ## replicaCount: 3 statefulSet: ## If true, use a statefulset instead of a deployment for pod management. ## This allows to scale replicas to more than 1 pod ## enabled: true
alertmanager
をInstallしないのは,Grafana経由でのalertを行うため,必要がないからです
また,replicaとstatefulsetを設定することで,autohealing時にも取得していた値を継続させます.
実行scriptは,以下のように行っています
#!/bin/bash cd $(dirname $0) echo Tune shell options && { set -o errexit set -o nounset set -o xtrace } : Define Variables && { readonly PROJECT="${1:?}" readonly CLUSTER="${2:?}" readonly REGION="${3:?}" readonly NAMESPACE="${4:?}" readonly PROMETHEUS_CHART_VERSION=8.8.0 } : Set target project/cluser and tiller && { gcloud config set project "${PROJECT}" gcloud container clusters get-credentials ${CLUSTER} --region "${REGION}" kubectl config set-context $(kubectl config current-context) --namespace="${NAMESPACE}" helm init --client-only } : Deploy Prometheus using Helm && { helm upgrade --install \ --namespace="${NAMESPACE}" \ --version="${PROMETHEUS_CHART_VERSION}" \ -f="values.yaml" \ "prometheus" \ stable/prometheus }
解説をしていくと,
Set target project/cluster and tiller
に関しては,gcloudの設定と,clusterのcredentialsを取得し,helmの初期化を行っています.helmの初期化は,clusterにhelmを設定済みと仮定して,--client-only
での初期化を行っています
Deploy Prometheus using Helm
では,設定したvaluesに従って,prometheusをInstallしています.
ちなみに,Prometheusを外に出していないのは,Grafanaのみを外から見れるようにIngressを設定するためです.Prometheus自体を試したい場合は,kubectl port-forward svc/prometheus-server 3000:80
とport forwardingして,localhost:3000
にアクセスすることでクエリの実行が可能になります
GrafanaのSetupについて
GrafanaのHelmの設定項目は
github.com
こちらにありますが,Prometheusと同様に,helm inspect values stable/grafana > grafana_values.yaml
でYamlの記法を確認できます
自分が設定するものは以下のとおりです
rbac: namespace: true replicas: 3 ## Enable persistence using Persistent Volume Claims ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ ## persistence: enabled: true ## Pod annotations podAnnotations: # 秘密情報が入れ替わったときにpodを入れ替えるためにhash値をアノテーションしておく checksum/extra-secret: ${SECRET_CHECKSUM:?} ## Expose the grafana service to be accessed from outside the cluster (LoadBalancer service). ## or access it from within the cluster (ClusterIP service). Set the service type and the port to serve it. ## ref: http://kubernetes.io/docs/user-guide/services/ ## service: type: NodePort ingress: enabled: true annotations: kubernetes.io/ingress.global-static-ip-name: "static-ip" kubernetes.io/ingress.allow-http: "false" ingress.gcp.kubernetes.io/pre-shared-cert: "cert-name" path: /* hosts: - ${DOMAIN_URL} # Use an existing secret for the admin user. admin: existingSecret: "grafana-admin" grafana.ini: server: domain: ${DOMAIN_URL} root_url: https://${DOMAIN_URL} auth.github: enabled: true client_id: ${AUTH_GITHUB_CLIENT_ID:?} client_secret: ${AUTH_GITHUB_CLIENT_SECRET:?} scopes: user:email,read:org auth_url: https://github.com/login/oauth/authorize token_url: https://github.com/login/oauth/access_token api_url: https://api.github.com/user allow_sign_up: true allowed_organizations: organization_name auth.google: enabled: ture client_id: "${AUTH_GOOGLE_CLIENT_ID:?}" client_secret: "${AUTH_GOOGLE_CLIENT_SECRET:?}" scopes: https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email auth_url: https://accounts.google.com/o/oauth2/auth token_url: https://accounts.google.com/o/oauth2/token allowd_domains: domain.example.com allow_sign_up: true
Grafanaでは,ログイン認証に様々なauthを利用できますが,上記の設定ではGithubとGoogle認証を導入しています. 実際のkeyに関しては環境変数経由等で入れることを想定しています.導入すると,このようなログイン画面になります
Ingressの設定もHelmで行うことができます.certやstatic-ipは任意の環境でお願いします
grafanaのAdminユーザとパスワードはSecret経由で取得することが推奨されています
apiVersion: v1 kind: Secret metadata: name: grafana-admin namespace: ${NAMESPACE:?} type: Opaque data: admin-user: ${ADMIN_USER:?} admin-password: ${ADMIN_PASSWORD:?}
また,
## Pod annotations podAnnotations: # 秘密情報が入れ替わったときにpodを入れ替えるためにhash値をアノテーションしておく checksum/extra-secret: ${SECRET_CHECKSUM:?}
これを設定することで,例えばKeyやIDを入れ替えたときにPodを自動的に入れ替えるようにしています.Grafana.iniが更新されても,自動で再読込をするわけではないので,grafana.iniに設定するものも含めてhash値にしてannotationsに設定することで,設定を自動で更新できるようにしています
#!/bin/bash cd $(dirname $0) echo Tune shell options && { set -o errexit set -o nounset set -o xtrace } : Define Variables && { readonly PROJECT="${1:?}" readonly CLUSTER="${2:?}" readonly REGION="${3:?}" readonly NAMESPACE="${4:?}" # set secret env # readonly ADMIN_USER="${5:?}" # readonly ADMIN_PASSWORD="${6:?}" # readonly AUTH_GITHUB_CLIENT_ID="${7:?}" # readonly AUTH_GITHUB_CLIENT_SECRET="${8:?}" # readonly AUTH_GOOGLE_CLIENT_ID="${9:?}" # readonly AUTH_GOOGLE_CLIENT_SECRET="${10:?}" readonly GRAFANA_CHART_VERSION=2.2.1 } : Set target project/cluser and tiller && { gcloud config set project "${PROJECT}" gcloud container clusters get-credentials ${CLUSTER} --region "${REGION}" kubectl config set-context $(kubectl config current-context) --namespace="${NAMESPACE}" helm init --client-only } : Create Secrets && { sigil -p -f secrets.yaml \ NAMESPACE="${NAMESPACE}" \ ADMIN_USER="$(echo ${ADMIN_USER} | base64 -w0)" \ ADMIN_PASSWORD="$(echo ${ADMIN_PASSWORD} | base64 -w0)" \ | kubectl apply -f - --record } : Deploy Grafana using Helm && { sigil -p -f grafana_values.yaml \ DOMAIN_URL="${DOMAIN_URL}" \ AUTH_GITHUB_CLIENT_ID="${AUTH_GITHUB_CLIENT_ID}" \ AUTH_GITHUB_CLIENT_SECRET="${AUTH_GITHUB_CLIENT_SECRET}" \ AUTH_GOOGLE_CLIENT_ID="${AUTH_GOOGLE_CLIENT_ID}" \ AUTH_GOOGLE_CLIENT_SECRET="${AUTH_GOOGLE_CLIENT_SECRET}" \ SECRET_CHECKSUM=$(echo \ ${ADMIN_USER} \ ${ADMIN_PASSWORD} \ ${AUTH_GITHUB_CLIENT_ID} \ ${AUTH_GITHUB_CLIENT_SECRET} \ ${AUTH_GOOGLE_CLIENT_ID} \ ${AUTH_GOOGLE_CLIENT_SECRET} \ | sha256sum) \ | helm upgrade --install \ --namespace="${NAMESPACE}" \ --version="${GRAFANA_CHART_VERSION}" \ --values=- \ "grafana" \ stable/grafana }
sigilを使って,Yamlのkey等を環境変数を利用して書き換え,grafanaをInstallしています.
詳細なスクリプトは本記事TopのGithubリンクにまとめてあります.
また,promQLでの設定やalert, terraformを利用したIaCもまとめていこうと思います
Prometheusについての参考記事
Grafanaについての参考記事
社会人1年目を振り返って
今日で社会人1年目が終わるのでかんたんに振り返ろうと思います。ただのメモ書き
仕事について
自分はデータ解析者としてとある会社に新卒入社しました。もともとインターンをしていたというのもあり、会社自体の雰囲気は掴んでいたものの、インターンのときと異なる部署へ配属されたので、0からのスタートとなりました。 しかし、自分のやりたかったデータ解析から案件作成、分析から開発まで一通り自分で回すようなスタイルで仕事ができたおかげで、この1年で基本的なデータ活用施策の流れであったり、要所要所でのポイントなどを考えながら仕事をすることはできるようになりました。 また、自分の興味がエンジニアリングに向き始めたときに、勝手に分析とエンジニアリングの2足のわらじを履いていても誰も何も言わず自由にやれた部署であったので自由に仕事をすることはできたと思います。
また、いくつかのメディア取材を頂いたり、外部講演やイベントへのブース出展とデモアプリ等の作成、社内表彰での登壇など、露出の機会ももらえて1年間での学びは大きかったように思います。
ただ、この1年目での心残りは、もう少ししっかりやりたいことを明確にしたうえで仕事の優先順位を決めなかったため、タスク量が増えすぎてコントロールできなくなったり、保守的な考えに走って自ら周りを巻き込むような働き方ができなかったことが挙げられます。2年目は自分がやるべきこととやれること、やりたいことをはっきりさせた上で、よりスキル的な部分での学びのある仕事の仕方をしていきたいと思います。
個人について
入社してからというもの、休日も平日も仕事のことで頭がいっぱいになっていてプライベートをあまり楽しめていなかったりしたなと感じています。On Offの切り替えはしっかりした上で生産性高く仕事に取り組みたいです。 また、個人としての勉学という意味では1年目はあまりできなかった気がします。あくまで業務に必要な知識やスキルを業務内外でひたすら断片的に習得するといった感じで、体系的に何かを身につけるといった部分は弱かった気がします。 特に、個人としてのアウトプット量が学生の頃に比べてかなり落ちてしまいました。社内でのアウトプットを優先してしまっていたためというのと、学んだことを体系立てる作業を怠っていた結果だと思います。 個人としての目標を決めて、定期的にアウトプットを心がけて行きたいです。まずは月一くらいでしっかり何かしらのアウトプットを出せる学びを心がけます
来年度について
分析業務よりは、SRE的な仕事や、プロダクトを作るという部分をもっとやりたいなと感じたため、来年度はSREエンジニアとしてJob Changeをしようと思っています。 特に自分は、データサイエンティストやデータプランナーと呼ばれる人がもっとデータ活用をしやすくなるような仕組みづくりや基盤づくりをしていきたいと考えています。 この分野はこの1年でのハードスキルがあまり活かせないかもしれないですが、分析もエンジニアリングもできるからこそ、上記が実現できて、そのためのスキルを習得し利益貢献するために頑張っていきたいと思います。
Kubernetes完全ガイド読了
TL;DR
- Kubernetes初学者〜中級者には最適な本だと感じた
- 基本的なk8sの説明から、helm,Prometheus,Spinnaker, Istioといった周辺サービスまで一通り網羅されており、この本から更に学習を進められる
- 買ってよかった1冊。k8s学びたい人は買って損はない本だと思う
概要
- 最近、業務でk8sを触ることが多くなってきて、今までなんとなくで触ってきたので改めて1から勉強し直そうと考え、この本に出会った

Kubernetes完全ガイド (impress top gear)
- 作者: 青山真也
- 出版社/メーカー: インプレス
- 発売日: 2018/09/21
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
- 自分のインフラ周りの知識で言うと、Dockerをちょっと触ったことある、くらいのものだった
- もともとデータサイエンスをやるつもりで入社したが、データプロダクトのSREや基盤開発が楽しくなり、Jobチェンジ中といった感じの状況
この本のいいところ
この本には乗っていないこと
- k8s周辺のOSSの細かい説明やチュートリアル
- helmとかPrometheusとかそのあたりは、公式ドキュメントを読みながら進めたほうがいいかもしれない
- ただ、この本でもサラッとは紹介されており、どんな問題を解決するためのOSSなのかみたいなことはつかめるはず
- 2019/03/17現在、Istio on GKEがBetaになっており、この本のInstall方法ではなく、Istio-Enabledなclusterを利用して自分はIstioを試していた。実行自体は特に問題なかった
terraform google providerの2.0へのUpgradeでBigtableをふっ飛ばした話
TL;DR
bigtable_instance
とcontainer_node_pool
の記法が大きく変わったから、同じResource名を利用して、variableで環境分けている場合は注意しよう- plan結果をしっかり見て、v2へ上げたときにresourceの再作成が行われないようにしよう
概要
terraform-google-providerのv2が正式にリリースされました www.terraform.io github.com
これにともなって、いくつかのResource定義の記法が変更されており、特に注意しなければならない変更があります。
それは、google_container_node_pool
とgoogle_bigtable_instance
です
google_container_node_poolについて
こちらで大きく変わったのは、name_prefix
を利用できなくなったことです。
従来、以下のように記述していたものが
resource "google_container_node_pool" "example" { name_prefix = "example-np-" zone = "us-central1-a" cluster = "${google_container_cluster.example.name}" node_count = 1 node_config { machine_type = "${var.machine_type}" } lifecycle { create_before_destroy = true } }
prefixを利用する場合はrandom_id
というリソースから名前を生成する形がGuideに記述されています
variable "machine_type" {} resource "google_container_cluster" "example" { name = "example-cluster" zone = "us-central1-a" initial_node_count = 1 remove_default_node_pool = true } resource "random_id" "np" { byte_length = 11 prefix = "example-np-" keepers = { machine_type = "${var.machine_type}" } } resource "google_container_node_pool" "example" { name = "${random_id.np.dec}" zone = "us-central1-a" cluster = "${google_container_cluster.example.name}" node_count = 1 node_config { machine_type = "${var.machine_type}" } lifecycle { create_before_destroy = true } }
これによって、従来の記法で定義されていたNode_poolと、v2の記法で書かれたNode_poolが同一のものではなくなり、再作成となります。 回避方法は、Variable等を利用して、すでに作成されているnode_poolの名前をhard codingしてnameを定義する必要があります
google_bigtable_instance
これが非常に厄介で、Guideを引用すると
resource "google_bigtable_instance" "instance" { name = "tf-instance" cluster_id = "tf-instance-cluster" zone = "us-central1-b" num_nodes = 3 storage_type = "HDD" }
のような記法が
resource "google_bigtable_instance" "instance" { name = "tf-instance" cluster { cluster_id = "tf-instance-cluster" zone = "us-central1-b" num_nodes = 3 storage_type = "HDD" } }
このような変更となり、clusterのfield内に定義するようになりました。
これだけならまぁ、対応可能な範疇です。
しかし問題なのは、instance_type
を定義していた場合です。
例えば、v1のときに、以下のような記法で開発環境と本番環境を作成していたとします
# dev.tfvars bigtable = { instance_type = "DEVELOPMENT" num_nodes = "0" }
# prod.tfvars bigtable = { instance_type = "PRODUCTION" num_nodes = "3" }
### この2つを環境ごとに切り分けて読み込む variable bigtable { type = "map" default = { instance_type = "OVERWRITE" num_nodes = "OVERWRITE" } } resource "google_bigtable_instance" "instance" { name = "tf-instance" instance_type = "${var.bigtable["instance_type"]}" cluster_id = "tf-instance-cluster" zone = "us-central1-b" num_nodes = "${var.bigtable["num_nodes"]}" storage_type = "HDD" }
ところが、v2系ではDEVELOPMENT
の場合、そもそもnum_nodes
を定義してはいけません。以下、公式抜粋
num_nodes - (Optional) The number of nodes in your Cloud Bigtable cluster. Required, with a minimum of 3 for a PRODUCTION instance. Must be left unset for a DEVELOPMENT instance.
つまり、このように定義していた場合、devとprdでresourceを2つ定義しなければなりません。
# dev.tfvars bigtable = { instance_type = "DEVELOPMENT" }
# prod.tfvars bigtable = { instance_type = "PRODUCTION" num_nodes = "3" }
### この2つを環境ごとに切り分けて読み込む ### countを利用して、条件分岐させる variable bigtable { type = "map" default = { instance_type = "OVERWRITE" num_nodes = "OVERWRITE" } } resource "google_bigtable_instance" "instance" { count = "${var.bigtable["instance_type"] == "PRODUCTION" ? 1 : 0}" name = "tf-instance" instance_type = "${var.bigtable["instance_type"]}" cluster { cluster_id = "tf-instance-cluster" zone = "us-central1-b" num_nodes = "${var.bigtable["num_nodes"]}" storage_type = "HDD" } } # devはnum_nodesを定義しない resource "google_bigtable_instance" "instance_dev" { count = "${var.bigtable["instance_type"] == "DEVELOPMENT" ? 1 : 0}" name = "tf-instance" instance_type = "${var.bigtable["instance_type"]}" cluster { cluster_id = "tf-instance-cluster" zone = "us-central1-b" storage_type = "HDD" } }
同一resourceは定義できないので、devとprodで2つのresourceを定義します。
そうなると、resource名が変更として扱われ、同一のname
やcluster_id
だったとしても再作成となります。(これを見逃していてふっ飛ばしました。しっかりplan見ようねって話なのですが...)
上記の例の場合、prodはresource名を変更していないので、changed扱い、devは新規作成及び旧instance
の削除となります。
オレ的開発環境管理(Docker)
個人のPCの開発環境の管理ってどうしてますか?
Dockerを利用して複数人の開発環境を統一できたり、簡単に環境を再構築したりWebアプリのテストができたりと、Dockerの開発環境の恩恵を大いに受けています。
個人での開発や分析でも特にこの恩恵は強く、どのマシンでも、例えばPythonやRの分析環境を持ち歩いたり、jupyterのextensionも含めてimageにしてしまえば本当に分析環境をまるっと持ち出せるのが便利でずっとDocker使ってますが、しばしばこんな問題にあたりませんか?
MacとWindowsのvolume周りのコマンドが違ってつらい
例えば、Docker for Macでは、
docker run -v ./:/home/user/ <image>
のような指定で簡単にカレントディレクトリとコンテナのデータ共有ができると思いますが、Docker for windowsだとどうでしょうか。ご存知の方も多いでしょうが、これでは動きません。Windows(cmd)でカレントディレクトリの共有をしようとすると、基本的にフルパスを指定します(C:hogehogeみたいな感じで)
これ、めちゃくちゃ不便ですよね。私は家だとWindowsがメインで、研究室だとMacなので、同じDockerfileやdocker-composeでも、volume周りだけどうしても環境の統一ができない状態でした。
現在の開発/分析環境の管理方法
簡単にいうと、Makefileを使ってMacでもWindowsでも動くコマンドを管理して、make経由でコマンドを実行するようにしています。
分析環境の管理例(python)
ディレクトリ構成
workspace/
├Dockerfile
├dev.mk
まずはDockerfile。個人の設定そのまま入れているので、 パッケージ群とjupyterのextension設定を反映している。 パッケージとして注意していることは、tensorflowが入っていることでこれはインストール順でnumpy等の依存関係が上書きされるため、tensorflowだけ最後にインストールするようにしている。 個人の開発環境なのでrootで使っているが、外部サーバーに置く場合などはuserやセキュリティ設定は変えてます。
FROM python:3.6 MAINTAINER kyo-bad RUN apt-get update \ && apt-get install -y screen vim \ && pip3 install --upgrade pip \ && pip3 install --upgrade jupyter pandas matplotlib seaborn scikit-learn jupyter_nbextensions_configurator jupyterthemes \ && pip3 install --upgrade tensorflow \ && jupyter nbextensions_configurator enable --user \ && git clone https://github.com/lambdalisue/jupyter-vim-binding $(jupyter --data-dir)/nbextensions/vim_binding \ && jt -t monokai -T -N -altmd -vim WORKDIR /root/workspace
続いて、dev.mkというmakefileを作成する。中身はこんな感じ
.PHONY: build run docker/* WORK_DIR := $(CURDIR) LOG_DIR := $(CURDIR) BUILDER_IMAGE := python:myenv BUILDER_WORK_DIR := /root/workspace BUILDER_CMD := docker run --rm -it -v $(WORK_DIR):$(BUILDER_WORK_DIR) -w $(BUILDER_WORK_DIR) -p 6006:6006 -p 8888:8888 $(BUILDER_IMAGE) setup: docker build -t $(BUILDER_IMAGE) . rebuild-image: docker rmi $(BUILDER_IMAGE) docker build -t $(BUILDER_IMAGE) . attach: $(BUILDER_CMD) bash jupyter: $(BUILDER_CMD) jupyter notebook --ip 0.0.0.0 --no-browser --port 8888 --allow-root tensorboard: $(BUILDER_CMD) tensorboard --logdir=$(LOGDIR)
Makefileで管理することのメリット
WIndowsでもMacでも同じコマンドで動く
これが一番大きいです。カレントディレクトリを共有する場合のコマンドの違いを、makeのCURDIR
で隠蔽することで参照方法を統一している。
注意点としては、WORK_DIR
をユーザのホームディレクトリにしてはいけないこと。これは、volume共有によってbashのログとかドットファイル類がホストにも共有されてしまうため。
長ったらしいコマンドを打たなくていい。
例えば、イメージのビルドだったら、
make -f dev.mk setup
これでいいし、jupyterの起動だったら
make -f dev.mk jupyter
でホストにサーバが立ち上がる。
angularでも似たような管理をしているので参考までに
Dockerfile(dev)
FROM node:8-alpine MAINTAINER kyo-bad RUN apk update \ && apk add --no-cache --update alpine-sdk build-base python yarn \ && yarn cache clean \ && yarn global add @angular/cli@1.6.6 \ && ng set --global packageManager=yarn \ && apk del alpine-sdk \ && rm -rf /tmp/* *.tar.gz ~/.npm \ && yarn cache clean \ && adduser -D appuser COPY --chown=appuser:appuser project /home/appuser/app ENV HOME=/home/appuser USER appuser WORKDIR ${HOME}/app CMD [ "yarn && ng serve --host 0.0.0.0" ]
Makefileは以下
.PHONY: build run new yarn serve docker/* BUILDER_IMAGE := angular-yarn-alpine:myenv BUILDER_WORK_DIR := /home/appuser/app BUILDER_APP_NAME := app BUILDER_CMD := docker run --rm -it -v $(CURDIR)/$(BUILDER_APP_NAME)/$(BUILDER_APP):$(BUILDER_WORK_DIR) -w $(BUILDER_WORK_DIR) -p 4200:4200 $(BUILDER_IMAGE) setup: docker build -t $(BUILDER_IMAGE) . rebuild-image: docker rmi $(BUILDER_IMAGE) docker build -t $(BUILDER_IMAGE) . new: docker run --rm -v $(CURDIR):$(BUILDER_WORK_DIR) -w $(BUILDER_WORK_DIR) $(BUILDER_IMAGE) ng new $(BUILDER_APP_NAME) --routing yarn: $(BUILDER_CMD) yarn serve: $(BUILDER_CMD) ng serve --host 0.0.0.0 --poll 2000 attach: $(BUILDER_CMD) sh