miyazi888の覚え書き日記

学習したことを書き留めてます。

minikubeでingressを使う

k8sを学習していく中でingressという単語に遭遇し、どうやらLoad Balancer的なものであるらしい、ということまでは理解した。
ただ、実際に触ってみた方が理解しやすいかと思い、minikubeでingressを動作させてみた。

前提

以下の環境で動作確認した。

  • ubuntu 20.04
  • minikube v1.24.0
  • kubectl v1.20.8-dispatcher

それからdockerとdocker-composeが動作する環境であること。

動作確認用のdockerファイル作成

まずは動作確認用に2つのdocker imageを作成する。

mkdir html1
touch html1/index.html

html1/index.html

<html>
  <h1>sample page1</h1>
</html>
mkdir html2
touch html2/index.html
<html>
  <h1>sample page2</h1>
</html>
touch Dockerfile1

Dockerfile1

FROM 'nginx:latest'

COPY html1/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch Dockerfile2

Dockerfile2

FROM 'nginx:latest'

COPY html2/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch docker-compose.yaml

docker-compose.yaml

version: '3'
 
services:
  nginx1:
    build:
      context: ./
      dockerfile: Dockerfile1
    image: nginx-sample1
    ports:
      - 8080:80
  nginx2:
    build:
      context: ./
      dockerfile: Dockerfile2
    image: nginx-sample2
    ports:
      - 8081:80

Docker image作成と動作確認

ここまで出来たら、とりあえず上記の設定でdockerコンテナが狙ったとおりに動作するか確認。

docker-compose up -d
curl localhost:8080

以下のようなレスポンスが返ってきたら成功
<html>
  <h1>sample page1</h1>
</html>

curl localhost:8081
<html>
  <h1>sample page2</h1>
</html>

正しく動作することがわかったらコンテナを停止。

docker-compose stop

ここまでで正しく動作するdocker imageが作成されていることになる。

docker imageにタグを付ける

docker imageは出来たものの、このままではminikubeで扱えない(latestタグのimageがうまく扱えない模様)のでタグを付ける。
まずは作成されたdocker imageのIDを確認。

docker images

IDがわかったらタグ付けする。

docker tag <nginx-sample1のimage id> nginx-sample1:v1
docker tag <nginx-sample2のimage id> nginx-sample2:v1

minkube起動

minikubeを起動。

minikube start

イメージ取り込み

先程、作成したdocker imageをminikubeにロードする。
最後のminikube image lsはminikubeのimageの一覧を表示するコマンド。

minikube image load nginx-sample1:v1
minikube image load nginx-sample2:v1
minikube image ls

取り込んだイメージが使えるか確認

minikubeに取り込んだimageが本当に動作するか、ここでも動作確認する。

kubectl create deploy test1 --image=nginx-sample1:v1
kubectl expose deploy test1 --type=NodePort --port=8080 --target-port=80
minikube service test1 --url

ターミナルにURLが表示されるので、ブラウザで確認。
sample page1が表示されたら成功。

もう1つのイメージも動作確認する。

kubectl create deploy test2 --image=nginx-sample2:v1
kubectl expose deploy test2 --type=NodePort --port=8080 --target-port=80
minikue service test2 --url

こちらも表示されたURLにアクセスし、sample page2が表示されたら、OK。

ingressがminikubeで有効かどうかを確認

ingressはminikubeのaddonとして提供されている模様。
まずはaddonのリストを調べる。

minikube addons list

もしingressがenabledになっていなかったら有効にする。

minikube addons enable ingress
minikube addons list

ingressを設定する

やっと本番。ingressを追加する。

touch ingress.yaml

下の設定ではhttp://test.local/にアクセスするとsample page1のpodにアクセスし、http://test.local/test2にアクセスするとsample page2のpodにアクセスする設定。

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-test
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - host: test.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: test1
                port:
                  number: 8080
          - path: /test2
            pathType: Prefix
            backend:
              service:
                name: test2
                port:
                  number: 8080

下記のコマンドでingressを適用する。

kubectl apply -f ingress.yaml
watch kubectl get ingress

watchで表示されるingressでaddressの部分にloocalhostが表示されるまで待つ。
上で作成したingressはhostを指定しているので/etc/hostsも以下ように追記する。

/etc/hosts

127.0.0.1 test.local

これで準備完了。

動作確認

別のターミナルを起動して以下のコマンドを実行する。

minikube tunnel

元のターミナルに戻ってからcurlコマンドでレスポンスを確認する。
ブラウザでももちろん、OK。

curl http://test.local

sample page1が返ってきたら動作OK。

<html>
  <h1>sample page1</h1>
</html>
curl http://test.local/test2

こちらはsample page2が返ってきたらOK。

<html>
  <h1>sample page2</h1>
</html>

というわけでminikubeでingressを動作させることが出来た。

後片付け

下記のコマンドでingressとserviceとdeploymentを削除。

kubectl delete ingress ingress-test
kubectl delete svc test1 test2
kubectl delete deploy test1 test2

次に/etc/hostsから下記の設定を削除。
/etc/hosts

127.0.0.1 test.local

minikubeのimageから今回追加したimageを削除。

minikube image rm nginx-sample1:v1
minikube image rm nginx-sample2:v1

最後にminikubeを停止して、後片付け完了。

minikube stop

minikubeでローカルのdockerイメージを使う

きっかけ

kubernetesの学習目的でminikubeをインストール。 学習中、minikubeでもローカルのdocker imageを使用したくなった。

ネットで検索するとだいたい、下記のような方法が記載されていた。

eval $(minikube docker-env)

ところが、自分の環境では、これではローカルのdocker imageを使うことが出来なかった。

minikubeのruntimeにcontainerdを使っていることが原因ではないかと推測。 なぜなら初回のminikube start時にruntimeにcontainerdを強制されたから。

そこで他に方法はないかとネットを検索するとminikubeの公式にimageなるサブコマンドがあることに気づいた。 これを使えばどうにかローカルのイメージを使えるのでは・・・ と思い実験してみた。 https://minikube.sigs.k8s.io/docs/commands/image/

結論

minikube image loadを使うことでminikube上でもホスト側のイメージを取り込んで使用することが出来た。

minikube image load <image-name>:<image-tag>

動作確認

以下、本当にローカルのイメージをminikubeで使うことができるのか実験してみた。

前提

動作確認した環境は以下のとおり。 上にも記述したようにminikubeのruntimeはcontainerd。

  • ubuntu 20.04
  • minikube v1.24.0
  • kubectl v1.20.8-dispatcher

動作確認用のdockerイメージ作成

htmlファイル作成

まずは動作確認用に下記のようなHTMLを表示するイメージを作成。

mkdir html
touch html/index.html

html/index.html

<html>
  <h1>sample page</h1>
</html>

dockerイメージ作成

次に以下のようにDockerFileとdocker-compose.ymlを作成。

touch Dockerfile

Dockerfile

FROM 'nginx:latest'

COPY html/index.html /usr/share/nginx/html/index.html

RUN service nginx start
touch docker-compose.yaml

docker-compose.yaml

version: '3'
 
services:
  nginx:
    build: ./
    image: nginx-sample
    ports:
      - 80:80

dockerイメージの動作確認

ここで一旦、dockerイメージの作成と動作確認を行う。

docker-compose up -d

起動したら、ブラウザでlocalhostにアクセスする。 「sample page」と表示されれば、成功。

イメージにタグ付けする

最後に作成したイメージにタグ付けする。 この段階ですでにイメージそのものは作成されているけど、タグはlatestとなっていると思う。 なぜかminikubeではlatestタグのイメージは扱えなさそうなのでlatest以外のタグを付ける(latestが使えない理由はわからなかった・・・) ここでv1タグを付ける。

docker tag <docker image id> nginx-sample:v1

minikubeに作成したdockerイメージを起動できることを確認

起動

まずはminikubeそのものを起動

minikube start

イメージを取り込む

ここでminikubeにローカルのイメージを取り込み。

minikube image load nginx-sample:v1

minikubeで扱うことができるイメージの一覧は以下のコマンドで確認できた。

minikube image ls

取り込んだイメージを使う

以下のコマンドで取り込んだイメージのdeploymentの作成と公開を行う。

kubectl create deployment test --image=nginx-sample:v1
kubectl expose deployment test --type=NodePort --port=80
minikube service test --url

画面に表示されたURLにブラウザでアクセスし、sample pageが出たらアクセスOK。 自分の環境ではこれで動作した。

後片付け

動作確認が済んだので以下のような後片付けをして完了。

minikube stop

kubectl delete svc test
kubectl delete deploy test
minikube image rm nginx-sample:v1

docker-compose down
docker rmi nginx-sample:latest
docker rmi nginx-sample:v1

2020年振り返り

立ち位置

フリーでプログラマとしてプロジェクトに参加。今年は3月まではA社。4月からはB社のプロジェクトに参画中。

利用した技術

Go / Gin Frmaework

A社はGoでバックエンドを書いてた。それからGoの仕事が舞い込みそうな話しがきたので個人的にサンプル的なコードを書いてカンみたいなものを維持していたが、結局タイミング的なものもあって仕事には繋がらなかった。残念。

ES6 / Vue.js + vuex

A社のフロントエンドがvue.js + es6だったので書いた。日本でvue.jsが流行ったのはとっかかりやすさが整備されていたからなんだろうな、と改めて感じた。

Ruby / Rails

B社の仕事がRailsAPIサーバ + ReactのSPAというよくある構成。バグ調査、機能追加の為に触った。自分の主戦場はこちらだけど、今年は仕事の都合上、フロント寄りの実装の方を多く触った。

TypeScript / React + redux B社の仕事のフロントエンドがこれだった。Reactは以前にも書いてはいたけど、ほぼ忘れていたので個人的にキャッチアップをしていった。途中フロントの全面書き直しが始まり、いきなりTypeScriptになった。まだまだ手に馴染んでないけど、型定義が柔軟すぎて面白い反面、型職人がいなくなったら混乱する現場とかあるんじゃなかろうか、とも思った。たぶん、ライブラリ以外では柔軟すぎる型定義は使わない方が良いんじゃないかと思う。今年一番触っているのはこの辺り。フロントはたぶんTypeScriptが標準言語になる雰囲気を感じた。

Python

B社のサービスのある部分が画像解析を行っていて、画像解析部分がPythonで記述されていた。運用的なことをやっていたので書かなかったけど読んだ。

Docker / docker-compose

今年は使っただけ。開発環境の構築ではほぼ必須になったと思う。来年はBuildpackを触ってみることにする。

AWS lambda

B社のサービスでPDFを出力する機能があって元々使われていた。ただPDFの容量がデカくなるとレスポンス上限に引っかかってエラーになっていたので、これを全面的に直した。PDF化にはpuppeterを使った。AWS lambdaでpuppeterを使うには工夫が必要だが、この記事のおかげでなんとかなった。 https://dev.classmethod.jp/articles/run-headless-chrome-puppeteer-on-aws-lambda/

Google Cloud Platform

なぜか今年は関わった2社共にGCPメインだった。GCPは初めてだったが、特殊な使い方をしているわけではなかった為にすぐに慣れた。一時はAWSオンリーだったけど、GCPが盛り返してきているのを感じた。

Kubernetes

B社の基盤。ほんのちょっとyamlを記述した程度で本格的に触ることはなかった。だいぶ世の中に定着した印象。来年は個人的にも学習してみることにする。ここ数年の盛り上がりを受けて良い感じの教材が増えているので学習にはなにも困らないだろう。

GraphQL

個人的に学習していた。ブログにまとめたりしたかったが、うまくまとめられなかった。残念。ServerをGo、Clientをnuxt.js + apolloで実装してみて、なんとなくのイメージは掴めたのは良かった。

gRPC

これも個人的に学習していた。Hello Worldレベルのことしか出来ていないけど、型定義書いて、Goでクライアントとサーバを建ててちょっと動かしてみた。これもイメージは掴めた。

WebRTC

仕事では微妙にさわったが、未だにピンときてない。けども数年したら当たり前に使われるようになる気がした。来年あたりちゃんと触ってみようと思う。

ワークスタイル

A社までは普通に出勤してた。 B社からいきなりフルリモートになった。もちろん、コロナの影響。 以前にもひと月ほどリモートで働いたことはあったが、今回はずっとリモート。現在の継続中。未だに誰ともリアルで対面したことがない。 おかげで生活がガラッと変わった。

通勤がなくなったり、時間の拘束がだいぶなくなったことは本当に良かったけれども、成果を出す為に働き過ぎたり、通勤がなくなったおかげか、体調を崩すことが増えてしまった。

特に11月以降は体調面に加えて、謎の無気力感に襲われて仕事にも微妙に影響が出てしまった。原因は結局のところ運動不足に起因するものだったようで、現状は午後3時になったら30分程度、散歩+縄跳びをする、軽い筋トレ+ストレッチですることで改善した。 健康、本当に大事。

あとはA社でもB社でも最年長だけど、技術的には一番下っ端な気がするので、なんとかがんばるしかないと思った次第。

まとめ

来年もなんとか生き残れるようにやれることをやるしかない。 あと本当に健康大事。

echoでリクエストパラメータをチェックするには

echoでリクエストパラメータを取得し、構造体に割り当てる方法はわかったのですが、ここまで出来ると今度はvalidateの方法が知りたくなり、調べてみました。
echoにはvalidateそのものは用意されていないのですが、インターフェイスだけが用意されていて、このインターフェイスに合わせて実装していくようです。 公式にはサンプルとして、有名なバリデーションライブラリであるところのvalidatorを使ったサンプルがあったので、これを打ち込んで検証してみました。

https://github.com/go-playground/validator

プログラム

Validateというメソッドを持った関数をechoに登録し、必要なところでValidate(構造体ポインタ)を渡す感じで呼び出すのが基本のようです。
今回であれば、CustomValidatorという構造体を登録しています。
validatorは構造体のアノテーションにvalidateしたい内容を記述していくスタイルです。今回は必須項目チェックを行うので、requiredをアノテーション内で指定しています。

package main

import (
    "net/http"

    "github.com/labstack/echo"
    "gopkg.in/go-playground/validator.v9"
)

type User struct {
    Name string `json:"name" validate:"required"`
}

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

type Error struct {
    Error string `json:"error"`
}

func main() {
    e := echo.New()
  // validatorを登録
    e.Validator = &CustomValidator{validator: validator.New()}
    e.POST("/", test)
    e.Logger.Fatal(e.Start(":1111"))
}

func test(c echo.Context) error {
    u := new(User)
  // ここでリクエストパラメータを構造体に
    if err := c.Bind(u); err != nil {
        return c.JSON(http.StatusBadRequest, err)
    }
  // Validateを実施
    if err := c.Validate(u); err != nil {
        return c.JSON(http.StatusBadRequest, &Error{Error: err.Error()})
    }
    return c.JSON(http.StatusOK, u)
}

実行

JSON形式でNameに対して空文字を送信するとエラーとなります。

curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":""}'
{"error":"Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag"}

curl -X POST http://localhost:1111 -H 'Content-Type: application/json' -d '{"Name":"test"}'
{"name":"test"}

Golangで構造体の配列を持つ構造体を初期化

微妙にわからなかったのでメモ代わりに。
Golangで以下のような構造があった時、初期化する方法がわからなかった。
構造体配列を持つ、構造体の時の初期化方法がわかってなかった。

type Store struct {
    Items []Item `validate:"dive"`
}

type Item struct {
    Name  string `validate:"required"` // 名前は必須
    Price int    `validate:"lte=100"`  // 値段は100以下
}

以下のようにすればよかった。

func main() {
    obj := Store{
        Items: []Item{
            {Name: "item1", Price: 99},
            {Name: "", Price: 100},
            {Name: "item3", Price: 101},
        },
    }
}

echoでリクエストパラメータを構造体に割り当てる

昨日のブログでリクエストパラメータの取得方法はわかりました。
ただ、昨日のやり方ではパラメータを一つ一つ変換するコードを書かないといけません。
Railsであれば、ParamにHashですべて格納されているので悩む必要もないのですが・・・

昨日のブログ
http://miyazi888.hatenablog.com/entry/2019/08/11/201510

そこで公式を調べてみた所、Bindという機能がechoでは提供されていて、これはリクエストパラメータを構造体に割り当てるもののようでした。
さっそく試してみました。

パラメータを構造体に変換

今回のサンプルではName,Priceというフィールドを持つItemという構造体を用意して、その構造体にリクエストパラメータを割り当てます。
割り当てる方法は構造体にアノテーションを定義してそこにリクエスト内の変数名を指定することで、割り当てを定義できます。
その上でBind関数を使うことで割当完了です。

プログラム

package main

import (
    "net/http"
    "strconv"

    "github.com/labstack/echo"
)

type Item struct {
    Name  string `json:"name" form:"name" query:"name"`
    Price int    `json:"price" form:"price" query:"price"`
}

func main() {
    e := echo.New()
    e.GET("/items", getItems)
    e.POST("/item", createItem)
    e.Logger.Fatal(e.Start(":1111"))
}

func getItems(c echo.Context) error {
    item := new(Item)
    if err := c.Bind(item); err != nil {
        return nil
    }
    return c.String(http.StatusOK, "name = "+item.Name+" price = "+strconv.Itoa(item.Price))
}

func createItem(c echo.Context) error {
    item := new(Item)
    if err := c.Bind(item); err != nil {
        return nil
    }
    return c.String(http.StatusOK, "name = "+item.Name+" price = "+strconv.Itoa(item.Price))
}

実行

go run main.goでサーバを起動した後、curlコマンドで動作確認します。
上からjson、form、queryでnameとpriceを渡すコマンドで、それぞれのやり方で正しく構造体にデータが割あたっていることがわかります。

curl -X POST http://localhost:1111/item -H 'Content-Type: application/json' -d '{"name":"title1", "price": 123}' > name = title1 price = 123
curl -X POST localhost:1111/item -d 'name=item1' -d 'price=123' > name = item1 price = 123
curl http://localhost:1111/items\?name\=title1\&price\=123  > name = title1 price = 123

echoのリクエストパラメータの受け取り方

リクエストパラメータの受け取り方を公式サイトを見ながら検証してみました。
と言ってもほぼ公式サイトに載ってるサンプルではあるのですが・・・
リクエストパラメータを取得する方法もシンプルなので下のプログラムを見たら、すぐにわかると思います。

リクエストパスに組み込まれているパラメータを取得する時にはParamメソッドを使う。
リクエストパスの後ろのクエリパラメータで指定されているものを取得する時にはQueryParamメソッドを使う。
POSTなどでフォームで送信しているパラメータを取得する時にはFormValueメソッドを使う。
要はパラメータの送信手段ごとに対応するメソッドが用意されている、ということです。

RailsだとすべてParamsに格納されているので、Railsに慣れた人だとちょっと戸惑うかもしれない仕様かもしれないです。

パラメータの受け取り

URLごとにリクエストパラメータを送信する手段が違うので、これで動作確認ができると思います。

プログラム

package main

import (
    "net/http"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/item/:id", getItem) // path parameter
    e.GET("/items", getItems)   // query parameter
    e.POST("/item", createItem) // form parameter
    e.Logger.Fatal(e.Start(":1111"))
}

func getItem(c echo.Context) error {
    id := c.Param("id")
    return c.String(http.StatusOK, "id = "+id)
}

func getItems(c echo.Context) error {
    name := c.QueryParam("name")
    return c.String(http.StatusOK, "name = "+name)
}

func createItem(c echo.Context) error {
    name := c.FormValue("name")
    return c.String(http.StatusOK, "create name = "+name)
}

実行

go run main.go<br>

で起動した後、別のターミナルからコマンドを叩いてみて下さい。
それぞれ返却される文字列が違うことで動作を確認できるかと思います。

curl http://localhost:1111/item/134 > id = 134
curl http://localhost:1111/items\?name\=test_name > name = test_name
curl -F "name=test-name" -X POST http://localhost:1111/item  > create name = test-name