経緯
社内でモノリシックな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の内容表示の実装
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
結構重いので軽量化検討できそう。
FROM node:12.16.1
WORKDIR /usr/src/app
COPY ["package.json", "yarn.lock", "./"]
RUN yarn install
COPY . .
ENTRYPOINT [ "yarn", "start" ]
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的なディレクトリ構造にして実装しました。
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()
}
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
}
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"`
}
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
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
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
実装
クライアント側
apiVersion: v1
kind: Service
metadata:
name: client-cluster-ip-service
spec:
type: ClusterIP
selector:
component: client
ports:
- port: 3000
targetPort: 3000
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側
apiVersion: v1
kind: Service
metadata:
name: api-cluster-ip-service
spec:
type: ClusterIP
selector:
component: api
ports:
- port: 8080
targetPort: 8080
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
データベース
apiVersion: v1
kind: Service
metadata:
name: psql
spec:
type: ClusterIP
ports:
- name: "psql-port"
protocol: "TCP"
port: 5432
targetPort: 5432
selector:
role: db
---
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
永続化に必要らしいのでこちらも入れる
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-persistent-volume-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
ingres
ロードバランサー的な役割をしてくれるらしいです。
今回は、検証しやすいように外部からもAPIにアクセスできるようにしました。
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も作る必要ありそうとか
色々、取り入れるには残課題があるので、近々そこを解消していきたいと思ってます。