Dapr: State Management

Save and Get State
State yönetimi, herhangi bir yeni, eski, monolith veya microservice uygulamasının en yaygın ihtiyaçlarından biridir. Farklı veritabanı kütüphaneleri ile uğraşmak ve bunları test etmek ve hataları handle etmek hem zor hem de zaman alıcı olabilir. Bir uygulamanın save, get ve delete statelerini yönetmek için key/value state API’ ı kullanılır.
Örnek:
Aşağıdaki kod örneği, Dapr sidecarına sahip bir sipariş işleme hizmetiyle siparişleri işleyen bir uygulamayı genel hatlarıyla açıklar. Sipariş işleme hizmeti, durumu bir Redis state deposunda depolamak için Dapr’ı kullanır.
Set up a state store
State store bileşeni, Dapr’ın bir veritabanıyla iletişim kurmak için kullandığı bir kaynağı temsil eder. Dapr init‘i self-hosted modda çalıştırdığınızda, Dapr varsayılan bir Redis statestore.yaml oluşturur ve yerel makinenizde şu konumda bir Redis state store çalıştırır:
- Windows,
%UserProfile%\.dapr\components\statestore.yaml
- Linux/MacOS,
~/.dapr/components/statestore.yaml
Statestore.yaml bileşeniyle, uygulama kodunda değişiklik yapmadan temel bileşenleri kolayca değiştirebilirsiniz.
Save and retrieve a single state
Aşağıdaki örnek, Dapr state yönetimi API’ını kullanarak tek bir key/value çiftinin nasıl kaydedileceğini ve alınacağını gösterir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE\_STORE\_NAME = "statestore";
public static void main(String\[\] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//Using Dapr SDK to save and get state
client.saveState(STATE\_STORE\_NAME, "order_1", Integer.toString(orderId)).block();
client.saveState(STATE\_STORE\_NAME, "order_2", Integer.toString(orderId)).block();
Mono<State<String>> result = client.getState(STATE\_STORE\_NAME, "order_1", String.class);
log.info("Result after get" + result);
}
}
}
Yukarıdaki örnek uygulama için bir Dapr sidecarını başlatmak için aşağıdaki komut çalıştırılır:
dapr run –app-id orderprocessing –app-port 6001 –dapr-http-port 3601 –dapr-grpc-port 60001 mvn spring-boot:run
Delete state
State silmek için Dapr SDK’larından yararlanan kod örnekleri aşağıdadır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
public static void main(String\[\] args) throws InterruptedException{
String STATE\_STORE\_NAME = "statestore";
//Using Dapr SDK to delete the state
DaprClient client = new DaprClientBuilder().build();
String storedEtag = client.getState(STATE\_STORE\_NAME, "order_1", String.class).block().getEtag();
client.deleteState(STATE\_STORE\_NAME, "order_1", storedEtag, null).block();
}
}
Yukarıdaki örnek uygulama için bir Dapr sidecarını başlatmak için aşağıdaki komut çalıştırılır:
1
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
Save and retrieve multiple states
Aşağıda, birden fazla state kaydetmek ve getirmek için Dapr SDK’larından yararlanan kod örnekleri bulunmaktadır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import java.util.Arrays;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String\[\] args) throws InterruptedException{
String STATE\_STORE\_NAME = "statestore";
//Using Dapr SDK to retrieve multiple states
DaprClient client = new DaprClientBuilder().build();
Mono<List<State<String>>> resultBulk = client.getBulkState(STATE\_STORE\_NAME,
Arrays.asList("order\_1", "order\_2"), String.class);
}
}
Yukarıdaki örnek uygulama için bir Dapr sidecarını başlatmak için aşağıdaki komut çalıştırılır:
1
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
Perform state transactions
State transactionlarını gerçekleştirmek için Dapr SDK’larından yararlanan kod örnekleri aşağıdadır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE\_STORE\_NAME = "statestore";
public static void main(String\[\] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
List<TransactionalStateOperation<?>> operationList = new ArrayList<>();
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.UPSERT,
new State<>("order_3", Integer.toString(orderId), "")));
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.DELETE,
new State<>("order_2")));
//Using Dapr SDK to perform the state transactions
client.executeStateTransaction(STATE\_STORE\_NAME, operationList).block();
log.info("Order requested: " + orderId);
}
}
}
Yukarıdaki örnek uygulama için bir Dapr sidecarını başlatmak için aşağıdaki komut çalıştırılır:
1
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
State Query API
State Query API state store bileşenlerinde depolanan key/value verilerini sorgulamak, filtrelemek ve sıralamak için kullanılır. Query API veri tabanı sorgulama dili değildir. Arka planda Query API tarafından iletilen sorgu isteği bir veri tabanı sorgulama diline dönüştürülerek state store bileşeni tarafından çalıştırılır. HTTP POST/PUT veya gRPC aracılığıyla sorgu istekleri gönderilebilir. Sorgu isteğe bağlı aşağıdaki 3 kriteri içermelidir.
- filter
- sort
- page
Filter
Filter, her düğümün tekli veya çok işlenenli işlemi temsil ettiği bir ağaç biçiminde sorgu koşullarını belirtir.
Operator | İşlenler | Açıklama |
EQ | key:value | key == value |
IN | key:[]value | key == value[0] OR key == value[1] OR .. OR key == value[n] |
AND | []operation | operation[0] AND operation[1] AND .. AND operation[n] |
OR | []operation | operation[0] OR operation[1] OR .. OR operation[n] |
Sort
Sort, sıralı bir key:order değerleri dizisidir.
- Key: State store ’da bulunan anahtar
- Order: Sıralama düzenini belirten isteğe bağlı bir kriterdir. Artan sıralama için “ASC”, azalan sıralama için “DESC” kriteri kullanılır. Kriter belirtilmezse artan sıralama varsayılandır.
Page
Page, limit ve token parametreleri içerir.
- Limit: Sayfa boyutunu belirler.
- Token: Bileşen tarafından döndürülen ve sonraki sayfayı işaret eden bir belirteçtir.
Limitations
State Query API aşağıdaki sınırlamalara sahiptir.
- State store’da bulunan actor state’leri sorgulamak için SQL query’lerini destekleyen belirli veritabanları kullanılmalıdır.
- API Dapr encrypted state store’lar ile çalışamaz.
Örnek:
California eyaletindeki tüm çalışanlar bul ve çalışan kimliklerine göre azalan sıraya göre getir.
1
2
3
4
5
6
7
8
9
10
{
"filter": {
"EQ": { "state": "CA" }
"sort": \[
{
"key": "person.id",
"order" : "DESC"
}
\]
}
SQL Karşılığı
1
2
3
4
SELECT * FROM c WHERE
state = "CA"
ORDER BY
person.id DESC
How-to : Share state between applications
Uygulamalar arası state paylaşımı gerekli olduğunda farklı mimarilerin farklı ihtiyaçları olabilir. Dapr, state paylaşımı için aşağıdaki key prefix leri desteklemektedir.
Key prefix | Tanım |
appid | Varsayılandır, durumu yalnızca verilen appid üzerinden yönetmeye izin verir. Tüm state key lerinin öününe eklenir. |
name | State lerin tutulduğu component in adıdır. Farklı uygulamalar aynı state deposunu kullanabilir. |
namespace | appid ile birlikte kullanıldığında belirli bir namespace te yönetilir. Aynı appid ile farklı namespace te bulunan uygulamaların aynı state deposunu kullanabilmesini sağlar. |
none | Ön ek kullanılmamış olur. State ler farkı depolarla paylaşılır. |
Specifying a state prefix strategy
“keyPrefix” ile meta veri anahtarı belirlenmektedir.
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: production
spec:
type: state.redis
version: v1
metadata:
name: keyPrefix
value: <key-prefix-strategy>
Örnekler
appid si myApp olan bir uygulama state inin redis state deposunda kaydedilmesi aşağıdaki gibidir. myApp | darth olarak key kaydedilmiştir. |
appid kullanımı
1
2
3
4
5
6
7
8
9
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '\[
{
"key": "darth",
"value": "nihilus"
}
\]'
namespace kullanımı
1
2
3
4
5
6
7
8
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '\[
{
"key": "darth",
"value": "nihilus"
}
\]'
name kullanımı
1
2
3
4
5
6
7
8
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '\[
{
"key": "darth",
"value": "nihilus"
}
\]'
none
1
2
3
4
5
6
7
8
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '\[
{
"key": "darth",
"value": "nihilus"
}
\]'
How-To : Encrypt application state
State ler şifreli olarak tutulmalıdır. Dapr, 128, 192 ve 256 bitlik anahtarlar ile Galois/Counter Mode (GCM) i destekler.
Dapr, otomatik şifrelemeye ek olarak, geliştiricilerin ve operasyon ekiplerinin bir anahtar döndürme stratejisini etkinleştirmesini kolaylaştırmak için birincil ve ikincil şifreleme anahtarlarını destekler. Bu özellik, tüm Dapr state store tarafından desteklenmektedir.
Otomatik Şifreleme
metadata section eklenerek yapılır.
1
2
3
4
5
metadata:
\- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey # key is optional.
Yukarıdaki yapılandırma ile, mysecret adlı bir anahtardaki gerçek şifreleme anahtarını içeren, şifreleme anahtarını mysecret adlı bir secret tan alacak şekilde yapılandırılmış bir Dapr state deposu oluşturulmuştur. 128 bit lik anahtar kullanımı önerilmektedir.
128 bitlik random anahtar üretimi
1
2
3
openssl rand 16 | hexdump -v -e '/1 "%02x"'
\# Result will be similar to "cb321007ad11a9d23f963bff600d58e0"
Aşağıdaki yapılandırma ile ikincil anahtar kullanımı da desteklenmektedir.
İkincil Anahtar Kullanımı
1
2
3
4
5
6
7
8
9
metadata:
\- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey
\- name: secondaryEncryptionKey
secretKeyRef:
name: mysecret2
key: mykey2
Dapr başladığında, meta veri bölümünde listelenen şifreleme anahtarlarını içeren secret lar getirir. Dapr, secretKeyRef.name alanını gerçek durum anahtarının sonuna eklediğinden, hangi durum öğesinin hangi anahtarla şifrelendiğini bilmektedir.
Bir anahtarı güncelleyebilmek için, yeni anahtarı içeren bir gizli diziye işaret etmek için birincil şifreleme anahtarı değiştirilmelidir. Eski birincil şifreleme anahtarı ikincilEncryptionKey’e taşındığında yeni veriler yeni anahtar kullanılarak şifrelenecek ve alınan eski verilerin şifresi ikincil anahtar kullanılarak çözülecektir. Eski anahtarla şifrelenen veri öğelerinde yapılan tüm güncellemeler, yeni anahtar kullanılarak yeniden şifrelenecektir.
State Store’ları
Dapr içerisinde state’leri yönetebileceğimiz birçok farklı state store’u kurabiliriz. Aynı zamanda bu state store’larını genişletedebiliyoruz. Bunun için https://github.com/dapr/components-contrib reposunu inceleyebiliriz. Hali hazırda geliştirilmiş olan store’ları kullanabildiğimiz gibi repository’de tanımı yapılan interface’i implement edecek şekilde kendi state store tanımımızı da yapabiliriz.
State storelarını uygulamamıza tanıtabilmek için diğer hizmetlerde olduğu gibi bir component tanımı yapmamız gerekmektedir. Bu component’in şablonu şu şekildededir;
Component Tanımı
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.<DATABASE>
version: v1
metadata:
\- name: <KEY>
value: <VALUE>
\- name: <KEY>
value: <VALUE>
Bu componentte görülen type alanı kullandığımız veritabanının tipini belirtmek için kullanılıyor. Veritabanına özel bilgileri ise(connection string gibi) metadata alanına koymamız gerekmektedir. Production ortamında kullandığımız veritabanlarının şifreleri gibi gizli olması gereken bilgileri doğrudan buraya yazmak yerine secret store kullanmalıyız.
Dapr hali hazırda birçok veritabanına destek vermektedir. Hangi veritabanlarının statestore olarak kullanabildiği bilgisinin güncel listesine https://docs.dapr.io/reference/components-reference/supported-state-stores/ adresinden erişilebilir.
Postgresql Örneği
Bunun için yukarda bahsettiğim bir component dosyası oluşturmamız gerekmektedir. Öncelikle postgres.yaml adında bir component oluşturup. İçeriğini
Component Tanımı
1
2
3
4
5
6
7
8
9
10
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: <NAME>
spec:
type: state.postgresql
version: v1
metadata:
\- name: connectionString
value: "<CONNECTION STRING>"
Şeklinde güncelleyelim. Burdaki connection stringimizi kendi veritabanımızı içerecek şekilde güncellemeliyiz. Örneğin
Connection String
1
"host=localhost user=postgres password=example port=5432 connect\_timeout=10 database=dapr\_test"
Daha sonrasında postgres veritabanını çalışır hale getirmemiz gerekmektedir. Bunun için bir docker podunda kaldırabiliriz.
Veritabanını ayağa kaldırmak
1
docker run -p 5432:5432 -e POSTGRES_PASSWORD=example postgres
Son aşama olarak stateleri tutabilmemiz için bir database oluşturmamız gerekmektedir. Bunu da aşağıdaki gibi yapabiliriz.
Tabloyu oluşturmak
1
create database dapr_test
Time-to-Live
Bazı durumlarda bildirilen state’in belirli bir zaman için geçerli olmasını isteyebiliriz. Bunun için programatik bir yaklaşım yapmak yerine doğrudan state store’un TTL özelliğini kullanabiliriz. Tabii ki her state store TTL özelliğine sahip olmayabilir. Hangi state storelarda bu özelliğin olduğunu takip etmek için yine https://docs.dapr.io/reference/components-reference/supported-state-stores/ listesi takip edilebilir. Eğer bu özelliğie sahip olmayan bir state store’a ttl değeri gönderilmeye çalışılırsa dapr bu alanı ignore edecektir.
Bir state’i kaydederken TTL koymak istersek aşağıdaki gibi bir kullanım oluşmaktadır.
TTL ile state kaydetme
1
2
3
4
5
6
7
8
9
10
11
12
13
#dependencies
from dapr.clients import DaprClient
#code
DAPR\_STORE\_NAME = "statestore"
with DaprClient() as client:
client.save\_state(DAPR\_STORE\_NAME, "order\_1", str(orderId), state_metadata={
'ttlInSeconds': '120'
})
Varolan bir ttl’i yok saymak için ttlInSeconds değerini -1 olarak göndermeliyiz. Bu sayede ttl geçersiz olacaktır.
Secret Store’lar
Daha önce bahsedildiği gibi componentlerin tanımında gizli/önemli bilgileri doğrudan düz string olarak tutmak/tanımlamak ciddi güvenlik problemlerine neden olabilmektedir. Dapr bu problemin çözümü için secretları kullanma yolunu açmıştır. Component tanımındak spec.metadata bölümü altında secret’lara referans verebiliyoruz. Hangi secret store’dan okuyacağını da ayrı olarak auth.secretstore şeklinde tanımlamamız gerekiyor, eğer kubernetes içerisinde kullanıyorsak ve bu tanımlamayı yapmadıysak doğrudan kubernetes’in secret store’u kullanıldığını varsayar ve aramasını orda gerçekleştirir. Secret store kullandığımı durumun sonucunda örnek component dosyamız şu şekilde oluşur.
Component Tanımı
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: servicec-inputq-azkvsecret-asbqueue
spec:
type: bindings.azure.servicebusqueues
version: v1
metadata:
-name: connectionString
secretKeyRef:
name: asbNsConnString
key: asbNsConnString
-name: queueName
value: servicec-inputq
auth:
secretStore: <SECRET\_STORE\_NAME>
Bu örnekte secret store kısmına hangi store’u kullanıyorsak onu tanımlamamız gerekmektedir. Eğer boş bırakırsak ve kubernetes içerisinde çalışıyorsak doğrudan kubernetes’in secret store’unu kullanır. Örnekte gördüğümüz gibi connection string alanını doğrudan component’e gömmek yerine secret store’a referans verecek şekilde tanımlamış olduk. Dapr bu tanımlamayı gördüğünde secret store içerisinde “asbNsConnString”’ine karşlık gelen değeri bulup ona göre componenti ayağa kaldırır. Eğer secret’ımız embedded bir key içeriyorsa bu içeriğe şu şekilde de erişebiliriz;
Component Tanımı
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
\- name: redisHost
value: localhost:6379
\- name: redisPassword
secretKeyRef:
name: redis-secret
key: redis-password
auth:
secretStore: <SECRET\_STORE\_NAME>