minikube をいじった時のメモ

minikube コマンド

  • minikube 起動: sudo /usr/local/bin/minikube start --vm-driver=none
  • minikube 停止: sudo /usr/local/bin/minikube stop

kubectl コマンド

  • 詳細: https://kubernetes.io/ja/docs/reference/kubectl/cheatsheet/
  • オブジェクト作成: kubectl create -f manifest.yaml
  • オブジェクト更新: kubectl apply -f manifest.yaml --record=true
  • オブジェクト削除: kubectl delete -f manifest.yaml
  • オブジェクト一覧: kubectl get po, kubectl get services
  • オブジェクト詳細: kubectl describe pods my-pod
  • Pod の数を変更する(リソースのスケーリング): kubectl scale --replicas=1 deployment/my-deployment
  • コンテナに入る: kubectl exec -it my-pod -- /bin/bash

コンテナの展開と削除

deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 5 
  selector:
    matchLabels:
      app: my-webapp
  template:
    metadata:
      labels:
        app: my-webapp
    spec:
      containers:
        - name: my-container
          image : nginx 
          ports:
            - containerPort: 80

service.yaml

apiVersion: v1
kind: Service 
metadata:
  name: my-service
spec:
  type: NodePort
  ports:
    - nodePort: 30000 
      port: 8080
      targetPort: 80
      protocol: TCP
  selector:
    app: my-webapp

実行してみる

$ kubectl create -f deploy.yaml -f service.yaml 
deployment.apps/my-deployment created
service/my-service created
$ kubectl get po
NAME                             READY   STATUS    RESTARTS   AGE
my-deployment-69f8f59c57-2hl8f   1/1     Running   0          21s
my-deployment-69f8f59c57-5q8lh   1/1     Running   0          21s
my-deployment-69f8f59c57-hz6ss   1/1     Running   0          21s
my-deployment-69f8f59c57-qqvmc   1/1     Running   0          21s
my-deployment-69f8f59c57-zppw8   1/1     Running   0          21s
$ kubectl delete -f service.yaml -f deploy.yaml
service "my-service" deleted
deployment.apps "my-deployment" deleted

YAMAHA ルータで RIP を使用する

2台のルータを RIP でルーティングします。
RT1 と RT2 はどちらも RTX810 です。
f:id:arrowislandnai:20210310004340p:plain

設定は簡単で rip use on するだけで動作します。
たぶん以下は RIPv1 で動作するはずです。

RT1

console prompt RT1
ip lan1 address 192.168.1.1/24
ip lan1 rip send off
ip lan1 rip receive off
ip lan2 address 192.168.0.1/24
rip use on

RT2

console prompt RT2
ip lan1 address 192.168.3.1/24
ip lan1 rip send off
ip lan1 rip receive off
ip lan2 address 192.168.0.2/24
rip use on

Docker Compose で WordPress を構築

さわって学ぶクラウドインフラ docker基礎からのコンテナ構築
第7章 複数コンテナをまとめて起動するDocker Compose
ラズパイで試しました。mysqlOfficial Images に
linux/arm がなかったので hypriot/rpi-mysql を使いました。

Google Cloud Platform と golang で画像掲示板を作る

f:id:arrowislandnai:20210129163734p:plain
作成した画像掲示

概要

リポジトリ: https://github.com/hiroygo/gcpbbs

Google Cloud Platform、Web APIgolang を勉強するために画像掲示板を作ってみました。

使用する Google Cloud Platform

画像掲示板の画面

index.html が画面です。見た目の調整には Bootstrap を使用しています。
画像掲示板の Web API と Fetch API で通信します。データは JSON でやり取りします。

画像掲示板の Web API

投稿

  • お名前(name)と本文(body)を JSONエンコード、画像も投稿する場合はバイナリで添付し、multipart/form-data で /posts に POST します
  • レスポンスは JSON で返ります

cURL

curl -F 'json={"name":"sophia", "body":"bowwow"};type=application/json' -F "attachment-file=@image.jpg;" localhost:8080/posts

リクエス

POST / HTTP/1.1
Host: localhost:8080/posts
Accept: */*
Content-Length: 1565
Content-Type: multipart/form-data; boundary=------------------------b4012d277e754b27
Expect: 100-continue
User-Agent: curl/7.68.0

--------------------------b4012d277e754b27
Content-Disposition: form-data; name="json"
Content-Type: application/json

{"name":"sophia", "body":"bowwow"}
--------------------------b4012d277e754b27
Content-Disposition: form-data; name="attachment-file"; filename="image.jpg"
Content-Type: image/jpeg

.....
--------------------------b4012d277e754b27--

正常処理時のレスポンス

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 31 Jan 2021 05:23:20 GMT
Content-Length: 170

{"name":"sophia","body":"bowwow","imageurl":"https://storage.googleapis.com/hogehoge/1e3edc6a-04dd-4ae1-9d3e-d442da32144c.jpeg","created_at":"2021-01-31T14:23:20+09:00"}

エラー時のレスポンス(ファイルサイズの大きな画像を添付した場合)

HTTP/1.1 413 Request Entity Too Large
Content-Type: application/json; charset=utf-8
Date: Sat, 06 Feb 2021 07:51:29 GMT
Content-Length: 37
 
{"error":"Request Entity Too Large"}

実装

func (sv *Server) postHandler(w http.ResponseWriter, r *http.Request) {
    var p Post

    // Content-Disposition のフィールドによって値がどこにセットされるかが変わる
    // フィールドに filename と name が存在するときは FormFile にセットされる
    // フィールドが name だけのときは FormValue にセットされる
    // e.g. Content-Disposition: form-data; name="json"; filename="json"
    data := r.FormValue("json")
    if data == "" {
        writeAndLogError(w, http.StatusBadRequest, fmt.Errorf("Empty JSON error"))
        return
    }
    if err := json.Unmarshal([]byte(data), &p); err != nil {
        writeAndLogError(w, http.StatusBadRequest, fmt.Errorf("Unmarshal error, %w", err))
        return
    }

    // 画像をストレージに登録する
    img, imgh, err := r.FormFile("attachment-file")
    switch err {
    case nil:
        if imgh.Size > maxFilesize {
            writeAndLogError(w, http.StatusRequestEntityTooLarge, fmt.Errorf("%s", http.StatusText(http.StatusRequestEntityTooLarge)))
            return
        }

        // 画像か確認する
        _, format, err := image.DecodeConfig(img)
        if err != nil {
            writeAndLogError(w, http.StatusUnsupportedMediaType, fmt.Errorf("DecodeConfig error, %w", err))
            return
        }
        // DecodeConfig で移動した offset を先頭に戻す
        if _, err := img.Seek(0, io.SeekStart); err != nil {
            writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("Seek error, %w", err))
            return
        }

        filename, err := randomFilename("." + format)
        if err != nil {
            writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("randomFilename error, %w", err))
        }
        url, err := sv.bucket.Upload(filename, img)
        if err != nil {
            writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("Upload error, %w", err))
            return
        }

        p.ImageURL = url
    case http.ErrMissingFile:
        p.ImageURL = ""
    default:
        writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("FormFile error, %w", err))
        return
    }

    // 投稿内容をデータベースに登録する
    ret, err := sv.db.Insert(p)
    if err != nil {
        writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("Insert error, %w", err))
        return
    }

    // 登録結果をクライアントに返す
    writeJSON(w, http.StatusOK, ret)
}

投稿一覧の取得

  • /posts に GET します
  • レスポンスは JSON で返ります。1 つの投稿には name, body, imageurl, created_at が含まれます。

cURL

curl localhost:8080/posts

レスポンス

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 31 Jan 2021 05:31:06 GMT
Content-Length: 279

[{"name":"こんにちは","body":"てすとです","imageurl":"","created_at":"2021-01-21T00:05:24+09:00"},{"name":"sophia","body":"bowwow","imageurl":"https://storage.googleapis.com/hogehoge/1e3edc6a-04dd-4ae1-9d3e-d442da32144c.jpeg","created_at":"2021-01-31T14:23:20+09:00"}]

実装

func (sv *Server) getHandler(w http.ResponseWriter, r *http.Request) {
    ps, err := sv.db.GetAll()
    if err != nil {
        writeAndLogError(w, http.StatusInternalServerError, fmt.Errorf("GetAll error, %w", err))
        return
    }
    writeJSON(w, http.StatusOK, ps)
}

Web API のテスト

server_test.go でテストしています。  

$ env DB_USER=user DB_PASS=pass INSTANCE_CONNECTION_NAME=hogehoge GCS_CREDSFILE=../key.json GCS_BUCKET=fugafuga go test -cover ./lib/ 
ok      github.com/hiroygo/gcpbbs/lib   2.696s  coverage: 58.6% of statements

投稿一覧の取得テスト

テスト前にテスト用データベースの作成し、テストしています。

func TestGetHandler(t *testing.T) {
    db, cleanup := openTestMySQL(t)
    defer cleanup(t)

    expecteds := []Post{
        Post{
            Name:     "gopher",
            Body:     "hello world!",
            ImageURL: "",
        },
        Post{
            Name:     "いぬ",
            Body:     "わんわん",
            ImageURL: "dog.jpg",
        },
    }
    insertIntoDB(t, db, expecteds)

    r := httptest.NewRequest(http.MethodGet, "/", nil)
    w := httptest.NewRecorder()
    sv := NewServer(nil, &mySQL{db})
    sv.getHandler(w, r)
    wr := w.Result()
    defer wr.Body.Close()

    if wr.StatusCode != http.StatusOK {
        t.Errorf("got error StatusCode, %v", wr.StatusCode)
    }

    var actuals []Post
    if err := json.NewDecoder(wr.Body).Decode(&actuals); err != nil {
        t.Fatal(err)
    }
    if len(expecteds) != len(actuals) {
        t.Fatalf("len does not match, len(expecteds) = %v != len(actuals) = %v", len(expecteds), len(actuals))
    }
    for i := range actuals {
        a := actuals[i]
        e := expecteds[i]
        if a.Name != e.Name || a.Body != e.Body || a.ImageURL != e.ImageURL {
            t.Errorf("want getHandler() = (%v %v, %v), got (%v, %v, %v)", e.Name, e.Body, e.ImageURL, a.Name, a.Body, a.ImageURL)
        }
    }
}

投稿テスト

テスト前にテスト用データベースを作成しています。 テスト用の Cloud Storage バケットも使い、テストしています。

func TestPostHandler(t *testing.T) {
    db, cleanup := openTestMySQL(t)
    // t.Parallel() で t.Run() が並行に動くときは defer でなく t.Cleanup() を使う
    t.Cleanup(func() { cleanup(t) })

    gcsClient := newTestGCSClient(t)
    t.Cleanup(func() {
        if err := gcsClient.Close(); err != nil {
            t.Fatal(err)
        }
    })

    bucketName, err := EnvGCSBucket()
    if err != nil {
        t.Fatal(err)
    }

    // create large png
    largePng := createTestPng(t, t.TempDir(), "largePng", maxFilesize)
    cases := []struct {
        name         string
        expectedName string
        expectedBody string
        expectedImg  []byte
        wantErr      bool
    }{
        {name: "jpeg", expectedName: "Sophia", expectedBody: "bowwow", expectedImg: loadFile(t, "../testdata/go.jpg"), wantErr: false},
        {name: "png", expectedName: "gopher", expectedBody: "hello", expectedImg: loadFile(t, "../testdata/go.png"), wantErr: false},
        {name: "noimage", expectedName: "koro", expectedBody: "wanwan", expectedImg: nil, wantErr: false},
        {name: "maxfilesize err", expectedName: "koro", expectedBody: "wanwan", expectedImg: loadFile(t, largePng), wantErr: true},
        {name: "not an image err", expectedName: "koro", expectedBody: "wanwan", expectedImg: loadFile(t, "../testdata/go.txt"), wantErr: true},
    }

    for _, c := range cases {
        c := c
        t.Run(c.name, func(t *testing.T) {
            t.Parallel()

            sv := NewServer(OpenGCSBucket(gcsClient, bucketName), &mySQL{db})
            r := newPostRequest(t, c.expectedName, c.expectedBody, c.expectedImg)
            w := httptest.NewRecorder()
            sv.postHandler(w, r)
            wr := w.Result()
            defer wr.Body.Close()

            if c.wantErr && wr.StatusCode != http.StatusOK {
                return
            }
            if wr.StatusCode != http.StatusOK {
                t.Errorf("got error StatusCode, %v", wr.StatusCode)
            }
            var actual Post
            if err := json.NewDecoder(wr.Body).Decode(&actual); err != nil {
                t.Fatal(err)
            }
            defer func() {
                if actual.ImageURL != "" {
                    deleteGCSObject(t, gcsClient, bucketName, path.Base(actual.ImageURL))
                }
            }()

            // CreatedAt はデータベースで NOW() するのでチェックしない
            if actual.Name != c.expectedName || actual.Body != c.expectedBody {
                t.Fatalf("want postHandler() = (%v, %v), got (%v, %v)",
                    c.expectedName, c.expectedBody, actual.Name, actual.Body)
            }
            if c.expectedImg != nil && actual.ImageURL == "" {
                t.Fatal("ImageURL is empty")
            }

            if c.expectedImg != nil {
                actualImg := downloadFile(t, actual.ImageURL)
                if !bytes.Equal(actualImg, c.expectedImg) {
                    t.Error("image does not match")
                }
            }
        })
    }
}

感想など

  • Google Cloud Platform は公式ドキュメントとサンプルコードが充実していたので、使いやすかったです
  • ログイン機能に使った Identity-Aware Proxy はコーディング不要で便利でした
  • golang の interface や testing、io の理解を深めることができました
  • サーバエラー時に JSON に error をそのまま入れていますが、error に外部に公開してはいけない情報などが含まれていた場合、良くない気がします
  • 画像掲示板の画面の作成では Fetch API や CORS について理解を深めることができました

参考にさせていただいたサイト

future-architect.github.io engineering.mercari.com qiita.com javorszky.co.uk deeeet.com

Raspberry Pi 上の Ubuntu コンテナで apt update すると The repository 'http...' is not signed. が発生する

現象

$ docker run --rm -it ubuntu:21.04
root@f35370427219:/# date
Thu Jan  1 00:00:00 UTC 1970
root@f35370427219:/# apt update
Get:1 http://ports.ubuntu.com/ubuntu-ports hirsute InRelease [269 kB]
Get:2 http://ports.ubuntu.com/ubuntu-ports hirsute-updates InRelease [90.7 kB]                                                                                                                                    
Err:1 http://ports.ubuntu.com/ubuntu-ports hirsute InRelease                                                                                                                                                      
  At least one invalid signature was encountered.
Get:3 http://ports.ubuntu.com/ubuntu-ports hirsute-backports InRelease [90.7 kB]
Err:2 http://ports.ubuntu.com/ubuntu-ports hirsute-updates InRelease                                                                                                                                              
  At least one invalid signature was encountered.
Err:3 http://ports.ubuntu.com/ubuntu-ports hirsute-backports InRelease                                                                                                                                            
  At least one invalid signature was encountered.
Get:4 http://ports.ubuntu.com/ubuntu-ports hirsute-security InRelease [90.7 kB]                                                                                                                                   
Err:4 http://ports.ubuntu.com/ubuntu-ports hirsute-security InRelease                                                                                                                                             
  At least one invalid signature was encountered.
Reading package lists... Done                                                                                                                                                                                     
W: GPG error: http://ports.ubuntu.com/ubuntu-ports hirsute InRelease: At least one invalid signature was encountered.
E: The repository 'http://ports.ubuntu.com/ubuntu-ports hirsute InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://ports.ubuntu.com/ubuntu-ports hirsute-updates InRelease: At least one invalid signature was encountered.
E: The repository 'http://ports.ubuntu.com/ubuntu-ports hirsute-updates InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://ports.ubuntu.com/ubuntu-ports hirsute-backports InRelease: At least one invalid signature was encountered.
E: The repository 'http://ports.ubuntu.com/ubuntu-ports hirsute-backports InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: http://ports.ubuntu.com/ubuntu-ports hirsute-security InRelease: At least one invalid signature was encountered.
E: The repository 'http://ports.ubuntu.com/ubuntu-ports hirsute-security InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.

対応

調べたら情報がありました。ちょっと見た感じ、特定の Ubuntu イメージとラズパイの組み合わせで起こるのかもしれません。 askubuntu.com

上記ページでも書かれていますが Ubuntu 18.04 だと動作しました。
最新イメージでなくてもいいので、これで良しとします。

$ docker run --rm -it ubuntu:18.04
root@4d6241812f23:/# date
Thu Feb 25 15:23:43 UTC 2021
root@4d6241812f23:/# apt update
Get:1 http://ports.ubuntu.com/ubuntu-ports bionic InRelease [242 kB]
Get:2 http://ports.ubuntu.com/ubuntu-ports bionic-updates InRelease [88.7 kB]
Get:3 http://ports.ubuntu.com/ubuntu-ports bionic-backports InRelease [74.6 kB]
Get:4 http://ports.ubuntu.com/ubuntu-ports bionic-security InRelease [88.7 kB]
Get:5 http://ports.ubuntu.com/ubuntu-ports bionic/main armhf Packages [1277 kB]
Get:6 http://ports.ubuntu.com/ubuntu-ports bionic/restricted armhf Packages [12.5 kB]
Get:7 http://ports.ubuntu.com/ubuntu-ports bionic/multiverse armhf Packages [157 kB]
Get:8 http://ports.ubuntu.com/ubuntu-ports bionic/universe armhf Packages [11.0 MB]
Get:9 http://ports.ubuntu.com/ubuntu-ports bionic-updates/restricted armhf Packages [14.9 kB]
Get:10 http://ports.ubuntu.com/ubuntu-ports bionic-updates/universe armhf Packages [1782 kB]
Get:11 http://ports.ubuntu.com/ubuntu-ports bionic-updates/main armhf Packages [1358 kB]
Get:12 http://ports.ubuntu.com/ubuntu-ports bionic-updates/multiverse armhf Packages [7497 B]
Get:13 http://ports.ubuntu.com/ubuntu-ports bionic-backports/main armhf Packages [11.2 kB]
Get:14 http://ports.ubuntu.com/ubuntu-ports bionic-backports/universe armhf Packages [11.0 kB]
Get:15 http://ports.ubuntu.com/ubuntu-ports bionic-security/main armhf Packages [984 kB]
Get:16 http://ports.ubuntu.com/ubuntu-ports bionic-security/multiverse armhf Packages [3499 B]
Get:17 http://ports.ubuntu.com/ubuntu-ports bionic-security/restricted armhf Packages [8087 B]
Get:18 http://ports.ubuntu.com/ubuntu-ports bionic-security/universe armhf Packages [1103 kB]
Fetched 18.2 MB in 7s (2525 kB/s)                                                                                                                                                                                 
Reading package lists... Done
Building dependency tree       
Reading state information... Done
3 packages can be upgraded. Run 'apt list --upgradable' to see them.

ローカル環境から Cloud SQL に Unix ソケットで接続する

詳細な解説

Cloud SQL への接続例

$ wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
$ chmod +x cloud_sql_proxy
$ gcloud init
$ sudo mkdir /cloudsql; sudo chmod 777 /cloudsql
$ ./cloud_sql_proxy -dir=/cloudsql &
$ mysql -u <USERNAME> -p -S /cloudsql/<INSTANCE_CONNECTION_NAME>