13
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

マイクロなマイクロサービスを作ってみた

Last updated at Posted at 2021-04-04

経緯

社内でモノリシックなRailsアプリケーションを分割することを検討しています。
技術調査も兼ねて、試しにぷちマイクロサービス作ってみました。

その後(追記)

結論、マイクロサービスアーキテクチャは見送ることにしました。
「組織体制はアーキテクチャは相関関係がある」というコンウェイの法則があります。
マイクロサービスアーキテクチャは、大規模な官僚主義体制のチームなどに採用するとメリットがあるアーキテクチャなので、スタートアップ企業などは採用しないほうが良いとの結論に至りました。

アーキテクチャ考えるのって難しいですね...

技術選定

フロントエンドには、社内で使用しているReactを使います。
バックエンドには、Railsでも良かったのですが、goを使ってみたかったのでgoのginというフレームワークを使います。

フロントエンド

  • React.js
  • Firebase Authentication

バックエンド

  • Golang
  • gin
  • PostgreSQL

その他

  • Kubernetes
  • Docker

テーブル構成

本の情報が取得できるというシンプルなものにしました。

テーブル名:book

カラム名 タイプ 備考
id integer primary
title string
author string

フロントエンド

Reactの簡単なアプリ作成
せっかくなのでFirebaseを使ってログイン機能を実装しました。
Firebase Authentication使うと認証機能が素早く作れるので、個人開発でサービス作るならこれな気がします。

セットアップ

React App 作成

create-react-app auth-test
cd auth-test

Reactに必要な機能のinstall

yarn add react-router-dom firebase bootstrap reactstrap react-loading-overlay formik yup axios

ログイン機能の実装

こちらの記事を参考にさせてもらいました!(感謝です)
https://qiita.com/zaburo/items/801bd288cec47bd28764

APIの内容表示の実装

Home.js
import React from 'react';
import firebase from '../Firebase';
import { Link } from 'react-router-dom';
import { Button } from 'reactstrap';
import axios from 'axios';


class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      booksList: []
    };
  }


  componentDidMount() {
    axios
      .get("/api/books")
      .then(res => this.setState({ booksList: res.data.data }))
      .catch(err => alert(err));
  }

  handleLogout = () => {
    firebase.auth().signOut();
  }

  render() {
    return (
      <div className="container">
        <p>Home</p>
        <ul>
          { this.state.booksList.map(book => <li>{book.id}:{book.title}:{book.author}</li>)}
        </ul>
        <Link to="/profile">Profileへ</Link>
        <br />
        <br />
        <Button onClick={this.handleLogout}>ログアウト</Button>
      </div>
    );
  }
}

export default Home;

Docker

結構重いので軽量化検討できそう。

Dockerfile
FROM node:12.16.1
WORKDIR /usr/src/app
COPY ["package.json", "yarn.lock", "./"]
RUN yarn install
COPY . .
ENTRYPOINT [ "yarn", "start" ]
docker-compose.yml
version: '3'
services:
  node:
    build:
      context: .
    tty: true
    environment:
      - NODE_ENV=development
    volumes:
    - ./:/usr/src/app
    command: sh -c "yarn start"
    ports:
    - "3000:3000"
    stdin_open: true # https://teratail.com/questions/249875

docker hub へpush

あとでkubernetesで使うのでpush

docker build -t hogehoge/auth-test:latest .
docker push hogehoge/auth-test:latest .

バックエンド

準備

gin

Golangのフレームワークが色々あったのですが、こちらが有名だったので使ってみました。
https://github.com/gin-gonic/gin

brew install go
mkdir gin_test && cd gin_test
go mod init gin_test
go get -u github.com/gin-gonic/gin

実装

今回はMVC的なディレクトリ構造にして実装しました。

main.go
package main

import (
	controllers "gin_test/controllers"
	"gin_test/models"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	db := models.SetupModels()

	r.Use(func(c *gin.Context){
		c.Set("db", db)
		c.Next()
	})

	r.GET("/books", controllers.FindBooks)
	r.POST("/books", controllers.CreateBook)
	r.GET("/books/:id", controllers.FindBook)
	r.PATCH("/books/:id", controllers.UpdateBook)
	r.DELETE("/books/:id", controllers.DeleteBook)
	r.Run()
}
models/setup.go
package models

import (
	"fmt"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/spf13/viper"
)

func SetupModels() *gorm.DB {
	viper.AutomaticEnv()

	viper_user := viper.Get("POSTGRES_USER")
	viper_password := viper.Get("POSTGRES_PASSWORD")
	viper_db := viper.Get("POSTGRES_DB")
	viper_host := viper.Get("POSTGRES_HOST")
	viper_port := viper.Get("POSTGRES_PORT")

	prosgret_conname := fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v sslmode=disable", viper_host, viper_port, viper_user, viper_db, viper_password)

	fmt.Println("conname is\t\t", prosgret_conname)

	db, err := gorm.Open("postgres", prosgret_conname)
	if err != nil {
		panic("Failed to connect to database!")
	}

	db.AutoMigrate(&Book{})

	m := Book{Author: "author1", Title: "title1"}

	db.Create(&m)

	return db
}

models/book.go
package models
type Book struct {
	ID uint `json:"id" gorm:"primary_key"`
	Title string `json:"title"`
	Author string `json:"author"`
}

type CreateBookInput struct {
	Title string `json:"title" binding:"required"`
	Author string `json:"author" binding:"required"`
}

type UpdateBookInput struct {
	Title string `json:"title"`
	Author string `json:"author"`
}
controllers/book.go
package controllers

import (
	models "gin_test/models"
	"net/http"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
)

func FindBooks(c *gin.Context) {
	db := c.MustGet("db").(*gorm.DB)

	var books []models.Book
	db.Find(&books)

	c.JSON(http.StatusOK, gin.H{"data": books})
}


func CreateBook(c *gin.Context) {
	db := c.MustGet("db").(*gorm.DB)
	var input models.CreateBookInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}

	book := models.Book{Title: input.Author, Author: input.Author}
	db.Create(&book)

	c.JSON(http.StatusOK, gin.H{"data": book})
}

func FindBook(c *gin.Context) {
	db := c.MustGet("db").(*gorm.DB)

	var book models.Book
	if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"data": book})
}

func UpdateBook(c *gin.Context) {
	db := c.MustGet("db").(*gorm.DB)

	var book models.Book
	if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var input models.UpdateBookInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	db.Model(&book).Updates(input)

	c.JSON(http.StatusOK, gin.H{"data": book})
}

func DeleteBook(c *gin.Context) {
	db := c.MustGet("db").(*gorm.DB)

	var book models.Book
	if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
		return
	}

	db.Delete(&book)

	c.JSON(http.StatusOK, gin.H{"data": true})
}

Docker

Dockerfile
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder
ENV GO111MODULE=on
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
ENV USER=appuser
ENV UID=10001
RUN adduser \
   --disabled-password \
   --gecos "" \
   --home "/nonexistent" \
   --shell "/sbin/nologin" \
   --no-create-home \
   --uid "${UID}" \
   "${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
RUN go mod vendor
RUN ls
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/hello -mod vendor main.go

FROM scratch
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /go/bin/hello /go/bin/hello
USER appuser:appuser
ENTRYPOINT ["/go/bin/hello"]
EXPOSE 8080
docker-compose.yaml
version: '3.7'
volumes:
  database_data:
    driver: local
services:
  db:
    image: 'postgres:latest'
    ports:
      - '5432:5432'
    expose:
      - 5432
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - database_data:/var/lib/postgresql/data
  web:
    build: .
    ports:
      - '8080:8080'
    environment:
      POSTGRES_HOST: db
      POSTGRES_PORT: 5432
    env_file:
      - .env
    links:
      - db

APIを試してみる

docker-compose build
docker-compose up
curl http://localhost:8080/books

これでget取得できるはず。

Docker hub へ push

これもあとでkubernetesで使います。

docker build -t hogehoge/gin_test:latest .
docker push hogehoge/gin_test:latest .

kubernetes

下準備

こちら参考にさせてもらいました!
https://qiita.com/kitsuki00/items/d69edca9d6d9af38fc95

minikube

ローカルで試すなら必要なのでインストールしました。
https://kubernetes.io/ja/docs/tasks/access-application-cluster/ingress-minikube/

自分はインストール時にハマったんですが、以下のissueで解決できました。
https://github.com/kubernetes/minikube/issues/7332

minikube config set vm-driver hyperkit
minikube delete
minikube start
minikube addons enable ingress

実装

クライアント側

client-cluster-ip-deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: client-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: client
  ports:
  - port: 3000
    targetPort: 3000
client-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: client
  template:
    metadata:
      labels:
        component: client
    spec:
      containers:
      - name: client
        image: ktoshiya/auth-test:latest
        ports:
        - containerPort: 3000
        resources:
          limits:
            memory: 512Mi
            cpu: "1"
          requests:
            memory: 256Mi
            cpu: "0.2"
        imagePullPolicy: Always

API側

api-cluster-ip-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: api
  ports:
  - port: 8080
    targetPort: 8080
api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
      - name: api
        image: ktoshiya/gin-test:latest
        ports:
        - containerPort: 8080
        resources:
          limits:
            memory: 512Mi
            cpu: "1"
          requests:
            memory: 256Mi
            cpu: "0.2"
        imagePullPolicy: Always
        env:
        - name: POSTGRES_HOST
          value: psql.default.svc.cluster.local
        - name: POSTGRES_PORT
          value: '5432'
        - name: POSTGRES_DB
          value: postgres
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          value: postgres

データベース

postgres-cluster-ip-deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: psql
spec:
  type: ClusterIP
  ports:
    - name: "psql-port"
      protocol: "TCP"
      port: 5432
      targetPort: 5432
  selector:
    role: db
postgres-deployment.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: init-db-sql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: psql
spec:
  replicas: 1
  serviceName: psql
  selector:
    matchLabels:
      role: db
  template:
    metadata:
      labels:
        role: db
    spec:
      containers:
      - name: psql-container
        image: postgres:latest
        resources:
          limits:
            memory: 512Mi
            cpu: "1"
          requests:
            memory: 256Mi
            cpu: "0.2"
        env:
        - name: POSTGRES_DB
          value: postgres
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          value: postgres
        volumeMounts:
        - name: init-sql-configmap
          mountPath: /docker-entrypoint-initdb.d
        - name: datadir
          mountPath: /var/lib/postgresql/data
      volumes:
        - name: init-sql-configmap
          configMap:
            name: init-db-sql
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 5G

永続化に必要らしいのでこちらも入れる

database-persistent-volume-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-persistent-volume-claim
spec:
  accessModes:
  -   ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

ingres

ロードバランサー的な役割をしてくれるらしいです。
今回は、検証しやすいように外部からもAPIにアクセスできるようにしました。

ingress-service.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - path: /?(.*)
            backend:
              serviceName: client-cluster-ip-service
              servicePort: 3000
          - path: /api/?(.*)
            backend:
              serviceName: api-cluster-ip-service
              servicePort: 8080

デプロイ

kubectl apply -f .

minikube起動

minikube start

ip確認

minikube ip

取得したipでclientに接続

http://192.168.64.2

ダッシュボードでkubernetesの状態も確認できます。

minikube dashboard

ログインすると情報が表示されるはず

感想

半日くらいでサクッと作れました。これを本番環境で使えると便利そうです。
課題はいくつかあって、バックエンドはMVCのようなディレクトリ構造でしたが、DDDのようなディレクトリ構造のほうが良いかもとか
クライアント側にはまだ、POSTの実装をしていないのでそのうちしたいとか
開発環境にkubernetesを使うのは大げさな気がするので、開発環境用のdockerも作る必要ありそうとか
色々、取り入れるには残課題があるので、近々そこを解消していきたいと思ってます。

13
16
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?