├── .env.dev ├── .env.prod ├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── config.yml ├── database ├── handler.go └── mongo │ └── mongo.go ├── kubernetes ├── db-seed.yaml ├── deployment.yaml └── service.yaml ├── loadtest ├── .env.dev ├── .env.prod ├── Dockerfile ├── deployment.yaml ├── locust │ ├── locustfile.py │ ├── run.sh │ ├── todos_pb2.py │ └── todos_pb2_grpc.py └── service.yaml ├── main.go ├── models └── models.go └── server ├── grpc ├── grpc.go ├── todos.pb.go └── todos.proto └── runner.go /.env.dev: -------------------------------------------------------------------------------- 1 | # App 2 | SERVER_PORT=8001 3 | DB_HOST=todo-api-mongodb.default.svc.cluster.local 4 | DB_PORT=27017 5 | 6 | # General 7 | ENV=dev 8 | COMPONENT=todo-api 9 | ROLE=grpc-server 10 | CLUSTER_NAME=any 11 | CLUSTER_ZONE=any 12 | 13 | # Service 14 | SERVICE_CONFIG=./kubernetes/service.yaml 15 | SERVICE_NAME=todo-api 16 | SERVICE_PORT=80 17 | 18 | # Deployment 19 | IMAGE_NAME=todo-api-go-grpc 20 | CONTAINER_PORT=8001 21 | CONTAINER_REGISTRY=local 22 | DEPLOYMENT_CONFIG=./kubernetes/deployment.yaml 23 | DB_MIGRATIONS_JOB_CONFIG_SEED=./kubernetes/db-seed.yaml 24 | 25 | # Docker 26 | DOCKER_BUILD_ARGS=PROJECT=github.com/stphivos/todo-api-go-grpc ENV=dev 27 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | # App 2 | SERVER_PORT=8001 3 | DB_HOST=todo-api-mongodb.default.svc.cluster.local 4 | DB_PORT=27017 5 | 6 | # General 7 | ENV=prod 8 | COMPONENT=todo-api 9 | ROLE=grpc-server 10 | CLUSTER_NAME=todoapp-prod-cluster 11 | CLUSTER_ZONE=us-central1-a 12 | 13 | # Service 14 | SERVICE_CONFIG=./kubernetes/service.yaml 15 | SERVICE_NAME=todo-api 16 | SERVICE_PORT=80 17 | 18 | # Deployment 19 | IMAGE_NAME=gcr.io/todoapp-123123/todo-api-go-grpc 20 | CONTAINER_PORT=8001 21 | CONTAINER_REGISTRY=gcr 22 | DEPLOYMENT_CONFIG=./kubernetes/deployment.yaml 23 | DB_MIGRATIONS_JOB_CONFIG_SEED=./kubernetes/db-seed.yaml 24 | 25 | # Docker 26 | DOCKER_BUILD_ARGS=PROJECT=github.com/stphivos/todo-api-go-grpc ENV=prod 27 | DOCKER_MACHINE_NAME=default 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.pyc 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # https://github.com/golang/dep/blob/master/docs/FAQ.md#should-i-commit-my-vendor-directory 16 | vendor/ 17 | 18 | # Mac 19 | .DS_Store 20 | 21 | # IDE 22 | .vscode/ 23 | .idea/ 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.9.2-alpine AS build 2 | 3 | # Build args 4 | ARG ENV 5 | ARG PROJECT 6 | ENV PROJECT_SRC=/go/src/${PROJECT} 7 | 8 | # Install tools required to build the project 9 | # We will need to run `docker build --no-cache .` to update those dependencies 10 | RUN apk add --no-cache git 11 | RUN go get github.com/golang/dep/cmd/dep 12 | 13 | # Gopkg.toml and Gopkg.lock lists project dependencies 14 | # These layers will only be re-built when Gopkg files are updated 15 | COPY Gopkg.lock Gopkg.toml ${PROJECT_SRC}/ 16 | WORKDIR ${PROJECT_SRC} 17 | 18 | # Install library dependencies 19 | RUN dep ensure -vendor-only 20 | 21 | # Copy all project and build it 22 | # This layer will be rebuilt when ever a file has changed in the project directory 23 | COPY . ${PROJECT_SRC}/ 24 | RUN go build -o /project/server 25 | 26 | # Copy config file to output directory 27 | COPY ./config.yml /project/ 28 | COPY ./.env.${ENV} /project/.env 29 | 30 | # This results in a small single layer image 31 | FROM alpine:latest 32 | 33 | # Move output directory from the previous build to the new one 34 | COPY --from=build /project /project 35 | 36 | WORKDIR /project 37 | ENTRYPOINT ["./server"] 38 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/BurntSushi/toml" 6 | packages = ["."] 7 | revision = "b26d9c308763d68093482582cea63d69be07a0f0" 8 | version = "v0.3.0" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/golang/protobuf" 13 | packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"] 14 | revision = "c65a0412e71e8b9b3bfd22925720d23c0f054237" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/jinzhu/configor" 19 | packages = ["."] 20 | revision = "6ecfe629230f24c2e92c7894c6ab21b83b757a39" 21 | 22 | [[projects]] 23 | name = "github.com/joho/godotenv" 24 | packages = ["."] 25 | revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b" 26 | version = "v1.2.0" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "golang.org/x/net" 31 | packages = ["context","http2","http2/hpack","idna","internal/timeseries","lex/httplex","trace"] 32 | revision = "0ed95abb35c445290478a5348a7b38bb154135fd" 33 | 34 | [[projects]] 35 | branch = "master" 36 | name = "golang.org/x/text" 37 | packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] 38 | revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "google.golang.org/genproto" 43 | packages = ["googleapis/rpc/status"] 44 | revision = "4eb30f4778eed4c258ba66527a0d4f9ec8a36c45" 45 | 46 | [[projects]] 47 | name = "google.golang.org/grpc" 48 | packages = [".","balancer","balancer/base","balancer/roundrobin","codes","connectivity","credentials","encoding","grpclb/grpc_lb_v1/messages","grpclog","internal","keepalive","metadata","naming","peer","resolver","resolver/dns","resolver/passthrough","stats","status","tap","transport"] 49 | revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef" 50 | version = "v1.9.2" 51 | 52 | [[projects]] 53 | branch = "v2" 54 | name = "gopkg.in/mgo.v2" 55 | packages = [".","bson","internal/json","internal/sasl","internal/scram"] 56 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655" 57 | 58 | [[projects]] 59 | branch = "v2" 60 | name = "gopkg.in/yaml.v2" 61 | packages = ["."] 62 | revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" 63 | 64 | [solve-meta] 65 | analyzer-name = "dep" 66 | analyzer-version = 1 67 | inputs-digest = "221a046a89165cbc945b04cdcf6351d10fc3f679fc0a451e4090cae21ef5f01a" 68 | solver-name = "gps-cdcl" 69 | solver-version = 1 70 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/golang/protobuf" 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/jinzhu/configor" 31 | 32 | [[constraint]] 33 | name = "github.com/joho/godotenv" 34 | version = "1.2.0" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "golang.org/x/net" 39 | 40 | [[constraint]] 41 | name = "google.golang.org/grpc" 42 | version = "1.9.2" 43 | 44 | [[constraint]] 45 | branch = "v2" 46 | name = "gopkg.in/mgo.v2" 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Go microservice using gRPC And MongoDB on Kubernetes 2 | 3 | Source-code of walkthrough blog posts: 4 | * **Part 1**: [A Go microservice using gRPC and MongoDB](http://pstylianides.com/a-go-microservice-using-grpc-and-mongodb/) 5 | * **Part 2**: [Deploy and scale a multi-env service on Kubernetes](http://pstylianides.com/deploy-and-scale-a-multi-env-service-on-kubernetes/) 6 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | server: 2 | type: grpc 3 | host: '' 4 | port: 8000 5 | 6 | database: 7 | type: mongo 8 | host: 192.168.99.100 9 | port: 27017 10 | -------------------------------------------------------------------------------- /database/handler.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stphivos/todo-api-go-grpc/database/mongo" 7 | "github.com/stphivos/todo-api-go-grpc/models" 8 | ) 9 | 10 | // Handler ... 11 | type Handler interface { 12 | GetTodos() ([]models.Todo, error) 13 | } 14 | 15 | // Create ... 16 | func Create(config *models.Config) (Handler, error) { 17 | var db Handler 18 | var err error 19 | 20 | switch config.Database.Type { 21 | case "mongo": 22 | db, err = mongo.NewHandler(config) 23 | default: 24 | err = fmt.Errorf("Database type %v is not supported", config.Database.Type) 25 | } 26 | 27 | return db, err 28 | } 29 | -------------------------------------------------------------------------------- /database/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stphivos/todo-api-go-grpc/models" 7 | "gopkg.in/mgo.v2" 8 | ) 9 | 10 | // Handler ... 11 | type Handler struct { 12 | *mgo.Session 13 | } 14 | 15 | // NewHandler ... 16 | func NewHandler(config *models.Config) (*Handler, error) { 17 | session, err := mgo.Dial(fmt.Sprintf("mongodb://%v:%v", config.Database.Host, config.Database.Port)) 18 | handler := &Handler{ 19 | Session: session, 20 | } 21 | return handler, err 22 | } 23 | 24 | // GetTodos ... 25 | func (db *Handler) GetTodos() ([]models.Todo, error) { 26 | session := db.getSession() 27 | defer session.Close() 28 | 29 | todos := []models.Todo{} 30 | err := session.DB("TodosDB").C("todos").Find(nil).All(&todos) 31 | 32 | return todos, err 33 | } 34 | 35 | func (db *Handler) getSession() *mgo.Session { 36 | return db.Session.Copy() 37 | } 38 | -------------------------------------------------------------------------------- /kubernetes/db-seed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | labels: 5 | env: $ENV 6 | component: $COMPONENT 7 | role: mongodb-seed 8 | name: $SERVICE_NAME-db-seed-$TAG 9 | namespace: default 10 | spec: 11 | completions: 1 12 | activeDeadlineSeconds: 120 13 | template: 14 | metadata: 15 | labels: 16 | env: $ENV 17 | component: $COMPONENT 18 | role: mongodb-seed 19 | spec: 20 | containers: 21 | - name: $SERVICE_NAME-db-seed 22 | image: bitnami/mongodb 23 | command: 24 | - mongo 25 | - TodosDB 26 | - --host 27 | - todo-api-mongodb 28 | - --eval 29 | - 'db.todos.insert({ "title": "Add my todos", "tag": "default", "priority": 1 });' 30 | restartPolicy: Never 31 | -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: $SERVICE_NAME-$COLOR 5 | namespace: default 6 | labels: 7 | env: $ENV 8 | color: $COLOR 9 | component: $COMPONENT 10 | role: $ROLE 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | env: $ENV 16 | color: $COLOR 17 | component: $COMPONENT 18 | role: $ROLE 19 | template: 20 | metadata: 21 | labels: 22 | env: $ENV 23 | color: $COLOR 24 | component: $COMPONENT 25 | role: $ROLE 26 | tag: "$TAG" 27 | spec: 28 | containers: 29 | - name: $SERVICE_NAME 30 | image: $IMAGE_NAME:$TAG 31 | env: 32 | - name: COLOR 33 | value: $COLOR 34 | ports: 35 | - containerPort: $CONTAINER_PORT 36 | name: transport 37 | readinessProbe: 38 | tcpSocket: 39 | port: transport 40 | initialDelaySeconds: 15 41 | livenessProbe: 42 | tcpSocket: 43 | port: transport 44 | initialDelaySeconds: 30 45 | resources: 46 | # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu 47 | requests: 48 | cpu: 100m 49 | memory: 100Mi 50 | limits: 51 | cpu: 100m 52 | memory: 100Mi 53 | -------------------------------------------------------------------------------- /kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | env: $ENV 6 | component: $COMPONENT 7 | role: $ROLE 8 | name: $SERVICE_NAME 9 | namespace: default 10 | spec: 11 | selector: 12 | env: $ENV 13 | color: $COLOR 14 | component: $COMPONENT 15 | role: $ROLE 16 | ports: 17 | - port: $SERVICE_PORT 18 | targetPort: $CONTAINER_PORT 19 | name: http 20 | -------------------------------------------------------------------------------- /loadtest/.env.dev: -------------------------------------------------------------------------------- 1 | # General 2 | COMPONENT=todo-api 3 | ROLE=load-tests 4 | CLUSTER_NAME=any 5 | CLUSTER_ZONE=any 6 | 7 | # Service 8 | SERVICE_CONFIG=./service.yaml 9 | SERVICE_NAME=locust-master 10 | 11 | # Deployment 12 | IMAGE_NAME=todo-api-loadtests 13 | CONTAINER_REGISTRY=local 14 | DEPLOYMENT_CONFIG=./deployment.yaml 15 | -------------------------------------------------------------------------------- /loadtest/.env.prod: -------------------------------------------------------------------------------- 1 | # General 2 | COMPONENT=todo-api 3 | ROLE=load-tests 4 | CLUSTER_NAME=todoapp-prod-cluster 5 | CLUSTER_ZONE=us-central1-a 6 | 7 | # Service 8 | SERVICE_CONFIG=./service.yaml 9 | SERVICE_NAME=locust-master 10 | 11 | # Deployment 12 | IMAGE_NAME=gcr.io/todoapp-123123/todo-api-loadtests 13 | CONTAINER_REGISTRY=gcr 14 | DEPLOYMENT_CONFIG=./deployment.yaml 15 | 16 | # Docker 17 | DOCKER_MACHINE_NAME=default 18 | -------------------------------------------------------------------------------- /loadtest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/stphivos/locustgrpc 2 | ADD locust /loadtests 3 | WORKDIR /loadtests 4 | RUN chmod 755 ./run.sh 5 | ENTRYPOINT ["./run.sh"] 6 | -------------------------------------------------------------------------------- /loadtest/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: locust-master 5 | namespace: default 6 | labels: 7 | component: $COMPONENT 8 | role: $ROLE 9 | type: master 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | component: $COMPONENT 15 | role: $ROLE 16 | type: master 17 | template: 18 | metadata: 19 | labels: 20 | component: $COMPONENT 21 | role: $ROLE 22 | type: master 23 | tag: "$TAG" 24 | spec: 25 | containers: 26 | - name: locust 27 | image: $IMAGE_NAME:$TAG 28 | imagePullPolicy: IfNotPresent 29 | env: 30 | - name: LOCUST_MODE 31 | value: master 32 | - name: TARGET_HOST 33 | value: todo-api.default.svc.cluster.local:80 34 | ports: 35 | - name: loc-master-web 36 | containerPort: 8089 37 | protocol: TCP 38 | - name: loc-master-p1 39 | containerPort: 5557 40 | protocol: TCP 41 | - name: loc-master-p2 42 | containerPort: 5558 43 | protocol: TCP 44 | resources: 45 | requests: 46 | cpu: 0.5 47 | memory: 512Mi 48 | --- 49 | apiVersion: extensions/v1beta1 50 | kind: Deployment 51 | metadata: 52 | name: locust-worker 53 | labels: 54 | component: $COMPONENT 55 | role: $ROLE 56 | type: worker 57 | spec: 58 | replicas: 14 59 | selector: 60 | matchLabels: 61 | component: $COMPONENT 62 | role: $ROLE 63 | type: worker 64 | template: 65 | metadata: 66 | labels: 67 | component: $COMPONENT 68 | role: $ROLE 69 | type: worker 70 | tag: "$TAG" 71 | spec: 72 | initContainers: 73 | - name: locust-wait-master 74 | image: busybox 75 | command: ['sh', '-c', 'until nslookup $SERVICE_NAME; do echo waiting for $SERVICE_NAME; sleep 2; done;'] 76 | containers: 77 | - name: locust 78 | image: $IMAGE_NAME:$TAG 79 | imagePullPolicy: IfNotPresent 80 | env: 81 | - name: LOCUST_MODE 82 | value: worker 83 | - name: LOCUST_MASTER 84 | value: locust-master 85 | - name: TARGET_HOST 86 | value: todo-api.default.svc.cluster.local:80 87 | resources: 88 | requests: 89 | cpu: 0.4 90 | memory: 512Mi 91 | -------------------------------------------------------------------------------- /loadtest/locust/locustfile.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import time 3 | import todos_pb2 4 | import todos_pb2_grpc 5 | 6 | from locust import Locust, TaskSet, task, events 7 | 8 | 9 | class GrpcClient: 10 | def __init__(self, host): 11 | channel = grpc.insecure_channel(host) 12 | self.stub = todos_pb2_grpc.TodosStub(channel) 13 | 14 | def __getattr__(self, name): 15 | func = self.stub.__getattribute__(name) 16 | 17 | def wrapper(*args, **kwargs): 18 | start_time = time.time() 19 | try: 20 | response = func(*args, **kwargs).SerializeToString() 21 | except Exception as e: 22 | total_time = int((time.time() - start_time) * 1000) 23 | events.request_failure.fire(request_type="grpc", name=name, response_time=total_time, exception=e) 24 | print e 25 | else: 26 | total_time = int((time.time() - start_time) * 1000) 27 | events.request_success.fire(request_type="grpc", name=name, response_time=total_time, response_length=len(response)) 28 | 29 | return wrapper 30 | 31 | 32 | class GrpcLocust(Locust): 33 | def __init__(self, *args, **kwargs): 34 | super(GrpcLocust, self).__init__(*args, **kwargs) 35 | self.client = GrpcClient(self.host) 36 | 37 | 38 | class ApiUser(GrpcLocust): 39 | min_wait = 100 40 | max_wait = 1000 41 | 42 | class task_set(TaskSet): 43 | @task() 44 | def get_todos(self): 45 | self.client.GetTodos(todos_pb2.Request(token='xyz')) 46 | -------------------------------------------------------------------------------- /loadtest/locust/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | LOCUST="/usr/local/bin/locust" 4 | LOCUS_OPTS="--host=$TARGET_HOST" 5 | LOCUST_MODE=${LOCUST_MODE:-standalone} 6 | 7 | if [[ "$LOCUST_MODE" = "master" ]]; then 8 | LOCUS_OPTS="$LOCUS_OPTS --master" 9 | elif [[ "$LOCUST_MODE" = "worker" ]]; then 10 | LOCUS_OPTS="$LOCUS_OPTS --slave --master-host=$LOCUST_MASTER" 11 | fi 12 | 13 | echo "$LOCUST $LOCUS_OPTS" 14 | 15 | ${LOCUST} ${LOCUS_OPTS} 16 | -------------------------------------------------------------------------------- /loadtest/locust/todos_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: todos.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='todos.proto', 20 | package='grpc', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x0btodos.proto\x12\x04grpc\"\x18\n\x07Request\x12\r\n\x05token\x18\x01 \x01(\t\"p\n\x08Response\x12\"\n\x05todos\x18\x01 \x03(\x0b\x32\x13.grpc.Response.Todo\x1a@\n\x04Todo\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0b\n\x03tag\x18\x03 \x01(\t\x12\x10\n\x08priority\x18\x04 \x01(\x05\x32\x34\n\x05Todos\x12+\n\x08GetTodos\x12\r.grpc.Request\x1a\x0e.grpc.Response\"\x00\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _REQUEST = _descriptor.Descriptor( 29 | name='Request', 30 | full_name='grpc.Request', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='token', full_name='grpc.Request.token', index=0, 37 | number=1, type=9, cpp_type=9, label=1, 38 | has_default_value=False, default_value=_b("").decode('utf-8'), 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | options=None, file=DESCRIPTOR), 42 | ], 43 | extensions=[ 44 | ], 45 | nested_types=[], 46 | enum_types=[ 47 | ], 48 | options=None, 49 | is_extendable=False, 50 | syntax='proto3', 51 | extension_ranges=[], 52 | oneofs=[ 53 | ], 54 | serialized_start=21, 55 | serialized_end=45, 56 | ) 57 | 58 | 59 | _RESPONSE_TODO = _descriptor.Descriptor( 60 | name='Todo', 61 | full_name='grpc.Response.Todo', 62 | filename=None, 63 | file=DESCRIPTOR, 64 | containing_type=None, 65 | fields=[ 66 | _descriptor.FieldDescriptor( 67 | name='id', full_name='grpc.Response.Todo.id', index=0, 68 | number=1, type=9, cpp_type=9, label=1, 69 | has_default_value=False, default_value=_b("").decode('utf-8'), 70 | message_type=None, enum_type=None, containing_type=None, 71 | is_extension=False, extension_scope=None, 72 | options=None, file=DESCRIPTOR), 73 | _descriptor.FieldDescriptor( 74 | name='title', full_name='grpc.Response.Todo.title', index=1, 75 | number=2, type=9, cpp_type=9, label=1, 76 | has_default_value=False, default_value=_b("").decode('utf-8'), 77 | message_type=None, enum_type=None, containing_type=None, 78 | is_extension=False, extension_scope=None, 79 | options=None, file=DESCRIPTOR), 80 | _descriptor.FieldDescriptor( 81 | name='tag', full_name='grpc.Response.Todo.tag', index=2, 82 | number=3, type=9, cpp_type=9, label=1, 83 | has_default_value=False, default_value=_b("").decode('utf-8'), 84 | message_type=None, enum_type=None, containing_type=None, 85 | is_extension=False, extension_scope=None, 86 | options=None, file=DESCRIPTOR), 87 | _descriptor.FieldDescriptor( 88 | name='priority', full_name='grpc.Response.Todo.priority', index=3, 89 | number=4, type=5, cpp_type=1, label=1, 90 | has_default_value=False, default_value=0, 91 | message_type=None, enum_type=None, containing_type=None, 92 | is_extension=False, extension_scope=None, 93 | options=None, file=DESCRIPTOR), 94 | ], 95 | extensions=[ 96 | ], 97 | nested_types=[], 98 | enum_types=[ 99 | ], 100 | options=None, 101 | is_extendable=False, 102 | syntax='proto3', 103 | extension_ranges=[], 104 | oneofs=[ 105 | ], 106 | serialized_start=95, 107 | serialized_end=159, 108 | ) 109 | 110 | _RESPONSE = _descriptor.Descriptor( 111 | name='Response', 112 | full_name='grpc.Response', 113 | filename=None, 114 | file=DESCRIPTOR, 115 | containing_type=None, 116 | fields=[ 117 | _descriptor.FieldDescriptor( 118 | name='todos', full_name='grpc.Response.todos', index=0, 119 | number=1, type=11, cpp_type=10, label=3, 120 | has_default_value=False, default_value=[], 121 | message_type=None, enum_type=None, containing_type=None, 122 | is_extension=False, extension_scope=None, 123 | options=None, file=DESCRIPTOR), 124 | ], 125 | extensions=[ 126 | ], 127 | nested_types=[_RESPONSE_TODO, ], 128 | enum_types=[ 129 | ], 130 | options=None, 131 | is_extendable=False, 132 | syntax='proto3', 133 | extension_ranges=[], 134 | oneofs=[ 135 | ], 136 | serialized_start=47, 137 | serialized_end=159, 138 | ) 139 | 140 | _RESPONSE_TODO.containing_type = _RESPONSE 141 | _RESPONSE.fields_by_name['todos'].message_type = _RESPONSE_TODO 142 | DESCRIPTOR.message_types_by_name['Request'] = _REQUEST 143 | DESCRIPTOR.message_types_by_name['Response'] = _RESPONSE 144 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 145 | 146 | Request = _reflection.GeneratedProtocolMessageType('Request', (_message.Message,), dict( 147 | DESCRIPTOR = _REQUEST, 148 | __module__ = 'todos_pb2' 149 | # @@protoc_insertion_point(class_scope:grpc.Request) 150 | )) 151 | _sym_db.RegisterMessage(Request) 152 | 153 | Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), dict( 154 | 155 | Todo = _reflection.GeneratedProtocolMessageType('Todo', (_message.Message,), dict( 156 | DESCRIPTOR = _RESPONSE_TODO, 157 | __module__ = 'todos_pb2' 158 | # @@protoc_insertion_point(class_scope:grpc.Response.Todo) 159 | )) 160 | , 161 | DESCRIPTOR = _RESPONSE, 162 | __module__ = 'todos_pb2' 163 | # @@protoc_insertion_point(class_scope:grpc.Response) 164 | )) 165 | _sym_db.RegisterMessage(Response) 166 | _sym_db.RegisterMessage(Response.Todo) 167 | 168 | 169 | 170 | _TODOS = _descriptor.ServiceDescriptor( 171 | name='Todos', 172 | full_name='grpc.Todos', 173 | file=DESCRIPTOR, 174 | index=0, 175 | options=None, 176 | serialized_start=161, 177 | serialized_end=213, 178 | methods=[ 179 | _descriptor.MethodDescriptor( 180 | name='GetTodos', 181 | full_name='grpc.Todos.GetTodos', 182 | index=0, 183 | containing_service=None, 184 | input_type=_REQUEST, 185 | output_type=_RESPONSE, 186 | options=None, 187 | ), 188 | ]) 189 | _sym_db.RegisterServiceDescriptor(_TODOS) 190 | 191 | DESCRIPTOR.services_by_name['Todos'] = _TODOS 192 | 193 | # @@protoc_insertion_point(module_scope) 194 | -------------------------------------------------------------------------------- /loadtest/locust/todos_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import todos_pb2 as todos__pb2 5 | 6 | 7 | class TodosStub(object): 8 | # missing associated documentation comment in .proto file 9 | pass 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.GetTodos = channel.unary_unary( 18 | '/grpc.Todos/GetTodos', 19 | request_serializer=todos__pb2.Request.SerializeToString, 20 | response_deserializer=todos__pb2.Response.FromString, 21 | ) 22 | 23 | 24 | class TodosServicer(object): 25 | # missing associated documentation comment in .proto file 26 | pass 27 | 28 | def GetTodos(self, request, context): 29 | # missing associated documentation comment in .proto file 30 | pass 31 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 32 | context.set_details('Method not implemented!') 33 | raise NotImplementedError('Method not implemented!') 34 | 35 | 36 | def add_TodosServicer_to_server(servicer, server): 37 | rpc_method_handlers = { 38 | 'GetTodos': grpc.unary_unary_rpc_method_handler( 39 | servicer.GetTodos, 40 | request_deserializer=todos__pb2.Request.FromString, 41 | response_serializer=todos__pb2.Response.SerializeToString, 42 | ), 43 | } 44 | generic_handler = grpc.method_handlers_generic_handler( 45 | 'grpc.Todos', rpc_method_handlers) 46 | server.add_generic_rpc_handlers((generic_handler,)) 47 | -------------------------------------------------------------------------------- /loadtest/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: $SERVICE_NAME 5 | labels: 6 | component: $COMPONENT 7 | role: $ROLE 8 | spec: 9 | ports: 10 | - port: 8089 11 | targetPort: loc-master-web 12 | protocol: TCP 13 | name: loc-master-web 14 | - port: 5557 15 | targetPort: loc-master-p1 16 | protocol: TCP 17 | name: loc-master-p1 18 | - port: 5558 19 | targetPort: loc-master-p2 20 | protocol: TCP 21 | name: loc-master-p2 22 | selector: 23 | component: $COMPONENT 24 | role: $ROLE 25 | type: master 26 | type: LoadBalancer 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/jinzhu/configor" 7 | "github.com/joho/godotenv" 8 | "github.com/stphivos/todo-api-go-grpc/models" 9 | "github.com/stphivos/todo-api-go-grpc/server" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Todo's service...") 14 | 15 | config := getConfig() 16 | srv := getServer(config) 17 | 18 | err := srv.Start() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | log.Println("Exiting...") 24 | } 25 | 26 | func getConfig() *models.Config { 27 | err := godotenv.Load() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | config := new(models.Config) 33 | err = configor.Load(config, "config.yml") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | log.Println("Configuration:", *config) 39 | return config 40 | } 41 | 42 | func getServer(config *models.Config) server.Runner { 43 | srv, err := server.Create(config) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | log.Println("Accepting requests:") 48 | return srv 49 | } 50 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | ) 6 | 7 | // Config ... 8 | type Config struct { 9 | Server struct { 10 | Type string 11 | Host string `env:"SERVER_HOST"` 12 | Port uint `env:"SERVER_PORT"` 13 | } 14 | Database struct { 15 | Type string 16 | Host string `env:"DB_HOST"` 17 | Port uint `env:"DB_PORT"` 18 | } 19 | } 20 | 21 | // Todo ... 22 | type Todo struct { 23 | ID bson.ObjectId `bson:"_id"` 24 | Title string `bson:"title"` 25 | Tag string `bson:"tag"` 26 | Priority int32 `bson:"priority"` 27 | } 28 | -------------------------------------------------------------------------------- /server/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | 8 | "github.com/stphivos/todo-api-go-grpc/database" 9 | "github.com/stphivos/todo-api-go-grpc/models" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | // Runner ... 15 | type Runner struct { 16 | Config *models.Config 17 | Database database.Handler 18 | } 19 | 20 | // NewRunner ... 21 | func NewRunner(config *models.Config) (*Runner, error) { 22 | db, err := database.Create(config) 23 | runner := &Runner{ 24 | Config: config, 25 | Database: db, 26 | } 27 | return runner, err 28 | } 29 | 30 | // Start ... 31 | func (srv *Runner) Start() error { 32 | listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", srv.Config.Server.Host, srv.Config.Server.Port)) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | opts := []grpc.ServerOption{} 38 | server := grpc.NewServer(opts...) 39 | 40 | RegisterTodosServer(server, srv) 41 | 42 | err = server.Serve(listener) 43 | 44 | return err 45 | } 46 | 47 | // GetTodos ... 48 | func (srv *Runner) GetTodos(ctx context.Context, req *Request) (*Response, error) { 49 | todos, err := srv.Database.GetTodos() 50 | if err != nil { 51 | // Log error but don't stop the server 52 | log.Println(err) 53 | 54 | return nil, err 55 | } 56 | 57 | res := &Response{ 58 | Todos: srv.mapTodos(todos...), 59 | } 60 | 61 | log.Println(req.Token, ":", res) 62 | return res, err 63 | } 64 | 65 | func (srv *Runner) mapTodos(todos ...models.Todo) []*Response_Todo { 66 | grpcTodos := []*Response_Todo{} 67 | for _, todo := range todos { 68 | grpcTodos = append(grpcTodos, &Response_Todo{ 69 | Id: todo.ID.Hex(), 70 | Title: todo.Title, 71 | Tag: todo.Tag, 72 | Priority: todo.Priority, 73 | }) 74 | } 75 | return grpcTodos 76 | } 77 | -------------------------------------------------------------------------------- /server/grpc/todos.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: todos.proto 3 | 4 | /* 5 | Package grpc is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | todos.proto 9 | 10 | It has these top-level messages: 11 | Request 12 | Response 13 | */ 14 | package grpc 15 | 16 | import proto "github.com/golang/protobuf/proto" 17 | import fmt "fmt" 18 | import math "math" 19 | 20 | import ( 21 | context "golang.org/x/net/context" 22 | grpc1 "google.golang.org/grpc" 23 | ) 24 | 25 | // Reference imports to suppress errors if they are not otherwise used. 26 | var _ = proto.Marshal 27 | var _ = fmt.Errorf 28 | var _ = math.Inf 29 | 30 | // This is a compile-time assertion to ensure that this generated file 31 | // is compatible with the proto package it is being compiled against. 32 | // A compilation error at this line likely means your copy of the 33 | // proto package needs to be updated. 34 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 35 | 36 | type Request struct { 37 | Token string `protobuf:"bytes,1,opt,name=token" json:"token,omitempty"` 38 | } 39 | 40 | func (m *Request) Reset() { *m = Request{} } 41 | func (m *Request) String() string { return proto.CompactTextString(m) } 42 | func (*Request) ProtoMessage() {} 43 | func (*Request) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 44 | 45 | func (m *Request) GetToken() string { 46 | if m != nil { 47 | return m.Token 48 | } 49 | return "" 50 | } 51 | 52 | type Response struct { 53 | Todos []*Response_Todo `protobuf:"bytes,1,rep,name=todos" json:"todos,omitempty"` 54 | } 55 | 56 | func (m *Response) Reset() { *m = Response{} } 57 | func (m *Response) String() string { return proto.CompactTextString(m) } 58 | func (*Response) ProtoMessage() {} 59 | func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 60 | 61 | func (m *Response) GetTodos() []*Response_Todo { 62 | if m != nil { 63 | return m.Todos 64 | } 65 | return nil 66 | } 67 | 68 | type Response_Todo struct { 69 | Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` 70 | Title string `protobuf:"bytes,2,opt,name=title" json:"title,omitempty"` 71 | Tag string `protobuf:"bytes,3,opt,name=tag" json:"tag,omitempty"` 72 | Priority int32 `protobuf:"varint,4,opt,name=priority" json:"priority,omitempty"` 73 | } 74 | 75 | func (m *Response_Todo) Reset() { *m = Response_Todo{} } 76 | func (m *Response_Todo) String() string { return proto.CompactTextString(m) } 77 | func (*Response_Todo) ProtoMessage() {} 78 | func (*Response_Todo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1, 0} } 79 | 80 | func (m *Response_Todo) GetId() string { 81 | if m != nil { 82 | return m.Id 83 | } 84 | return "" 85 | } 86 | 87 | func (m *Response_Todo) GetTitle() string { 88 | if m != nil { 89 | return m.Title 90 | } 91 | return "" 92 | } 93 | 94 | func (m *Response_Todo) GetTag() string { 95 | if m != nil { 96 | return m.Tag 97 | } 98 | return "" 99 | } 100 | 101 | func (m *Response_Todo) GetPriority() int32 { 102 | if m != nil { 103 | return m.Priority 104 | } 105 | return 0 106 | } 107 | 108 | func init() { 109 | proto.RegisterType((*Request)(nil), "grpc.Request") 110 | proto.RegisterType((*Response)(nil), "grpc.Response") 111 | proto.RegisterType((*Response_Todo)(nil), "grpc.Response.Todo") 112 | } 113 | 114 | // Reference imports to suppress errors if they are not otherwise used. 115 | var _ context.Context 116 | var _ grpc1.ClientConn 117 | 118 | // This is a compile-time assertion to ensure that this generated file 119 | // is compatible with the grpc package it is being compiled against. 120 | const _ = grpc1.SupportPackageIsVersion4 121 | 122 | // Client API for Todos service 123 | 124 | type TodosClient interface { 125 | GetTodos(ctx context.Context, in *Request, opts ...grpc1.CallOption) (*Response, error) 126 | } 127 | 128 | type todosClient struct { 129 | cc *grpc1.ClientConn 130 | } 131 | 132 | func NewTodosClient(cc *grpc1.ClientConn) TodosClient { 133 | return &todosClient{cc} 134 | } 135 | 136 | func (c *todosClient) GetTodos(ctx context.Context, in *Request, opts ...grpc1.CallOption) (*Response, error) { 137 | out := new(Response) 138 | err := grpc1.Invoke(ctx, "/grpc.Todos/GetTodos", in, out, c.cc, opts...) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return out, nil 143 | } 144 | 145 | // Server API for Todos service 146 | 147 | type TodosServer interface { 148 | GetTodos(context.Context, *Request) (*Response, error) 149 | } 150 | 151 | func RegisterTodosServer(s *grpc1.Server, srv TodosServer) { 152 | s.RegisterService(&_Todos_serviceDesc, srv) 153 | } 154 | 155 | func _Todos_GetTodos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc1.UnaryServerInterceptor) (interface{}, error) { 156 | in := new(Request) 157 | if err := dec(in); err != nil { 158 | return nil, err 159 | } 160 | if interceptor == nil { 161 | return srv.(TodosServer).GetTodos(ctx, in) 162 | } 163 | info := &grpc1.UnaryServerInfo{ 164 | Server: srv, 165 | FullMethod: "/grpc.Todos/GetTodos", 166 | } 167 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 168 | return srv.(TodosServer).GetTodos(ctx, req.(*Request)) 169 | } 170 | return interceptor(ctx, in, info, handler) 171 | } 172 | 173 | var _Todos_serviceDesc = grpc1.ServiceDesc{ 174 | ServiceName: "grpc.Todos", 175 | HandlerType: (*TodosServer)(nil), 176 | Methods: []grpc1.MethodDesc{ 177 | { 178 | MethodName: "GetTodos", 179 | Handler: _Todos_GetTodos_Handler, 180 | }, 181 | }, 182 | Streams: []grpc1.StreamDesc{}, 183 | Metadata: "todos.proto", 184 | } 185 | 186 | func init() { proto.RegisterFile("todos.proto", fileDescriptor0) } 187 | 188 | var fileDescriptor0 = []byte{ 189 | // 207 bytes of a gzipped FileDescriptorProto 190 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xcb, 0x4a, 0xc4, 0x30, 191 | 0x14, 0x86, 0x4d, 0x2f, 0x5a, 0x4f, 0xb1, 0x48, 0x74, 0x11, 0xba, 0xb1, 0x74, 0x55, 0x11, 0xb2, 192 | 0xa8, 0xbe, 0x83, 0xfb, 0xe0, 0xca, 0x9d, 0x9a, 0x50, 0x82, 0x43, 0x4f, 0x26, 0x39, 0xb3, 0x98, 193 | 0xc7, 0x98, 0x37, 0x1e, 0x92, 0xb6, 0x03, 0xb3, 0x3b, 0x5f, 0xf2, 0xf3, 0x5f, 0xa0, 0x26, 0xd4, 194 | 0x18, 0xa4, 0xf3, 0x48, 0xc8, 0x8b, 0xc9, 0xbb, 0xbf, 0xfe, 0x05, 0xee, 0x94, 0xd9, 0x1f, 0x4c, 195 | 0x20, 0xfe, 0x0c, 0x25, 0xe1, 0xbf, 0x99, 0x05, 0xeb, 0xd8, 0x70, 0xaf, 0x16, 0xe8, 0x4f, 0x0c, 196 | 0x2a, 0x65, 0x82, 0xc3, 0x39, 0x18, 0xfe, 0x1a, 0x25, 0x1a, 0x83, 0x60, 0x5d, 0x3e, 0xd4, 0xe3, 197 | 0x93, 0x8c, 0x1e, 0x72, 0xfb, 0x96, 0x5f, 0xa8, 0x51, 0x2d, 0x8a, 0xf6, 0x1b, 0x8a, 0x88, 0xbc, 198 | 0x81, 0xcc, 0xea, 0xd5, 0x32, 0xb3, 0x3a, 0xa5, 0x58, 0xda, 0x19, 0x91, 0xad, 0x29, 0x11, 0xf8, 199 | 0x23, 0xe4, 0xf4, 0x33, 0x89, 0x3c, 0xbd, 0xc5, 0x93, 0xb7, 0x50, 0x39, 0x6f, 0xd1, 0x5b, 0x3a, 200 | 0x8a, 0xa2, 0x63, 0x43, 0xa9, 0x2e, 0x3c, 0x7e, 0x40, 0x19, 0xbd, 0x03, 0x7f, 0x83, 0xea, 0xd3, 201 | 0xd0, 0x72, 0x3f, 0x6c, 0x65, 0xd2, 0x9a, 0xb6, 0xb9, 0xee, 0xd6, 0xdf, 0xfc, 0xde, 0xa6, 0xdd, 202 | 0xef, 0xe7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x41, 0xf9, 0xcb, 0x08, 0x06, 0x01, 0x00, 0x00, 203 | } 204 | -------------------------------------------------------------------------------- /server/grpc/todos.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package grpc; 4 | 5 | service Todos { 6 | rpc GetTodos(Request) returns (Response) {} 7 | } 8 | 9 | message Request { 10 | string token = 1; 11 | } 12 | 13 | message Response { 14 | message Todo { 15 | string id = 1; 16 | string title = 2; 17 | string tag = 3; 18 | int32 priority = 4; 19 | } 20 | 21 | repeated Todo todos = 1; 22 | } 23 | -------------------------------------------------------------------------------- /server/runner.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stphivos/todo-api-go-grpc/models" 7 | "github.com/stphivos/todo-api-go-grpc/server/grpc" 8 | ) 9 | 10 | // Runner ... 11 | type Runner interface { 12 | Start() error 13 | } 14 | 15 | // Create ... 16 | func Create(config *models.Config) (Runner, error) { 17 | var srv Runner 18 | var err error 19 | 20 | switch config.Server.Type { 21 | case "grpc": 22 | srv, err = grpc.NewRunner(config) 23 | default: 24 | err = fmt.Errorf("Server type %v is not supported", config.Server.Type) 25 | } 26 | 27 | return srv, err 28 | } 29 | --------------------------------------------------------------------------------