ローカル環境 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"}