Wood_のブログ

python マルチプロセスでプロセスの終了を検出する

やりたいこと

各プロセスに永続する処理を実行させた際に、1つでも例外で終了したら他すべてのプロセスを終了させ、プログラム全体を終了させたい。

コード

import multiprocessing
import time

def worker(n):
    while True:
        time.sleep(n)
        if n==2:
            raise Exception("error")

if __name__ == '__main__':
    processes = []
    # 3つのプロセスを起動
    p1 = multiprocessing.Process(target=worker, args=(1,))
    p2 = multiprocessing.Process(target=worker, args=(2,))
    p3 = multiprocessing.Process(target=worker, args=(3,))
    p1.start()
    p2.start()
    p3.start()
    processes.append(p1)
    processes.append(p2)
    processes.append(p3)

    # どれかのプロセスが終了するまで待機
    while all([p.is_alive() for p in processes]):
        time.sleep(1)
        continue
    # すべてのプロセスを終了させる
    for p in processes:
        p.terminate()
        p.join()

出力 2秒後に例外が発生し、プログラム全体終了します。

Process Process-2:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/wood_stock/work/tmp/multi_prosecc_exc.py", line 8, in worker
    raise Exception("error")
Exception: error

LINE Bot で買い物リストを管理

きっかけ

お買い物の際、家に帰りついた時に「あ、、あれ買ってなかった、、、」となることがよくあります。1度出かけた後、その買い物だけのために外出するのは腰が上がらないので、次の機会にと思うのですが次も必ず買い忘れます。 これを防止するためメモアプリやTODOアプリをダウンロードし買い物リストを作成する事をやってきたのですが、そのアプリ自体を開く事を忘れるので、結果買い忘れます、、、。

そこでLINEであれば必ず起動するので、買い物リストが目に入る事も増え、買い忘れることはないのではないかと思いました。 買い物リストを管理できそうなbotを探してみたのですが見つからなかったので、今回LINE Botを作成してみました。

やりたいこと

  • ラインのトーク画面で、買い物リストの追加・編集する
  • 家族などと買い物リストを共有する

作ったLINEBot

(未完成な部分や、不完全な動作がある可能性はありますので、重要なデータ等の追加はご遠慮ください。)
友だち追加

構成

  • LINE Message API
    • WebHook
    • 自動応答メッセージ
  • FireBase
    • Functions (WebHookの処理、買い物リスト編集、メッセージ返信)
    • FireStore (買い物リストデータ保存)

作成手順

1. LINE Bot の作成

Botを作成するために、プロバイダーとチャネルを手順を参考に作成しました。
Messaging APIを始めよう | LINE Developers

2.自動応答メッセージ作成

送信されたメッセージ毎に返信する定型文を設定することができたので、使い方確認に使用しました。

「使い方」と送信された場合の応答メッセージを設定しています。

3. FireBase プロジェクト作成、環境作成、デプロイ

こちらのドキュメントを参考に、プロジェクト作成から一旦動作する関数をデプロイしました。firestore のエミュレータを使用すればローカルでもfunctionsとfirestoreが動作し、firestoreへのデータの追加などが本番環境のように行えたので開発がやりやすかったです。

はじめに: 最初の関数の記述、テスト、デプロイ  |  Cloud Functions for Firebase

4. functionsでの処理作成

functionsでLINE MeeageAPIのwebhookを受け取り、リスト追加などの処理を追加しました。
LINE MessageAPIのwebhookは、発生したイベントごとにイベントタイプが割り当てられています。今回は、友達追加時のfollowイベントと、メッセージ受信時のmessageイベントを使用しています。

イベント毎に受信するメッセージは、こちらのAPIリファレンスから確認できます。
メッセージ(Webhook)を受信する | LINE Developers

// 関数の作成
export const lineWebHook = functions
  .https
  .onRequest(async (req, res) => {
    if (req.method == "POST") {
      const reqBody = req.body;
      const reqEvents = reqBody.events;
      await eventHandle(reqEvents);
    }

    res.json({result: "OK"});
  });

// 発生したイベント毎の処理
const eventHandle = async (events: any[]) => {
  events.forEach(async (event) => {
    switch (event.type) {
    case "follow":
      // 友達追加された時の処理
      follow(event);
      break;
    case "message":
      // メッセージを受信した時の処理
      message(event);
      break;
    }
  });
};

実際のトーク画面

追加

複数項目をまとめて追加出来るよう、改行に対応しています。

リスト確認

各項目に連番を振って管理しています。

購入完了

割り当てられた番号を入力することで購入完了とし、リストから除外しています。
こちらも複数まとめて購入完了と出来るよう、改行に対応しています。

やってみたいこと

  • 購入されない項目のリマインド機能
    • functionsの定期実行で、一定期間購入されていない項目があれば通知する?
    • メッセージの応答ではなく配信は、無料プランの場合200通までしか送れない
  • 共有リストの場合、他のメンバーが追加した項目がある場合に通知
  • リッチメッセージで、購入完了処理を1タップで行えるようにする

vite+Vue3 単体テストを書いてみる

WebアプリケーションのフロントエンドフレームワークとしてVue3をよく使用しています。
ただ、テストコードまでは作成できておらず、コードの品質を担保することが出来ていませんでした。
今回は、公式の Vue Test Utils にあるチュートリアルを実践してみます。

テストコードを書くより、テストを走らせるまでの準備が大変でした。

A Crash Course | Vue Test Utils (vuejs.org)

実行環境

$ node -v
v18.14.0
$ npm --version
9.4.2

プロジェクト作成

vite を使用し、Vue3のプロジェクトを作成します。

# プロジェクト作成
$ npm create vite@latest
✔ Project name: … vue-test-project
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript

# 開発サーバー起動
$ cd vue-test-project
$ npm install
$ npm run dev
> vue-test-project@0.0.0 dev
> vite

  VITE v4.1.2  ready in 185 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

起動し、指定されたURLにアクセスします。

テスト用ライブラリの追加

Vueコンポーネントテスト

まずは、公式のVue Test Utils を参考にインストールします。

Installation | Vue Test Utils (vuejs.org)

$ npm install --save-dev @vue/test-utils

テストランナーで推奨されているjestをインストール

次にインストールするvue-jestに合わせるため、バージョンを固定します。

$ npm install --save-dev jest@28.1.3

jestでVueコンポーネント(.vue)をテストする場合、vue-jestというtransformerが必要という事でした。調べてもわからなかったのですが、おそらくテスト用にファイルを変換してくれるものだと思います。

こちらはVuejestのバージョンによって、パッケージとバージョンが決まっているようでした。今回はVue3 , jest@28.x なので@vue/vue3-jest@28.xをインストールしています。

GitHub - vuejs/vue-jest: Jest Vue transformer

$ npm install --save-dev @vue/vue3-jest@28

次に、テストに使用するテスト環境のためののライブラリです。 一度テストを実行したときのエラーにかかれてあったため、追加でインストールしました。 デフォルトではnodeの環境を使用するが、コンポーネントのテストをする場合はjsdomが必要なようです。

DOM 操作 · Jest

npm install --save-dev jest-environment-jsdom

ここでVueコンポーネントのテストに必要なライブラリは揃いました。

以下は不要なのですが、JavaScriptコードをテストするための準備になります。

JavaScriptテスト

jestでES6(ES2015)で書かれたJavaScriptコードをテストするためには、BabelをCommonJSに変換が必要だそうです。この変換を行うためbabel-jestをインストールします。@babel/coreはbabel-jestを使用するためにインストールしています。

babel-jest - npm

$ npm install --save-dev babel-jest@28
$ npm install --save-dev @babel/core

babelのプラグインでコードの変換する際のターゲット環境を設定できます。この環境のプリセットを設定できるライブライをインストールします。

 npm install --save-dev @babel/preset-env

Jestの設定

package.jsonにscriptsでテスト実行コマンドを追加します。 今回は/testsディレクトリ以下にテストコードを作成していくため、jest testsとしています。

{
  "scripts": {
    "test:unit": "jest tests"
  }
}

他の設定もpackage.jsonにまとめて追加します。jest.config.jsonという名前で別ファイルを準備してもいいみたいです。

  • testEnvironment: テスト環境
  • testEnvironmentOptions: テスト環境オプション
  • moduleFileExtensions: モジュールが使用するファイル拡張子
  • transform: トランスフォーマーが変換するファイルの指定
  • moduleNameMapper: 正規表現でimportされたモジュール名にマッチするものを置き換え
    • import Component from ../../src/components/HelloWorld と書いていたが src/ までのパスを@/で書くことが出来るようになります。
{
    "jest": {
        "testEnvironment": "jsdom",
        "testEnvironmentOptions": {
          "customExportConditions": [
            "node",
            "node-addons"
          ]
        },
        "moduleFileExtensions": [
          "js",
          "json",
          "vue"
        ],
        "transform": {
          ".*\\.(js)$": "babel-jest",
          ".*\\.(vue)$": "@vue/vue3-jest"
        },
        "moduleNameMapper": {
          "^@/(.*)$": "<rootDir>/src/$1"
        }
      }
}

Jestの設定 · Jest

Babelの設定

babelの設定もpackage.jsonに書いていきます。babel.config.jsという名前で別ファイルを準備してもいいみたいです。

  • presets: プリセットとして利用するプラグインのリスト
    • node: current とすることで、現在のnode.jsに合わせたコードに変換をしてくれる
{
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "node": "current"
          }
        }
      ]
    ]
  }
}

Presets · Babel

これでやっとテストの準備ができました。(実際は何度もテスト実行し、足りていないライブラリや、設定を追加していきました。)

テストコード作成

ここからはチュートリアルに従って、テストコードを書いていきます。Options APIのスタイルで書かれていたが、新しいComposition APIのスタイルに変更しています。 本来はテストコードを作成し、そのテストが通るように修正していくのですが、こちらは全ての修正が完了した状態になります。

テスト対象コンポーネント

/src/components/TodoApp.vue

<script setup>
import { ref } from "vue"
const todos = ref([
    {
        id: 1,
        text: "Learn Vue.js 3",
        completed: false
    }
]);

const newTodo = ref("");

const createTodo = () => {
    todos.value.push(
        {
            id: 2,
            text: newTodo,
            completed: false
        }
    );
};
</script>
<template>
    <div>
        <div v-for="todo in todos" :key="todo.id" data-test="todo" :class="[todo.completed ? 'completed' : '']">
            {{ todo.text }}
            <input type="checkbox" v-model="todo.completed" data-test="todo-checkbox" />
        </div>

        <form data-test="form" @submit.prevent="createTodo">
            <input data-test="new-todo" v-model="newTodo" />
            <input type="submit">
        </form>
    </div>
</template>

テストコード

/tests/unit/TodoApp.spec.js

import { mount } from "@vue/test-utils"
import TodoApp from "@/components/TodoApp"

test("renders a todo", () => {
    const wrapper = mount(TodoApp);

    const todo = wrapper.get("[data-test='todo']");

    expect(todo.text()).toBe("Learn Vue.js 3");
})

test("creates a todo", async () => {
    const wrapper = mount(TodoApp);

    await wrapper.get("[data-test='new-todo']").setValue("New todo");
    await wrapper.get("[data-test='form']").trigger("submit");

    expect(wrapper.findAll("[data-test='todo']")).toHaveLength(2);
})

test("completes a todo", async () => {
    const wrapper = mount(TodoApp);

    await wrapper.get("[data-test='todo-checkbox']").setValue(true);

    expect(wrapper.get("[data-test='todo']").classes()).toContain("completed");
})

テスト実行

$ npm run test:unit

> vue-test-project@0.0.0 test:unit
> jest tests

 PASS  tests/unit/TodoApp.spec.js
  ✓ renders a todo (21 ms)
  ✓ creates a todo (8 ms)
  ✓ completes a todo (5 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.887 s, estimated 1 s
Ran all test suites matching /tests/i.

カバレッジ出力

簡単な見方としては

  • Stmts: C0 実行された行の網羅率
  • Branch: C1 ifなのど分岐された処理の網羅率
  • Funcs: 各関数呼び出し網羅率
  • Lines: ファイルの各実行可能行が実行されたかの網羅率。Stmts と同じ?
$ npm run test:unit -- --coverage

> vue-test-project@0.0.0 test:unit
> jest tests --coverage

[vue-jest]: Not found tsconfig.json.

 PASS  tests/unit/TodoApp.spec.js
  ✓ renders a todo (22 ms)
  ✓ creates a todo (8 ms)
  ✓ completes a todo (4 ms)

-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------|---------|----------|---------|---------|-------------------
All files    |     100 |      100 |     100 |     100 |                   
 TodoApp.vue |     100 |      100 |     100 |     100 |                   
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.903 s, estimated 1 s
Ran all test suites matching /tests/i.

/coverage/lcov-report/index.htmlにレポートも出力されるようです。便利。

ローカル環境 kubernetesでWebアプリ起動

kubernetesの基礎習得のため、Webアプリの起動にトライした記事になります。
goで作成したWebアプリをkubernetesクラスタで起動し、DBのデータの追加・参照できるようににしていきます。

構成図

環境

OS: Windows10 docker: 20.10.10 kind: 0.17.0

事前準備

kubernetesクラスタの準備とDBを起動します。お試しなので全てローカルで起動します。クラウドを使用した場合料金がかかるので、一旦試してみたい方はこの方法で試すと良いかなと思います。

クラスタ起動

kind を使用し、Dockerコンテナでマルチノードのクラスタを起動します。プライベートなコンテナレジストリからイメージを取得するため、認証情報(deploy token)をsecret.json経由で与えています。

kind-cluster.yaml

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: webapp-cluster
nodes:
- role: control-plane
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json
- role: worker
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json
- role: worker
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json

secret.json

{
    "auths": {
        "registry.gitlab.com": {
            "auth": "****"
        }
    }
}
$  kind create cluster --config kind-cluster.yaml

$ docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}"
46911f618580    kindest/node:v1.25.3    webapp-cluster-control-plane
1bcddfaf552f    kindest/node:v1.25.3    webapp-cluster-worker
aac1c231d95d    kindest/node:v1.25.3    webapp-cluster-worker2

クラスタを起動すると、1つのマスタ(control-plane)と2つのワーカ(woker)コンテナが起動していることが確認できます。

DB起動

kindで起動すると3つのコンテナが作成され、kindというDockerネットワークが作成されます。このネットワーク内に、DBコンテナを起動していきます。DBはPostgres14を使用します。

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
d67081ee3cc5   bridge    bridge    local
d705790474c7   host      host      local
62f01985424e   kind      bridge    local ⇐ これ
0d2e79587807   none      null      local

# DBコンテナ起動
# --nat=kind ネットワーク指定
# -e POSTGRES_PASSWORD=password パスワードを環境変数で設定
$ docker run --name db  -e POSTGRES_PASSWORD=password -d --net kind postgres:14

$ docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}"
6aaa1e918b9f    postgres:14     db
46911f618580    kindest/node:v1.25.3    webapp-cluster-control-plane
1bcddfaf552f    kindest/node:v1.25.3    webapp-cluster-worker
aac1c231d95d    kindest/node:v1.25.3    webapp-cluster-worker2

DB、テーブル作成

コンテナへ接続し、psqlコマンドで直接作成していきます。

# コンテナ接続
$ docker exec -it <コンテナID> bash
# Postgres DB接続
$ psql -U postgres

# DB作成
create database sample;
# DB切り替え
\c sample
# テーブル作成
create table users(
  id serial,
  name varchar
);

Webアプリの作成

golang echoフレームワークでuserの作成、参照を行えるエンドポイントを定義します。

  • GET: /users/:id パスで指定されたidのユーザを返す
  • POST: /users ユーザを1件追加し、追加されたユーザを返す

src/server.go

package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "gitlab.com/test/kubernetes-web-app/src/handler"
)

func main() {
    handler.Init()

    e := echo.New()

    e.GET("/users/:id", handler.GetUser())
    e.POST("/users", handler.AddUser())

    e.Logger.Fatal(e.Start(":1323"))
}

src/handler/user.go

package handler

import (
    "database/sql"
    "fmt"
    "net/http"
    "os"

    "github.com/labstack/echo/v4"
    _ "github.com/lib/pq"
)

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}

var content string
var Db *sql.DB

func Init() {
    var err error
    Db, err = sql.Open(
        "postgres",
        fmt.Sprintf(
            "host=%s port=%s user=%s password=%s dbname=sample sslmode=disable",
            os.Getenv("DB_HOST"),
            os.Getenv("DB_PORT"),
            os.Getenv("DB_USER"),
            os.Getenv("DB_PASSWORD"),
        ),
    )
    if err != nil {
        panic(err)
    }
}

func GetUser() echo.HandlerFunc {
    return func(c echo.Context) error {
        id := c.Param("id")
        user := User{}
        if err := Db.QueryRow("SELECT id, name FROM users where id = $1", id).Scan(&user.Id, &user.Name); err != nil {
            return echo.NewHTTPError(http.StatusInternalServerError, err)
        }
        return c.JSON(http.StatusOK, user)
    }
}

func AddUser() echo.HandlerFunc {
    return func(c echo.Context) error {
        name := c.FormValue("name")
        newId := 0
        if err := Db.QueryRow("INSERT INTO users(name) VALUES($1) RETURNING id", name).Scan(&newId); err != nil {
            return echo.NewHTTPError(http.StatusInternalServerError, err)
        }

        user := User{}
        if err := Db.QueryRow("SELECT id, name FROM users where id = $1", newId).Scan(&user.Id, &user.Name); err != nil {
            return echo.NewHTTPError(http.StatusInternalServerError, err)
        }
        return c.JSON(http.StatusOK, user)
    }
}

Dockerイメージの作成

作成したアプリをDockerイメージ化して、コンテナレジストリへ追加します。リポジトリへPushしたときに自動で実行されるようGitlab-runner設定と、.gitlab-ci.ymlファイルでそのコマンドを記載しています。ただこのファイルでは Dockerイメージにlatestタグしか付かず、上書きされる形になるので、ビルドイメージごとにタグを切り替えていけるよう修正が必要です。

Dockerfile

apiVersion: v1
kind: Service
metadata:
  name: webapp-service
spec:
  ports:
  - port: 1323
    targetPort: 1323
    protocol: TCP
    name: http
  selector:
    app: webapp
  type: LoadBalancer

.gitlab-ci.yml

stages:
  - build

image-build:
  stage: build
  tags:
    - docker
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker info
    - docker build -t ${CI_REGISTRY}/test/kubernetes-web-app . 
    - docker tag ${CI_REGISTRY}/test/kubernetes-web-app ${CI_REGISTRY}/test4038/kubernetes-web-app:latest
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker image push --all-tags ${CI_REGISTRY}/test/kubernetes-web-app

kubernetes マニフェスト作成、起動

Webアプリをdeploymentで起動し、そのPodへのアクセスを分散するめLoadbalancerを起動します。DBの接続情報であるuser/paswordはコードに直接記載したくないので、Secretを作成しそこから取得できるようにしています。

deployment, service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-deployment
  labels:
    app: webapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: webapp
        image: registry.gitlab.com/test4038/kubernetes-web-app:latest
        ports:
        - containerPort: 1323
        env:
        - name: DB_HOST
          value: db
        - name: DB_PORT
          value: "5432"
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: webapp-secret
              key: user
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: webapp-secret
              key: password
---
apiVersion: v1
kind: Service
metadata:
  name: webapp-service
spec:
  ports:
  - port: 1323
    targetPort: 1323
    protocol: TCP
    name: http
  selector:
    app: webapp
  type: LoadBalancer

secret .datakey : value の形で複数の値を設定できます。値はbase64エンコードしてから設定する必要があります。

apiVersion: v1
kind: Secret
metadata:
  name: webapp-secret
type: Opaque
data:
  user: cG9zdGdyZXMK # postgres
  password: cGFzc3dvcmQK # epassword

マニフェストが作成できたので、kubernetesクラスタでアプリを起動していきます。

# Secret作成
$ kubectl apply -f secret.yaml

# マニフェストから起動
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

動作確認

curlコマンドでHTTPリクエストし、userの追加、参照が出来ることを確認しています。

# ポートフォワーディング
$ kubectl port-forward service/webapp-service 1323:1323

# アクセス確認
# POST user作成
$ curl -X POST localhost:1323/users -d 'name=hoge'
{"id":1,"name":"hoge"}
# GET user参照
$ curl localhost:1323/users/1
{"id":1,"name":"hoge"}

kubernetesでwebアプリケーションを起動

kubernetes の基礎的な勉強の一環で、Webアプリケーションの構築からアクセスまでを実践しています。

構成

クラスタの構築にはkindを使用し、ローカル環境でDockerコンテナを使い3つのノードを起動します。(Dockerがインストールされていること)

アプリは Go の WebFramework Echo を使用して簡単なものを作成し、Deploymentで3つのPodを起動させます。
アクセスはLoadBalancerで各Podに分散するようになっています。

コードをGitLabで管理していたため、DockerイメージはGitLabのコンテナレジストリを使用しています。

Webアプリケーション作成

まずは直接ローカルでWebアプリケーションを起動して、動作確認してみます。
GETメソッドで2つエンドポイントを定義した簡単なものです。

  • / : HelloWorld を返す
  • /users : クエリパラメータで受け取った name, email 値を返す

main.go

package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
)

type User struct {
    Name  string `json:"name" query:"name"`
    Email string `json:"email" query:"email"`
}

func main() {
    e := echo.New()

    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })

    e.GET("/users", func(c echo.Context) error {
        u := new(User)
        if err := c.Bind(u); err != nil {
            return err
        }
        return c.JSON(http.StatusOK, u)
    })

    e.Logger.Fatal(e.Start(":1323"))
}

echo.labstack.com

動作確認

2つのエンドポイントにアクセスし、レスポンスが返ってくることを確認

# アプリケーション起動
$ go run main.go

# アクセス
$ curl http://localhost:1323/
Hello, World!
$ curl http://localhost:1323/users?name=hoge&email=hoge@example.com
{"name":"hoge","email":"hoge@example.com"}

Dockerイメージ作成

kubernetesで起動させるためDockerイメージ化し、コンテナ上で起動できるか確認

Dockerfile

FROM golang:1.19.5-buster as build

WORKDIR /app
COPY go.mod ./
COPY go.sum ./
COPY server.go ./

RUN go mod download
RUN go build -o /webapp

FROM gcr.io/distroless/base-debian11

WORKDIR /

COPY --from=build /webapp /webapp

USER nonroot:nonroot

ENTRYPOINT ["/webapp"]

コンテナで起動して動作確認

同じくコンテナでもアクセスを確認

# イメージビルド
$ docker image build -t webapp .
# コンテナ起動
$ docker run -p 1323:1323 webapp

# アクセス
$ curl http://localhost:1323/
Hello, World!
$ curl http://localhost:1323/users?name=hoge&email=hoge@example.com
{"name":"hoge","email":"hoge@example.com"}

コンテナレジストリへDockerイメージのpush

GitLab Runner を使用して、パイプライン実行時にイメージのbuild,コンテナレジストリへのpush を行います
Runner は自前で起動せずGitLabのSharedRunnerを有効化し、共有されているものを使用します

.gitlab-ci.yml

stages:
  - build

image-build:
  stage: build
  tags:
    - docker
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker info
    - docker build -t ${CI_REGISTRY}/kubernetes-web-app:${CI_COMMIT_SHA} . 
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker image push ${CI_REGISTRY}/kubernetes-web-app:${CI_COMMIT_SHA}

GitLab DeployTokenの発行

コンテナレジストリからDockerイメージを取得するため、DeployTokenを発行します
「Setting」⇒「Repository」⇒「Depoy tokens」

Pull an Image from a Private Registry | Kubernetes

kuberntesクラスタの起動

クラスタの定義はkind-cluster.yaml で定義し,Dockerコンテナで起動。GitLabのコンテナレジストリから、Dockerイメージを取得するためconfig.jsonをマウントしてクラスタに認証情報を与えます。

config.json

発行したDeployTokenを{username}:{password} の形式でbase64エンコードしたものをauthに使用します。
Scopeはread_registryが必要です。

# DeployToken base64エンコード
$ echo -n {username}:{password} | base64 
Z2l*******enZf

# config.jaon
{
    "auths": {
        "registry.gitlab.com": {
            "auth": "Z2l*******enZf"
        }
    }
}

kind-cluster.yaml

config.json を各ノードにマウントし、GitLabの認証情報を与えます

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: webapp-cluster
nodes:
- role: control-plane
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json
- role: worker
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json
- role: worker
  extraMounts:
  - containerPath: /var/lib/kubelet/config.json
    hostPath: ./secret.json

ノードの定義ファイルが作成できたら、クラスタを起動していきます

$ kind create cluster --config kind-cluster.yaml

Pod,Serviceリソースの作成, アクセス確認

Deployment, Service にマニフェストを作成し、クラスタ上で起動します。kindで作成されるクラスタでは、LoadBalancerを作成してもコンテナ外からアクセス出来ないため、一時的にポートフォワーディングを行いアクセスする。

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-deployment
  labels:
    app: webapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: webapp
        image: registry.gitlab.com/kubernetes-web-app:latest
        ports:
        - containerPort: 1323

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: webapp-service
spec:
  ports:
  - port: 1323
    targetPort: 1323
    protocol: TCP
    name: http
  selector:
    app: webapp
  type: LoadBalancer
# リソース作成
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

# ポートフォワーディング
$ kubectl port-forward service/webapp-service 1323:1323

# アクセス
$ curl http://localhost:1323/
Hello, World!
$ curl http://localhost:1323/users?name=hoge&email=hoge@example.com
{"name":"hoge","email":"hoge@example.com"}

SQL 縦持ち、横持ち変換

縦持ち、横持ちテーブルの相互変換方法
こちらで簡単に、SQLの確認を行えます。(paiza.io MySQL Online)

縦 ⇒ 横

使用するテーブル

id child_id name
1 1 "1-1"
1 2 "1-2"
2 1 "2-1"
2 2 "2-2"
2 3 "2-3"
3 1 "3-1"
3 2 "3-2"

SQL MAX句は、GROUP BY でグループ化した場合は集約関数を使う必要があるため使用しているだけで、関数としての意味はないです。

select 
    id,
    MAX(CASE WHEN child_id = 1 THEN name END) as child_id_1,
    MAX(CASE WHEN child_id = 2 THEN name END) as child_id_2,
    MAX(CASE WHEN child_id = 3 THEN name END) as child_id_3
FROM vertical
GROUP BY id;

実行結果

id child_id_1 child_id_2 child_id_3
1 "1-1" "1-2" NULL
2 "2-1" "2-2" "2-3"
3 "3-1" NULL NULL

横 ⇒ 縦

使用するテーブル

id child_id_1 child_id_2 child_id_3
1 1-1 1-2
2 2-1 2-2 2-3
3 3-1

SQL

SELECT * FROM (
    SELECT id, 1 as child_id, child_id_1 as name  FROM horizon
    UNION ALL
    SELECT id, 2 as child_id, child_id_2 as name le FROM horizon
    UNION ALL
    SELECT id, 3 as child_id, child_id_3 as name FROM horizon
) AS vertical
WHERE name  is NOT NULL;

実行結果

id child_id name
1 1 1-1
2 1 2-1
3 1 3-1
1 2 1-2
2 2 2-2
2 3 2-3

標準出力、標準エラー出力 をファイルに出力する

# 標準出力
$ echo hogehoge > out.txt

# 標準エラー出力
$ echoo 2> out.txt

# 標準出力 & 標準エラー出力
$ echo hogehoge > out.txt 2>&1

# ※追記したい場合
$ echo hogehoge >> out.txt 2>>&1

# ※出力を破棄する場合
$ echo hogehoge > /dev/nulll 2>&1