Kubernetes praticky: DAPR jako přenositelná aplikační platforma pro cloud-native aplikace - state store a pub/sub

V minulém díle jsem popisoval proč DAPR a jeho základní architekturu. Dnes si DAPR vyzkoušíme. Všechny soubory potřebné pro dnešní článek najdete na mém GitHubu

Instalace DAPR

Instalace začíná tím, že si nainstalujeme DAPR CLI.

wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

Následně přes CLI nahodíme DAPR v AKS.

dapr init --kubernetes 

Příprava backend služeb

Pro různé služby DAPRu potřebuju nějakou backend implementaci a v mém případě to budou Azure PaaS služby. Pojďme si (v bash) tyto služby zprovoznit. Konkrétně půjde o CosmosDB, Service Bus, Blob Storage a Event Hub.

export resourceGroup=akstemp

# Cosmos DB
export cosmosdbAccount=mujdaprcosmosdb
az cosmosdb create -n $cosmosdbAccount -g $resourceGroup
az cosmosdb sql database create -a $cosmosdbAccount -n daprdb -g $resourceGroup
az cosmosdb sql container create -g $resourceGroup -a $cosmosdbAccount -d daprdb -n statecont -p "/id"

# Service Bus
export servicebus=mujdaprservicebus
az servicebus namespace create -n $servicebus -g $resourceGroup
az servicebus topic create -n orders --namespace-name $servicebus -g $resourceGroup
az servicebus namespace authorization-rule create --namespace-name $servicebus \
  -g $resourceGroup \
  --name daprauth \
  --rights Send Listen Manage

# Blob Storage
export storageaccount=mujdaprstorageaccount
az storage account create -n $storageaccount -g $resourceGroup --sku Standard_LRS --kind StorageV2
export storageConnection=$(az storage account show-connection-string -n $storageaccount -g $resourceGroup --query connectionString -o tsv)
az storage container create -n daprcontainer --connection-string $storageConnection

# Event Hub
export eventhub=mujdapreventhub
az eventhubs namespace create -g $resourceGroup -n $eventhub --sku Basic
az eventhubs eventhub create -g $resourceGroup --namespace-name $eventhub -n dapreventhub --message-retention 1
az eventhubs eventhub authorization-rule create \
  -g $resourceGroup \
  --namespace-name $eventhub \
  --eventhub-name dapreventhub \
  -n daprauth \
  --rights Listen Send

DAPR bude nabízet jednotlivé služby mým aplikacím, ale musíme mu říct, kde najde backend implementaci. DAPR používá custom resource (CRD) s kind Component a v něm jsou konfigurační údaje pro každou komponentu. Do nich potřebujeme dosadit connection stringy do služeb, které jsme před chvilkou vytvořili. Aby to šlo jednoduše, udělal jsem z toho Helm šablonu a tu teď nasadíme.

cd dapr
helm upgrade dapr-components ./dapr-components --install \
  --set cosmosdb.url=$(az cosmosdb show -n $cosmosdbAccount -g $resourceGroup --query documentEndpoint -o tsv) \
  --set cosmosdb.masterKey=$(az cosmosdb keys list -n $cosmosdbAccount -g $resourceGroup --type keys --query primaryMasterKey -o tsv) \
  --set cosmosdb.database=daprdb \
  --set cosmosdb.collection=statecont \
  --set serviceBus.connectionString=$(az servicebus namespace authorization-rule keys list --namespace-name $servicebus -g $resourceGroup --name daprauth --query primaryConnectionString -o tsv) \
  --set blob.storageAccount=$storageaccount \
  --set blob.key=$(az storage account keys list -n $storageaccount -g $resourceGroup --query [0].value -o tsv) \
  --set blob.container=daprcontainer \
  --set eventHub.connectionString=$(az eventhubs eventhub authorization-rule keys list --namespace-name $eventhub -g $resourceGroup --eventhub-name dapreventhub --name daprauth --query primaryConnectionString -o tsv)

Pojďme se namátkou podívat jak jedna taková definice vypadá - třeba state store.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: dapr-state-cosmosdb
spec:
  type: state.azure.cosmosdb
  metadata:
  - name: url
    value: {{ .Values.cosmosdb.url }}
  - name: masterKey
    value: {{ .Values.cosmosdb.masterKey }}
  - name: database
    value: {{ .Values.cosmosdb.database }}
  - name: collection
    value: {{ .Values.cosmosdb.collection }}

V metadatech uvádíme název a v typu k jaké DAPR službě to patří - v našem případě state store.

State store

První si vyzkoušíme ukládání nějakého stavu do key/value systému. Může jít o session state, obsah nákupního košíku, cache, aktuální stav uživatele (je online, je připraven v počítačové hře na další zápas apod.). V době psaní článku jsou jako backend implementace podporované Redis, Cosmos DB, Etcd, Consul, Cassandra, MongoDB, Memcached, Zookeeper, Cloud Firestore a Couchbase. My jsme při instalaci zvolili Cosmos DB.

Do clusteru pošlu Pod a všimněte si jeho anotací. Ty informují DAPR kontroler o tom, že chceme v tomto Podu využívat jeho služeb a také, že chceme jako jméno mít pod1.

kind: Pod
apiVersion: v1
metadata:
  name: pod1
  annotations:
    dapr.io/enabled: "true"
    dapr.io/id: "pod1"
spec:
  containers:
    - name: ubuntu
      image: tkubica/mybox
      resources:
        requests:
          cpu: 10m
          memory: 32M
        limits:
          cpu: 100M
          memory: 128M

Pošleme do clusteru. Všimněte si, že DAPR vám do Podu sám přidal side-car kontejner.

kubectl apply -f pod1.yaml

kubectl describe pod pod1 | grep Image:
  Image:         tkubica/mybox
  Image:         docker.io/daprio/dapr:latest

Skočíme do kontejneru a vyzkoušíme si state store API. Žádné ověřování, žádná nutnost znát co je na backendu. Jednoduchý REST na uložení a jednoduchý REST na přečtení.

kubectl exec -ti pod1 -- /bin/bash
curl -X POST http://localhost:3500/v1.0/state \
  -H "Content-Type: application/json" \
  -d '[
        {
          "key": "00-11-22",
          "value": "Tomas"
        }
      ]'

curl http://localhost:3500/v1.0/state/00-11-22
"Tomas"

Jednoduché. Podívejme se, jak jsou data vidět přímo v Cosmos DB.

Všimněte si například, že id obsahuje nejen náš klíč (00-11-22), ale i jméno, které jsme uváděli v anotaci - v našem případě pod1. Můžete tak mít ve state store několik namespace podle potřeby. Také doporučuji vaší pozornosti etag. Psát do store můžete paralelně z několika Podů a co když se dva rozhodnou změnit stávající záznam a každý jinak. Vyhraje ten kdo dřív začal nebo ten kdo poslední končil? To si můžete v DAPR vybrat a on použije vhodné možnosti s ohledem na backend implementaci.

Publish/Subscribe pattern

Stále častější patternem pro integraci mikroslužeb mezi sebou je pub/sub mítsto přímé komunikace. Takové řešení má bez nějakých složitostí přímo v sobě vyrovnání zátěže (fronta může působit jako buffer), výbornou metriku pro autoscaling služeb (délka nevyřízené fronty), nepotřebuji circuit breaker, protože na nic nečekám apod. První co vás v Azure napadne je Service Bus a to je určitě dobrá volba. Do kódu dáte SDK nebo použijite generické AMQP 1.0 a jedete. Ale jinde možná zvolíte jinou frontu - třeba jen Redis nebo naopak na druhé straně spektra Kafku. DAPR v době psaní článku podporuje Kafku, RabbitMQ, Azure Service Bus, Redis a NATS. Já si DAPR nastavil na Service Bus.

Budeme potřebovat dvě okna. V tom prvním bude odesílání zpráv.

kubectl apply -f pod1.yaml
kubectl exec -ti pod1 -- bash

V druhém okně si nahodíme Pod připravený pro Python kód. DAPR totiž funguje tak, že v okamžiku, kdy má pro příjemce zprávu, mu ji pošle na API, které aplikace vystaví. Jinak řečeno můj kód nemusí pollovat zprávy, vědět kam se napojit, autentizovat a tak dále. Místo toho vystaví endpoint a DAPR mu do něj naservíruje zprávu. Založme si Python Pod a skočme do interaktivního Python prostředí.

kind: Pod
apiVersion: v1
metadata:
  name: python1
  annotations:
    dapr.io/enabled: "true"
    dapr.io/id: "python1"
    dapr.io/port: "5000"
spec:
  containers:
    - name: python
      image: python:3
      command: ["/bin/sh"]
      args: ["-c", "pip install flask flask_cors && tail -f /dev/null"]
      ports:
        - containerPort: 5000
      resources:
        requests:
          cpu: 10m
          memory: 32M
        limits:
          cpu: 100M
          memory: 512M
kubectl apply -f python.yaml
kubectl exec -ti python1 -- python

Do něj teď vložíme následující kód. Ten vystavuje 2 API - jedno je /dapr/subscribe, kterým informuje DAPR o tom, jaké topic chce přijímat. Druhé je /orders, kde orders je právě název topicu. Tyto endpointy poběží na portu 5000 a v python.yaml jsme o tom DAPR informovali.

import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys

app = flask.Flask(__name__)
CORS(app)

@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
    return jsonify(['orders'])

@app.route('/orders', methods=['POST'])
def a_subscriber():
    print(f'orders: {request.json}', flush=True)
    return json.dumps({'success':True}), 200, {'ContentType':'application/json'} 

app.run()

Aplikace se rozeběhla a je vidět, že se jí DAPR hned na něco ptá - většinou to nikam nevedlo (k dalším službám se totiž ještě dostaneme), ale /dapr/subscribe našel.

* Serving Flask app "__main__" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [04/Dec/2019 05:13:42] "GET /dapr/config HTTP/1.1" 404 -
127.0.0.1 - - [04/Dec/2019 05:13:42] "GET /dapr/subscribe HTTP/1.1" 200 -
127.0.0.1 - - [04/Dec/2019 05:13:43] "OPTIONS /binding-blob HTTP/1.1" 404 -
127.0.0.1 - - [04/Dec/2019 05:13:43] "OPTIONS /binding-eventhub HTTP/1.1" 404 -

V druhém okně s pod1 pošleme jednoduchý POST do topic orders. Všimněte si, že ten ještě neexistuje a i Service Bus je zcela prázdný.

curl -X POST http://localhost:3500/v1.0/publish/orders \
	-H "Content-Type: application/json" \
	-d '{
       	     "orderCreated": "ABC01"
      }'

Hned se podívejte do okna s Python a v logu vidím, že do něj DAPR šťouchnul a zprávu mu předal.

orders: {'id': '7a074991-84cd-405b-8b37-94614ff9db7e', 'source': 'pod1', 'type': 'com.dapr.event.sent', 'specversion': '0.3', 'datacontenttype': 'application/json', 'data': {'orderCreated': 'ABC01'}}
127.0.0.1 - - [04/Dec/2019 05:16:01] "POST /orders HTTP/1.1" 200 -

V Service Bus vidím, že DAPR založil topic orders.

Je tu i python1 jako subscriber.

No a skutečně přes Service Bus jedna zpráva proběhla.

Dnes jsme měli dost práce se setupem, tak se na další služby, které DAPR nabízí, podíváme v příštím dále. Dnes jsme viděli jak jednoduše se dá ukládat state do key/value systému bez jakékoli dependence na technické implementaci napozadí. Totéž jsme si vyzkoušeli s posíláním zpráv mezi Pody s využitím pub/sub patternu. Tady nejen že mám jednoduché REST rozhraní, ale DAPR přímo sám aktivně předává zprávy příjemci - ten je tedy nemusí pollovat, DAPR do něj šťouchne. Příště tedy vzhůru na další DAPR služby - zejména binding je úžasná věc a zkusíme i další. V mezičase si nainstalujte DAPR v AKS a začněte zkoušet.



Kubernetes praticky: nejmocnější Service Mesh část 2 - traffic management Kubernetes Kontejnery
Kubernetes praticky: nejmocnější Service Mesh část 1 - Istio retry, circuit breaker, copy, balancing Kubernetes Kontejnery
Postupné nasazování a testování aplikací na lidech - kanárci, A/B, green/blue na příkladu kadeřníka Kubernetes Kontejnery
Kubernetes praticky: DAPR jako přenositelná aplikační platforma pro cloud-native aplikace - bindings Kubernetes Kontejnery Serverless
Kubernetes praticky: Service Mesh zaměřený na rychlost a efektivitu - Linkerd Kubernetes Kontejnery