KubernetesでGolangのPodにHttp/2を通信
概要
KubernetesでGolangのPodにHttp/2を通信させる。
NginxにtlsでHttp/2通信すことできたから、Golangも楽勝でしょ♪
とか思ってたらそこそこ詰まったのでメモしとくっす
基本的には以下2つで構築した環境を前提に記載する
自己証明書関連の設定
自己証明書の作成とSecret登録
minikubeでTLS通信して裏側のNginxでHTTP/2.0を受けるの「自己証明書の作成」=>「自己証明書をSecretsとして登録」の通りにして、go.ucwork.local
にtls通信できるように準備する
証明書をgolangのpodに登録
volumesに証明書を登録したsecretを設定し、podの任意のパスにマウントする。
apiVersion: v1 kind: Service metadata: name: go labels: app: go spec: ports: - port: 443 selector: app: go tier: backend type: LoadBalancer --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: go-pv-claim labels: app: go spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi --- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: go labels: app: go spec: selector: matchLabels: app: go tier: backend strategy: type: Recreate template: metadata: labels: app: go tier: backend spec: containers: - image: shintaro0123/golang:latest name: go env: - name: GO_DB_HOST value: go-mysql - name: GO_DB_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password ports: - containerPort: 443 name: go volumeMounts: - name: go-persistent-storage mountPath: /var/www/html - name: tls-cert # ②podの好きなパスにマウントする mountPath: /go/tls # ↑ volumes: - name: go-persistent-storage persistentVolumeClaim: claimName: go-pv-claim - name: tls-cert # ①作成したsecretを指定 secret: # ↑ secretName: tls-secret # ↑
Golangファイルの作成
http2のパッケージ取得
$ go get -u golang.org/x/net/http2
http2を受けることができるようgoファイル作成
kubernetesでローカル環境にNGINX, golang, MySQL環境作ってみるこの時の記述に引っ張られてDB関連のことも記載されているが、
ポイントは以下2点。それ以外は消しちゃってもいい世界
- マウントした証明書のパスを指定してListen
srv.ListenAndServeTLS("/go/tls/tls.crt", "/go/tls/tls.key")
- http2で通信を受けることができように設定
http2.ConfigureServer(srv, nil)
最初ネットで調べてListenAndServeTLS(":443", "/go/tls/tls.crt", "/go/tls/tls.key")
これでなんとかやっていたが、どうしても「CrashLoopBackOff」になりpodが立ち上がらなかった・・・
そもそもgo初心者なんで、まぁ細々したことは学習しながら学んでいこう。
main.go
package main import ( "database/sql" "fmt" "log" "net/http" "os" "golang.org/x/net/http2" _ "github.com/go-sql-driver/mysql" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", handler) srv := &http.Server{ Addr: ":443", Handler: mux, } http2.VerboseLogs = true http2.ConfigureServer(srv, nil) log.Fatal(srv.ListenAndServeTLS("/go/tls/tls.crt", "/go/tls/tls.key")) } func handler(w http.ResponseWriter, r *http.Request) { /** URLパス表示 */ w.Write([]byte("url path is " + r.URL.Path[1:] + "\n")) /** DB接続 */ var dbConnectQuery string dbConnectQuery = "root:" + os.Getenv("GO_DB_PASSWORD") + "@tcp(" + os.Getenv("GO_DB_HOST") + ":3306)/ucwork" db, err := sql.Open("mysql", dbConnectQuery) if err != nil { panic(err.Error()) } defer db.Close() // 関数がリターンする直前に呼び出される rows, err := db.Query("SELECT * FROM user") // if err != nil { panic(err.Error()) } columns, err := rows.Columns() // カラム名を取得 if err != nil { panic(err.Error()) } values := make([]sql.RawBytes, len(columns)) scanArgs := make([]interface{}, len(values)) for i := range values { scanArgs[i] = &values[i] } for rows.Next() { err = rows.Scan(scanArgs...) if err != nil { panic(err.Error()) } var value string for i, col := range values { // Here we can check if the value is nil (NULL value) if col == nil { value = "NULL" } else { value = string(col) } w.Write([]byte(columns[i] + ": " + value + "\n")) } fmt.Println("-----------------------------------") } }
ビルドする
kubernetesでローカル環境にNGINX, golang, MySQL環境作ってみる
ここの「golang」にあるように、ビルドしてデプロイ
$ env GOOS=linux GOARCH=amd64 go build main.go
$ # 生成されたmainをpodに反映させる
検証する
curlでアクセスしてみるとHTTP/2でアクセスできた!!
$ curl --resolve go.ucwork.local:443:`minikube ip` -k https://go.ucwork.local -v ... > GET / HTTP/2 > Host: go.ucwork.local > User-Agent: curl/7.54.0 > Accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS updated)! < HTTP/2 200 < content-type: text/plain; charset=utf-8 < content-length: 66 < date: Sat, 30 Mar 2019 20:28:25 GMT < url path is id: 1 name: taro id: 2 name: jiro id: 3 name: hanako * Connection #0 to host go.ucwork.local left intact
まとめ
早くgolang自体の勉強したいのになかなか環境構築で手こずるw
やっと最低限くらいができたんで、あとはSKAFFOLD使ったローカル開発方式だけまとめて、golang自体の勉強にスイッチしよう!!!
minikubeでTLS通信して裏側のNginxでHTTP/2.0を受ける
概要
minikubeでnginx, MySQL, Go環境を作ることはできた。
これから作るアプリケーションではgRPCを導入したいので、HTTP/2.0による通信を行いたい。
一旦minikube裏側のnginx podにtlsで通信しアクセスログにHTTP/2.0が出力されることを目標にする。
minikubeを起動する
$ # とりあえず全部消しとく $ minikube stop $ minikube delete $ rm -rf ~/.minikube $ $ # hyperkitで起動 $ minikube start --vm-driver=hyperkit 😄 minikube v0.35.0 on darwin (amd64) 🔥 Creating hyperkit VM (CPUs=2, Memory=2048MB, Disk=20000MB) ... 💿 Downloading Minikube ISO ... 184.42 MB / 184.42 MB [============================================] 100.00% 0s 📶 "minikube" IP address is 192.168.64.52 🐳 Configuring Docker as the container runtime ... ✨ Preparing Kubernetes environment ... 💾 Downloading kubelet v1.13.4 💾 Downloading kubeadm v1.13.4 🚜 Pulling images required by Kubernetes v1.13.4 ... 🚀 Launching Kubernetes v1.13.4 using kubeadm ... ⌛ Waiting for pods: apiserver proxy etcd scheduler controller addon-manager dns 🔑 Configuring cluster permissions ... 🤔 Verifying component health ..... 💗 kubectl is now configured to use "minikube" 🏄 Done! Thank you for using minikube! $ minikube status host: Running kubelet: Running apiserver: Running kubectl: Correctly Configured: pointing to minikube-vm at 192.168.64.52
ingressに関する設定
ingress controllerをインストール
minikube addons list
にあるingressを有効化するとingress controller podが立ち上がるが、これを使うとingress controllerから裏側のnginx podにssl通信を通すことが出来ない。
ingress controllerにenable-ssl-passthrough
オプションを指定すると裏側までssl通信を行うことができるが、minikube addonsではそれが指定できない。
k8sのパッケージ管理ツールhemlをインストールして利用する。
controller.extraArgs.enable-ssl-passthroughをオプションに指定してインストール
$ # ingress controllerをインストールする $ ## helmが動くようにする $ helm init --upgrade $ $ ## tiller-deploy-xxx podがRunningになることを確認 $ kubectl -n kube-system get pods NAME READY STATUS RESTARTS AGE ... tiller-deploy-6d6cc8dcb5-mfvbx 1/1 Running 0 41s $ $ ## helmでingress controllerのインストール $ helm install \ --namespace kube-system \ --set controller.hostNetwork=true \ --set controller.kind=DaemonSet \ --set controller.extraArgs.enable-ssl-passthrough="" \ stable/nginx-ingress $ $ ## xxx-nginx-ingress-controller-xxx podがRunningになることを確認 $ kubectl -n kube-system get pods NAME READY STATUS RESTARTS AGE foolhardy-whale-nginx-ingress-controller-s62mk 1/1 Running 0 7h foolhardy-whale-nginx-ingress-default-backend-bb4cdcf54-vqjkq 1/1 Running 0 7h
ingressの設定
基本的にはホスト名に応じてどのserviceにアクセスさせるか記載しただけだが
重要なポイントとしてはnginx.ingress.kubernetes.io/ssl-passthrough
annotationを指定してるとこ。
これでingressから裏側のpodにssl通信を通すことができる
ingress.yaml
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: ucwork.local annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/ssl-passthrough: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" spec: rules: - host: nginx.ucwork.local http: paths: - backend: serviceName: nginx servicePort: 443
他のサイト見てるとここのspec直下にtlsを指定しているものもあるが
ssl-passthrough annotationを指定すると必要なくなった。。
ingress生成
さっきのingressgファイルをcreateじゃ
$ kubectl create -f ingress.yaml ingress.extensions "ucwork.local" created $ $ kubectl get ing NAME HOSTS ADDRESS PORTS AGE ucwork.local nginx.ucwork.local 80 3h
裏側のNginxの設定
自己証明書を作成してtls通信を実現する。
生成した鍵や証明書はSecretsにより利用。
鍵や証明書を利用するための設定ファイル(nginx.conf)はConfigMapにより設定する
自己証明書の作成
自己証明書作成時は色々聞かれるが
Common Nameにワイルドカード*.ucwork.local
を指定し、他は設定せずenter連打
$ openssl genrsa 2048 > server.key; $ $ openssl req -new -key server.key > server.csr ... Common Name (eg, fully qualified host name) []:*.ucwork.local ... $ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
自己証明書をSecretsとして登録
登録しとくとSecrets名を指定して利用できる!
$ kubectl create secret tls tls-secret --key ./server.key --cert ./server.crt secret "tls-secret" created $ # secretできてる $ kubectl get secret NAME TYPE DATA AGE default-token-x548f kubernetes.io/service-account-token 3 17m tls-secret kubernetes.io/tls 2 18s
Nginx設定ファイル(nginx.conf)をConfigMapとして指定
nginx/conf/nginx.conf
server { listen 443 ssl http2; server_name nginx.ucwork.local; server_tokens off; access_log /var/log/nginx/ucwork_ssl_access.log; error_log /var/log/nginx/ucwork_ssl_error.log; ssl_certificate /etc/nginx/tls/tls.crt; ssl_certificate_key /etc/nginx/tls/tls.key; location / { root /usr/share/nginx/html; index index.html index.htm; } }
設定ファイルの格納されたディレクトリを指定してConfigMap作成
$ kubectl create configmap nginx-conf --from-file=nginx/conf $ kubectl get configmap NAME DATA AGE nginx-conf 1 18s $ $ # confの内容がconfigmapに入ってる!! $ kubectl describe configmap +[master] Name: nginx-conf Namespace: default Labels: <none> Annotations: <none> Data ==== nginx.conf: ---- server { listen 443 ssl http2; server_name nginx.ucwork.local; server_tokens off; access_log /var/log/nginx/ucwork_ssl_access.log; error_log /var/log/nginx/ucwork_ssl_error.log; ssl_certificate /etc/nginx/tls/tls.crt; ssl_certificate_key /etc/nginx/tls/tls.key; location / { root /usr/share/nginx/html; index index.html index.htm; } } Events: <none>
NginxのService, deployment, pod, PVC周りの設定ファイル準備
以下マニュフェストを作成(マニュフェストっていうのかな・・?)
nginx-deployment.yaml
apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 443 selector: app: nginx tier: frontend type: LoadBalancer --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nx-pv-claim labels: app: nginx spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi --- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: nginx labels: app: nginx spec: selector: matchLabels: app: nginx tier: frontend strategy: type: Recreate template: metadata: labels: app: nginx tier: frontend spec: containers: - image: shintaro0123/nginx:release-n-1.0.5 name: nginx ports: - containerPort: 443 name: nginx volumeMounts: - name: nginx-conf mountPath: /etc/nginx/conf.d - name: tls-cert mountPath: /etc/nginx/tls - name: nginx-persistent-storage mountPath: /var/www/html volumes: - name: nginx-persistent-storage persistentVolumeClaim: claimName: nx-pv-claim - name: nginx-conf configMap: name: nginx-conf - name: tls-cert secret: secretName: tls-secret
ポイント的には以下くらいかな
- name: nginx-confでconfigmapを指定。コンテナの/etc/nginx/conf.dにマウントする
- name: tls-certでsecretを指定。tlsを見に行っている場所(/etc/nginx/tls)にマウントする
ちなみにshintaro0123/nginx:release-n-1.0.5
は
こんな感じのDockerfileで生成されたimage。同階層に適当なindex.htmlをおいてもらえればいい
Dockerfile
FROM nginx:1.15.8 LABEL maintainer="shintaro.0112@gmail.com" RUN mkdir -p /etc/nginx/tls RUN chown -R root:root /etc/nginx/tls RUN chmod -R 600 /etc/nginx/tls ADD ./index.html /usr/share/nginx/html
NginxのService, deployment, pod, PVC周り作成
コマンド実行
$ kubectl create -f nginx-deployment.yaml service "nginx" created persistentvolumeclaim "nx-pv-claim" created deployment.apps "nginx" created
動作確認
ログを確認しながらhttpsでアクセスしてみる!
$ # アクセスしてみる $ curl --resolve nginx.ucwork.local:443:`minikube ip` -k https://nginx.ucwork.local $ $ # 裏のNginxのログ吐き出す $ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-7cbf788fd4-lk6q2 1/1 Running 0 1m $ kubectl exec -it nginx-7cbf788fd4-lk6q2 bash root@nginx-7cbf788fd4-lk6q2:/# root@nginx-7cbf788fd4-lk6q2:/# tail -f /var/log/nginx/ucwork_ssl_access.log 192.168.64.52 - - [27/Mar/2019:12:20:59 +0000] "GET / HTTP/2.0" 200 149 "-" "curl/7.54.0" $ # HTTP/2.0出てるうううううううう!!!
ちなみにcurl -k https://`minikube ip` -H "Host: nginx.ucwork.local"
ではTLSのハンドシェイクでホスト名が読み取れず、HTTP/1.1になってしまった。。。。これでだいぶつまずいた・・・
もちろん/etc/hostsファイルにminikube ipとドメイン名の関係を書いてブラウザからアクセスしてもらっても全然問題ない
まとめ
TLS通信が絡むことでものすごいつまずいた。
minikube addonsのingressでなんとか頑張ろうとしたり、curlがハンドシェイクしなかったり。。。
あとはminikbue起動時に自動でこの辺の処理実行してくれるようにしたりして 実際のアプリ開発進めていこう!!!!!!
DockerHubとGitHubの連携(k8s用)
概要
k8sで自分のアプリケーション作ってみたときに
Dockerfile修正してgithubにpush、dockerのimageはdockerhubにpushって
都度都度コマンド打つの面倒臭いなって思ってたら
Docker HubにGithubとの連携なるボタンがあったからやってみる
やりたいこと(できたこと)
GitHubにタグをpushしたら DockerHubがそれをキャッチして新たなイメージをビルド そしてよろしくタグの名前ができる!!
Docker Hubでの設定
GitHubとの連携登録
①の「Builds」を押して、②の「Link to Link to GitHub」を押して オラーーーーってパスワードとか言われるがままに打ち込んで連携しちゃう
ビルドのルール作成
私の場合、1つのレポジトリにNGINXやgolang、MySQLのDockerfileが複数あるので
①で対象のレポジトリを選択。
Dockerfileに関する変更があった時のみビルド実行したいので
ブランチベースではなくタグを見つけたらビルドするように設定する
②のSource Type:Tag
後は以下の感じで設定してみる
- Build Context:レポジトリのルートから対象Dockerfileまでのディレクトリ
Source:
/^n-[0-9.]+$/
Docker Tag:
release-{sourceref}
- ヒットさせたタグの名称を使ってDocker imageのタグ作成
実験
実際に動くか実験してみると変更が反映されて
sample page => sample page2
になってる!!
$ # Dockerfileでコンテナに渡してるindex.htmlを適当に修正 $ git diff <body> - <p>sample page</p> + <p>sample page2</p> </body> $ $ # commit&tag&push $ git add . $ git commit -m "html少し修正" $ git push origin $ git tag -a 1.0.0 -m "初回タグ付け" $ git push origin 1.0.0 $ $ # Docke Hubブラウザでbuildsみるとキャッチできてた $ $ # 対象のタグを指定してデプロイ $ git diff --- a/nginx-deployment.yaml +++ b/nginx-deployment.yaml spec: containers: - - image: shintaro0123/nginx:1.0.4 + - image: shintaro0123/nginx:release-1.0.0 $ $ kubectl apply -f nginx-deployment.yaml $ curl http://`minikube ip` -H 'Host: nginx.ucwork.local' <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>sample page</title> </head> <body> <p>sample page2</p> </body> </html>
まとめ
なんか思ったより簡単にできた。
色々設定できるみたいだけどまぁとりあえず自動でビルドして欲しいだけだからこんなもんでいいやという気持ち
kubernetesでローカル環境にNGINX, golang, MySQL環境作ってみる
概要
k8sのチュートリアルやってみたし自分用のアプリケーション作ってみる!
こんな感じの構成にしたい。DB接続までやってみよう。
構築
クラスタ作成
k8sのチュートリアル通りminikube使ってローカル環境にクラスタ作る。
ちなみにこれ基本Mac想定
$ minikube stop $ # deleteしとかないとちょくちょく起動しないときある $ minikube delete $ # virtualbox重いのでhyperkitで行く $ minikube start --vm-driver=hyperkit
フロントエンド
とりあえずNginx起動させてWelcome to nginx!
表示させたい
Dockerファイルを作成する
Dockerfile
FROM nginx:1.15.8 LABEL maintainer="shintaro.0112@gmial.com" EXPOSE 80 COPY ./conf/nginx.conf /etc/nginx/conf.d/nginx.conf
./conf/nginx.conf
server { listen 8080; server_name nginx.ucwork.local; access_log /var/log/nginx/ucwork_access.log; error_log /var/log/nginx/ucwork_error.log; location / { root /usr/share/nginx/html; index index.html index.htm; } }
Docker Hubにレポジトリ登録
Docker Hubでアカウント登録し、ローカルでログインしとく。
$ docker login $ # 登録したusername $ # passwordを打ち込む
commitしてpush
$ docker build -t shintaro0123/nginx:1.0.0 . $ docker run -d -p 8080:8080 --name my-nginx-app shintaro0123/nginx:1.0.0 $ docker commit -m "何かしらのコメント" my-nginx-app shintaro0123/nginx:1.0.0 $ docker push shintaro0123/nginx:1.0.0
deployment, pod, service, pvc作成
nginx-deployment.yaml
はこんな感じにする
apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 8080 selector: app: nginx tier: frontend type: LoadBalancer --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nx-pv-claim labels: app: nginx spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi --- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: nginx labels: app: nginx spec: selector: matchLabels: app: nginx tier: frontend strategy: type: Recreate template: metadata: labels: app: nginx tier: frontend spec: containers: - image: shintaro0123/nginx:1.0.0 name: nginx ports: - containerPort: 8080 name: nginx volumeMounts: - name: nginx-persistent-storage mountPath: /var/www/html volumes: - name: nginx-persistent-storage persistentVolumeClaim: claimName: nx-pv-claim
全部立ててみよう!
$ kubectl create -f nginx-deployment.yaml $ # 一度作成して何か修正したらcreateじゃなくてapply使うっぽい $ # kubectl apply -f nginx-deployment.yaml
Ingress作成
podにアクセスするためにはserviceを経由するが、service経由すると都度公開されるポートが変わったりで大変そうなので、IngressとIngressControllerなるものを使う。
アプリケーションレベルでロードバランシングするL7LBっつう奴らしい。
$ minikube addons list $ # ingressの有効化 $ minikube addons enable ingress
ingressの宣言ingress.yaml
を作成。hostの名前は自分の好きにして良い
ingressが効いてるの確認するために80->8080ポートにアクセスさせてる
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: ucwork.local annotations: nginx.org/server-snippet: "proxy_ssl_verify off;" spec: rules: - host: nginx.ucwork.local http: paths: - backend: serviceName: nginx servicePort: 8080 - host: go.ucwork.local http: paths: - backend: serviceName: go servicePort: 8080
実際にingress作成
$ kubectl create -f ingress/ingress.yaml $ # 一度作成して何か修正したらcreateじゃなくてapply使うっぽい $ # kubectl apply -f ingress/ingress.yaml
動作確認
$ # 各種起動状態の確認 $ minikube addons list $ # dashboardの有効化 $ minikube addons enable dashboard $ # ブラウザが開いて各種状態がわかるページ出てくる $ minikube dashboard $ # コマンド叩いて確認してもいい $ kubectl get all $ $ # httpでアクセスしてWelcome to nginx!出れば素敵! $ curl http://`minikube ip`/ -H 'Host: nginx.ucwork.local' <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style>
バックエンド
MySQL
Secret作成
パスワードとか公開しちゃいけないものをいい感じで使えるSecret利用
yourpassword
部分を自分の好きなパスワードにする
kubectl create secret generic mysql-pass --from-literal=password=yourpassword
Dockerファイル作成
Dockerfile
FROM mysql:8.0.15 LABEL maintainer="shintaro.0112@gmial.com" # 設定ファイルを配置 # (MySQLは設定ファイルの権限が777だと読み込まない) COPY ./conf/charset.cnf /etc/mysql/conf.d/charset.cnf RUN chmod 644 /etc/mysql/conf.d/* RUN apt-get update && \ apt-get install -y locales && \ rm -rf /var/lib/apt/lists/* && \ echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen ja_JP.UTF-8 ENV LC_ALL ja_JP.UTF-8
./conf/charset.cnf
[mysqld] explicit_defaults_for_timestamp = 1 character-set-server=utf8 sql_mode=NO_ENGINE_SUBSTITUTION [mysql] default-character-set=utf8
Docker Hubレポジトリにpush
commitしてpush
$ docker build -t shintaro0123/mysql:1.0.0 . $ docker run -d --name my-mysql-app shintaro0123/mysql:1.0.0 $ docker commit -m "何かしらのコメント" my-mysql-app shintaro0123/mysql:1.0.0
deployment, pod, service, pvc作成
mysql-deployment.yaml
はこんな感じ
apiVersion: v1 kind: Service metadata: name: go-mysql labels: app: go spec: ports: - port: 3306 selector: app: go tier: mysql clusterIP: None --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pv-claim labels: app: go spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi --- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: go-mysql labels: app: go spec: selector: matchLabels: app: go tier: mysql strategy: type: Recreate template: metadata: labels: app: go tier: mysql spec: containers: - image: shintaro0123/mysql:1.0.0 name: mysql env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pv-claim
全部立ててみーる
$ kubectl create -f mysql-deployment.yaml $ # 一度作成して何か修正したらcreateじゃなくてapply使うっぽい $ # kubectl apply -f mysql-deployment.yaml
動作確認
MySQLに接続できることを確認。パスワードはSecretで設定したものを入れる
$ # podの名前を確認 $ kubectl get pods $ # 確認したpod NAME $ kubectl exec -it [pod NAME] bash root@go-mysql-7469cd46ff-5xdwh:/# mysql -u root -p Enter password: mysql> # データベースとテーブル作ってデータ入れとく mysql> create database ucwork; Query OK, 1 row affected (0.01 sec) mysql> use ucwork Database changed mysql> create table users(id int AUTO_INCREMENT NOT NULL PRIMARY KEY, name varchar(255) not null); Query OK, 0 rows affected (0.03 sec) mysql> mysql> insert into users values(1, "taro"); Query OK, 1 row affected (0.02 sec)
golang
goアプリケーションの作成
go全然書いたこと無いんでとりあえず、httpリクエスト受けて、MySQLからなんか取ってくるところを書く。とにかく動けばいいの精神レベル(これからちゃんと勉強したいな・・・)
main.go
package main import ( "database/sql" "fmt" "net/http" "os" _ "github.com/go-sql-driver/mysql" ) func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) } func handler(w http.ResponseWriter, r *http.Request) { /** URLパス表示 */ w.Write([]byte("url path is " + r.URL.Path[1:] + "\n")) /** DB接続 */ var dbConnectQuery string dbConnectQuery = "root:" + os.Getenv("GO_DB_PASSWORD") + "@tcp(" + os.Getenv("GO_DB_HOST") + ":3306)/ucwork" db, err := sql.Open("mysql", dbConnectQuery) if err != nil { panic(err.Error()) } defer db.Close() // 関数がリターンする直前に呼び出される rows, err := db.Query("SELECT * FROM users") // if err != nil { panic(err.Error()) } columns, err := rows.Columns() // カラム名を取得 if err != nil { panic(err.Error()) } values := make([]sql.RawBytes, len(columns)) scanArgs := make([]interface{}, len(values)) for i := range values { scanArgs[i] = &values[i] } for rows.Next() { err = rows.Scan(scanArgs...) if err != nil { panic(err.Error()) } var value string for i, col := range values { // Here we can check if the value is nil (NULL value) if col == nil { value = "NULL" } else { value = string(col) } w.Write([]byte(columns[i] + ": " + value + "\n")) } fmt.Println("-----------------------------------") } }
goインストールしてビルドじゃ!!コンテナ用にクロスコンパイルする
$ env GOOS=linux GOARCH=amd64 go build main.go
Dockerファイル作成
コンパイルして出力されたmainファイルをコンテナに渡して起動させる
Dockerfile
FROM golang:1.11.5 LABEL maintainer="shintaro.0112@gmial.com" EXPOSE 8080 WORKDIR /go/src/app RUN go get github.com/go-sql-driver/mysql COPY main ./ CMD ["./main"]
Docker Hubレポジトリにpush
buildしてrunしてcommitしてpush
$ docker build -t shintaro0123/golang:1.0.0 . $ docker run -d --name my-golang-app shintaro0123/golang:1.0.0 $ docker commit -m "何かしらのコメント" my-golang-app shintaro0123/golang:1.0.0
deployment, pod, service, pvc作成
go-deployment.yaml
こんな感じで作った
apiVersion: v1 kind: Service metadata: name: go labels: app: go spec: ports: - port: 8080 selector: app: go tier: backend type: LoadBalancer --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: go-pv-claim labels: app: go spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi --- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: go labels: app: go spec: selector: matchLabels: app: go tier: backend strategy: type: Recreate template: metadata: labels: app: go tier: backend spec: containers: - image: shintaro0123/golang:1.0.0 name: go env: - name: GO_DB_HOST value: go-mysql - name: GO_DB_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password ports: - containerPort: 8080 name: go volumeMounts: - name: go-persistent-storage mountPath: /var/www/html volumes: - name: go-persistent-storage persistentVolumeClaim: claimName: go-pv-claim
動作確認
ingressで設定したホスト名でアクセスするとよろしくgo serviceにアクセスされる
URLのパスとデータベースに格納された値一覧が表示される。
$ curl http://`minikube ip`/abcd -H 'Host: go.ucwork.local' $ # ブラウザから見たい場合は minikube ipコマンドで出力されたipとhost名をhostsに指定
まとめ
あとはNGINXのコンテナにReactとTypeScriptでアプリケーション作ってgolangにAPIリクエスト投げる感じにしたいなぁ
webでアクセスしてDBから情報取ってくるだけなのに結構いろんなとこで詰まった・・
k8s自体は奥が深そうなので、都度都度学習していき学んでいこう。。
TODO
- MySQLのDDL,SQLを起動時に自動実行
- DockerHubとgithunの連携
- nghttpxによるhttp/2対応
- gRPCやりたい
- docker commit、push、デプロイの自動化。Skaffold?
- そもそもgolangの勉強
参考にさせてもらったサイト
kubernetes(k8s)のチュートリアルをざっくりやって見る
概要
前々から気になってはいたものの
仕事に追われて遊べなかったKubernetisで遊んでみた。
とりあえず公式のTutorialをローカル環境(Mac)で一通りやってわかったことをざっくりとメモしとこう
kubernetes(k8s)とは
いわゆるコンテナオーケストラレーションらしい。
dockerとかで使うコンテナを用いて運用する際に必要なやつ。
コンテナのメモリとかcpuの制御、コンテナのデプロイ、ロールバックとか色々してくれる素敵なやつ。
ローカル環境開発ではdocker使ったことあるけど、本番でも使えれば!なんて人は勉強する価値あるかと。
基本的な構成
クラスター構成
k8sはデプロイなどを管理する「Master」とそれ以外の「Node」というクラスターで構成されている。
node構成
nodeは複数の「pod」で構成され、podには「container」と永続化するデータを格納する「volume」が複数存在する。
下準備
minikubeインストール
ローカル環境でクラスターを使うには、minikubeとDocker Desktop for Macを使う2パターンあるみたい。Tutorialではminikube使ってたんでそれで行くことにする。
# minikubeインストール brew cask install minikube # VirtualBox重たくて嫌いなんでhyperkitでクラスタ起動する ## hyperkit導入 curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit chmod +x docker-machine-driver-hyperkit sudo mv docker-machine-driver-hyperkit /usr/local/bin/ sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit ## minikubeでクラスタ起動 minikube start --vm-driver=hyperkit ## virtualboxで起動しようとしてエラーになったからdeleteするとうまくいった ## minikube delete
実際に触って見るぞ
アプリケーションをクラスタにデプロイする
dockerのイメージを指定してデプロイできるのだよ
# デプロイ kubectl run kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1 --port=8080 # デプロイされてるの確認 kubectl get deployments kubectl get pods
外部からアクセスできるようにservice生成
# 外部公開(service生成) kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080 kubectl get services # 公開してるポート番号の取得 export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}') echo NODE_PORT=$NODE_PORT # 実際にアクセス curl $(minikube ip):$NODE_PORT
podのスケールアップ・スケールダウン
アクセス負荷を考慮してスケールアップしたりダウンしたりコマンドでちょろっとできる
スケールアップ
# スケールアップ(pod4つに増やす) kubectl scale deployments/kubernetes-bootcamp --replicas=4 # 増えてるー kubectl get deployments kubectl get pods -o wide
スケールダウン
# スケールダウン(pods2つにする) kubectl scale deployments/kubernetes-bootcamp --replicas=2 # 減ってるー kubectl get deployments kubectl get pods -o wide
ロードバランシング
なんどもアクセスすると異なるpodにアクセス(ロードバランシング)していることがわかる
# アクセスして見る kubectl describe services/kubernetes-bootcamp export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}') echo NODE_PORT=$NODE_PORT # 毎度違うpodにアクセスしてるーー curl $(minikube ip):$NODE_PORT
アプリケーションのアップデート、ロールバック
アップデート
# set imageで更新したいバージョンを指定 kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2 # version2に更新されてる kubectl describe services/kubernetes-bootcamp ## rollout でもversion upできるらしいもじゃ kubectl rollout status deployments/kubernetes-bootcamp
ロールバック
# ロールバックできる!! kubectl rollout undo deployments/kubernetes-bootcamp # versionが戻ってる kubectl describe services/kubernetes-bootcamp
まとめ
ざっくりと理解。次は以下交えて実際のアプリケーション作ってみよー
jestでモック
概要
いちいち書くほどでもないが、2ヶ月前に初めてjestで単体テスト書いた時は「どうやってモックにしてスタブ作るのやぁああ」ってなってたし、一応メモしとこう
検証コード
テスト対象のコード
import * as [module] from "xxx"
形式とimport [module] from "xxx"
形式でインストールしているモジュールをモック&スタブにしてみる。
import React from "react"; import ExportDefault from "./export_default"; import * as ExportMulti from "./export_multi"; const App = () => { const stringMulti = ExportMulti.returnString; const functionMulti = ExportMulti.returnFunction; const stringDefault = ExportDefault; return ( <> <div id="stringMulti">stringMulti: {stringMulti}</div> <div id="functionMulti">functionMulti: {functionMulti()}</div> <div id="stringDefault">stringDefault: {stringDefault}</div> </> ); }; export default App;
const returnString = "string from default"; export default returnString;
export const returnString = "string from multi"; export const returnFunction = () => "function from multi";
テストコード
jest.mock
でimport [module] from "xxx"
、jest.spyOn
でimport * as [module] from "xxx"
形式をモック、スタブにできそう
import { mount } from "enzyme"; import React from "react"; import App from "../../../src/components/jest_mock/App"; import * as ExportMulti from "../../../src/components/jest_mock/export_multi"; jest.mock("../../../src/components/jest_mock/export_default", () => { return "mock default string"; }); describe("App.tsx", () => { beforeEach(() => { jest.resetModules(); }); it("all import mock", () => { /** 準備 */ const returnFunctionMock = jest.spyOn(ExportMulti, "returnFunction"); returnFunctionMock.mockReturnValueOnce("mock multi function"); /** 実行 */ const wrapper = mount(<App />); /** 検証 */ expect(returnFunctionMock).toBeCalledTimes(1); expect(wrapper).toMatchSnapshot(); }); });
ぶつかった点
jest.mockはdescribe, beforeEach, it内で宣言しても有効にならない。。。のでスタブ指定した文字列をテストの都度指定できない。。
今の所そんなケースないのでとりあえずこれでいこう
React Context.Consumerをモック化
概要
jestで単体テスト書いてる時にReactのContext.Consumerをモック化という壁にぶつかったんでメモしとく
実装
テストしたいコンポーネント
こんな感じでConsumer使って変数とかメソッド渡してる奴をモック(スタブ)にしたい。
import React from "react"; import ThemeContext from "./theme-context"; const ThemeTogglerButton = () => { // The Theme Toggler Button receives not only the theme // but also a toggleTheme function from the context return ( <ThemeContext.Consumer> {({ theme, toggleTheme }) => ( <button onClick={toggleTheme} style={{ backgroundColor: theme.background }}> Toggle Theme </button> )} </ThemeContext.Consumer> ); }; export default ThemeTogglerButton;
こんな感じでReact.createContext
してる
import React from "react"; import { themes } from "./themes"; // Make sure the shape of the default value passed to // createContext matches the shape that the consumers expect! const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => { }, }); export default ThemeContext;
今回あんまり重要じゃないけどthemesはこんな感じ
export const themes = { dark: { background: "#222222", foreground: "#ffffff", }, light: { background: "#eeeeee", foreground: "#000000", }, };
テストコード
色々試行錯誤したけど、jest.mock
をimportの後に記載するといい感じでモック(スタブ)にしてくれた!
import { mount, shallow } from "enzyme"; import React from "react"; import ThemeTogglerButton from "../../../src/containers/context/theme-toggler-button"; /** ThemeContext.Consumerをモック化 */ let theme: any; let toggleTheme: jest.Mock<{}>; jest.mock("../../../src/containers/context/theme-context", () => { return { Consumer: (props: any) => props.children({ theme, toggleTheme }), }; }); describe("theme-toggler-button.tsx", () => { beforeEach(() => { jest.resetModules(); theme = { background: "green", }; toggleTheme = jest.fn(); }); it("snapshot test", () => { /** 準備 */ /** 実行 */ const wrapper = mount(<ThemeTogglerButton />); expect(wrapper).toMatchSnapshot(); /** 検証 */ }); it("onClick test", () => { /** 準備 */ /** 実行 */ const wrapper = mount(<ThemeTogglerButton />); wrapper.simulate("click"); /** 検証 */ // ちゃんと自分で用意したメソッドが呼び出されてる!! expect(toggleTheme).toBeCalledTimes(1); }); });
まとめ
英語のサイト見て色々検証したけどうまくいかず。。。。
describe内でjest.mock
記載してもうまくいかないし、、、
とりあえずこれでやりたいことはできてる&もう悩みたくないのでとりあえずこれで行くとする
ちなみに悩んだ歴史
- 有力そうな3サイトを参考に実装もうまくいかず。。
- material-uiを疑う日々
export default withStyles(style)(Login);
のwithStyleが悪さしてんのかなーとか疑うが何も起こらず
- ThemeContextをrequireするとちゃんとモックが有効だが、importだと有効にならない!!
- この辺からmock, doMockとかの意味をちゃんと調べて気合いとガッツでtry&Errorしてたらimportでもちゃんとモック化できた!!