🎃

【Kubernetes】Nginx経由でAPIに接続

2022/02/10に公開

はじめに

KubernetesとGoの勉強にため、以下のようなアプリを作成しました。

実装の詳細は、以下のリポジトリをご覧いただけますと幸いです。
https://github.com/wakabaseisei
今回は、フロントエンドのリクエストを、バックエンドAPIのエンドポイントにリバースプロキシする流れをつらつら書いていこうと思います。

使用技術

フロントエンドはReact、バックエンドはGoで実装します。APIには、Nginx経由でリクエストを投げます。何の変哲のないシンプルな構成です。

この記事で説明しないこと

今回は、Reactの具体的な実装や、Goのコードの詳細には触れません。

実装

クラスタ作成

APIを公開するPodを作成するために、以下のDeploymentを定義します。

goapi.yml
apiVersion: v1
kind: Service
metadata:
  name: goapi
  labels:
    app: goapi
spec:
  selector:
    app: goapi
  ports:
    - protocol: TCP
      port: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: goapi
  labels:
    app: goapi
spec:
  replicas: 1
  selector:
    matchLabels:
      app: goapi
  template:
    metadata:
      labels:
        app: goapi
    spec:
      containers:
        - name: goapi
          image: wakabaseisei/goapi:latest
          resources:
            limits:
              memory: "128Mi"
              cpu: "500m"
          ports:
            - containerPort: 8080
              protocol: TCP

続いて、Nginxの設定ファイルを、ConfigMapで記述します。これを使って、後述のNginxコンテナにVolumeとしてマウントします。

nginx-config.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx.conf: |
    user nginx;
    worker_processes  auto;
    error_log  /var/log/nginx/error.log;
    events {
      worker_connections  10240;
    }
    http {
      log_format  main
              'remote_addr:$remote_addr\t'
              'time_local:$time_local\t'
              'method:$request_method\t'
              'uri:$request_uri\t'
              'host:$host\t'
              'status:$status\t'
              'bytes_sent:$body_bytes_sent\t'
              'referer:$http_referer\t'
              'useragent:$http_user_agent\t'
              'forwardedfor:$http_x_forwarded_for\t'
              'request_time:$request_time';

      access_log    /var/log/nginx/access.log main;

      # バーチャルサーバを立てる
      server {
          listen       80;

	  # APIへのリクエストは、"/users"から始まる
	  # ”/users(/:id)”のリクエストは、http://goapi:8080(/:id)にリバースプロキシする
          location /users {
            proxy_pass  http://goapi:8080;
          }

          # rootのコンテンツを返す
	  # これは、後述するReactプロジェクトでyarn buildしたファイルをコピーしたもの
          location / {
            root  /usr/share/nginx/html;
            index index.html;
          }
      }
    }

フロントエンド用のPodを、Deploymentで宣言します。
Serviceを、NodePortで設定しており、ブラウザでは、以下でアクセスします。
"https://localhost:30090"
まだ、アクセスしても、何も起こりません。

node-cli.yml
apiVersion: v1
kind: Service
metadata:
  name: node
spec:
  type: NodePort
  selector:
    app: node
  ports:
    - port: 80
      targetPort: node-port
      nodePort: 30090
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node
  labels:
    app: node
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node
  template:
    metadata:
      labels:
        app: node
    spec:
      containers:
        - name: node
          image: wakabaseisei/reactapp:latest
          resources:
            limits:
              memory: "128Mi"
              cpu: "500m"
          ports:
            - containerPort: 80
              name: node-port
              protocol: TCP
	  # 下記のvolumesを、/etc/nginxにマウントすることで、nginxの設定ファイルとして読み込まれる
          volumeMounts:
            - mountPath: /etc/nginx
              readOnly: true
              name: nginx-conf
      # 先ほど定義したConfigMapをvolumesとして宣言している
      volumes:
        - name: nginx-conf
          configMap:
            name: nginx-conf
            items:
              - key: nginx.conf
                path: nginx.conf

フロントエンド

axiosを使って、Nginxサーバにリクエストを送信しています。
axiosの共通設定部分です。

const.ts
export const BASE_URI = "http://localhost:30090";

export enum ResponseStatusCode {
  Success = 200,
  Fail = 500,
}
axios.ts
import axios from "axios";
import { BASE_URI } from "../Config/const";

const instance = axios.create({
  baseURL: BASE_URI,
  headers: {
    "Content-Type": "application/json",
  },
});

export default instance;

以下は、Userの削除リクエストを送信している箇所です。

UserScreen.tsx
 const deleteUser = async (userId: number) => {
    try {
      const apiResponseResult = await instance.delete(`/users/${userId}`);
      console.log(apiResponseResult);
      await fetchUsers(true);
    } catch (error) {
      alert("削除に失敗しました");
      console.error("ユーザー削除失敗");
    }
  };

これで、
"http://localhost:30090/users/${userId}"
にDELETEリクエストが投げられます。
リクエストは、外部公開用のNodePortタイプのServiceが受け入れます。
NodePortは、0.0.0.0:30090で公開されており、受け入れられた後、ClusterIPの80番ポートに転送されます。
次は、同じラベル・セレクター(app: node)を持つPodの中の、targetPortに指定されている、Nginxコンテナに向かって転送され、containerPortの80でNginxコンテナまで辿り着きます。

Nginxは、設定ファイルで定義した通り、80番ポートでリクエストを待ち受けており、"/users/:userId"へのリクエストは、バックエンドAPIに転送されます。
今回の例だと、無事に削除処理が実行されて、結果がクライアントに返されます。

それでは、Podで扱えるように、コンテナのイメージを作成します。

Dockerfile
FROM node:14 AS builder
RUN apt-get update && apt-get install -y build-essential && apt-get install -y openjdk-8-jdk && apt-get install -y lsof
WORKDIR /frontend
COPY ./frontend .
WORKDIR /frontend/app
RUN yarn install && \
    yarn build

FROM nginx:1.20-alpine AS prod

COPY --from=builder /frontend/app/build /usr/share/nginx/html

CMD [ "nginx", "-g", "daemon off;" ]

ビルド用と配布用とで、ターゲットを分けています。
配布用は、yarn buildで生成されたファイルを、nginxイメージの/usr/share/nginx/htmlにコピーしています。
マルチステージビルドに関しては、以下をご参照ください。
https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

Dockerfileをもとに、ビルドして、DockerHubにプッシュしています。

$ docker build . --target prod -t wakabaseisei/reactapp -f docker/node/Dockerfile
$ docker push wakabaseisei/goapi:latest

バックエンド

今回は、GoのWebフレームワークとして、Ginを使っています。ルーティング処理をしている箇所を以下に掲載します。

Routing.go
package infrastructure

import (
	"github.com/gin-gonic/gin"
	"github.com/wakabaseisei/runapp/interfaces/controllers"
)

type Routing struct {
	DB   *DB
	Gin  *gin.Engine
	Port string
}

func NewRouting(db *DB) *Routing {
	c := NewConfig()
	r := &Routing{
		DB:   db,
		Gin:  gin.Default(),
		Port: c.Routing.Port,
	}
	r.setRouting()
	return r
}

func (r *Routing) setRouting() {
	userController := controllers.NewUserController(r.DB)
	r.Gin.GET("/users", func(c *gin.Context) { userController.GetAll(c) })
	r.Gin.GET("/users/:id", func(c *gin.Context) { userController.Get(c) })
	r.Gin.POST("/users", func(c *gin.Context) { userController.Post(c) })
	r.Gin.DELETE("/users/:id", func(c *gin.Context) { userController.Delete(c) })
	r.Gin.PUT("/users/:id", func(c *gin.Context) { userController.Update(c) })
}

func (r *Routing) Run() {
	r.Gin.Run(r.Port)
}

Userの作成・取得(全件・個別)・更新・削除を受け付ける、エンドポイントを用意しています。各APIは、"/users"で始まっています。
他の実装部分もご覧になりたい場合は、以下のリポジトリをご覧ください。
https://github.com/wakabaseisei/runapp

Dockerfileです。
開発用とビルド用と配布用とで、ターゲットを分けています。

Dockerfile
# 開発用イメージ
FROM golang:1.17.2-alpine3.14 as dev
ENV CGO_ENABLED=0

COPY ./src/go.mod /go/src/go.mod
COPY ./src/go.sum /go/src/go.sum

RUN apk update \
  && apk add --no-cache git

WORKDIR /go/src
RUN go mod download

COPY src/ /go/src/

# ビルド用イメージ
FROM golang:1.17.2-alpine3.14 as builder

ENV CGO_ENABLED=0

COPY src/go.mod /go/src/go.mod
COPY src/go.sum /go/src/go.sum

RUN apk update \
  && apk add --no-cache git

WORKDIR /go/src
RUN go mod download

COPY src/ /go/src/
RUN GOOS=linux go build -o ./binary

# 本番用イメージ
FROM scratch as prod

COPY --from=builder /go/src/binary /go/src/

EXPOSE 8080
CMD ["/go/src/binary"]

Podのコンテナで扱うイメージをビルドして、DockerHubにプッシュしています。

$ docker build . --target prod -t wakabaseisei/goapi -f docker/go/Dockerfile
$ docker push wakabaseisei/goapi:latest

あとは、Kubernetesの各リソースをアプライしていきます。

Discussion