Ruby microservices
With Docker
About me
● Serhii Koba
● Position
○ Web Team lead at MobiDev
○ Lecturer at KhAI
● Fullstack
○ BE: Ruby, Go, Php, Python
○ FE: Vue JS
○ DO: Docker, Ansible
○ IoT, Blockchain, Android
● Blogger a bit
● Personal moto:
Until we learn - we live
Social media:
● http://1devblog.org
● github: @sergey-koba-mobidev
● twitter: @KobaSerhii
● facebook: @kobaserhii
What we will talk about
● How Docker helps building Ruby Microservices
● Automating development processes
● Repeatable environment
● Optimization of Docker images
● Refactoring Docker Compose files
● Central logging with Docker
● Routing requests to microservices
● Deploy?
What we will not talk about
● When to use microservices
● What is the best framework/language for microservices
● How to migrate to microservices
● How to install Docker, Docker Compose
● Docker for Dummies :)
● Is Rails good for Microservices
Docker
Two words about Docker
● Application build and deploy tool
● Containers based virtualization (shares
host OS kernel)
● Each container is based on image
● Each piece of software (service) should be
a container
● Lightweight
● Secure
https://www.docker.com/what-container
Ruby microservice: Sinatra + Trailblazer
# service.rb
# https://github.com/bark-iot/
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 80
get '/houses' do
result = House::List.(user_id: USER['id'])
if result.success?
body
House::Representer.for_collection.new(result['models']
).to_json
else
status 422
body
result['contract.default'].errors.messages.uniq.to_json
end
end
# concepts/house/operations/list.rb
class House < Sequel::Model(DB)
class List < Trailblazer::Operation
step Contract::Build()
step Contract::Validate()
step :list_by_user_id
#...
def list_by_user_id(options, params:, **)
options['models'] = House.where(user_id:
params[:user_id]).all
options['models']
end
end
end
Hi @apotonick
Dockerfile
FROM ruby:2.5.0
# Pg
RUN apt-get update -qq && apt-get install 
-y build-essential libpq-dev lsb-release
ENV APP_ROOT /app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT
EXPOSE 80
# Bundle
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN gem update bundler && bundle install --jobs 4
# Copy the rest of source
COPY . /app
Build image
docker build -f Dockerfile.test -t
house-service/test .
List images
docker images
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE
house-service/test | latest | c9b0fe5b01d1 | 2 minutes ago | 985MB
Run
docker run -i -p 80:80
house-service/test bundle exec ruby
service.rb
985 MB
Dockerfile: Use Alpine linux
FROM ruby:2.5.0-alpine
# Pg
RUN apk --update --upgrade add postgresql-dev git
build-base
ENV APP_ROOT /app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT
EXPOSE 80
# Bundle
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN gem update bundler && bundle install --jobs 4
# Copy the rest of source
COPY . /app
Build image
docker build -f Dockerfile.test -t
house-service/test .
List images
docker images
REPOSITORY | TAG | IMAGE ID | CREATED |
SIZE
house-service/test | latest | ab58dd76e0d3 | 2 minutes ago |
320MB
320 MB
Dockerfile: Multistage builds
FROM ruby:2.5.0-alpine as bundler
# CODE FROM PREVIOUS SLIDE
# Stage 2
FROM ruby:2.5.0-alpine
RUN apk --update --upgrade add postgresql-dev
EXPOSE 80
ENV APP_ROOT /app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT
# Copy the rest of source
COPY . /app
COPY --from=bundler /usr/local/bundle
/usr/local/bundle
Build image
docker build -f Dockerfile.test -t
house-service/test .
List images
docker images
REPOSITORY | TAG | IMAGE ID | CREATED |
SIZE
house-service/test | latest | 8f5a46611b7e | 2 minutes ago |
127MB
127 MB
Dockerfile: Results
8x
Docker Compose
Docker Compose: Yml file
Let's assume each service is placed in a
separate folder
./house-service
./user-service
./device-service
...
house-service:
build: ../house-service
command: bundle exec ruby service.rb
networks:
- my-network
ports:
- 80
volumes:
- ../house-service/:/app
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
RACK_ENV: development
stdin_open: true
tty: true
Docker Compose: Multiple services
services:
db:
image: postgres:latest
ports:
- "5432:5432"
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
redis:
image: redis:4.0.5
house-service:
…
user-service:
…
user-service:
build: ../user-service
command: bundle exec ruby service.rb
networks:
- my-network
ports:
- 80
volumes:
- ../user-service/:/app
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
RACK_ENV: development
stdin_open: true
tty: true
Run services: docker-compose up -d
16 lines
Docker Compose: extension fields
● Yaml anchors and extend
● Docker Compose file version 3.4
● “X-” sections (extension fields)
x-base-service: &base-service
command: bundle exec ruby
service.rb
networks:
- my-network
ports:
- 80
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
stdin_open: true
tty: true
users-service:
<< : *base-service
build: ../users-service
volumes:
- ../users-service/:/app
5 lines
Docker Compose: Production
In production we want to do things
differently:
● Use images instead of build
● Remove stdin and tty
● Remove volumes with app
code
● Remove test containers
x-base-service: &base-service
command: bundle exec ruby service.rb
networks:
- my-network
ports:
- 80
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
RACK_ENV: production
users-service:
<< : *base-service
image: myapp/users-service:latest
Two options:
● Two separate docker-compose.yml files
● Overriding files
Docker Compose: Multiple Files
Run Development
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# docker-compose.yml PRODUCTION
house-service:
image: myapp/house-service:latest
command: bundle exec ruby service.rb
networks:
- my-network
ports:
- 80
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_DB: my_db
# docker-compose.dev.yml DEV
house-service:
build: ../user-service
command: bundle exec ruby service.rb
environment:
RACK_ENV: development
Central Logging
Logging
● View logs from multiple Docker containers
● Tracing data across multiple containers
● Tracing user action across multiple containers
● Aggregate logs
● Search logs
View logs on a single container
docker-compose -f docker-compose.yml -f
docker-compose.dev.yml logs house-service
Logging: ELK stack
Logging: ELK with Docker
logstash:
build: docker/logstash/
command: logstash -f
/etc/logstash/conf.d/logstash.conf
ports:
- "12201:12201/udp"
kibana:
build: docker/kibana/
ports:
- "5601:5601"
elasticsearch:
image: elasticsearch:latest
command: elasticsearch
-Enetwork.host=0.0.0.0
ports:
- "9200:9200"
- "9300:9300"
environment:
ES_JAVA_OPTS: "-Xms750m
-Xmx750m"
volumes:
- /usr/share/elasticsearch/data
Logging: ELK with Docker
# kibana.yml
port: 5601
host: "0.0.0.0"
elasticsearch_url: "http://elasticsearch:9200"
elasticsearch_preserve_host: true
kibana_index: ".kibana"
default_app_id: "discover"
request_timeout: 300000
shard_timeout: 0
verify_ssl: true
bundled_plugin_ids:
- plugins/dashboard/index
- ...
// logstash.conf
input {
gelf {}
}
output {
elasticsearch {
hosts => "elasticsearch:9200"
}
}
Logging: Gelf driver for Docker containers
x-logging: &logging
driver: gelf
options:
gelf-address: 'udp://localhost:12201'
tag: '{{.Name}}'
house-service:
<< : *logging
user-service:
<< : *logging
Template
variable
Logging: Add log messages to Ruby service
class House < Sequel::Model(DB)
class Create < Trailblazer::Operation
step Model(House, :new)
step :generate_key_and_secret
step Contract::Persist()
step :log_success
failure :log_failure
#...
def log_success(options, params:, model:, **)
LOGGER.info "[#{self.class}] Created house with params #{params.to_json}. House:
#{House::Representer.new(model).to_json}"
end
def log_failure(options, params:, **)
LOGGER.info "[#{self.class}] Failed to create house with params #{params.to_json}"
end
end
Logging: Kibana
Routing
Routing: first try
house-service:
...
ports:
- 3000
user-service:
...
ports:
- 3001
device-service:
...
ports:
- 3002
# house-service/service.rb
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 3000
# user-service/service.rb
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 3001
http://localhost:3000
http://localhost:3001
Routing: Docker haproxy
lb:
image: dockercloud/haproxy
links:
- house-service
volumes:
-
/var/run/docker.sock:/var/run/docker.sock
ports:
- 80:80
house-service:
...
environment:
VIRTUAL_HOST: "*/houses*"
VIRTUAL_HOST_WEIGHT: 102
http://samos-it.com/posts/docker-multi-website-single-ip-host-haproxy.html
Docker
Swarm
Compatible
Possible routers
- Docker cloud proxy (Deprecated)
- Traefik https://traefik.io/
- Caddy https://caddyserver.com/
- Zookeeper
Conclusion
Conclusion
● All in one environment and tools for Ruby microservices
● Minimized Docker images
● Minimized Docker Compose file
● Central logging and tracing using ELK
● Routing (balancing) requests
What’s next?
● Authorization layers?
● gRPC for inter communication?
● Message broker?
● Migrating monolith Rails app to Microservices? :)

Ruby microservices with Docker - Sergii Koba

  • 1.
  • 2.
    About me ● SerhiiKoba ● Position ○ Web Team lead at MobiDev ○ Lecturer at KhAI ● Fullstack ○ BE: Ruby, Go, Php, Python ○ FE: Vue JS ○ DO: Docker, Ansible ○ IoT, Blockchain, Android ● Blogger a bit ● Personal moto: Until we learn - we live Social media: ● http://1devblog.org ● github: @sergey-koba-mobidev ● twitter: @KobaSerhii ● facebook: @kobaserhii
  • 3.
    What we willtalk about ● How Docker helps building Ruby Microservices ● Automating development processes ● Repeatable environment ● Optimization of Docker images ● Refactoring Docker Compose files ● Central logging with Docker ● Routing requests to microservices ● Deploy?
  • 4.
    What we willnot talk about ● When to use microservices ● What is the best framework/language for microservices ● How to migrate to microservices ● How to install Docker, Docker Compose ● Docker for Dummies :) ● Is Rails good for Microservices
  • 5.
  • 6.
    Two words aboutDocker ● Application build and deploy tool ● Containers based virtualization (shares host OS kernel) ● Each container is based on image ● Each piece of software (service) should be a container ● Lightweight ● Secure https://www.docker.com/what-container
  • 7.
    Ruby microservice: Sinatra+ Trailblazer # service.rb # https://github.com/bark-iot/ require 'sinatra' set :bind, '0.0.0.0' set :port, 80 get '/houses' do result = House::List.(user_id: USER['id']) if result.success? body House::Representer.for_collection.new(result['models'] ).to_json else status 422 body result['contract.default'].errors.messages.uniq.to_json end end # concepts/house/operations/list.rb class House < Sequel::Model(DB) class List < Trailblazer::Operation step Contract::Build() step Contract::Validate() step :list_by_user_id #... def list_by_user_id(options, params:, **) options['models'] = House.where(user_id: params[:user_id]).all options['models'] end end end Hi @apotonick
  • 8.
    Dockerfile FROM ruby:2.5.0 # Pg RUNapt-get update -qq && apt-get install -y build-essential libpq-dev lsb-release ENV APP_ROOT /app RUN mkdir $APP_ROOT WORKDIR $APP_ROOT EXPOSE 80 # Bundle COPY Gemfile /app/Gemfile COPY Gemfile.lock /app/Gemfile.lock RUN gem update bundler && bundle install --jobs 4 # Copy the rest of source COPY . /app Build image docker build -f Dockerfile.test -t house-service/test . List images docker images REPOSITORY | TAG | IMAGE ID | CREATED | SIZE house-service/test | latest | c9b0fe5b01d1 | 2 minutes ago | 985MB Run docker run -i -p 80:80 house-service/test bundle exec ruby service.rb 985 MB
  • 9.
    Dockerfile: Use Alpinelinux FROM ruby:2.5.0-alpine # Pg RUN apk --update --upgrade add postgresql-dev git build-base ENV APP_ROOT /app RUN mkdir $APP_ROOT WORKDIR $APP_ROOT EXPOSE 80 # Bundle COPY Gemfile /app/Gemfile COPY Gemfile.lock /app/Gemfile.lock RUN gem update bundler && bundle install --jobs 4 # Copy the rest of source COPY . /app Build image docker build -f Dockerfile.test -t house-service/test . List images docker images REPOSITORY | TAG | IMAGE ID | CREATED | SIZE house-service/test | latest | ab58dd76e0d3 | 2 minutes ago | 320MB 320 MB
  • 10.
    Dockerfile: Multistage builds FROMruby:2.5.0-alpine as bundler # CODE FROM PREVIOUS SLIDE # Stage 2 FROM ruby:2.5.0-alpine RUN apk --update --upgrade add postgresql-dev EXPOSE 80 ENV APP_ROOT /app RUN mkdir $APP_ROOT WORKDIR $APP_ROOT # Copy the rest of source COPY . /app COPY --from=bundler /usr/local/bundle /usr/local/bundle Build image docker build -f Dockerfile.test -t house-service/test . List images docker images REPOSITORY | TAG | IMAGE ID | CREATED | SIZE house-service/test | latest | 8f5a46611b7e | 2 minutes ago | 127MB 127 MB
  • 11.
  • 12.
  • 13.
    Docker Compose: Ymlfile Let's assume each service is placed in a separate folder ./house-service ./user-service ./device-service ... house-service: build: ../house-service command: bundle exec ruby service.rb networks: - my-network ports: - 80 volumes: - ../house-service/:/app environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db RACK_ENV: development stdin_open: true tty: true
  • 14.
    Docker Compose: Multipleservices services: db: image: postgres:latest ports: - "5432:5432" environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db redis: image: redis:4.0.5 house-service: … user-service: … user-service: build: ../user-service command: bundle exec ruby service.rb networks: - my-network ports: - 80 volumes: - ../user-service/:/app environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db RACK_ENV: development stdin_open: true tty: true Run services: docker-compose up -d 16 lines
  • 15.
    Docker Compose: extensionfields ● Yaml anchors and extend ● Docker Compose file version 3.4 ● “X-” sections (extension fields) x-base-service: &base-service command: bundle exec ruby service.rb networks: - my-network ports: - 80 environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db stdin_open: true tty: true users-service: << : *base-service build: ../users-service volumes: - ../users-service/:/app 5 lines
  • 16.
    Docker Compose: Production Inproduction we want to do things differently: ● Use images instead of build ● Remove stdin and tty ● Remove volumes with app code ● Remove test containers x-base-service: &base-service command: bundle exec ruby service.rb networks: - my-network ports: - 80 environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db RACK_ENV: production users-service: << : *base-service image: myapp/users-service:latest
  • 17.
    Two options: ● Twoseparate docker-compose.yml files ● Overriding files Docker Compose: Multiple Files Run Development docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d # docker-compose.yml PRODUCTION house-service: image: myapp/house-service:latest command: bundle exec ruby service.rb networks: - my-network ports: - 80 environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_pass POSTGRES_DB: my_db # docker-compose.dev.yml DEV house-service: build: ../user-service command: bundle exec ruby service.rb environment: RACK_ENV: development
  • 18.
  • 19.
    Logging ● View logsfrom multiple Docker containers ● Tracing data across multiple containers ● Tracing user action across multiple containers ● Aggregate logs ● Search logs View logs on a single container docker-compose -f docker-compose.yml -f docker-compose.dev.yml logs house-service
  • 20.
  • 21.
    Logging: ELK withDocker logstash: build: docker/logstash/ command: logstash -f /etc/logstash/conf.d/logstash.conf ports: - "12201:12201/udp" kibana: build: docker/kibana/ ports: - "5601:5601" elasticsearch: image: elasticsearch:latest command: elasticsearch -Enetwork.host=0.0.0.0 ports: - "9200:9200" - "9300:9300" environment: ES_JAVA_OPTS: "-Xms750m -Xmx750m" volumes: - /usr/share/elasticsearch/data
  • 22.
    Logging: ELK withDocker # kibana.yml port: 5601 host: "0.0.0.0" elasticsearch_url: "http://elasticsearch:9200" elasticsearch_preserve_host: true kibana_index: ".kibana" default_app_id: "discover" request_timeout: 300000 shard_timeout: 0 verify_ssl: true bundled_plugin_ids: - plugins/dashboard/index - ... // logstash.conf input { gelf {} } output { elasticsearch { hosts => "elasticsearch:9200" } }
  • 23.
    Logging: Gelf driverfor Docker containers x-logging: &logging driver: gelf options: gelf-address: 'udp://localhost:12201' tag: '{{.Name}}' house-service: << : *logging user-service: << : *logging Template variable
  • 24.
    Logging: Add logmessages to Ruby service class House < Sequel::Model(DB) class Create < Trailblazer::Operation step Model(House, :new) step :generate_key_and_secret step Contract::Persist() step :log_success failure :log_failure #... def log_success(options, params:, model:, **) LOGGER.info "[#{self.class}] Created house with params #{params.to_json}. House: #{House::Representer.new(model).to_json}" end def log_failure(options, params:, **) LOGGER.info "[#{self.class}] Failed to create house with params #{params.to_json}" end end
  • 25.
  • 26.
  • 27.
    Routing: first try house-service: ... ports: -3000 user-service: ... ports: - 3001 device-service: ... ports: - 3002 # house-service/service.rb require 'sinatra' set :bind, '0.0.0.0' set :port, 3000 # user-service/service.rb require 'sinatra' set :bind, '0.0.0.0' set :port, 3001 http://localhost:3000 http://localhost:3001
  • 28.
    Routing: Docker haproxy lb: image:dockercloud/haproxy links: - house-service volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - 80:80 house-service: ... environment: VIRTUAL_HOST: "*/houses*" VIRTUAL_HOST_WEIGHT: 102 http://samos-it.com/posts/docker-multi-website-single-ip-host-haproxy.html Docker Swarm Compatible
  • 29.
    Possible routers - Dockercloud proxy (Deprecated) - Traefik https://traefik.io/ - Caddy https://caddyserver.com/ - Zookeeper
  • 30.
  • 31.
    Conclusion ● All inone environment and tools for Ruby microservices ● Minimized Docker images ● Minimized Docker Compose file ● Central logging and tracing using ELK ● Routing (balancing) requests What’s next? ● Authorization layers? ● gRPC for inter communication? ● Message broker? ● Migrating monolith Rails app to Microservices? :)