Hei alle sammen! Som en del av kursarbeidet mitt undersøkte jeg mulighetene til en slik innenlandsk skyplattform som
Hva ønsker du å motta?
Etter å ha lansert en virtuell maskin med en webserver, kan du gå til verten og få et vakkert brukergrensesnitt, spesifisere databaser som kilder for videre arbeid, lage dashbord og grafer.
Grunnversjonen har en betydelig ulempe - den er ikke feiltolerant i det hele tatt. Det vil si at hele funksjonaliteten til applikasjonen avhenger av levedyktigheten til en virtuell maskin. Hvis den nekter eller 10 personer åpner brukergrensesnittet samtidig, vil det oppstå problemer.
De kan løses enkelt: du trenger bare å... distribuere mange identiske virtuelle maskiner med en webserver og plassere dem under en L3-balanser. Men ikke alt er så klart her. Grafana lagrer brukerinnstillinger (stier til databaser, dashbord, grafer, etc.) direkte på disken til den virtuelle maskinen. Derfor, hvis vi endrer noen innstillinger i brukergrensesnittet, vil disse endringene bare reflekteres på den virtuelle maskinen som balanseren sendte oss til. Dette vil føre til inkonsekvente innstillinger for applikasjonen vår, og forårsake problemer med oppstart og bruk.
Her vil en annen database komme til unnsetning, for eksempel MySQL eller tilsvarende. Vi forteller Grafana at hun bør lagre brukerinnstillinger i denne "reserve"-databasen. Etterpå vil det være nok å spesifisere banen til denne databasen én gang på hver maskin, og redigere alle andre brukerinnstillinger på noen av de virtuelle maskinene; de vil utvides til de andre.
Her er et diagram over den endelige applikasjonsinfrastrukturen:
La oss lære å løfte med hendene
MySQL og ClickHouse
Før du implementerte en slik applikasjon med et klikk på en knapp, var det nødvendig å lære å håndtere hver av komponentene og integrere dem med hverandre.
Her vil Yandex.Cloud hjelpe oss, som gir L3-balansere, ClickHouse og MySQL som administrerte tjenester. Brukeren trenger bare å spesifisere parametrene og vente til plattformen bringer alt i orden.
Jeg registrerte meg, opprettet en sky og en betalingskonto. Etter det gikk jeg til skyen og satte opp MySQL- og ClickHouse-klynger med minimale innstillinger. Jeg ventet til de ble aktive.
Du må også huske å opprette en database i hver klynge og konfigurere tilgang til den ved hjelp av pålogging og passord. Jeg vil ikke gå inn på detaljer her - alt er ganske åpenbart i grensesnittet.
Den ikke-åpenbare detaljen var at disse databasene har mange verter, som sikrer deres feiltoleranse. Grafana krever imidlertid nøyaktig én vert for hver database den jobber med. Lang lesning c-<cluster_id>.rw.mdb.yandexcloud.net
tilordnet den gjeldende aktive hovedverten til klyngen med den tilsvarende IDen. Dette er hva vi vil gi til Grafana.
Internett server
Nå er det opp til webserveren. La oss heve en vanlig virtuell maskin med Linux og manuelt konfigurere Grafana på den.
La oss koble til via ssh og installere de nødvendige pakkene.
sudo apt-get install -y apt-transport-https software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
sudo add-apt-repository "deb https://packages.grafana.com/enterprise/deb stable main"
sudo apt-get update
sudo apt-get install -y grafana-enterprise
Etter det, la oss kjøre Grafana under systemctl og installere plugin for å jobbe med ClickHouse (ja, den følger ikke med i grunnpakken).
sudo systemctl start grafana-server
sudo systemctl enable grafana-server
sudo grafana-cli plugins install vertamedia-clickhouse-datasource
Det er det, etter det med en enkel kommando
sudo service grafana-server start
vi starter webserveren. Nå kan du skrive inn den eksterne IP-adressen til den virtuelle maskinen i nettleseren, spesifisere port 3000 og se det vakre Grafana-grensesnittet.
Men ikke skynd deg, før du setter opp Grafana, må du huske å fortelle den veien til MySQL for å lagre innstillingene der.
Hele konfigurasjonen av Grafana-nettserveren er i filen /etc/grafana/grafana.ini
. Den nødvendige linjen ser slik ut:
;url =
Vi setter verten til MySQL-klyngen. Den samme filen inneholder login og passord for tilgang til Grafana i bildet ovenfor, som som standard begge er like admin
.
Du kan bruke sed-kommandoer:
sudo sed -i "s#.*;url =.*#url = mysql://${MYSQL_USERNAME}:${MYSQL_PASSWORD}@${MYSQL_CLUSTER_URI}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${GRAFANA_USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${GRAFANA_PASSWORD}#" /etc/grafana/grafana.ini
Det er på tide å starte webserveren på nytt!
sudo service grafana-server restart
Nå i Grafana UI vil vi spesifisere ClickHouse som en datakilde.
Jeg var i stand til å oppnå en fungerende konfigurasjon med følgende innstillinger:
Jeg spesifiserte som URL https://c-<cluster_id>.rw.mdb.yandexcloud.net:8443
Alle! Vi har én fungerende virtuell maskin med en webserver koblet til CH og MySQL. Du kan allerede laste opp datasettet til ClickHouse og bygge dashboards. Men vi har ennå ikke nådd målet vårt og har ikke utplassert en fullverdig infrastruktur.
Packer
Yandex.Cloud lar deg lage et diskbilde av en eksisterende virtuell maskin, og på grunnlag av den - et hvilket som helst antall maskiner som er identiske med hverandre. Det er akkurat dette vi skal bruke. For å enkelt sette sammen bildet, ta verktøyet
Json-filen vår vil bestå av to blokker: Builders og Provisioners. Den første blokken beskriver parametrene til selve bildet som en enhet, og den andre blokken beskriver instruksjoner for å fylle det med nødvendig innhold.
Builders
{
"builders": [
{
"type": "yandex",
"endpoint": "{{user `endpoint`}}",
"folder_id": "<folder_id>",
"subnet_id": "{{user `subnet_id`}}",
"zone": "{{user `zone`}}",
"labels": {},
"use_ipv4_nat": true,
"use_internal_ip": false,
"service_account_key_file": "<service_account_key_file>",
"image_name": "grafana-{{timestamp}}",
"image_family": "grafana",
"image_labels": {},
"image_description": "GRAFANA",
"source_image_family": "ubuntu-1804-lts",
"disk_size_gb": 3,
"disk_type": "network-hdd",
"ssh_username": "ubuntu"
}
],
...
}
I denne malen må du angi identifikatoren for delen i skyen der du vil lage bildet, samt banen til filen med nøklene fra tjenestekontoen som tidligere er opprettet i denne delen. Du kan lese mer om opprettelse av tjenestekontoer og nøkler i form av en fil i den tilsvarende delen
Denne konfigurasjonen sier at diskbildet vil bygges basert på plattformen ubuntu-1804-lts
, plassert i den aktuelle brukerdelen i bildefamilien GRAFANA
under navnet grafana-{{timestamp}}
.
Proviantører
Nå kommer den mer interessante delen av konfigurasjonen. Den vil beskrive sekvensen av handlinger som må utføres på den virtuelle maskinen før den fryser tilstanden til et diskbilde.
{
...,
"provisioners": [
{
"type": "shell",
"pause_before": "5s",
"scripts": [
"prepare-ctg.sh"
]
},
{
"type": "file",
"source": "setup.sh",
"destination": "/opt/grafana/setup.sh"
},
{
"type": "shell",
"execute_command": "sudo {{ .Vars }} bash '{{ .Path }}'",
"pause_before": "5s",
"scripts": [
"install-packages.sh",
"grafana-setup.sh",
"run-setup-at-reboot.sh"
]
}
]
}
Her er alle handlinger delt inn i 3 stadier. På det første trinnet kjøres et enkelt skript som lager en hjelpekatalog.
prepare-ctg.sh:
#!/bin/bash
sudo mkdir -p /opt/grafana
sudo chown -R ubuntu:ubuntu /opt/grafana
På neste trinn plasserer vi et skript i denne katalogen, som må startes umiddelbart etter at den virtuelle maskinen er startet. Dette skriptet vil sette brukervariablene som må registreres i Grafana-konfigurasjonen og starte webserveren på nytt.
setup.sh:
#!/bin/bash
CLUSTER_ID="<cluster_id>"
USERNAME="<username>"
PASSWORD="<password>"
sudo sed -i "s#.*;url =.*#url = mysql://${USERNAME}:${PASSWORD}@c-${CLUSTER_ID}.rw.mdb.yandexcloud.net#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${PASSWORD}#" /etc/grafana/grafana.ini
sudo service grafana-server restart
Etter dette er det 3 ting igjen å gjøre:
1) installer pakker
2) kjør Grafana under systemctl og installer ClickHouse-plugin
3) sett setup.sh-skriptet i startkøen umiddelbart etter at du har slått på den virtuelle maskinen.
install-packages.sh:
#!/bin/bash
sudo systemd-run --property='After=apt-daily.service apt-daily-upgrade.service' --wait /bin/true
sudo apt-get install -y apt-transport-https
sudo apt-get install -y software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
sudo add-apt-repository "deb https://packages.grafana.com/enterprise/deb stable main"
sudo apt-get update
sudo apt-get install -y grafana-enterprise
grafana-setup.sh:
#!/bin/bash
sudo systemctl start grafana-server
sudo systemctl enable grafana-server
sudo grafana-cli plugins install vertamedia-clickhouse-datasource
run-setup-at-reboot.sh:
#!/bin/bash
chmod +x /opt/grafana/setup.sh
cat > /etc/cron.d/first-boot <<EOF
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
@reboot root /bin/bash /opt/grafana/setup.sh > /var/log/yc-setup.log 2>&1
EOF
chmod +x /etc/cron.d/first-boot;
Nå gjenstår det bare å kjøre Packer og få utdatabildet plassert i den angitte delen. Når du oppretter en virtuell maskin, kan du velge den som en oppstartsdisk, og etter lansering vil du motta en ferdig Grafana-webserver.
Forekomstgruppe og balanserer
Når vi har et diskbilde som lar oss lage mange identiske Grafana-webservere, kan vi opprette en forekomstgruppe. På Yandex.Cloud-plattformen refererer dette begrepet til foreningen av virtuelle maskiner som har de samme egenskapene. Når du oppretter en forekomstgruppe, konfigureres prototypen til alle maskinene i denne gruppen, og deretter egenskapene til selve gruppen (for eksempel minimum og maksimum antall aktive maskiner). Hvis det nåværende nummeret ikke oppfyller disse kriteriene, vil instansgruppen selv fjerne unødvendige maskiner eller lage nye i sitt eget bilde.
Som en del av oppgaven vår vil vi lage en forekomstgruppe av webservere som vil bli generert fra det tidligere opprettede diskbildet.
Det som virkelig er bemerkelsesverdig er det siste gruppeoppsettet. Målgruppen i integrasjon med Load Balancer vil hjelpe deg med å konfigurere en L3-balanserer på toppen av de virtuelle maskinene til denne gruppen ved å klikke på et par knapper.
Da jeg satte opp balanseren, implementerte jeg to viktige punkter:
- Jeg fikk balanseren til å akseptere brukertrafikk på port 80 og omdirigere den til port 3000 på de virtuelle maskinene, akkurat der Grafana bor.
- Jeg satte opp å sjekke levedyktigheten til maskiner ved å pinge dem til port 3000.
Mini oppsummering
Til slutt var vi i stand til manuelt å distribuere ønsket applikasjonsinfrastruktur, og nå har vi en svært robust Grafana-tjeneste. Du trenger bare å vite IP-adressen til balanseren som inngangspunkt til applikasjonen og verten for ClickHouse-klyngen for å laste datasettet inn i det.
Det virker som en seier? Ja, seier. Men noe forvirrer meg likevel. Hele prosessen ovenfor krever mange manuelle trinn og er ikke skalerbar i det hele tatt; Jeg vil gjerne automatisere den hvis mulig. Det er dette neste avsnitt vil bli viet til.
Terraform integrasjon
Vi vil igjen bruke et verktøy fra HashiCorp som heter
Alt arbeid med Terraform kommer ned til å skrive en konfigurasjonsfil (*.tf
) og opprettelse av infrastruktur basert på det.
Variabler
Helt i begynnelsen av filen vil vi inkludere variabler som bestemmer hvor og hvordan den fremtidige infrastrukturen skal distribueres.
variable "oauth_token" {
type = string
default = "<oauth-token>"
}
variable "cloud_id" {
type = string
default = "<cloud-id>"
}
variable "folder_id" {
type = string
default = "<folder_id>"
}
variable "service_account_id" {
type = string
default = "<service_account_id>"
}
variable "image_id" {
type = string
default = "<image_id>"
}
variable "username" {
type = string
default = "<username>"
}
variable "password" {
type = string
default = "<password>"
}
variable "dbname" {
type = string
default = "<dbname>"
}
variable "public_key_path" {
type = string
default = "<path to ssh public key>"
}
Hele applikasjonsdistribusjonsprosessen kommer ned til å bygge et diskbilde og angi disse variablene. La meg forklare hva de er ansvarlige for:
oauth_token — et symbol for tilgang til skyen. Kan fås av
cloud_id — skyidentifikator der vi vil distribuere applikasjonen
mappe_id — seksjonsidentifikator der vi vil distribuere applikasjonen
service_account_id — tjenestekontoidentifikator i den tilsvarende delen av skyen.
image_id — identifikator for diskbildet oppnådd med Packer
brukernavn и passord — brukernavn og passord for å få tilgang til både databaser og Grafana-nettserveren
dbnavn - databasenavn i CH- og MySQL-klynger
offentlig_nøkkelbane - bane til filen med din offentlige ssh-nøkkel, som du kan bruke til å koble til under navnet ubuntu
til virtuelle maskiner med webservere
Leverandøroppsett
Nå må du konfigurere Terraform-leverandøren - i vårt tilfelle, Yandex:
provider "yandex" {
token = var.oauth_token
cloud_id = var.cloud_id
folder_id = var.folder_id
zone = "ru-central1-a"
}
Du vil legge merke til at vi her bruker variablene definert ovenfor.
Nettverk og klynger
Nå skal vi lage et nettverk der elementer av infrastrukturen vår vil kommunisere, tre undernett (ett i hver region) og heve CH- og MySQL-klynger.
resource "yandex_vpc_network" "grafana_network" {}
resource "yandex_vpc_subnet" "subnet_a" {
zone = "ru-central1-a"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.1.0.0/24"]
}
resource "yandex_vpc_subnet" "subnet_b" {
zone = "ru-central1-b"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.2.0.0/24"]
}
resource "yandex_vpc_subnet" "subnet_c" {
zone = "ru-central1-c"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.3.0.0/24"]
}
resource "yandex_mdb_clickhouse_cluster" "ch_cluster" {
name = "grafana-clickhouse"
environment = "PRODUCTION"
network_id = yandex_vpc_network.grafana_network.id
clickhouse {
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
}
zookeeper {
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 10
}
}
database {
name = var.dbname
}
user {
name = var.username
password = var.password
permission {
database_name = var.dbname
}
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
}
resource "yandex_mdb_mysql_cluster" "mysql_cluster" {
name = "grafana_mysql"
environment = "PRODUCTION"
network_id = yandex_vpc_network.grafana_network.id
version = "8.0"
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
database {
name = var.dbname
}
user {
name = var.username
password = var.password
permission {
database_name = var.dbname
roles = ["ALL"]
}
}
host {
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
}
Som du kan se, er hver av de to klyngene skapt ganske feiltolerante ved å være plassert i tre tilgjengelighetssoner.
Webservere
Det ser ut til at vi kunne fortsette i samme ånd, men jeg fikk problemer. Før dette reiste jeg først en MySQL-klynge, og først etter det, vel vitende om ID-en, samlet jeg et diskbilde med den nødvendige konfigurasjonen, der jeg spesifiserte verten til klyngen. Men nå vet vi ikke klynge-ID-en før vi lanserte Terraform, inkludert på tidspunktet for å bygge bildet. Derfor måtte jeg ty til følgende
Ved å bruke Amazons metadatatjeneste vil vi sende noen parametere til den virtuelle maskinen, som den godtar og behandler. Vi trenger maskinen for å gå til metadataene bak MySQL-klyngeverten og brukernavn-passordet, som brukeren spesifiserte i Terraform-filen, etter oppstart. La oss endre innholdet i filen litt setup.sh
, som kjører når den virtuelle maskinen er slått på.
setup.sh:
#!/bin/bash
CLUSTER_URI="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/mysql_cluster_uri)"
USERNAME="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/username)"
PASSWORD="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/password)"
sudo sed -i "s#.*;url =.*#url = mysql://${USERNAME}:${PASSWORD}@${CLUSTER_URI}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${PASSWORD}#" /etc/grafana/grafana.ini
sudo service grafana-server restart
Intansegruppe og balanserer
Etter å ha gjenoppbygd et nytt diskbilde, kan vi endelig legge til filen vår for Terraform.
La oss indikere at vi ønsker å bruke et eksisterende diskbilde:
data "yandex_compute_image" "grafana_image" {
image_id = var.image_id
}
La oss nå opprette en forekomstgruppe:
resource "yandex_compute_instance_group" "grafana_group" {
name = "grafana-group"
folder_id = var.folder_id
service_account_id = var.service_account_id
instance_template {
platform_id = "standard-v1"
resources {
memory = 1
cores = 1
}
boot_disk {
mode = "READ_WRITE"
initialize_params {
image_id = data.yandex_compute_image.grafana_image.id
size = 4
}
}
network_interface {
network_id = yandex_vpc_network.grafana_network.id
subnet_ids = [yandex_vpc_subnet.subnet_a.id, yandex_vpc_subnet.subnet_b.id, yandex_vpc_subnet.subnet_c.id]
nat = "true"
}
metadata = {
mysql_cluster_uri = "c-${yandex_mdb_mysql_cluster.mysql_cluster.id}.rw.mdb.yandexcloud.net:3306/${var.dbname}"
username = var.username
password = var.password
ssh-keys = "ubuntu:${file("${var.public_key_path}")}"
}
network_settings {
type = "STANDARD"
}
}
scale_policy {
fixed_scale {
size = 6
}
}
allocation_policy {
zones = ["ru-central1-a", "ru-central1-b", "ru-central1-c"]
}
deploy_policy {
max_unavailable = 2
max_creating = 2
max_expansion = 2
max_deleting = 2
}
load_balancer {
target_group_name = "grafana-target-group"
}
}
Det er verdt å være oppmerksom på hvordan vi sendte det inn i metadataene cluster_uri
, username
и password
. Det er disse den virtuelle maskinen vil ta ut ved oppstart og sette inn Grafana-konfigurasjonen.
Det er opp til balansereren.
resource "yandex_lb_network_load_balancer" "grafana_balancer" {
name = "grafana-balancer"
listener {
name = "grafana-listener"
port = 80
target_port = 3000
external_address_spec {
ip_version = "ipv4"
}
}
attached_target_group {
target_group_id = yandex_compute_instance_group.grafana_group.load_balancer.0.target_group_id
healthcheck {
name = "healthcheck"
tcp_options {
port = 3000
}
}
}
}
Litt sukker
Det er bare litt igjen. Etter at infrastrukturen er distribuert, må du gå til Grafana-grensesnittet og manuelt legge til CH-klyngen (ID-en som fortsatt må hentes) som en datakilde. Men Terraform kjenner klynge-ID-en. La oss overlate ham til å bringe saken ut i livet.
La oss legge til en ny leverandør - Grafana, og gi henne balansererens IP-adresse som vert. Alle endringer som Terraform gjør på maskinen hvor balanseren bestemmer vil vokse i MySQL, og derfor på alle andre maskiner.
provider "grafana" {
url = "http://${[for s in yandex_lb_network_load_balancer.grafana_balancer.listener: s.external_address_spec.0.address].0}"
auth = "${var.username}:${var.password}"
}
resource "grafana_data_source" "ch_data_source" {
type = "vertamedia-clickhouse-datasource"
name = "grafana"
url = "https://c-${yandex_mdb_clickhouse_cluster.ch_cluster.id}.rw.mdb.yandexcloud.net:8443"
basic_auth_enabled = "true"
basic_auth_username = var.username
basic_auth_password = var.password
is_default = "true"
access_mode = "proxy"
}
La oss gre håret vårt
La oss vise balanserings-IP-adressen og verten for ClickHouse-klyngen
output "grafana_balancer_ip_address" {
value = [for s in yandex_lb_network_load_balancer.grafana_balancer.listener: s.external_address_spec.0.address].0
}
output "clickhouse_cluster_host" {
value = "https://c-${yandex_mdb_clickhouse_cluster.ch_cluster.id}.rw.mdb.yandexcloud.net:8443"
}
Kan løpe
Alle! Konfigurasjonsfilen vår er klar, og vi kan, ved å sette variablene, fortelle Terraform å heve alt vi beskrev ovenfor. Hele prosessen tok meg omtrent 15 minutter.
På slutten kan du se en vakker melding:
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:
clickhouse_cluster_host = https://c-c9q14ipa2ngadqsbp2iq.rw.mdb.yandexcloud.net:8443
grafana_balancer_ip_address = 130.193.50.25
Og i skyen vil elementer av den hevet infrastrukturen være synlige:
Oppsummere
Nå, ved å bruke Grafana som eksempel, kan hver av dere distribuere applikasjoner med en vidstrakt skyarkitektur på Yandex.Cloud-plattformen. Nyttige verktøy fra HashiCorp som Packer og Terraform kan hjelpe deg med dette. Jeg håper noen finner denne artikkelen nyttig :)
PS Nedenfor vil jeg legge ved en lenke til depotet hvor du kan finne ferdige oppskrifter for Packer og Terraform, fragmenter av dem jeg ga i denne artikkelen.
Kilde: www.habr.com