In this “Exploring Dapr” series, I’m exploring some of its capabilities and sharing my thoughts on it. Dapr is an event-driven portable runtime for building micro-services, for more information I refer to the documentation. Be aware that Dapr is currently under constant evolution, so some of the described behavior or findings might not be accurate anymore in the future.
This blog post is based on Dapr version 0.3 and it explains how you can run your Dapr application in Kubernetes. We’ll start from the publish / subscribe sample application, that I’ve described over here. I’m using the local Kubernetes cluster that ships with Docker Desktop and Azure Container Registry as my private container store.
Install Dapr in your Kubernetes cluster
First things first, let’s enable Dapr in our cluster. The installation procedure is clearly described over here, so no need to repeat it.
After installation, you should see three Dapr pods running:
- Dapr operator
- Dapr sidecar injector
- Dapr placement
$ kubectl get pods NAME READY STATUS RESTARTS AGE dapr-operator-76888fdcb9-n4pkm 1/1 Running 4 33d dapr-placement-666b996945-whzv5 1/1 Running 4 33d dapr-sidecar-injector-744d97578f-x5lm7 1/1 Running 4 33d
Create an Azure Container Registry
- Create an Azure Container Registry
- Provide the necessary details Enable administrative access, which is acceptable for dev purposes. For enterprise grade security, it’s better to use a service principal.
- Go to the Access keys tab and copy the username and password, you’ll need it in the next steps.
Publish your image to an Azure container registry
- Create Docker image, starting from the sample described in this blog. This is a standard ASP.NET Core 3.0 app, with Docker support enabled.
docker build . -t pubsubapp:0.0.1
- Run the docker image locally, to validate if it works
docker run -p 5000:80 pubsubapp:0.0.1
- Link Docker to your private container registry and log in
docker login tvhdaprregistry.azurecr.io -u tvhdaprregistry
- In order to push to a private registry, tag the image with the registry name as prefix
docker tag pubsubapp:0.0.1 tvhdaprregistry.azurecr.io/pubsubapp:0.0.1
- Now you can push your Docker image to the Azure Container Registry
docker push tvhdaprregistry.azurecr.io/pubsubapp:0.0.1
- Validate if your image is uploaded to the Azure Container Registry:
Please, remember that you better perform these steps during a build pipeline, in real enterprise-grade projects.
Run your Dapr application in Kubernetes
- Remember that we’ve created a Dapr publish / subscribe component description
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: pubsub-azure-service-bus spec: type: pubsub.azure.servicebus metadata: - name: connectionString value: ###
- Deploy now the Dapr publish / subscribe component inside the Kubernetes cluster
kubectl apply -f Components/pubsub_azureServiceBus.yaml
- Create a Kubernetes secret that will be used to pull the images from the Azure Container Registry
kubectl create secret docker-registry tvh-dapr-acr-secret --docker-server tvhdaprregistry.azurecr.io --docker-username=tvhdaprregistry --docker-password dB=<Password>
- Create a Kubernetes deployment file. Remark two important things in the YAML description:
- We use the previously created secret to pull the image from ACR
- We add Dapr annotations to the deployment, to ensure a Dapr sidecar will be injected
apiVersion: apps/v1 kind: Deployment metadata: name: pubsub-app labels: app: pubsub-app spec: replicas: 1 selector: matchLabels: app: pubsub-app template: metadata: labels: app: pubsub-app annotations: dapr.io/enabled: "true" dapr.io/id: "pubsubapp" dapr.io/port: "80" dapr.io/log-level: "debug" spec: containers: - name: pubsubapp image: tvhdaprregistry.azurecr.io/pubsubapp:0.0.1 ports: - containerPort: 80 imagePullSecrets: - name: tvh-dapr-acr-secret
- Apply the deployment to your Kubernetes cluster
$ kubectl apply -f Kubernetes/pubsubapp_deployment.yaml deployment.apps/pubsub-app created
- You should see the deployment being ready pretty rapidly
$ kubectl get deployments pubsub-app NAME READY UP-TO-DATE AVAILABLE AGE pubsub-app 1/1 1 1 10s
- Let’s have a look at the pod that has been created
$ kubectl get pods NAME READY STATUS RESTARTS AGE pubsub-app-5bb66bd4bd-ccrm5 2/2 Running 0 3m11s
- When you look into the Pod, you’ll see that a dapr sidecar container got injected
$ kubectl describe pod pubsub-app-5bb66bd4bd-ccrm5 ... Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 4m52s default-scheduler Successfully assigned default/pubsub-app-5bb66bd4bd-ccrm5 to docker-desktop Normal Pulled 4m51s kubelet, docker-desktop Container image "tvhdaprregistry.azurecr.io/pubsubapp:0.0.1" already present on machine Normal Created 4m50s kubelet, docker-desktop Created container pubsubapp Normal Started 4m50s kubelet, docker-desktop Started container pubsubapp Normal Pulling 4m50s kubelet, docker-desktop Pulling image "docker.io/daprio/dapr:latest" Normal Pulled 4m49s kubelet, docker-desktop Successfully pulled image "docker.io/daprio/dapr:latest" Normal Created 4m48s kubelet, docker-desktop Created container daprd Normal Started 4m48s kubelet, docker-desktop Started container daprd
- When I look into the Dapr container logs, I see that:
- The Azure Service Bus pub/sub component was loaded
- The application was discovered at port 80
- Dapr is running by default on HTTP port 3500 in Kubernetes
kubectl logs pubsub-app-5bb66bd4bd-ccrm5 daprd time="2019-12-290T13:56:59Z" level=info msg="starting Dapr Runtime -- version 0.3.0 -- commit v0.3.0-rc.0-1-gfe6c306-dirty" time="2019-12-290T13:56:59Z" level=info msg="log level set to: debug" time="2019-12-290T13:56:59Z" level=info msg="kubernetes mode configured" time="2019-12-290T13:56:59Z" level=info msg="dapr id: pubsubapp" time="2019-12-290T13:56:59Z" level=info msg="loaded component pubsub-azure-service-bus (pubsub.azure.servicebus)" time="2019-12-290T13:56:59Z" level=info msg="application protocol: http. waiting on port 80" time="2019-12-290T13:56:59Z" level=info msg="application discovered on port 80" time="2019-12-290T13:57:01Z" level=info msg="Initialized service discovery to kubernetes" time="2019-12-290T13:57:01Z" level=warning msg="failed to init actors: actors: state store must be present to initialize the actor runtime" time="2019-12-290T13:57:01Z" level=info msg="http server is running on port 3500" time="2019-12-290T13:57:01Z" level=info msg="gRPC server is running on port 50001" time="2019-12-290T13:57:01Z" level=info msg="dapr initialized. Status: Running. Init Elapsed 1662.3049999999998ms"
- Create a Kubernetes Service description, which will be used to expose our Order API on the cluster nodes. Remark that this is just for testing, no security is applied.
apiVersion: v1 kind: Service metadata: name: pubsub-app-service labels: app: pubsub-app-service spec: type: NodePort ports: - port: 8080 targetPort: 80 nodePort: 30501 protocol: TCP name: http selector: app: pubsub-app
- Apply the service to your Kubernetes cluster
kubectl apply -f Kubernetes/pubsubapp_service.yaml
- Test the application on localhost:30501. Submit an order
- Check the application logs and see that the order got processed correctly.
$ kubectl logs pubsub-app-5bb66bd4bd-ccrm5 pubsubapp Microsoft.Hosting.Lifetime[0] http://[::]:80 Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. Microsoft.Hosting.Lifetime[0] Hosting environment: Production Microsoft.Hosting.Lifetime[0] Content root path: /app Tvh.Dapr.PubSub.Orders.Controllers.OrderController[0] Order with id 123 received! Tvh.Dapr.PubSub.Orders.Controllers.OrderController[0] Order with id 123 published with status OK! Tvh.Dapr.PubSub.Orders.Controllers.OrderController[0] Order with id 123 processed!
Conclusion
You see that all the plumbing was related to Kubernetes and is not really Dapr-specific. If you set your annotations correctly, Dapr gets successfully injected into your Pods. I had some issues with connecting my application to the Dapr sidecar, until I noticed in the logs that it listens by default on HTTP port 3500.
One important remark is about security. My little sample app contains both an API and the background worker in a single application. Although I completely understand that it makes sense to split these into two applications, there are also arguments to keep them together (e.g. simplicity). Keeping both the API and worker in one application imposes some security risks, as also your local inbound HTTP endpoints – that allow Dapr to submit messages into your container – might get (publicly) exposed.
Cheers
Toon