Gökçenur Çınar
Yazılım Geliştirme Uzmanı

Dapr: State Management

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.

Diagram showing state management of example service

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&lt;State<String&gt;> 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&lt;List<State<String&gt;>> 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&lt;TransactionalStateOperation<?&gt;> 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: &lt;key-prefix-strategy&gt;
    

 Ö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.&lt;DATABASE&gt;
  version: v1
  metadata:
  \- name: &lt;KEY&gt;
    value: &lt;VALUE&gt;
  \- name: &lt;KEY&gt;
    value: &lt;VALUE&gt;

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: &lt;NAME&gt;
spec:
  type: state.postgresql
  version: v1
  metadata:
  \- name: connectionString
    value: "&lt;CONNECTION STRING&gt;"

Ş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: &lt;SECRET\_STORE\_NAME&gt;

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: &lt;SECRET\_STORE\_NAME&gt;