元営業WEBエンジニアのアプリ開発日記

営業出身のWEB系エンジニアが気になったものから作ってはメモを残してくブログ

KubernetesでGolangのPodにHttp/2を通信

概要

KubernetesGolangのPodにHttp/2を通信させる。
NginxにtlsでHttp/2通信すことできたから、Golangも楽勝でしょ♪
とか思ってたらそこそこ詰まったのでメモしとくっす

基本的には以下2つで構築した環境を前提に記載する

自己証明書関連の設定

自己証明書の作成とSecret登録

minikubeでTLS通信して裏側のNginxでHTTP/2.0を受けるの「自己証明書の作成」=>「自己証明書をSecretsとして登録」の通りにして、go.ucwork.localtls通信できるように準備する

証明書を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-passthroughannotationを指定してるとこ。
これで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」を押して オラーーーーってパスワードとか言われるがままに打ち込んで連携しちゃう f:id:shintaro-0112:20190301005020p:plain

ビルドのルール作成

私の場合、1つのレポジトリにNGINXやgolangMySQLのDockerfileが複数あるので
①で対象のレポジトリを選択。

Dockerfileに関する変更があった時のみビルド実行したいので
ブランチベースではなくタグを見つけたらビルドするように設定する
②のSource Type:Tag

後は以下の感じで設定してみる

  • Build Context:レポジトリのルートから対象Dockerfileまでのディレクト
  • Source:/^n-[0-9.]+$/

    • ヒットさせるタグの名称
    • NGINX, golang, MySQLそれぞれビルドのトリガーを分けるために /^n-[0-9.]+$//^g-[0-9.]+$//^m-[0-9.]+$/の様に接頭文字をつけてみた
  • Docker Tag:release-{sourceref}

    • ヒットさせたタグの名称を使ってDocker imageのタグ作成

f:id:shintaro-0112:20190301005140p:plain

実験

実際に動くか実験してみると変更が反映されて
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接続までやってみよう。

  • フロント
    • NGINX
    • (React)
    • (TypeScript)
  • バックエンド

構築

クラスタ作成

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でアプリケーション作ってgolangAPIリクエスト投げる感じにしたいなぁ

webでアクセスしてDBから情報取ってくるだけなのに結構いろんなとこで詰まった・・
k8s自体は奥が深そうなので、都度都度学習していき学んでいこう。。

TODO

  • MySQLDDL,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構成
nodeは複数の「pod」で構成され、podには「container」と永続化するデータを格納する「volume」が複数存在する。

下準備

minikubeインストール

ローカル環境でクラスターを使うには、minikubeDocker 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

まとめ

ざっくりと理解。次は以下交えて実際のアプリケーション作ってみよー

  • 自分のアプリに準じたConfigMapを作成してデプロイ
  • PersistantVolume(PV), PersistantVolumeClaim(PVC)を絡めてDBデータの永続化対応
  • hostPathを用いてmacとコンテナのディレクトリリンク
    • docker-composeで言う所のvolumes的なのがしたい。
    • ローカル環境ではわざわざimageのバージョンアップではなく変更に準じてコンテナの内容が変わるようにしたいから。。

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.mockimport [module] from "xxx"jest.spyOnimport * 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記載してもうまくいかないし、、、
とりあえずこれでやりたいことはできてる&もう悩みたくないのでとりあえずこれで行くとする

ちなみに悩んだ歴史