├── post ├── VERSION ├── requirements.txt ├── charts │ └── post │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── service.yaml │ │ └── deployment.yaml │ │ ├── .helmignore │ │ └── values.yaml ├── Dockerfile ├── helpers.py ├── .gitlab-ci.yml └── post_app.py ├── ui ├── VERSION ├── README.md ├── charts │ └── ui │ │ ├── Chart.yaml │ │ ├── .helmignore │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── NOTES.txt │ │ └── deployment.yaml │ │ └── values.yaml ├── Gemfile ├── Dockerfile ├── config.ru ├── views │ ├── create.haml │ ├── index.haml │ ├── layout.haml │ └── show.haml ├── middleware.rb ├── Gemfile.lock ├── helpers.rb ├── .gitlab-ci.yml └── ui_app.rb ├── mongodb ├── charts │ └── mongodb │ │ ├── .helmignore │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── svc.yaml │ │ ├── _helpers.tpl │ │ ├── pvc.yaml │ │ ├── secrets.yaml │ │ └── deployment.yaml │ │ ├── Chart.yaml │ │ ├── values.yaml │ │ └── README.md └── .gitlab-ci.yml ├── README.md └── docker-compose.yml /post/VERSION: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /ui/VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/.helmignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # UI service of Microservices Reddit application 2 | -------------------------------------------------------------------------------- /post/requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus_client==0.0.21 2 | flask==0.12.2 3 | pymongo==3.5.1 4 | structlog==17.2.0 5 | -------------------------------------------------------------------------------- /ui/charts/ui/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for UI service 3 | name: ui 4 | version: 0.1.0 5 | maintainers: 6 | - name: UI dev team 7 | email: ui-dev@mail.com 8 | -------------------------------------------------------------------------------- /post/charts/post/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for Post service 3 | name: post 4 | version: 0.1.0 5 | maintainers: 6 | - name: Post dev team 7 | email: post-dev@mail.com 8 | -------------------------------------------------------------------------------- /post/Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM python:3.6.0-alpine 2 | FROM python:2.7 3 | WORKDIR /app 4 | ADD requirements.txt /app 5 | RUN pip install -r requirements.txt 6 | ADD . /app 7 | EXPOSE 5000 8 | ENTRYPOINT ["python", "post_app.py"] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raddit 2 | 3 | This is an example of a microservices application used in CI/CD demo with **Gitlab CI**, **Kubernetes**, and **Helm** 4 | 5 | [Link to the blog post](http://artemstar.com/2018/01/15/cicd-with-kubernetes-and-gitlab/) 6 | -------------------------------------------------------------------------------- /ui/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'sinatra-contrib' 5 | gem 'haml' 6 | gem 'bson_ext' 7 | gem 'faraday' 8 | gem 'puma' 9 | gem 'prometheus-client' 10 | gem 'rack' 11 | gem 'rufus-scheduler' 12 | gem 'tzinfo-data' 13 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.3 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential 4 | 5 | ENV APP_HOME /app 6 | RUN mkdir $APP_HOME 7 | WORKDIR $APP_HOME 8 | 9 | ADD Gemfile* $APP_HOME/ 10 | RUN bundle install 11 | 12 | ADD . $APP_HOME 13 | CMD ["puma"] 14 | -------------------------------------------------------------------------------- /post/charts/post/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{- if contains "ClusterIP" .Values.service.type }} 2 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "post.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 3 | echo "Visit http://127.0.0.1:8080 to use your application" 4 | kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} 5 | {{- end }} 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | mongo: 5 | image: mongo:3.4 6 | 7 | post: 8 | build: ./post 9 | environment: 10 | - POST_DATABASE_HOST=mongo 11 | depends_on: 12 | - mongo 13 | 14 | ui: 15 | build: ./ui 16 | environment: 17 | - POST_SERVICE_HOST=post 18 | - POST_SERVICE_PORT=5000 19 | ports: 20 | - 9292:9292 21 | depends_on: 22 | - post 23 | -------------------------------------------------------------------------------- /ui/config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rack' 3 | require 'prometheus/middleware/collector' 4 | require 'prometheus/middleware/exporter' 5 | require_relative 'ui_app' 6 | require_relative 'middleware' 7 | 8 | use Metrics 9 | use Rack::Deflater, if: ->(_, _, _, body) { body.any? && body[0].length > 512 } 10 | use Prometheus::Middleware::Collector 11 | use Prometheus::Middleware::Exporter 12 | 13 | run Sinatra::Application 14 | -------------------------------------------------------------------------------- /ui/charts/ui/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /post/charts/post/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /ui/views/create.haml: -------------------------------------------------------------------------------- 1 | %h2 Add blog post 2 | %form{ action: '/new', method: 'post', role: 'form'} 3 | .form-group 4 | %label{for: 'title'} Title: 5 | %input{name:'title', placeholder: 'cool title', class: 'form-control', id: 'title'} 6 | .form-group 7 | %label{for: 'link'} Link: 8 | %input{name:'link', placeholder: 'awesome link', class: 'form-control', id: 'link'} 9 | .form-group 10 | %input{class: 'btn btn-primary', type: 'submit', value:'Post it!'} 11 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | MongoDB can be accessed via port 27017 on the following DNS name from within your cluster: 2 | {{ template "mongodb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local 3 | 4 | To connect to your database run the following command: 5 | 6 | kubectl run {{ template "mongodb.fullname" . }}-client --rm --tty -i --image bitnami/mongodb --command -- mongo --host {{ template "mongodb.fullname" . }} {{- if .Values.mongodbRootPassword }} -p {{ .Values.mongodbRootPassword }}{{- end -}} 7 | 8 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "mongodb.fullname" . }} 5 | labels: 6 | app: {{ template "mongodb.fullname" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | spec: 11 | type: {{ .Values.serviceType }} 12 | ports: 13 | - name: mongodb 14 | port: 27017 15 | targetPort: mongodb 16 | selector: 17 | app: {{ template "mongodb.fullname" . }} 18 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/Chart.yaml: -------------------------------------------------------------------------------- 1 | appVersion: 3.6.1 2 | description: NoSQL document-oriented database that stores JSON-like documents with 3 | dynamic schemas, simplifying the integration of data in content-driven applications. 4 | engine: gotpl 5 | home: https://mongodb.org 6 | icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png 7 | keywords: 8 | - mongodb 9 | - database 10 | - nosql 11 | maintainers: 12 | - email: containers@bitnami.com 13 | name: Bitnami 14 | name: mongodb 15 | sources: 16 | - https://github.com/bitnami/bitnami-docker-mongodb 17 | version: 0.4.22 18 | -------------------------------------------------------------------------------- /ui/charts/ui/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "ui.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "ui.fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s" $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /post/charts/post/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "post.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "post.fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s" $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "mongodb.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "mongodb.fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s" $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.persistence.enabled }} 2 | kind: PersistentVolumeClaim 3 | apiVersion: v1 4 | metadata: 5 | name: {{ template "mongodb.fullname" . }} 6 | spec: 7 | accessModes: 8 | - {{ .Values.persistence.accessMode | quote }} 9 | resources: 10 | requests: 11 | storage: {{ .Values.persistence.size | quote }} 12 | {{- if .Values.persistence.storageClass }} 13 | {{- if (eq "-" .Values.persistence.storageClass) }} 14 | storageClassName: "" 15 | {{- else }} 16 | storageClassName: "{{ .Values.persistence.storageClass }}" 17 | {{- end }} 18 | {{- end }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /ui/charts/ui/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "ui.fullname" . }} 5 | labels: 6 | app: {{ template "ui.name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.externalPort }} 14 | targetPort: {{ .Values.service.internalPort }} 15 | protocol: TCP 16 | name: {{ .Values.service.name }} 17 | selector: 18 | app: {{ template "ui.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /post/charts/post/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "post.fullname" . }} 5 | labels: 6 | app: {{ template "post.name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.externalPort }} 14 | targetPort: {{ .Values.service.internalPort }} 15 | protocol: TCP 16 | name: {{ .Values.service.name }} 17 | selector: 18 | app: {{ template "post.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ template "mongodb.fullname" . }} 5 | labels: 6 | app: {{ template "mongodb.fullname" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | type: Opaque 11 | data: 12 | {{ if .Values.mongodbRootPassword }} 13 | mongodb-root-password: {{ .Values.mongodbRootPassword | b64enc | quote }} 14 | {{ else }} 15 | mongodb-root-password: {{ randAlphaNum 10 | b64enc | quote }} 16 | {{ end }} 17 | {{ if .Values.mongodbPassword }} 18 | mongodb-password: {{ .Values.mongodbPassword | b64enc | quote }} 19 | {{ else }} 20 | mongodb-password: {{ randAlphaNum 10 | b64enc | quote }} 21 | {{ end }} -------------------------------------------------------------------------------- /post/charts/post/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for post. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | # nameOverride: post 5 | replicaCount: 1 6 | image: 7 | repository: dockerhub/post 8 | tag: latest 9 | pullPolicy: IfNotPresent 10 | 11 | envVars: 12 | POST_DATABASE_HOST: mongodb 13 | POST_DATABASE_PORT: 27017 14 | 15 | service: 16 | name: post 17 | type: ClusterIP 18 | externalPort: 5000 19 | internalPort: 5000 20 | 21 | nodeSelector: 22 | cloud.google.com/gke-nodepool: default-pool 23 | 24 | resources: {} 25 | # We usually recommend not to specify default resources and to leave this as a conscious 26 | # choice for the user. This also increases chances charts run on environments with little 27 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 28 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 29 | # limits: 30 | # cpu: 100m 31 | # memory: 128Mi 32 | # requests: 33 | # cpu: 100m 34 | # memory: 128Mi 35 | -------------------------------------------------------------------------------- /ui/charts/ui/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $serviceName := include "ui.fullname" . -}} 3 | {{- $servicePort := .Values.service.externalPort -}} 4 | apiVersion: extensions/v1beta1 5 | kind: Ingress 6 | metadata: 7 | name: {{ template "ui.fullname" . }} 8 | labels: 9 | app: {{ template "ui.name" . }} 10 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 11 | release: {{ .Release.Name }} 12 | heritage: {{ .Release.Service }} 13 | annotations: 14 | {{- range $key, $value := .Values.ingress.annotations }} 15 | {{ $key }}: {{ $value | quote }} 16 | {{- end }} 17 | spec: 18 | rules: 19 | {{- range $host := .Values.ingress.hosts }} 20 | - host: {{ $host }} 21 | http: 22 | paths: 23 | - path: /* 24 | backend: 25 | serviceName: {{ $serviceName }} 26 | servicePort: {{ $servicePort }} 27 | {{- end -}} 28 | {{- if .Values.ingress.tls }} 29 | tls: 30 | {{ toYaml .Values.ingress.tls | indent 4 }} 31 | {{- end -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /ui/middleware.rb: -------------------------------------------------------------------------------- 1 | require 'prometheus/client' 2 | 3 | class Metrics 4 | def initialize(app) 5 | @app = app 6 | prometheus = Prometheus::Client.registry 7 | @request_count = Prometheus::Client::Counter.new( 8 | :ui_request_count, 9 | 'App request rount' 10 | ) 11 | @request_response_time = Prometheus::Client::Histogram.new( 12 | :ui_request_response_time, 13 | 'Request response time' 14 | ) 15 | prometheus.register(@request_response_time) 16 | prometheus.register(@request_count) 17 | end 18 | 19 | def call(env) 20 | request_started_on = Time.now 21 | env['REQUEST_ID'] = SecureRandom.uuid # add unique ID to each request 22 | @status, @headers, @response = @app.call(env) 23 | request_ended_on = Time.now 24 | # prometheus metrics 25 | @request_response_time.observe({ path: env['REQUEST_PATH'] }, 26 | request_ended_on - request_started_on) 27 | @request_count.increment(method: env['REQUEST_METHOD'], 28 | path: env['REQUEST_PATH'], 29 | http_status: @status) 30 | [@status, @headers, @response] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /post/helpers.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from pymongo import MongoClient 3 | from pymongo.errors import ConnectionFailure 4 | from json import dumps 5 | from flask import request 6 | 7 | 8 | log = structlog.get_logger() 9 | 10 | 11 | def http_healthcheck_handler(mongo_host, mongo_port, version): 12 | postdb = MongoClient(mongo_host, int(mongo_port), 13 | serverSelectionTimeoutMS=2000) 14 | try: 15 | postdb.admin.command('ismaster') 16 | except ConnectionFailure: 17 | postdb_status = 0 18 | else: 19 | postdb_status = 1 20 | 21 | status = postdb_status 22 | healthcheck = { 23 | 'status': status, 24 | 'dependent_services': { 25 | 'postdb': postdb_status 26 | }, 27 | 'version': version 28 | } 29 | return dumps(healthcheck) 30 | 31 | 32 | def log_event(event_type, name, message, params={}): 33 | request_id = request.headers['Request-Id'] \ 34 | if 'Request-Id' in request.headers else None 35 | if event_type == 'info': 36 | log.info(name, service='post', request_id=request_id, 37 | message=message, params=params) 38 | elif event_type == 'error': 39 | log.error(name, service='post', request_id=request_id, 40 | message=message, params=params) 41 | -------------------------------------------------------------------------------- /ui/charts/ui/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http://{{ . }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "ui.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ template "ui.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "ui.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "ui.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /ui/charts/ui/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "ui.fullname" . }} 5 | labels: 6 | app: {{ template "ui.name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ template "ui.name" . }} 16 | release: {{ .Release.Name }} 17 | spec: 18 | containers: 19 | - name: {{ .Chart.Name }} 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 21 | imagePullPolicy: {{ .Values.image.pullPolicy }} 22 | {{- if .Values.envVars}} 23 | env: 24 | {{- range $key, $value := .Values.envVars }} 25 | - name: "{{ $key }}" 26 | value: "{{ $value }}" 27 | {{- end }} 28 | {{- end }} 29 | ports: 30 | - containerPort: {{ .Values.service.internalPort }} 31 | livenessProbe: 32 | httpGet: 33 | path: / 34 | port: {{ .Values.service.internalPort }} 35 | readinessProbe: 36 | httpGet: 37 | path: / 38 | port: {{ .Values.service.internalPort }} 39 | resources: 40 | {{ toYaml .Values.resources | indent 12 }} 41 | {{- if .Values.nodeSelector }} 42 | nodeSelector: 43 | {{ toYaml .Values.nodeSelector | indent 8 }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /ui/views/index.haml: -------------------------------------------------------------------------------- 1 | - unless @posts.nil? or @posts.empty? 2 | - @posts.each do |post| 3 | %div{ id: 'postlist'} 4 | %div{class: 'panel'} 5 | %div{class: 'panel-heading'} 6 | %div{class: 'text-center'} 7 | %div{class: 'row'} 8 | .col-sm-1 9 | %form{:action => "/post/#{post['_id']['$oid']}/vote/1", :method => "post", id: "form-upvote" } 10 | %input{:type => "hidden", :name => "_method", :value => "post"} 11 | %button{type: "submit", class: "btn btn-default btn-sm"} 12 | %span{ class: "glyphicon glyphicon-menu-up" } 13 | %h4{class: 'pull-center'} #{post['votes']} 14 | %form{:action => "/post/#{post['_id']['$oid']}/vote/-1", :method => "post", id: "form-downvote"} 15 | %input{:type => "hidden", :name => "_method", :value=> "post"} 16 | %button{type: "submit", class: "btn btn-default btn-sm"} 17 | %span{ class: "glyphicon glyphicon-menu-down" } 18 | .col-sm-8 19 | %h3{class: 'pull-left'} 20 | %a{href: "/post/#{post['_id']['$oid']}"} #{post['title']} 21 | .col-sm-3 22 | %h4{class: 'pull-right'} 23 | %small 24 | %em #{Time.at(post['created_at'].to_i).strftime('%d-%m-%Y')} 25 | %br #{Time.at(post['created_at'].to_i).strftime('%H:%M')} 26 | .panel-footer 27 | %a{href: "#{post['link']}", class: 'btn btn-link'} Go to the link 28 | -------------------------------------------------------------------------------- /ui/charts/ui/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for ui-chart. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | # nameOverride: ui 5 | replicaCount: 2 6 | image: 7 | repository: dockerbub/ui 8 | tag: latest 9 | pullPolicy: IfNotPresent 10 | 11 | envVars: 12 | POST_SERVICE_HOST: post 13 | POST_SERVICE_PORT: 5000 14 | 15 | service: 16 | name: ui 17 | type: NodePort 18 | externalPort: 9292 19 | internalPort: 9292 20 | 21 | nodeSelector: 22 | cloud.google.com/gke-nodepool: default-pool 23 | 24 | ingress: 25 | enabled: true 26 | # Used to create an Ingress record. 27 | hosts: 28 | - example.com # change to the domain name of your application 29 | annotations: 30 | kubernetes.io/ingress.global-static-ip-name: raddit-static-ip 31 | kubernetes.io/ingress.class: "gce" 32 | kubernetes.io/tls-acme: "true" 33 | # kubernetes.io/ingress.allow-http: "false" 34 | tls: 35 | - secretName: ui-tls 36 | hosts: 37 | - example.com # change to the domain name of your application 38 | resources: {} 39 | # We usually recommend not to specify default resources and to leave this as a conscious 40 | # choice for the user. This also increases chances charts run on environments with little 41 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 42 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 43 | # limits: 44 | # cpu: 100m 45 | # memory: 128Mi 46 | # requests: 47 | # cpu: 100m 48 | # memory: 128Mi 49 | -------------------------------------------------------------------------------- /post/charts/post/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "post.fullname" . }} 5 | labels: 6 | app: {{ template "post.name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ template "post.name" . }} 16 | release: {{ .Release.Name }} 17 | spec: 18 | containers: 19 | - name: {{ .Chart.Name }} 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 21 | imagePullPolicy: {{ .Values.image.pullPolicy }} 22 | {{- if .Values.envVars}} 23 | env: 24 | {{- range $key, $value := .Values.envVars }} 25 | - name: "{{ $key }}" 26 | value: "{{ $value }}" 27 | {{- end }} 28 | {{- end }} 29 | ports: 30 | - containerPort: {{ .Values.service.internalPort }} 31 | livenessProbe: 32 | httpGet: 33 | path: /posts 34 | port: {{ .Values.service.internalPort }} 35 | readinessProbe: 36 | httpGet: 37 | path: /posts 38 | port: {{ .Values.service.internalPort }} 39 | resources: 40 | {{ toYaml .Values.resources | indent 12 }} 41 | {{- if .Values.nodeSelector }} 42 | nodeSelector: 43 | {{ toYaml .Values.nodeSelector | indent 8 }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/values.yaml: -------------------------------------------------------------------------------- 1 | ## Bitnami MongoDB image version 2 | ## ref: https://hub.docker.com/r/bitnami/mongodb/tags/ 3 | ## 4 | image: bitnami/mongodb:3.6.1-r0 5 | 6 | ## Specify a imagePullPolicy 7 | ## 'Always' if imageTag is 'latest', else set to 'IfNotPresent' 8 | ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images 9 | ## 10 | # imagePullPolicy: 11 | 12 | ## MongoDB admin password 13 | ## ref: https://github.com/bitnami/bitnami-docker-mongodb/blob/master/README.md#setting-the-root-password-on-first-run 14 | ## 15 | # mongodbRootPassword: 16 | 17 | ## MongoDB custom user and database 18 | ## ref: https://github.com/bitnami/bitnami-docker-mongodb/blob/master/README.md#creating-a-user-and-database-on-first-run 19 | ## 20 | # mongodbUsername: 21 | # mongodbPassword: 22 | # mongodbDatabase: 23 | 24 | ## Kubernetes service type 25 | serviceType: ClusterIP 26 | 27 | nodeSelector: 28 | cloud.google.com/gke-nodepool: default-pool 29 | 30 | ## Enable persistence using Persistent Volume Claims 31 | ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ 32 | ## 33 | persistence: 34 | enabled: true 35 | ## mongodb data Persistent Volume Storage Class 36 | ## If defined, storageClassName: 37 | ## If set to "-", storageClassName: "", which disables dynamic provisioning 38 | ## If undefined (the default) or set to null, no storageClassName spec is 39 | ## set, choosing the default provisioner. (gp2 on AWS, standard on 40 | ## GKE, AWS & OpenStack) 41 | ## 42 | storageClass: ssd 43 | accessMode: ReadWriteOnce 44 | size: 8Gi 45 | 46 | ## Configure resource requests and limits 47 | ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ 48 | ## 49 | resources: 50 | requests: 51 | memory: 256Mi 52 | cpu: 100m 53 | -------------------------------------------------------------------------------- /mongodb/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - deploy 3 | 4 | variables: 5 | # GKE info 6 | CLUSTER_NAME: my-cluster 7 | ZONE: europe-west1-b 8 | PROJECT: example-123456 9 | 10 | CHART_PATH: ./charts/mongodb 11 | 12 | SERVICE_ACCOUNT: /etc/deploy/sa.json 13 | STAGE_NAMESPACE: raddit-stage 14 | PROD_NAMESPACE: raddit-prod 15 | STAGE_RELEASE_NAME: raddit-stage-mongodb 16 | PROD_RELEASE_NAME: raddit-prod-mongodb 17 | 18 | 19 | # deploy to staging environment 20 | deploy_staging: 21 | stage: deploy 22 | image: artemkin/helm-gke:1.0 23 | before_script: 24 | - mkdir -p /etc/deploy 25 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 26 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 27 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 28 | - helm init --client-only 29 | script: 30 | - helm upgrade --install 31 | --wait 32 | --namespace=${STAGE_NAMESPACE} 33 | ${STAGE_RELEASE_NAME} ${CHART_PATH} 34 | environment: 35 | name: staging 36 | only: 37 | - master 38 | 39 | # deploy to production environment (manual) 40 | deploy_prod: 41 | stage: deploy 42 | image: artemkin/helm-gke:1.0 43 | before_script: 44 | - mkdir -p /etc/deploy 45 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 46 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 47 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 48 | - helm init --client-only 49 | script: 50 | - helm upgrade --install 51 | --wait 52 | --namespace=${PROD_NAMESPACE} 53 | ${PROD_RELEASE_NAME} ${CHART_PATH} 54 | environment: 55 | name: production 56 | when: manual 57 | only: 58 | - master 59 | -------------------------------------------------------------------------------- /ui/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | backports (3.8.0) 5 | bson (1.12.5) 6 | bson_ext (1.12.5) 7 | bson (~> 1.12.5) 8 | concurrent-ruby (1.0.5) 9 | et-orbi (1.0.8) 10 | tzinfo 11 | faraday (0.13.1) 12 | multipart-post (>= 1.2, < 3) 13 | finagle-thrift (1.4.2) 14 | thrift (~> 0.9.3) 15 | haml (5.0.2) 16 | temple (>= 0.8.0) 17 | tilt 18 | multi_json (1.12.1) 19 | multipart-post (2.0.0) 20 | mustermann (1.0.0) 21 | prometheus-client (0.7.1) 22 | quantile (~> 0.2.0) 23 | puma (3.10.0) 24 | quantile (0.2.0) 25 | rack (2.0.3) 26 | rack-protection (2.0.0) 27 | rack 28 | rufus-scheduler (3.4.2) 29 | et-orbi (~> 1.0) 30 | sinatra (2.0.0) 31 | mustermann (~> 1.0) 32 | rack (~> 2.0) 33 | rack-protection (= 2.0.0) 34 | tilt (~> 2.0) 35 | sinatra-contrib (2.0.0) 36 | backports (>= 2.0) 37 | multi_json 38 | mustermann (~> 1.0) 39 | rack-protection (= 2.0.0) 40 | sinatra (= 2.0.0) 41 | tilt (>= 1.3, < 3) 42 | sucker_punch (2.0.4) 43 | concurrent-ruby (~> 1.0.0) 44 | temple (0.8.0) 45 | thread_safe (0.3.6) 46 | thrift (0.9.3.0) 47 | tilt (2.0.8) 48 | tzinfo (1.2.3) 49 | thread_safe (~> 0.1) 50 | tzinfo-data (1.2017.3) 51 | tzinfo (>= 1.0.0) 52 | zipkin-tracer (0.27.1) 53 | faraday (~> 0.8) 54 | finagle-thrift (~> 1.4.2) 55 | rack (>= 1.0) 56 | sucker_punch (~> 2.0) 57 | 58 | PLATFORMS 59 | ruby 60 | 61 | DEPENDENCIES 62 | bson_ext 63 | faraday 64 | haml 65 | prometheus-client 66 | puma 67 | rack 68 | rufus-scheduler 69 | sinatra 70 | sinatra-contrib 71 | tzinfo-data 72 | zipkin-tracer 73 | 74 | BUNDLED WITH 75 | 1.14.6 76 | -------------------------------------------------------------------------------- /ui/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html(lang="en") 3 | %head 4 | %meta(charset="utf-8") 5 | %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") 6 | %meta(name="viewport" content="width=device-width, initial-scale=1.0") 7 | %title="Raddit :: #{@title}" 8 | %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', integrity: "sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7", type: 'text/css', rel: 'stylesheet', crossorigin: 'anonymous' } 9 | %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css', integrity: 'sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r', type: 'text/css', rel: 'stylesheet', crossorigin: 'anonymous' } 10 | %script{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js', integrity: 'sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS', crossorigin: 'anonymous'} 11 | %body 12 | .navbar.navbar-default.navbar-static-top 13 | .container 14 | %button.navbar-toggle(type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse") 15 | %span.icon-bar 16 | %span.icon-bar 17 | %span.icon-bar 18 | %a.navbar-brand(href="/") Raddit 19 | %b version #{@@version} 20 | .navbar-collapse.collapse.navbar-responsive-collapse 21 | .container 22 | .row 23 | .col-lg-9 24 | - unless @flashes.nil? or @flashes.empty? 25 | - @flashes.each do |flash| 26 | .alert{class: flash[:type]} 27 | %strong #{flash[:message]} 28 | = yield 29 | .col-lg-3 30 | .well.sidebar-nav 31 | %h3 MENU 32 | %ul.nav.nav-list 33 | %li 34 | %a(href="/") All posts 35 | %li 36 | %a(href="/new") New post 37 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "mongodb.fullname" . }} 5 | labels: 6 | app: {{ template "mongodb.fullname" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: "{{ .Release.Name }}" 9 | heritage: "{{ .Release.Service }}" 10 | spec: 11 | template: 12 | metadata: 13 | labels: 14 | app: {{ template "mongodb.fullname" . }} 15 | spec: 16 | containers: 17 | - name: {{ template "mongodb.fullname" . }} 18 | image: "{{ .Values.image }}" 19 | imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} 20 | env: 21 | - name: MONGODB_ROOT_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: {{ template "mongodb.fullname" . }} 25 | key: mongodb-root-password 26 | - name: MONGODB_USERNAME 27 | value: {{ default "" .Values.mongodbUsername | quote }} 28 | - name: MONGODB_PASSWORD 29 | valueFrom: 30 | secretKeyRef: 31 | name: {{ template "mongodb.fullname" . }} 32 | key: mongodb-password 33 | - name: MONGODB_DATABASE 34 | value: {{ default "" .Values.mongodbDatabase | quote }} 35 | ports: 36 | - name: mongodb 37 | containerPort: 27017 38 | livenessProbe: 39 | exec: 40 | command: 41 | - mongo 42 | - --eval 43 | - "db.adminCommand('ping')" 44 | initialDelaySeconds: 30 45 | timeoutSeconds: 5 46 | readinessProbe: 47 | exec: 48 | command: 49 | - mongo 50 | - --eval 51 | - "db.adminCommand('ping')" 52 | initialDelaySeconds: 5 53 | timeoutSeconds: 1 54 | volumeMounts: 55 | - name: data 56 | mountPath: /bitnami/mongodb 57 | resources: 58 | {{ toYaml .Values.resources | indent 10 }} 59 | {{- if .Values.nodeSelector }} 60 | nodeSelector: 61 | {{ toYaml .Values.nodeSelector | indent 8 }} 62 | {{- end }} 63 | volumes: 64 | - name: data 65 | {{- if .Values.persistence.enabled }} 66 | persistentVolumeClaim: 67 | claimName: {{ template "mongodb.fullname" . }} 68 | {{- else }} 69 | emptyDir: {} 70 | {{- end -}} 71 | -------------------------------------------------------------------------------- /ui/helpers.rb: -------------------------------------------------------------------------------- 1 | def flash_danger(message) 2 | session[:flashes] << { type: 'alert-danger', message: message } 3 | end 4 | 5 | def flash_success(message) 6 | session[:flashes] << { type: 'alert-success', message: message } 7 | end 8 | 9 | def log_event(type, name, message, params = '{}') 10 | case type 11 | when 'error' 12 | logger.error('service=ui | ' \ 13 | "event=#{name} | " \ 14 | "request_id=#{request.env['REQUEST_ID']} | " \ 15 | "message=\'#{message}\' | " \ 16 | "params: #{params.to_json}") 17 | when 'info' 18 | logger.info('service=ui | ' \ 19 | "event=#{name} | " \ 20 | "request_id=#{request.env['REQUEST_ID']} | " \ 21 | "message=\'#{message}\' | " \ 22 | "params: #{params.to_json}") 23 | when 'warning' 24 | logger.warn('service=ui | ' \ 25 | "event=#{name} | " \ 26 | "request_id=#{request.env['REQUEST_ID']} | " \ 27 | "message=\'#{message}\' | " \ 28 | "params: #{params.to_json}") 29 | end 30 | end 31 | 32 | def http_request(method, url, params = {}) 33 | unless defined?(request).nil? 34 | settings.http_client.headers[:request_id] = request.env['REQUEST_ID'].to_s 35 | end 36 | 37 | case method 38 | when 'get' 39 | response = settings.http_client.get url 40 | JSON.parse(response.body) 41 | when 'post' 42 | settings.http_client.post url, params 43 | end 44 | end 45 | 46 | def http_healthcheck_handler(post_url, comment_url, version) 47 | post_status = check_service_health(post_url) 48 | comment_status = check_service_health(comment_url) 49 | 50 | status = if comment_status == 1 && post_status == 1 51 | 1 52 | else 53 | 0 54 | end 55 | 56 | healthcheck = { status: status, 57 | dependent_services: { 58 | comment: comment_status, 59 | post: post_status 60 | }, 61 | version: version } 62 | healthcheck.to_json 63 | end 64 | 65 | def check_service_health(url) 66 | name = http_request('get', "#{url}/healthcheck") 67 | rescue StandardError 68 | 0 69 | else 70 | name['status'] 71 | end 72 | 73 | def set_health_gauge(metric, value) 74 | metric.set( 75 | { 76 | version: VERSION 77 | }, 78 | value 79 | ) 80 | end 81 | -------------------------------------------------------------------------------- /ui/views/show.haml: -------------------------------------------------------------------------------- 1 | %div{ id: 'postlist'} 2 | %div{class: 'panel'} 3 | %div{class: 'panel-heading'} 4 | %div{class: 'text-center'} 5 | %div{class: 'row'} 6 | .col-sm-1 7 | %form{action: "#{@post['_id']['$oid']}/vote/1", method: "post"} 8 | %input{:type => "hidden", :name => "_method", :value => "post"} 9 | %button{type: "submit", class: "btn btn-default btn-sm"} 10 | %span{ class: "glyphicon glyphicon-menu-up" } 11 | %h4{class: 'pull-center'} #{@post['votes']} 12 | %form{:action => "#{@post['_id']['$oid']}/vote/-1", :method => "post"} 13 | %input{:type => "hidden", :name => "_method", :value=> "post"} 14 | %button{type: "submit", class: "btn btn-default btn-sm"} 15 | %span{ class: "glyphicon glyphicon-menu-down" } 16 | .col-sm-8 17 | %h3{class: 'pull-left'} 18 | %a{href: "#{@post['_id']['$oid']}"} #{@post['title']} 19 | .col-sm-3 20 | %h4{class: 'pull-right'} 21 | %small 22 | %em #{Time.at(@post['created_at'].to_i).strftime('%d-%m-%Y')} 23 | %br #{Time.at(@post['created_at'].to_i).strftime('%H:%M')} 24 | .panel-footer 25 | %a{href: "#{@post['link']}", class: 'btn btn-link'} Go to the link 26 | 27 | - unless @comments.nil? or @comments.empty? 28 | - @comments.each do |comment| 29 | .row 30 | .col-sm-8 31 | .panel.panel-default 32 | .panel-heading 33 | %strong #{comment['name']} 34 | - unless comment['email'].empty? 35 | (#{comment['email']}) 36 | %span{class: "text-muted pull-right"} 37 | %em #{Time.at(comment['created_at'].to_i).strftime('%H:%M')} #{Time.at(comment['created_at'].to_i).strftime('%d-%m-%Y')} 38 | .panel-body #{comment['body']} 39 | %form{action: "/post/#{@post['_id']['$oid']}/comment", method: 'post', role: 'form'} 40 | .col-sm-4 41 | .form-group 42 | %input{name: 'name', type: 'text', class: 'form-control', placeholder: 'name'} 43 | .col-sm-4 44 | .form-group 45 | %input{name: 'email', type: 'email', class: 'form-control', placeholder: 'email'} 46 | .col-sm-8 47 | .form-group 48 | %textarea{name: 'body', class: 'form-control', placeholder: 'put a nice comment :)'} 49 | %button{class: 'btn btn-block btn-primary'} Post my comment 50 | -------------------------------------------------------------------------------- /post/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - release 5 | - deploy 6 | 7 | variables: 8 | # GKE info 9 | CLUSTER_NAME: my-cluster 10 | ZONE: europe-west1-b 11 | PROJECT: example-123456 12 | 13 | CONTAINER_IMAGE: artemkin/post 14 | CHART_PATH: ./charts/post 15 | CONTAINER_IMAGE_BUILT: ${CONTAINER_IMAGE}:${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHA} 16 | CONTAINER_IMAGE_LATEST: ${CONTAINER_IMAGE}:latest 17 | CI_REGISTRY: index.docker.io # container registry URL 18 | # When using dind, it's wise to use the overlayfs driver for 19 | # improved performance. 20 | DOCKER_DRIVER: overlay2 21 | DOCKER_HOST: tcp://localhost:2375 # required since we use dind 22 | 23 | SERVICE_ACCOUNT: /etc/deploy/sa.json 24 | STAGE_NAMESPACE: raddit-stage 25 | PROD_NAMESPACE: raddit-prod 26 | STAGE_RELEASE_NAME: raddit-stage-post 27 | PROD_RELEASE_NAME: raddit-prod-post 28 | 29 | 30 | # build container image 31 | build: 32 | stage: build 33 | image: docker:latest 34 | services: 35 | - docker:dind 36 | script: 37 | - echo "Building Dockerfile-based application..." 38 | - docker build -t ${CONTAINER_IMAGE_BUILT} . 39 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} 40 | - echo "Pushing to the Container Registry..." 41 | - docker push ${CONTAINER_IMAGE_BUILT} 42 | 43 | # run tests against built image 44 | test: 45 | stage: test 46 | script: 47 | - exit 0 48 | 49 | # tag container image that passed the tests successfully 50 | # and push it to the registry 51 | release: 52 | stage: release 53 | image: docker:latest 54 | services: 55 | - docker:dind 56 | script: 57 | - echo "Pulling docker image from Container Registry" 58 | - docker pull ${CONTAINER_IMAGE_BUILT} 59 | - echo "Logging to Container Registry at ${CI_REGISTRY}" 60 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} 61 | - echo "Pushing to Container Registry..." 62 | - docker tag ${CONTAINER_IMAGE_BUILT} ${CONTAINER_IMAGE}:$(cat VERSION) 63 | - docker push ${CONTAINER_IMAGE}:$(cat VERSION) 64 | - docker tag ${CONTAINER_IMAGE}:$(cat VERSION) ${CONTAINER_IMAGE_LATEST} 65 | - docker push ${CONTAINER_IMAGE_LATEST} 66 | - echo "" 67 | only: 68 | - master 69 | 70 | # deploy to staging environment 71 | deploy_staging: 72 | stage: deploy 73 | image: artemkin/helm-gke:1.0 74 | before_script: 75 | - mkdir -p /etc/deploy 76 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 77 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 78 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 79 | - helm init --client-only 80 | script: 81 | - helm upgrade --install 82 | --set image.tag=$(cat VERSION) 83 | --set image.repository=${CONTAINER_IMAGE} 84 | --wait 85 | --namespace=${STAGE_NAMESPACE} 86 | ${STAGE_RELEASE_NAME} ${CHART_PATH} 87 | environment: 88 | name: staging 89 | only: 90 | - master 91 | 92 | # deploy to production environment (manual) 93 | deploy_prod: 94 | stage: deploy 95 | image: artemkin/helm-gke:1.0 96 | before_script: 97 | - mkdir -p /etc/deploy 98 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 99 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 100 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 101 | - helm init --client-only 102 | script: 103 | - helm upgrade --install 104 | --set image.tag=$(cat VERSION) 105 | --set image.repository=${CONTAINER_IMAGE} 106 | --wait 107 | --namespace=${PROD_NAMESPACE} 108 | ${PROD_RELEASE_NAME} ${CHART_PATH} 109 | environment: 110 | name: production 111 | when: manual 112 | only: 113 | - master 114 | -------------------------------------------------------------------------------- /ui/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - release 5 | - deploy 6 | 7 | variables: 8 | # GKE info 9 | CLUSTER_NAME: my-cluster 10 | ZONE: europe-west1-b 11 | PROJECT: example-123456 12 | DOMAIN_NAME: devops-by-practice.fun # application domain name 13 | 14 | CHART_PATH: ./charts/ui 15 | CONTAINER_IMAGE: artemkin/ui 16 | CONTAINER_IMAGE_BUILT: ${CONTAINER_IMAGE}:${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHA} 17 | CONTAINER_IMAGE_LATEST: ${CONTAINER_IMAGE}:latest 18 | CI_REGISTRY: index.docker.io # container registry URL 19 | DOCKER_DRIVER: overlay2 20 | DOCKER_HOST: tcp://localhost:2375 # required since we use dind 21 | 22 | SERVICE_ACCOUNT: /etc/deploy/sa.json 23 | STAGE_NAMESPACE: raddit-stage 24 | PROD_NAMESPACE: raddit-prod 25 | STAGE_RELEASE_NAME: raddit-stage-ui 26 | PROD_RELEASE_NAME: raddit-prod-ui 27 | 28 | # build container image 29 | build: 30 | stage: build 31 | image: docker:latest 32 | services: 33 | - docker:dind 34 | script: 35 | - echo "Building Dockerfile-based application..." 36 | - docker build -t ${CONTAINER_IMAGE_BUILT} . 37 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} 38 | - echo "Pushing to the Container Registry..." 39 | - docker push ${CONTAINER_IMAGE_BUILT} 40 | 41 | # run tests against built image 42 | test: 43 | stage: test 44 | script: 45 | - exit 0 46 | 47 | # tag container image that passed the tests successfully 48 | # and push it to the registry 49 | release: 50 | stage: release 51 | image: docker:latest 52 | services: 53 | - docker:dind 54 | script: 55 | - echo "Pulling docker image from Container Registry" 56 | - docker pull ${CONTAINER_IMAGE_BUILT} 57 | - echo "Logging to Container Registry at ${CI_REGISTRY}" 58 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} 59 | - echo "Pushing to Container Registry..." 60 | - docker tag ${CONTAINER_IMAGE_BUILT} ${CONTAINER_IMAGE}:$(cat VERSION) 61 | - docker push ${CONTAINER_IMAGE}:$(cat VERSION) 62 | - docker tag ${CONTAINER_IMAGE}:$(cat VERSION) ${CONTAINER_IMAGE_LATEST} 63 | - docker push ${CONTAINER_IMAGE_LATEST} 64 | - echo "" 65 | only: 66 | - master 67 | 68 | # deploy to staging environment 69 | deploy_staging: 70 | stage: deploy 71 | image: artemkin/helm-gke:1.0 72 | before_script: 73 | - mkdir -p /etc/deploy 74 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 75 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 76 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 77 | - helm init --client-only 78 | script: 79 | - helm upgrade --install 80 | --set image.tag=$(cat VERSION) 81 | --set ingress.enabled=false 82 | --set image.repository=${CONTAINER_IMAGE} 83 | --wait 84 | --namespace=${STAGE_NAMESPACE} 85 | ${STAGE_RELEASE_NAME} ${CHART_PATH} 86 | environment: 87 | name: staging 88 | only: 89 | - master 90 | 91 | # deploy to production environment (manual) 92 | deploy_prod: 93 | stage: deploy 94 | image: artemkin/helm-gke:1.0 95 | before_script: 96 | - mkdir -p /etc/deploy 97 | - echo ${service_account} | base64 -d > ${SERVICE_ACCOUNT} 98 | - gcloud auth activate-service-account --key-file ${SERVICE_ACCOUNT} 99 | - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT} 100 | - helm init --client-only 101 | script: 102 | - helm upgrade --install 103 | --set image.tag=$(cat VERSION) 104 | --set image.repository=${CONTAINER_IMAGE} 105 | --set ingress.hosts[0]=${DOMAIN_NAME} 106 | --set ingress.tls[0].secretName=ui-tls,ingress.tls[0].hosts={${DOMAIN_NAME}} 107 | --wait 108 | --namespace=${PROD_NAMESPACE} 109 | ${PROD_RELEASE_NAME} ${CHART_PATH} 110 | environment: 111 | name: production 112 | url: https://${DOMAIN_NAME} 113 | when: manual 114 | only: 115 | - master 116 | -------------------------------------------------------------------------------- /mongodb/charts/mongodb/README.md: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | 3 | [MongoDB](https://www.mongodb.com/) is a cross-platform document-oriented database. Classified as a NoSQL database, MongoDB eschews the traditional table-based relational database structure in favor of JSON-like documents with dynamic schemas, making the integration of data in certain types of applications easier and faster. 4 | 5 | ## TL;DR; 6 | 7 | ```bash 8 | $ helm install stable/mongodb 9 | ``` 10 | 11 | ## Introduction 12 | 13 | This chart bootstraps a [MongoDB](https://github.com/bitnami/bitnami-docker-mongodb) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. 14 | 15 | ## Prerequisites 16 | 17 | - Kubernetes 1.4+ with Beta APIs enabled 18 | - PV provisioner support in the underlying infrastructure 19 | 20 | ## Installing the Chart 21 | 22 | To install the chart with the release name `my-release`: 23 | 24 | ```bash 25 | $ helm install --name my-release stable/mongodb 26 | ``` 27 | 28 | The command deploys MongoDB on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. 29 | 30 | > **Tip**: List all releases using `helm list` 31 | 32 | ## Uninstalling the Chart 33 | 34 | To uninstall/delete the `my-release` deployment: 35 | 36 | ```bash 37 | $ helm delete my-release 38 | ``` 39 | 40 | The command removes all the Kubernetes components associated with the chart and deletes the release. 41 | 42 | ## Configuration 43 | 44 | The following tables lists the configurable parameters of the MongoDB chart and their default values. 45 | 46 | | Parameter | Description | Default | 47 | |----------------------------|-------------------------------------|----------------------------------------------------------| 48 | | `image` | MongoDB image | `bitnami/mongodb:{VERSION}` | 49 | | `imagePullPolicy` | Image pull policy | `Always` if `imageTag` is `latest`, else `IfNotPresent`. | 50 | | `mongodbRootPassword` | MongoDB admin password | `random alhpanumeric string (10)` | 51 | | `mongodbUsername` | MongoDB custom user | `nil` | 52 | | `mongodbPassword` | MongoDB custom user password | `random alhpanumeric string (10)` | 53 | | `mongodbDatabase` | Database to create | `nil` | 54 | | `serviceType` | Kubernetes Service type | `ClusterIP` | 55 | | `persistence.enabled` | Use a PVC to persist data | `true` | 56 | | `persistence.storageClass` | Storage class of backing PVC | `nil` (uses alpha storage class annotation) | 57 | | `persistence.accessMode` | Use volume as ReadOnly or ReadWrite | `ReadWriteOnce` | 58 | | `persistence.size` | Size of data volume | `8Gi` | 59 | 60 | The above parameters map to the env variables defined in [bitnami/mongodb](http://github.com/bitnami/bitnami-docker-mongodb). For more information please refer to the [bitnami/mongodb](http://github.com/bitnami/bitnami-docker-mongodb) image documentation. 61 | 62 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, 63 | 64 | ```bash 65 | $ helm install --name my-release \ 66 | --set mongodbRootPassword=secretpassword,mongodbUsername=my-user,mongodbPassword=my-password,mongodbDatabase=my-database \ 67 | stable/mongodb 68 | ``` 69 | 70 | The above command sets the MongoDB `root` account password to `secretpassword`. Additionally it creates a standard database user named `my-user`, with the password `my-password`, who has access to a database named `my-database`. 71 | 72 | Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, 73 | 74 | ```bash 75 | $ helm install --name my-release -f values.yaml stable/mongodb 76 | ``` 77 | 78 | > **Tip**: You can use the default [values.yaml](values.yaml) 79 | 80 | ## Persistence 81 | 82 | The [Bitnami MongoDB](https://github.com/bitnami/bitnami-docker-mongodb) image stores the MongoDB data and configurations at the `/bitnami/mongodb` path of the container. 83 | 84 | The chart mounts a [Persistent Volume](http://kubernetes.io/docs/user-guide/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning. 85 | -------------------------------------------------------------------------------- /ui/ui_app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/reloader' 3 | require 'json/ext' 4 | require 'haml' 5 | require 'uri' 6 | require 'prometheus/client' 7 | require 'rufus-scheduler' 8 | require 'logger' 9 | require 'faraday' 10 | require_relative 'helpers' 11 | 12 | # Dependent services 13 | POST_SERVICE_HOST ||= ENV['POST_SERVICE_HOST'] || '127.0.0.1' 14 | POST_SERVICE_PORT ||= ENV['POST_SERVICE_PORT'] || '4567' 15 | COMMENT_SERVICE_HOST ||= ENV['COMMENT_SERVICE_HOST'] || '127.0.0.1' 16 | COMMENT_SERVICE_PORT ||= ENV['COMMENT_SERVICE_PORT'] || '4567' 17 | POST_URL ||= "http://#{POST_SERVICE_HOST}:#{POST_SERVICE_PORT}" 18 | COMMENT_URL ||= "http://#{COMMENT_SERVICE_HOST}:#{COMMENT_SERVICE_PORT}" 19 | 20 | # App version 21 | VERSION ||= File.read('VERSION').strip 22 | @@version = VERSION 23 | 24 | configure do 25 | http_client = Faraday.new do |faraday| 26 | faraday.request :url_encoded # form-encode POST params 27 | faraday.adapter Faraday.default_adapter # make requests with Net::HTTP 28 | end 29 | set :http_client, http_client 30 | set :bind, '0.0.0.0' 31 | set :server, :puma 32 | set :logging, false 33 | set :mylogger, Logger.new(STDOUT) 34 | enable :sessions 35 | end 36 | 37 | # before each request 38 | before do 39 | session[:flashes] = [] if session[:flashes].class != Array 40 | env['rack.logger'] = settings.mylogger # set custom logger 41 | end 42 | 43 | # after each request 44 | after do 45 | request_id = env['REQUEST_ID'] || 'null' 46 | logger.info("service=ui | event=request | path=#{env['REQUEST_PATH']} | " \ 47 | "request_id=#{request_id} | " \ 48 | "remote_addr=#{env['REMOTE_ADDR']} | " \ 49 | "method= #{env['REQUEST_METHOD']} | " \ 50 | "response_status=#{response.status}") 51 | end 52 | 53 | # show all posts 54 | get '/' do 55 | @title = 'All posts' 56 | begin 57 | @posts = http_request('get', "#{POST_URL}/posts") 58 | rescue StandardError => e 59 | flash_danger('Can\'t show blog posts, some problems with the post ' \ 60 | 'service. Refresh?') 61 | log_event('error', 'show_all_posts', 62 | "Failed to read from Post service. Reason: #{e.message}") 63 | else 64 | log_event('info', 'show_all_posts', 65 | 'Successfully showed the home page with posts') 66 | end 67 | @flashes = session[:flashes] 68 | session[:flashes] = nil 69 | haml :index 70 | end 71 | 72 | # show a form for creating a new post 73 | get '/new' do 74 | @title = 'New post' 75 | @flashes = session[:flashes] 76 | session[:flashes] = nil 77 | haml :create 78 | end 79 | 80 | # talk to Post service in order to creat a new post 81 | post '/new/?' do 82 | if params['link'] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI] 83 | begin 84 | http_request('post', "#{POST_URL}/add_post", title: params['title'], 85 | link: params['link'], 86 | created_at: Time.now.to_i) 87 | rescue StandardError => e 88 | flash_danger("Can't save your post, some problems with the post service") 89 | log_event('error', 'post_create', 90 | "Failed to create a post. Reason: #{e.message}", params) 91 | else 92 | flash_success('Post successuly published') 93 | log_event('info', 'post_create', 'Successfully created a post', params) 94 | end 95 | redirect '/' 96 | else 97 | flash_danger('Invalid URL') 98 | log_event('warning', 'post_create', 'Invalid URL', params) 99 | redirect back 100 | end 101 | end 102 | 103 | # talk to Post service in order to vote on a post 104 | post '/post/:id/vote/:type' do 105 | begin 106 | http_request('post', "#{POST_URL}/vote", id: params[:id], 107 | type: params[:type]) 108 | rescue StandardError => e 109 | flash_danger('Can\'t vote, some problems with the post service') 110 | log_event('error', 'vote', 111 | "Failed to vote. Reason: #{e.message}", params) 112 | else 113 | log_event('info', 'vote', 'Successful vote', params) 114 | end 115 | redirect back 116 | end 117 | 118 | # show a specific post 119 | get '/post/:id' do 120 | begin 121 | @post = http_request('get', "#{POST_URL}/post/#{params[:id]}") 122 | rescue StandardError => e 123 | log_event('error', 'show_post', 124 | "Counldn't show the post. Reason: #{e.message}", params) 125 | halt 404, 'Not found' 126 | end 127 | 128 | begin 129 | @comments = http_request('get', "#{COMMENT_URL}/#{params[:id]}/comments") 130 | rescue StandardError => e 131 | log_event('error', 'show_post', 132 | "Counldn't show the comments. Reason: #{e.message}", params) 133 | # flash_danger("Can't show comments, some problems with the comment service") 134 | else 135 | log_event('info', 'show_post', 136 | 'Successfully showed the post', params) 137 | end 138 | @flashes = session[:flashes] 139 | session[:flashes] = nil 140 | haml :show 141 | end 142 | 143 | # talk to Comment service in order to comment on a post 144 | post '/post/:id/comment' do 145 | begin 146 | http_request('post', "#{COMMENT_URL}/add_comment", 147 | post_id: params[:id], 148 | name: params[:name], 149 | email: params[:email], 150 | created_at: Time.now.to_i, 151 | body: params[:body]) 152 | rescue StandardError => e 153 | log_event('error', 'create_comment', 154 | "Counldn't create a comment. Reason: #{e.message}", params) 155 | flash_danger("Can\'t save the comment, 156 | some problems with the comment service") 157 | else 158 | log_event('info', 'create_comment', 159 | 'Successfully created a new post', params) 160 | 161 | flash_success('Comment successuly published') 162 | end 163 | redirect back 164 | end 165 | 166 | # health check endpoint 167 | get '/healthcheck' do 168 | http_healthcheck_handler(POST_URL, COMMENT_URL, VERSION) 169 | end 170 | 171 | get '/*' do 172 | halt 404, 'Page not found' 173 | end 174 | -------------------------------------------------------------------------------- /post/post_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import prometheus_client 3 | import time 4 | import structlog 5 | import traceback 6 | from flask import Flask, request, Response, abort, logging 7 | from pymongo import MongoClient 8 | from bson.objectid import ObjectId 9 | from bson.json_util import dumps 10 | from helpers import http_healthcheck_handler, log_event 11 | 12 | 13 | CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8') 14 | POST_DATABASE_HOST = os.getenv('POST_DATABASE_HOST', '127.0.0.1') 15 | POST_DATABASE_PORT = os.getenv('POST_DATABASE_PORT', '27017') 16 | 17 | log = structlog.get_logger() 18 | 19 | app = Flask(__name__) 20 | 21 | 22 | def init(app): 23 | # appication version info 24 | app.version = None 25 | with open('VERSION') as f: 26 | app.version = f.read().rstrip() 27 | 28 | # prometheus metrics 29 | app.post_read_db_seconds = prometheus_client.Histogram( 30 | 'post_read_db_seconds', 31 | 'Request DB time' 32 | ) 33 | app.post_count = prometheus_client.Counter( 34 | 'post_count', 35 | 'A counter of new posts' 36 | ) 37 | # database client connection 38 | app.db = MongoClient( 39 | POST_DATABASE_HOST, 40 | int(POST_DATABASE_PORT) 41 | ).users_post.posts 42 | 43 | 44 | # Prometheus endpoint 45 | @app.route('/metrics') 46 | def metrics(): 47 | return Response(prometheus_client.generate_latest(), 48 | mimetype=CONTENT_TYPE_LATEST) 49 | 50 | # Retrieve information about all posts 51 | @app.route("/posts") 52 | def find_posts(): 53 | try: 54 | posts = app.db.find().sort('created_at', -1) 55 | except Exception as e: 56 | log_event('error', 'find_all_posts', 57 | "Failed to retrieve posts from the database. \ 58 | Reason: {}".format(str(e))) 59 | abort(500) 60 | else: 61 | log_event('info', 'find_all_posts', 62 | 'Successfully retrieved all posts from the database') 63 | return dumps(posts) 64 | 65 | 66 | # Vote for a post 67 | @app.route('/vote', methods=['POST']) 68 | def vote(): 69 | try: 70 | post_id = request.values.get('id') 71 | vote_type = request.values.get('type') 72 | except Exception as e: 73 | log_event('error', 'request_error', 74 | "Bad input parameters. Reason: {}".format(str(e))) 75 | abort(400) 76 | try: 77 | post = app.db.find_one({'_id': ObjectId(post_id)}) 78 | post['votes'] += int(vote_type) 79 | app.db.update_one({'_id': ObjectId(post_id)}, 80 | {'$set': {'votes': post['votes']}}) 81 | except Exception as e: 82 | log_event('error', 'post_vote', 83 | "Failed to vote for a post. Reason: {}".format(str(e)), 84 | {'post_id': post_id, 'vote_type': vote_type}) 85 | abort(500) 86 | else: 87 | log_event('info', 'post_vote', 'Successful vote', 88 | {'post_id': post_id, 'vote_type': vote_type}) 89 | return 'OK' 90 | 91 | 92 | # Add new post 93 | @app.route('/add_post', methods=['POST']) 94 | def add_post(): 95 | try: 96 | title = request.values.get('title') 97 | link = request.values.get('link') 98 | created_at = request.values.get('created_at') 99 | except Exception as e: 100 | log_event('error', 'request_error', 101 | "Bad input parameters. Reason: {}".format(str(e))) 102 | abort(400) 103 | try: 104 | app.db.insert({'title': title, 'link': link, 105 | 'created_at': created_at, 'votes': 0}) 106 | except Exception as e: 107 | log_event('error', 'post_create', 108 | "Failed to create a post. Reason: {}".format(str(e)), 109 | {'title': title, 'link': link}) 110 | abort(500) 111 | else: 112 | log_event('info', 'post_create', 'Successfully created a new post', 113 | {'title': title, 'link': link}) 114 | app.post_count.inc() 115 | return 'OK' 116 | 117 | 118 | # Retrieve information about a post 119 | # @zipkin_span(service_name='post', span_name='db_find_single_post') 120 | @app.route('/post/') 121 | def find_post(id): 122 | start_time = time.time() 123 | try: 124 | post = app.db.find_one({'_id': ObjectId(id)}) 125 | except Exception as e: 126 | log_event('error', 'post_find', 127 | "Failed to find the post. Reason: {}".format(str(e)), 128 | request.values) 129 | abort(500) 130 | else: 131 | stop_time = time.time() # + 0.3 132 | resp_time = stop_time - start_time 133 | app.post_read_db_seconds.observe(resp_time) 134 | log_event('info', 'post_find', 135 | 'Successfully found the post information', 136 | {'post_id': id}) 137 | return dumps(post) 138 | 139 | 140 | # Health check endpoint 141 | @app.route('/healthcheck') 142 | def healthcheck(): 143 | return http_healthcheck_handler(POST_DATABASE_HOST, 144 | POST_DATABASE_PORT, 145 | app.version) 146 | 147 | 148 | # Log every request 149 | @app.after_request 150 | def after_request(response): 151 | request_id = request.headers['Request-Id'] \ 152 | if 'Request-Id' in request.headers else None 153 | log.info('request', 154 | service='post', 155 | request_id=request_id, 156 | path=request.full_path, 157 | addr=request.remote_addr, 158 | method=request.method, 159 | response_status=response.status_code) 160 | return response 161 | 162 | 163 | # Log Exceptions 164 | @app.errorhandler(Exception) 165 | def exceptions(e): 166 | request_id = request.headers['Request-Id'] \ 167 | if 'Request-Id' in request.headers else None 168 | tb = traceback.format_exc() 169 | log.error('internal_error', 170 | service='post', 171 | request_id=request_id, 172 | path=request.full_path, 173 | remote_addr=request.remote_addr, 174 | method=request.method, 175 | traceback=tb) 176 | return 'Internal Server Error', 500 177 | 178 | 179 | if __name__ == "__main__": 180 | init(app) 181 | logg = logging.getLogger('werkzeug') 182 | logg.disabled = True # disable default logger 183 | # define log structure 184 | structlog.configure(processors=[ 185 | structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"), 186 | structlog.stdlib.add_log_level, 187 | # to see indented logs in the terminal, uncomment the line below 188 | # structlog.processors.JSONRenderer(indent=2, sort_keys=True) 189 | # and comment out the one below 190 | structlog.processors.JSONRenderer(sort_keys=True) 191 | ]) 192 | app.run(host='0.0.0.0', debug=True) 193 | --------------------------------------------------------------------------------