Ntime Dapr
Ntime Dapr
Distributed
Application
Runtime (Dapr)
Simplifying Microservices Applications
Development Through Proven and
Reusable Patterns and Practices
—
Radoslav Gatev
Foreword by Yaron Schneider,
Principal Software Engineer and Dapr co-founder,
Microsoft
Introducing Distributed
Application Runtime
(Dapr)
Simplifying Microservices
Applications Development Through
Proven and Reusable Patterns
and Practices
Radoslav Gatev
Foreword by Yaron Schneider,
Principal Software Engineer and Dapr co-founder, Microsoft
Introducing Distributed Application Runtime (Dapr): Simplifying Microservices
Applications Development Through Proven and Reusable Patterns and Practices
Radoslav Gatev
Gorna Oryahovitsa, Bulgaria
Acknowledgments��������������������������������������������������������������������������������������������������xix
Introduction������������������������������������������������������������������������������������������������������������xxi
v
Table of Contents
vi
Table of Contents
vii
Table of Contents
ix
Table of Contents
x
Table of Contents
xi
Table of Contents
Chapter 16: Using Dapr with the Azure Logic Apps Runtime������������������������������� 291
Azure Logic Apps Overview������������������������������������������������������������������������������������������������������� 291
Integration Between Dapr and Logic Apps�������������������������������������������������������������������������������� 292
Designing a Workflow��������������������������������������������������������������������������������������������������������������� 293
Summary���������������������������������������������������������������������������������������������������������������������������������� 296
Index��������������������������������������������������������������������������������������������������������������������� 297
xii
About the Author
Radoslav Gatev is a software architect and consultant
who specializes in designing and building complex and
vast solutions in Microsoft Azure. He helps companies all
over the world, ranging from startups to big enterprises,
to have highly performant and resilient applications that
utilize the cloud in the best and most efficient way possible.
Radoslav has been awarded a Microsoft Most Valuable
Professional (MVP) for Microsoft Azure for his ongoing
contributions to the community in this area. He strives for
excellence and enjoyment when working on the bleeding
edge of technology and is excited to work with Dapr. He frequently speaks and presents
at various conferences and participates in organizing multiple technical conferences in
Bulgaria.
xiii
About the Technical Reviewer
As a freelance Microsoft technologies expert, Kris van der
Mast helps his clients to reach their goals. Actively involved
in the global community, he is a Microsoft MVP since 2007
for ASP.NET and since 2016 for two disciplines: Azure and
Visual Studio and Development Technologies. Kris is also
a Microsoft ASP Insider, Microsoft Azure Advisor, aOS
ambassador, and Belgian Microsoft Extended Experts Team
(MEET) member. In the Belgian community, Kris is active as
a board member of the Belgian Azure User Group (AZUG)
and is Chairman of the Belgian User Group (BUG) Initiative. Since he started with .NET
back in 2002, he’s also been active on the ASP.NET forums where he is also a moderator.
His personal site can be found at www.krisvandermast.com. Kris is a public (inter)
national speaker and is a co-organizer of the CloudBrew conference.
Personal note:
While doing a book review, I also like to learn new things on the go. With this book
I sure did. I hope you will enjoy reading it at least as much as I did.
xv
Foreword
In the year leading up to the first release of Dapr as an open source project in October
2019, Haishi Bai (my partner in co-founding Dapr) and I observed just how much the
cloud-native space had matured. It had grown to provide ops and infrastructure teams
with first-class tools to run their workloads either on premises or in the cloud.
With the rise of Kubernetes (K8s), an entire ecosystem of platforms has sprung up
to provide the missing pieces for network security, traffic routing, monitoring, volume
management, and more.
Yet, something was missing.
The mission statement to make infrastructure “boring” was being realized, but for
developers, many if not all of the age-old challenges around distributed computing
continued to exist in cloud-native platforms, especially in microservice workloads where
complexity grows with each service added.
This is where Dapr comes in. First and foremost a developer-facing tool, Dapr focuses
on solving distributed systems challenges for cloud-native developers. But just like any
new technology, it’s critical to be able to understand its uses, features, and capabilities.
This book by Radoslav Gatev is the authoritative, technical, hands-on resource you
need to learn Dapr from the ground up. Up to date with version 1.0 of Dapr, this book
gives you all you need to know about the Dapr building blocks and APIs (Application
Programming Interfaces), when and how to use them, and includes samples in multiple
languages to get you started quickly. In addition to the Dapr APIs, you’ll also find
important information about how to debug Dapr-enabled applications, which is critical
to running Dapr in production.
Radoslav has extensive, in-depth knowledge of Dapr and is an active Dapr
contributor, participating in the Dapr community and helping others learn to use it
as well. He makes the project better by working with maintainers to report issues and
contribute content.
You really can’t go wrong with this book, and I highly recommend it to anyone who
wants to start developing applications with Dapr.
Yaron Schneider
Principal Software Engineer and Dapr Co-founder, Microsoft
xvii
Acknowledgments
In every venture uncommon to a self, there should be a great catalyst. I would like to
thank Apress and especially Joan Murray, Jill Balzano, Laura Berendson, Welmoed Spahr,
and everyone else involved in the publishing of this book. I had been thinking about
writing a book for quite some time, and I am grateful that Joan reached out to me. At that
moment I had a few conferences canceled, a few professional opportunities lost because
of the risks and the great uncertainty at the start of COVID-19. Fast-forward a year from
then, the book has been finished, and I am writing this. A year of lockdowns spent in
writing is a good year, after all.
Additionally, I would like to thank Mark Russinovich for being such an inspiration
and knowledge source for me. Sometimes, it takes just a tweet to change the life of a
person. He retweeted a blog post of mine about Dapr. It gained a lot of attention, and to a
large extent, because of that, Introducing Distributed Application Runtime (Dapr) is now
a reality. I would like to thank Yaron Schneider and all Dapr maintainers who are always
friendly and supportive. They helped a lot by answering some of the questions I’ve had
in the process.
I would also like to thank Kris van der Mast, the technical reviewer, for the excellent
feedback and suggestions that added immense value to this book.
I would like to thank Mihail Mateev who gave me the opportunity to do my first
public session a couple of years ago. Since then, we have been collaborating with a lot
of other folks to make some of the biggest conferences in Bulgaria possible. Of course,
thanks to the community that still finds them interesting, and from the fascinating
discussions, they sparkle. I would like to thank Martin Tatar, Cristina González Herrero,
and Irene Otero for their great help and continuous support to us, the Microsoft Most
Valuable Professionals.
I would like to thank Dimitar Mazhlekov with whom we have been friends,
teammates, business partners, and tech junkies. We have walked a long way and learned
a lot together.
I would like to thank all my teachers, professors, and mentors who supported me a
lot throughout the years. To find a good teacher is a matter of luck. And with you all, I am
the lucky person for being your student.
xix
Acknowledgments
I would like to express my gratitude for being able to work with organizations around
the globe that gave me exposure to their unique and intriguing challenges that helped
me gain so much knowledge and experience. Thanks to all the team members I met
there and for what I was able to learn from every one of them.
And last but not least, to my girlfriend Desislava, my parents, extended family, and
friends, thank you for the endless support throughout the years! Thank you for keeping
me sane and forgiving my absence when I get to work on something challenging.
xx
Introduction
Being able to work on various projects, one should be able to identify the common set
of issues every project faces. It doesn’t mean you can always apply the same solution
over and over again, but it puts a good structure. I was very lucky that early in my career,
I was pointed to the proper things to learn. I have already been using object-oriented
programming (OOP) for some time, but I was stunned when I read the book Design
Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson,
and Vlissides for the first time. It gave me the answers to some of the questions I’d been
asking myself. From there on, I am a strong believer that patterns do not only serve as
reusable solutions to common problems, they become a common lingo and teach you
how to think in an abstract way. Being able to work at a conceptual level, instead of
focusing too much on the details, I believe made me a better professional.
I heard of Distributed Application Runtime (Dapr) for the first time when it was
announced at Microsoft Ignite 2019, Microsoft’s annual conference for developers
and IT professionals. At first, the idea of it resonated within me but I didn’t completely
understand it, and so I decided to start playing with it. In September 2020, a transition
to an open governance model was announced to ensure that the project is open and
vendor neutral. Fast-forward to February 2021 when Dapr v1.0 was released. Now that
Dapr is stable and production-ready, it is also in the process of being donated to the
Cloud Native Computing Foundation (CNCF) as an Incubation project. By the time you
read this, it may be finalized.
Dapr greatly simplifies the development of Microservices applications. It works with
any language and any platform. You can containerize your applications or not, you can
use Kubernetes or not, you can deploy to the cloud or not. You can sense the freedom
here. From a development perspective, Dapr offers a number of capabilities grouped
and packaged as building blocks. Let’s face it. You will have to use some services that are
external to the application you are aiming to build. It is very normal to not try to reinvent
the wheel and build everything from scratch. By using the building blocks provided
by Dapr, you use those external services without thinking about any SDKs or specific
concepts imposed by the external service you are trying to integrate with. You just have
to know how to work with the building block. This simplifies the operations you want to
xxi
Introduction
execute on the target external services, and Dapr serves as the common denominator.
That’s why you can swap one technology with another in the scope of the building block,
that is, reconfiguring Dapr from persisting state to, say, Redis to MySQL, for example.
Some believe Dapr is the service mesh but done right. The reason for that is that
service meshes rely on the sidecar architecture as Dapr does. However, service meshes
are for network infrastructure, while Dapr provides reusable patterns that are easy to
apply and repeatable. In the future, I expect building blocks to expand in functionality
and maybe new building blocks to come to Dapr. With that, the reach to potential
external services will become so wide. For greenfield projects, this will mean that Dapr
can be put on the foundational level of decisions. Once you have it, you can, later on,
decide what specific message broker or what specific persistence medium to use for state
storage, for example. This level of freedom unlocks many opportunities.
Introducing Distributed Application Runtime (Dapr) aims to be your guide to
learning Dapr and using it for the first time. Some previous experience building
distributed systems will be helpful but is by no means required. The book is divided
into three parts. In the first part before diving into Dapr, a chapter is devoted to set the
ground for the basic concepts of Microservices applications. The following chapter
introduces Dapr: how it works and how to initialize and run it locally. The next chapter
covers the basics of containers and Kubernetes. Then all that knowledge is combined in
order to explore how Dapr works inside Kubernetes. The part wraps up by exploring the
various options to develop and debug Dapr applications, by leveraging the proper Visual
Studio code extensions – both locally and inside Kubernetes.
The second part of the book has a chapter devoted to each building block that
explores it in detail. The building blocks are:
• Service Invocation
• Resource Bindings
• Secrets
• Observability
xxii
Introduction
The final part of the book is about integrating Dapr with other technologies. The
first chapter outlines what middleware can be plugged into the request pipeline of Dapr.
Some of the middleware enable using protocols like the OAuth2 Client Credentials and
Authentication Code grants and OpenID Connect with various Identity Providers that
support them. The examples in the chapter use Azure Active Directory. The following
chapter discusses how to use Dapr with ASP.NET Core by leveraging the useful attributes
that come from the Dapr .NET SDK. The last two chapters cover how to combine Dapr
with the runtimes of Azure Functions and Azure Logic Apps.
Code samples accompany almost every chapter of the book. Most of them are
implemented in C#, but there are a few of them in Node.js, to emphasize the
multiple-language approach to microservices. You can find them at https://github.
com/Apress/introducing-dapr. You will need to have .NET and Node.js installed. Some
of the tips and tricks in the book are applicable only to Visual Studio Code (e.g., the
several extensions that are covered in Chapter 5: Debugging Dapr Applications), but you
can also use any code editor or IDE like Visual Studio. For some of the examples, you will
also need Docker on your machine and any Kubernetes cluster – either locally, as part of
Docker Desktop, or somewhere in the cloud.
I hope you enjoy the book. Good luck on your learning journey. Let’s start Dapr-
izing! I am happy to connect with you on social media:
LinkedIn: www.linkedin.com/in/radoslavgatev/
Twitter: https://twitter.com/RadoslavGatev
xxiii
PART I
Getting Started
CHAPTER 1
Introduction
to Microservices
Digitalization drives businesses in such a direction that every system should be
resilient and available all the time. In order to achieve that, you have to make certain
decisions about the application architecture. In this chapter, you will learn how systems
evolved from calculation machines built to serve a specific purpose to general-purpose
computers. Making the same parallel but on the software side, we will discuss what
Monolithic applications are along with their pros and cons. Then, we will go through the
need of having distributed applications dispersed across a network of computers. You will
also learn about the Microservices architecture as a popular way for building distributed
applications – how to design such applications, what challenges the Microservices
architecture brings, and some of the applicable patterns that are often used.
1
he first mechanical computer is considered to be the Difference Engine that was designed
T
by Charles Babbage in the 1820s for calculating and tabulating the values of polynomial
functions. Later on, he devised another machine called the Analytical Engine that aimed to
perform general-purpose computation. The concepts it was to employ can be found in modern
computers, although it was designed to be entirely mechanical.
3
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_1
Chapter 1 Introduction to Microservices
take a lot of space. Apart from the slowness of their operation, each of them was a point
of failure on its own. But over time, the mechanical parts started getting replaced by their
electric counterparts.
Hardware Progress
To perform some logic, you need some way of representing state – like 1 and 0 in the
modern binary computers. Likewise, the relay was identified as a viable component
that was widely known and available to be utilized for performing the “on” and “off”
switching. But still, relays were rather slow as they had a moving mechanical part – an
electromagnet opens or closes a metal contact between two conductors. Then vacuum
tubes gained traction as a way of switching. They didn’t have any moving parts; however,
they were still big, expensive, and nonefficient. Then they got replaced by transistors. But
imagine what is soldering thousands of discrete transistors in a complex circuit! There
will be faulty wirings that are hard to discover. Later on, the need for soldering discrete
transistors was avoided with integrated circuits where thousands of tiny transistors were
placed on small chips. This ultimately led the way to modern microprocessor technology.
These evolutionary steps were restricted by the speed, size, and cost of a single bit.
Early computers used to be expensive and could easily fill an entire room. Because
they weighed a lot, they were usually moved around using forklifts and transported
via cargo airplanes. Announced in 1956, IBM 305 RAMAC was the first computer to
use a random-access disk drive – the IBM 350 Disk Storage Unit that incorporated
50 24-inch-diameter rotating disks that could store 5 million 6-bit characters or the
equivalent of whopping 3.75 MB of data. This is the ancestor of every hard drive
produced ever since.
That’s how typically technology evolves. Advances of knowledge are being used to
build new things on top of the old knowledge base. Every decision at a time is restricted
by various boundaries we face – physical limits, cost, speed, size, purpose, and so on.
If you think about the purpose of the early computers, in the beginning, they were
devised with a sole purpose – from solving polynomial functions to cracking secret codes
ciphered by machines such as the German Enigma machine. There was a long way until
we could use general-purpose computers that are highly programmable.
Without making any generalizations, it will be highly inconvenient to build anything.
You have to manage all of the moving parts right from the beginning, which is a lot of
effort. For example, you had to either be a genius or be among one of the inventors to be
4
Chapter 1 Introduction to Microservices
able to use the computer in the 1950s. With the introduction of personal computers, it
started to get easier. The same applies to software development.
Software Progress
While hardware tried to address the physical aspects of computer systems, kind of the
same evolution happened with software. The early programs were highly dependent on
the architecture of the computer executing them.
Applications Development
When writing low-level code, you have to think about everything – machine instructions
and what registers to use and how. With the advances of modern compilers, we have a
comfortable abstraction to express just what the program should do without thinking
about what instructions will be executed by the Central Processing Unit (CPU).
Programs used to be a self-sustainable piece of code without any external dependencies.
Object-oriented programming is a paradigm that essentially gave us yet another powerful
abstraction. By utilizing the power of interfaces, we can reuse a lot of code. Every piece
of code that we use out of the box has an interface (or a contract that it serves). Software
libraries emerged, and they became the building blocks of modern software. Let’s face it:
system software, server software, frameworks, utilities, and all kinds of application software
are all built using well-known and widely used libraries and components.
Some programs are still dependent on some operating system features or third-party
components that were installed on the developer’s machine. And the typical case is that
the program you just downloaded does not run on your machine. “But it works on my
machine,” they would say.
A few years ago, containers started to gain more and more popularity. Containers are
the solution to make your code easily transferable across machines and environments by
packaging all application code along with all of its dependencies. By doing this, you are
effectively isolating the host machines and their current state from your code.
Infrastructure and Scalability
In the past, programs were running on a single machine and were used only on that
same machine. This was until computers could be connected to networks where they
could talk to each other. The machine that hosts applications is called a server, and the
other machines that use the applications are called clients.
5
Chapter 1 Introduction to Microservices
Over time what happened with some applications is that the number of clients
started to outgrow the capacity of the servers. To accommodate the ever-increasing
load resulted in adding more resources to each server, the so-called vertical scaling.
The application code is still the same but running on a beefier machine with a lot more
processing power and memory. At some point, the technological limits will be reached,
or it will become too expensive to continue adding power to a single machine. And in
case something happens with this machine, your application will become inaccessible
for all clients. The number of options to alleviate the issue was pretty much exhausted.
Then it became obvious that the applications should be distributed across different
servers. There are more instances of your applications running across a set of machines
instead of relying on a single big machine. The incoming load is typically distributed
across all machines. That’s called horizontal scaling.
To be able to achieve horizontal scaling, you have to make sure that every instance of
the application doesn’t hold any internal state, that is, your application is stateless. This
is needed because each application replica should be able to respond to any request. As
you replicate software across more instances, you are starting to treat your servers more
like cattle as opposed to much-loved pets. You don’t really care even if you lose an entire
server if you have a couple of others that are still healthy and taking traffic. Of course, as
with any decision, this comes with certain trade-offs.
Even if you achieve some level of scalability, it doesn’t mean that your application
is prepared to withstand future requirements. Not only traffic can grow but also the
application functionality evolves and extends. Respectively with functionality, team size
is also a subject of change.
In the next sections of this chapter, I will walk you through the two popular
architectural styles for building an application – the Monolithic and the Microservices
architecture. It doesn’t make sense to explain one without mentioning the other because
they have rather contradictory principles.
Monolithic Architecture
According to the Merriam-Webster dictionary, the definition of the word monolith is
“a single great stone often in the form of an obelisk or column” or “a massive structure.”
Taking the broad meaning of a massive structure, a Monolithic application is built as a
single unit. This single unit contains all your application logic. Internally, this Monolithic
application can consist of different layers. One of the layers could be the presentation
6
Chapter 1 Introduction to Microservices
layer, which deals with the user interface of the application; another layer can hold some
business logic; a third layer can be used for accessing the database. But it doesn’t mean
that those layers cannot be separated from one another in terms of a codebase. For
example, the business logic layer can spread across numerous small modules, each of
them implementing just a small part of the overall functionality. This generally improves
the quality of the code; however, those modules are still living in the same layer of the
application.
Figure 1-1 shows what typically happens when a user invokes Function A in a
Monolithic application. Function A passes the control to Function B, which depends
on Functions C and D (which depends on Function E), and when they return a result,
Function B will be able to pass the result to the user. Although the functionality is
separated into discrete functions, which are likely placed in separate class libraries, they
are all sharing common resources like the database and are running inside the same
process on the same machine.
7
Chapter 1 Introduction to Microservices
and implement its models, views, and controllers. You click the Run button of your IDE
and voilà, it is up and running on your machine. Everything can be debugged end to end.
It feels really natural and fast. The deployment is also easy – package the application and
move it to the server that will host it. Done.
Later on, several other developers can join you and work on this application. And
you probably won’t have serious conflicts for most of the things you are implementing,
as long as the application is not very big and you are just a handful of people.
When your server starts facing pressure from traffic growth, you can probably
replicate the application to multiple servers and put a load balancer in front. This way
each replica will receive a portion of all requests, and it won’t get overwhelmed. This
approach is the so-called horizontal scaling.
8
Chapter 1 Introduction to Microservices
scale out only the pieces of functionality in question to achieve a fine-grained density
with the hardware you have at hand.
Or maybe you want to start implementing new features with some new technology
that makes sense for the application you have and some of the team members are
knowledgeable about it. Let’s say it’s a different framework in a different language than
the ones you have based your Monolithic application on. Unfortunately, utilizing such
technology won’t be possible as you are locked in a certain execution model that spans
the whole application. And generally speaking, attempts for mixing various technologies
that bring different concepts don’t end well in the long term.
It’s a monolith. You cannot easily dismantle it as its components are tightly coupled.
If you waited too long, it might be that your application is a homogeneous mixture of
functions. Let’s see what is the case with the popular Microservices architecture.
M
icroservices Architecture
The Microservices architecture is an architectural pattern for building distributed
systems in such a way that they are comprised of different loosely coupled components
called services that run in different processes and are independently deployed.
Figure 1-2 shows how the Monolithic application you saw earlier in Figure 1-1 can
be shaped in the Microservices world. Each of the functions has its own service. The
services can be independently deployed and distributed across different machines.
But still, they are part of the same application and work as a whole. The services
communicate with each other by using a clearly defined Application Programming
Interface (API) via protocols such as HTTP, gRPC, AMQP, and others.
9
Chapter 1 Introduction to Microservices
For the sake of the example, I have oversimplified the process of converting a
Monolithic application to one based on the Microservices architecture. It’s not a straight
two-step process. It takes careful analysis and thorough design beforehand.
Designing Microservices
Now that you know that an application should be split into multiple pieces, how do
you define how big those pieces are? There are various approaches to tackle this. But
designing microservices is pretty much a piece of art. You can start by understanding
how the business that you are digitalizing works and what are its processes and rules.
And then identify what are the business capabilities that the application should provide.
You can probably group them into several categories and identify the main objects.
Alternatively, you can use some of the tactics from Domain-Driven Design. You
can decompose the main domain into subdomains and identify the Bounded Contexts
we have. This is a strategy to split a large problem into a set of small pieces with clear
relationships. Instead of defining a large ubiquitous model that will end up representing
a lot of perspectives, the Bounded Contexts enable you to project small parts of the
whole domain from different lenses. Having outlined the Bounded Contexts and the
10
Chapter 1 Introduction to Microservices
models, it’s easier to start thinking about the functionality of the service. Let me give you
an example. Let’s say we are designing an online store. We may have a Cart microservice,
a Payment microservice, and a Shipment microservice, among others. When a customer
purchases an item from the store, the underlying work will be performed by those
microservices. But each service sees the customer from a different perspective. The
Cart microservice sees the user as a Customer who added some product into the cart.
Customer is the entity representing the user in the context of managing a cart. To the
Payment service, the user is a Payer who uses a specific payment method to remit the
money. From the perspective of the Shipment service, the user is an entity named
Receiver that contains the user’s address. All three entities share the same identifier of a
user but have different attributes depending on the problem being addressed.
The goal of identifying the boundaries of a service is not just to make it as small as
possible. In the example in Figure 1-2, Function D and Function E are placed in the
same microservice as their functions belong to the same business capability. You should
not aim to create a microservice for the smallest piece of functionality. But ideally, a
microservice should comply with the Single-Responsibility Principle.
The first design of your microservices won’t stay forever. As the business evolves, you
will likely do some refactoring to refine the size and granularity of the services. Getting
back to the example with the online store, if you find yourself constantly merging the
information about the user from Payment and Shipment services, for example, Payment
calls the Shipment service upon every operation, there is a huge chance those two
should be merged into one service.
Each service is responsible for storing its data in some persistence layer. Depending
on the nature of data, it can be persisted in various types of databases. Some of the
data can be cached to offload the performance hit on the services and their respective
databases. The potential of using different technologies in each service depending on
the case is enormous. In contrast to monoliths, you can choose whatever programming
languages, frameworks, or databases that fit best the problem you are solving or the
expertise of the team.
B
enefits
The drawbacks of Monolithic applications, in general, are addressed by the
Microservices architecture.
11
Chapter 1 Introduction to Microservices
The main benefit of microservices is coming from the fact services run in different
processes. Each service is a standalone unit of deployment that is isolated from any other
parts of the application. If the load on this service starts to increase, then you can scale it
out on its own. In this way, you can achieve a better density by scaling only what needs to
be scaled.
This process isolation results in fault isolation. Let’s assume that there is a memory
leak in Function A in Service A. Service A won’t be stable, but fortunately it will fail on
its own. Since it is separated from other services, it won’t bring the whole application
down. Of course, it depends on how services are interconnected and what type of
communication is chosen – asynchronous or synchronous. If there is a huge chain of
synchronous requests across services, a failure in just one of them can affect a bigger
part of the application. Once the issue with Service A is resolved, it will be deployed
on its own while other services keep running. This is also a benefit that applies to the
deployment workflow of the entire application – small parts are getting rolled out as
opposed to the whole application at once. The deployment can be coordinated only
within the team that is responsible for this microservice.
When I covered the topics related to the design of microservices, I mentioned that
there were several ways to decompose the domain and manage the granularity of the
services. One alternative way of addressing the problem from the people collaboration
perspective is using the effect of Conway’s law2:
Any organization that designs a system will inevitably produce a design
whose structure is a copy of the organization’s communication structure.
From an architecture perspective, this observation means whatever the
communication style is inside the team, the software will reflect it. Flat teams that work
from the same location and have high cohesion between members tend to produce more
tightly coupled systems as opposed to distributed teams around the world. The more
you communicate with someone, the better chance you will interlace with their code
and create unneeded dependencies.
A different approach can be taken when an application based on the Microservices
architecture needs to be developed. If you apply Conway’s law in the opposite direction,
you will start with the people and the organizational structure first. Therefore, a team to
develop a particular service or a group of services can be formed. This team will own the
entire group of services end to end from design to deployment and management.
2
Conway, Melvin E., How Do Committees Invent?, Datamation magazine, April 1968
12
Chapter 1 Introduction to Microservices
The team will be organized around the capabilities that a service addresses as opposed
to being cross-functional and spanning across the entire application. And in general,
that’s what makes people happier because they fully own something and they don’t have
to switch the context as much.
If a single team owns a particular service, this gives the freedom to decide what
technology fits best the problem you are trying to solve. There is more room for doing
controlled experiments and attempts to utilize the latest technologies. For example,
one service can be based on Node.js and MongoDB, and another is implemented in
ASP.NET Core and SQL Server. Or maybe for the future, you have decided to move to
Python as the main programming language because why not? You can gradually start
reimplementing on a service-by-service basis and measure how each of them performs
on the new stack. Or you may decide that one service should take advantage of the event-
driven architecture while another needs to be a scheduled job running every once in a
while. You are free to use whatever programming model, framework, and language you
want.
The Microservices architecture may give you a better approach for building a bigger
application that scales well enough. But it doesn’t come for free though.
Downsides
The distributed nature of microservices brings a lot of complexity from the get-go.
Starting with the design, you need to decide where to put the boundaries and how to
control the scope of the services. Then you have to figure out what to use for interservice
communication. Does it make sense to have direct communication between services, or
would that create a long chain of requests and cause problems? Or should you only rely
on asynchronous communication via a message broker?
Moving to the development, developers are working just on a subset of all services.
They usually don’t know in greater detail how every other microservice works. This
can be both viewed as a benefit and a drawback. It may turn out that some complex
interaction between services doesn’t work as expected, so you have to collaborate with
other teams to be able to understand it and troubleshoot it. Traditional IDEs are working
well with Monolithic applications built around a single technology stack. You have to
find a way to debug end-to-end scenarios that span multiple services implemented with
multiple technologies.
13
Chapter 1 Introduction to Microservices
Abstract Infrastructure
A Microservices application is a set of tens to thousands of services. So you typically
provision N number of servers (either bare-metal or virtualized) that will host your
application and all its services. You have to find a way to place an instance of each
service on those servers.
14
Chapter 1 Introduction to Microservices
Let’s say you somehow managed to have your services running inside those
machines. One of them may utilize the CPU to the maximum level. A service may have
a dependency that conflicts with the version of the same dependency used in another
service. Because you cannot achieve good isolation between those services when they
run directly on the same host machine, you will have to allocate a separate machine just
to run a particular service. That’s not so efficient as you will need to have an operating
system (OS) installed on every machine. It takes space, and it’s the case of hardware not
being optimally utilized. Furthermore, you have to find a way to move services to a new
server when they fail. It’s not impossible to have a Microservices application running in
the above-mentioned setup, but it will be a very labor-intensive effort if you manage to
do it right. Most likely you will have to build some automation to keep your sanity. Let’s
see how to address those concerns.
Containers are a packaging mechanism relying on virtualization at the OS level. So when
you package your service as a container image, it will hold your compiled code along with all
its dependencies such as runtimes and libraries. It’s a sandbox environment for running a
service in an isolated way from all other services. Instead of virtualizing at the hardware level
by using a hypervisor to spin up more Virtual Machines (VMs) in a single physical machine,
multiple containers can share the same OS kernel. That is what makes them start much
faster and use fewer resources as you don’t have to install a guest OS for each container as
is the case with traditional VM-based deployments. Containers can even be limited to how
much memory and CPU to use. So you don’t have to worry about resource contention. One
of the first and most popular container platforms is Docker. Since Docker played a big part
in establishing what containers are today, Docker means different things to different people.
You will learn more about Docker in Chapter 3: Getting Up to Speed with Kubernetes.
The applications or services are packaged into a common format called a container
image. Images run as containers on a container runtime, for example, the Docker
Engine. Once you install the Docker Engine, you can execute any Docker image. But
please remember that the Microservices applications consist of a set of distributed
services. How do you distribute service containers across nodes? That is still a challenge!
You need some type of a cluster management system that runs across all nodes and
knows how to manage the lifecycle of those containers. For solutions that are based
on containers, you will hear about them as container orchestrators. If you assume the
containerized services are the musicians playing in an orchestra, the conductor is the
container orchestrator that stands in front of them and guides them altogether. It knows
15
Chapter 1 Introduction to Microservices
how many nodes you have at hand and how to distribute the workload across them. It
also continually monitors all containers and nodes and knows how to move containers
across nodes to provide better utilization of resources as well as reacting to unexpected
things – as both containers and nodes hosting them do go down. Choosing to base
your application on proven containerization technologies and orchestrators makes
things simpler as you stick to their proven concepts. One of the most popular container
orchestrators is Kubernetes, which together with Docker is the de facto standard
nowadays. I will cover both in more detail in Chapter 3: Getting Up to Speed with
Kubernetes.
A
PI Gateway
Let’s assume that you have designed and implemented your applications using the
Microservices architecture. You should have a suite of independently running services,
and each of them exposes some kind of API and knows how to communicate with other
services.
Typical users want to use a mobile application, a web application, or maybe a
combination of them to expose different parts of the business to people with different
roles. Back to the example with the online store, besides having the web and mobile
applications for consumers, we can have apps that the store employees use – one for
managing the products catalog, another for pricing, and one for managing the inventory.
Each user interaction happening in the UI of those clients will have to execute certain
operations that are performed by particular services. But if you are developing those
client applications – mobile, web, and so on – how do you know which particular
services to call, what particular endpoint to use, and in what order?
Client code will become very complex because of the following issues:
• Calling particular services means that clients know too much about
the way the services are decomposed internally. If some services
get refactored (either split to multiple or merged into one), those
changes should be also made on each client application as well.
16
Chapter 1 Introduction to Microservices
• SSL
• Rate limiting
• Caching
API Gateway is a communication pattern that introduces a service that sits between
clients and services to proxy traffic between them. It also serves as an abstraction layer
as clients don’t know which specific service was invoked as part of some operation.
Since it’s the central entry point that the client application uses to access the underlying
services, it can perform some additional functions like caching and logging, as shown in
Figure 1-3. It also provides a suitable way for clients to connect to underlying services as
they may not always natively support friendly protocols like HTTP and gRPC.
17
Chapter 1 Introduction to Microservices
If you find that each client needs a totally different set of data, it may make sense
to utilize a variation of API Gateway, called Backends for Frontends. Typically, mobile
applications display way less information and support a smaller set of functionality
compared to web applications. There’s an option to provide a client-specific API
Gateway as shown in Figure 1-4.
18
Chapter 1 Introduction to Microservices
hundreds or thousands of services. So you can take a step further and put several
services from a domain behind an API Gateway.
Saga
Saga is a pattern for managing data consistency across services. In the Microservices
world, data is split across multiple services because each service typically manages its
own data. Therefore, there is no way to execute transactions over a larger set of data. The
Saga pattern takes a different approach – distributed transaction happens as a sequence
of smaller local transactions each executing in sequential order and taking place on a
particular service. Once the service is done processing the transaction, it should report
that the operation has successfully finished so that the next services can take over and
execute their work. In case a local transaction fails, the previous services that have
successfully performed their part have to undo the work by executing compensating
transactions.
There are two approaches to implement the Saga pattern – orchestration and
choreography.
Orchestration
Saga can be implemented with a central controller called orchestrator as displayed in
Figure 1-5.
A service publishes a message in the Saga queue. This message contains all of the
details of the Saga that needs to be executed across different services. Once the Saga
orchestrator receives a message from the queue, it knows which services to call and in
19
Chapter 1 Introduction to Microservices
what order. Since it controls the order of the whole Saga, some of the operations can be
executed in parallel, and the risky operations can be left for the end of the Saga as their
retry would likely be a more expensive operation. Risky operations are those that should
not or cannot be reversed. For example, the Saga orchestrator from Figure 1-5 calls the
services in alphabetical order: Service A is called and executes successfully. Then the
orchestrator invokes Service B but it happens to fail, so it has to initiate the undo action
on Service A as it was previously executed successfully. Service C would have been called
only if Service B succeeded.
However, the Saga orchestrator happens to be a point of failure on its own. What
happens if the Saga orchestrator goes down while it is in the middle of a transaction?
This is the reason that Sagas are flowing through a message queue because it provides
fault tolerance. If the Saga orchestrator goes down, the message will return in the
queue. A new instance of the Saga orchestrator can get it and start the transaction from
the beginning. Therefore, operations happening on each service must be idempotent
because they can be retried multiple times.
A potential drawback of this approach is that the Saga orchestrator should be
designed, implemented, and maintained. It becomes a part of the application and
introduces coupling as it calls specific services in a specific order.
Choreography
Instead of depending on a central orchestrator, services can talk with each other via a
message broker as shown in Figure 1-6.
A service publishes a message to a particular queue designated for one event type.
Then this message gets processed by one or many services that are subscribed to this
type of message. Having performed the operation, each of the subscribing services is
20
Chapter 1 Introduction to Microservices
responsible to emit a message with the outcome in the same or a different queue. In the
case of success, new services are picking up the message and continuing the Saga; in the
case of failure, preceding services can undo the actions.
By using the choreography approach, services remain loosely coupled. However,
it may be very difficult to trace the execution of the entire Saga as it involves multiple
services. The role of the orchestrator is implicitly transferred to the services because
each of them is responsible for managing the resiliency of their operation as well as the
necessity to publish the result of their action (successful or not) so that other services
can execute their part accordingly.
Sidecar
There are some cases where certain applications cannot be modified. Imagine an
application that is provided by another company. Or maybe there is an application that
is built with some old technology that is not supported now so it’s not reasonable to try
extending it. Or in other cases, some common functionality that is used across many
services needs to be provided in a reusable way. The sidecar pattern helps to have this
functionality in a language- and framework-agnostic way by relying only on intraprocess
communication.
As shown in Figure 1-7, the application and the sidecar run side by side on the same
host where each of them runs in its own process or container.
The pattern is named after the sidecar motorcycle. In the same way, the sidecar is
attached to another application and provides some supporting features. Those could be,
for example, logging, monitoring, configuration management, health checking, and so on.
21
Chapter 1 Introduction to Microservices
Both the sidecar and the application share the same resources on the host, for example,
storage and network.
Let me walk you through a real example where the sidecar pattern can prove to be
useful. Consider that you have an old application that works over HTTP. At some point, a
requirement to switch it over to using HTTPS comes by. So one of the approaches would
be to reconfigure the application to work with HTTPS. But imagine that for some reason
you cannot change its code – for example, you cannot compile the source code of this
application. Furthermore, you cannot change the way it is hosted. A possible solution
that requires no changes on the application itself would be to deploy the NGINX proxy as
a sidecar and expose it externally. It will receive the incoming TLS connections and pass
the unencrypted requests to the application via HTTP.
Adopting Microservices
Whenever something new pops up on our technology radar, we as developers always
wonder whether to move on the bleeding edge and adopt the modern technology or
approach. The reality is that technology is developed to serve a purpose. In the case
of microservices, your applications will mostly benefit from improved scalability and
resiliency. There are also some organizational benefits. Big organizations are enabled
to utilize more people by splitting them into many smaller teams that do not overlap
so much as they own a single service or a group of services. When it comes to size,
according to the two-pizza rule, individual teams should not be bigger than what two
large pizzas can feed. There is no hard number, but in general, a team should consist of
fewer than ten people. The team size cannot increase indefinitely as communication
with more than ten people is very difficult and team members will most likely have many
conflicts in code.
Sounds good, but the reality is that not every organization will benefit from the
Microservices architecture as it creates some additional overhead right from the
beginning. Microservices should be rigorously designed from the beginning as this is a
foundational decision. If you want your project based on microservices to be successful,
you have to fully embrace the DevOps culture and practices. When people hear DevOps,
they are usually triggered by the tools for Continuous Integration and Continuous
Deployment. But holistically, DevOps is the convergence of people, processes, and
tools throughout the entire lifecycle of the software. The result of that is that there is
less complexity to be handled on case-by-case scenarios and almost everything is
22
Chapter 1 Introduction to Microservices
automated, which naturally leads to fewer failures and faster release cadence. And in
case of failure, the system can recover quicker. So the Microservices architecture can
turn out to be a lot of effort, in the beginning, for an organization aiming to adopt it.
Most applications that are now based on the Microservices architecture were once
a large monolith. And that’s the direction that most startup companies follow when
starting a new project. They usually don’t have the financial strength to support a bigger
team even though they may anticipate a big growth of users and load in the future. Most
startup companies start small – building their applications as monoliths because they
are simple to get started. As they evolve and grow, some capabilities of the monolith
can be isolated into discrete services until the whole application is refactored to
microservices and there is no trace of the monolith.
Summary
In this chapter, you learned how system evolution happens with small steps both from
a hardware and software perspective. I outlined what Monolithic applications are along
with their benefits and downsides. Then we transitioned to distributed applications
by explaining the Microservices architecture, which addresses a lot of the downsides
monoliths have. However, the Microservices architecture brings some challenges as well.
Being introduced to some of the important patterns and technologies, by now you have
an idea of how to overcome those challenges.
In the next chapter, I will introduce you to Distributed Application Runtime
(Dapr) and show you how you can benefit from it for the applications development of
distributed applications.
23
CHAPTER 2
Introduction to Dapr
In this chapter, you are going to learn about the value of Dapr and how it can help you to
build distributed applications. It adds value by providing building blocks that depend on
pluggable components. After initializing Dapr in Self-hosted mode, a simple distributed
Hello World application will be created once Dapr is initialized locally.
What Is Dapr?
Distributed Application Runtime (Dapr) is an open source project initiated by Microsoft.
It was announced at Microsoft Ignite 2019, Microsoft’s annual trade show for IT workers,
and since then it has been gaining a lot of interest and support from the community.
Dapr achieved stability and thus reached production readiness with its 1.0 release in
February 2021. As of March 2021, Dapr is in the process of being donated to the Cloud
Native Computing Foundation (CNCF) as an Incubation project. There are more and
more companies adopting Dapr.
According to the official website, Dapr is the following:
For our learning purposes, Dapr is a runtime that provides useful functionality that
spans programming languages, frameworks, diverse application types, and hosting
environments and supports a lot of use cases across different environments – cloud,
on-premises, and the edge. But first, let’s take a step back and think about how a
single service is usually designed and developed. First of all, you should choose what
programming model you want to base the development of the service on depending on
25
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_2
Chapter 2 Introduction to Dapr
the type of problem it solves. Should it be an API based on the request/response model
or an event-driven service for long-running tasks, or maybe should it be based on the
Actor model? You have to pick a technology that fits the programming needs. If you are
building a traditional Web API, you can choose from a variety of web frameworks – ASP.
NET Core, Flask, Express.js, and so on. If you want to implement something based on
the Actor model, some of the options are Akka and Service Fabric Reliable Actors, for
example. They all are going to help you implement the service you want; however,
each of them carries its own concepts, opinions, and programming model. Usually,
combining different concepts coming from several frameworks and libraries does not
end well. Application frameworks are built to guide you along the way and to protect you
from making common mistakes. And that’s why they are trying to enforce their rules so
strictly. For example, controllers, routes, and middleware in ASP.NET Core and Express
serve the same conceptual purpose; but they are implemented in a slightly different way
for both frameworks.
Following the same analogy, if you want to integrate with an external service to
persist some information, for example, Redis, you have to identify a library (in the
form of a NuGet package or NPM package, among others) that allows you to interface
with Redis in your programming language of choice. Then you must figure out how it
fits within the lifecycle of the framework you use for the application. Those common
concerns will be present across all services of the Microservices application that you
implement, and they have to be handled on a case-by-case basis for one service at
a time. Thinking about it in more general terms, it feels like the same-functioning
boilerplate code is piling up in different services most likely implemented in different
ways on each occasion.
26
Chapter 2 Introduction to Dapr
HTTP or gRPC, they can talk to Dapr as well! Although Dapr provides several Software
Development Kits (SDKs) for some of the popular languages, whether to use an SDK or
not at all is up to you. You will learn more about the SDKs later in the chapter.
O
ut-of-the-Box Patterns
Let’s navigate our way to the value proposition of Dapr. Dapr packages several useful
patterns that come out of the box that allow you to achieve a certain outcome. To name
a few functions, it can be calling another service, persisting some state, or utilizing
Publish and Subscribe. The value-added patterns are called building blocks. Depending
on how you combine the Dapr building blocks, you can achieve different things. The
building blocks can be used as a stable foundation because each of them provides a set
of capabilities that otherwise you have to implement from the ground up taking into
account the concepts of the framework that you use, or you can bring in an external
library and integrate it. If you are building a house, you are better off buying the bricks
instead of figuring out how to produce them yourself. Then you just have to know how to
lay the bricks (the Dapr building blocks).
For example, there is a Service Invocation building block that features service
discovery and communication with other services. You call Dapr via its API, and it
locates and invokes the target service for you and relays the result back. It’s pretty much
like a reverse proxy. You will learn more about this building block in Chapter 6: Service
Invocation.
There is another building block, State Management. You want your service to store
some state, but instead of dealing with the specifics of communication with a particular
service capable of persisting the state, you let Dapr do that for you. You just send some
state data to Dapr, and it knows how to communicate with the state store. Then when
you want to access the state, Dapr knows how to gather it from the underlying state store.
You will learn more about this building block in Chapter 8: State Management.
To elaborate more on the previous example, the fact that you outsource the work to
Dapr makes your application loosely coupled to the service you use for persisting the
state and gives you the freedom to switch technologies of the same type effortlessly. For
example, during development, you may use a Redis store deployed locally for persisting
the state. However, in production, you may decide to bind the State Management
building block to Cosmos DB or any other cache, database, or service in general
that is supported. And you can do this seamlessly without changing the code at all.
27
Chapter 2 Introduction to Dapr
Instead, you just need to modify the way Dapr state components are configured so that
Dapr is instructed to connect to another external service that will persist the state.
We will explore each of the building blocks in detail in Part 2 of the book, but very
briefly they are:
Dapr Components
Building blocks are the reusable pieces of functionality coming from the API of Dapr,
but internally they use one or multiple Dapr components that define how they are going
to work – for example, what connection information to use when connecting to an
external system like Redis. Components are the actual implementation of the building
block capability to talk to external services. They just implement a common interface
depending on the component type (state, pubsub, etc.), and that’s the reason they
are pluggable and easily substituted. You can change the underlying implementation
28
Chapter 2 Introduction to Dapr
of some of the building blocks and switch to using another service by changing the
underlying components that will service the request coming from the building block.
I know this can be somewhat confusing, and to simplify a bit, we can assume that the
building blocks are the generic functional pieces and their exact behavior is configured
by the respective components.
The components of a certain type share a similar configuration file that is often called
manifest. To be exact, a Dapr component is defined as a CustomResourceDefinition
(CRD), but that is probably too much information for now as we are going to explore this
in greater detail in Chapter 4: Running Dapr in Kubernetes Mode. For now, here is the
generalized format of a component manifest defined in YAML:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: <COMPONENT-NAME>
spec:
type: <COMPONENT-TYPE>
version: v1
initTimeout: <TIMEOUT-DURATION>
ignoreErrors: <BOOLEAN>
metadata:
- name: <METADATA-KEY>
value: <METADATA-VALUE>
- name: <METADATA-KEY>
value: <METADATA-VALUE>
In this case, the value of the metadata.name field is the custom-defined name of
the state component. Naming a component is entirely up to you, and later on you will
be able to reference this component by name. That’s the reason a building block can
work with multiple components. For example, when you want to store something in a
particular state store, you refer to it by specifying the name of the state component when
you call the State Management building block.
The scalar value in spec.type points to the particular component implementation.
The type names of the components are prefixed with the building block for which they
serve a purpose. For example, if you want a state component, for a type, you can specify
state.redis to use Redis as a state store. Or it can be state.azure.cosmosdb for Azure
29
Chapter 2 Introduction to Dapr
30
Chapter 2 Introduction to Dapr
service and sidecar is displayed isolated to keep things more logical, but these pairs
are not necessarily isolated on a network level when they are deployed to a host. Each
instance of your service knows that there is a sidecar running on a specific port should
it use any of the Dapr building blocks. And in the opposite direction, each Dapr sidecar
knows that it was brought up to serve a single service that is listening on a specific port.
For example, the sidecar may trigger some of your service’s endpoints whenever a new
message comes in or something happens in an external system. Or if you want to get
some data from a state store, you do so via the API hosted by this sidecar.
Figure 2-1. Applications with Dapr sidecars, building blocks, and components
Dapr sidecars contain the full set of all available building blocks and are described
in greater detail in Part 2 of the book. Keep in mind that a single component type can
be used by several building blocks. For example, the state components are used both in
the State Management building block and in the Actors building block. To illustrate, let’s
look at two examples.
Let’s say Service A wants to save some state. In order to do that, it invokes the State
Management building block and passes the state data and the name of the state store.
Then Dapr has to find a specific definition of a state component by looking for the
provided name. In Figure 2-1, there are only three state components shown – Redis,
Azure Cosmos DB, and etcd out of the many supported. As I already mentioned,
although those components are talking with different end services to persist state, they
all implement certain operations coming from the common interface defining all state
31
Chapter 2 Introduction to Dapr
components. And the State Management building block doesn’t care what the internal
implementation of a state component is as long as it conforms to the interface.
Alternatively, let’s say Service A wants to talk to Service B; it will use the Service Invocation
building block that’s provided by its sidecar. Then the Service Invocation building block
will query a service discovery component to find the address and port on which Service B’s
sidecar is running. Then Service A’s sidecar will instruct Service B’s sidecar to call a particular
method of Service B. The result will be proxied back to Service A via the system of sidecars.
Dapr does not only bring some reusable functionality packaged as building blocks,
but it also handles some cross-cutting concerns out of the box like observability – it
collects tracing, logs, and metrics information generated across services and the Dapr
runtime itself. Also, cross-sidecar communication can be secured out of the box by just
enabling mTLS (Mutual TLS). The standard TLS protocol only proves the identity of the
server to the client by providing the server certificate to the client. However, in zero-trust
network environments, Mutual TLS offers a way to verify the identities of the parties
staying on the two sides of an encrypted communication channel.
Several system services play a supporting role to Dapr:
32
Chapter 2 Introduction to Dapr
H
osting Modes
So far we have explored Dapr from a conceptual perspective – what it is and what
capabilities it has – and we touched on how it works behind the scenes. Now, let’s get a
bit closer to the more technical and practical side. Dapr can be initialized and therefore
hosted in two ways (hosting modes):
Kubernetes mode is the go-to mode for production usage that is battle-tested for
performance and scalability. Self-hosted mode is a good fit for development, but it also
gives you the most freedom to choose how to host your applications. For example, if
Kubernetes is not a good fit for you, you may want to try hosting Dapr in Self-hosted
mode with Service Fabric. But for certain things, for example, to make the Service
Invocation building block work, you have to deal with the underlying platform.
33
Chapter 2 Introduction to Dapr
D
ownload Dapr
The Dapr cross-platform CLI makes it easy to initialize Dapr and run, manage, and
monitor Dapr applications. Let’s see how to get it on your machine:
a. Windows: dapr_windows_amd64.zip
b. macOS: dapr_darwin_amd64.tar.gz
c. Linux: dapr_linux_amd64.tar.gz
a. On Windows, you can add the folder in which you placed Dapr into your
PATH environment variable. Or if you are like me, instead of polluting your
PATH variable with various directories, have just one directory to hold
all the binaries that need to be easily accessible and add it to the PATH
variable, for example, C:\binaries.
dapr --version
34
Chapter 2 Introduction to Dapr
As you can see, the CLI cannot resolve the Dapr runtime version because it’s not
initialized yet.
I nitialize Dapr
Next, let’s initialize Dapr locally, in the so-called Self-hosted mode, so that Dapr sidecar
binaries are downloaded, a few container instances (Redis, Zipkin, Dapr Placement
service) are started, and a default folder that holds all components is created.
Note As a prerequisite, you need to have Docker installed on your machine. You
can use Docker Desktop if you are on Windows or macOS, or you can install the
respective package for Docker Engine that is suitable for your Linux distribution.
Please note that if you use Docker Desktop on Windows, you should select its
Linux containers mode. More info here: https://docs.docker.com/engine/
install/.
In the terminal you opened previously, run dapr init to initialize Dapr. By default
it will download the most recent stable version of Dapr; however, you may want to
initialize a specific version, in which case it accepts a --runtime-version flag. Wait for
dapr init for a while to finish, and you will see the following output:
35
Chapter 2 Introduction to Dapr
Congratulations! If everything went well, Dapr should have been initialized in Self-
hosted mode. Let’s explore what this means.
If you navigate to %USERPROFILE%\.dapr\ on Windows or $HOME/.dapr/bin on
Linux or macOS, you will find a couple of files and folders that Dapr set up for itself. First,
in the root directory, there is a file named config.yaml. This file contains configurations,
and by default, the Dapr sidecars try to load it from this path. Many things can be
configured in Dapr – tracing, metrics, middleware, and others. We are going to explore
all options throughout the book. The daprd binary was downloaded and placed inside
the bin folder. Daprd is the binary of the Dapr runtime that you run as a sidecar process
when Dapr is in Self-hosted mode. In the components folder, you will find the initial
configuration of two components as YAML files – pubsub.yaml and statestore.yaml.
Those components were preconfigured to point to the dapr_redis container, which is
one of the Docker containers that were brought up as part of the dapr init command:
36
Chapter 2 Introduction to Dapr
If you run docker ps, you will see those three containers. I have simplified a bit the
output of the command by removing some of the columns:
IMAGE NAMES PORTS
daprio/dapr dapr_placement 0.0.0.0:6050->50005/tcp
openzipkin/zipkin dapr_zipkin 0.0.0.0:9411->9411/tcp
redis dapr_redis 0.0.0.0:6379->6379/tcp
Those Docker containers are very helpful when you run Dapr in Self-hosted mode
because you don’t have to think about running things yourself.
Now that Dapr is downloaded and initialized in Self-hosted mode, let’s start using it.
37
Chapter 2 Introduction to Dapr
• Hello service – This will return the word “hello” in strange random
casings, for example, “HElLo.”
• World service – This will return the word “world” translated into
various languages.
• Greeting service – This will call the other two services and return the
consolidated result.
1. First, you will need to create a new project folder named hello-
service and navigate into it.
2. Next, create a new file named app.js inside the project directory.
This will be a simple web application built with Express.js, a
popular web application framework for Node.js. The source code
38
Chapter 2 Introduction to Dapr
3. Next, open a terminal in the project folder and execute npm init
and follow the steps to create a package.json file. This file will hold
some information about your Node.js project; you just need to
follow the wizard.
4. In the same terminal, run npm install express, and it will
download the Express module along with all its dependencies.
Additionally, it will list Express as a dependency of your
application in the package.json file. After step 4, your package.json
file will look like this:
{
"name": "hello-service",
"version": "1.0.0",
"description": "",
"main": "app.js",
39
Chapter 2 Introduction to Dapr
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
}
}
6. Great, you’ve done it! Up until now, the Hello service was started
just like most Node.js applications are usually started. Make sure to
stop the execution of the previous command as it will keep using
port 8088. Now let’s make sure Dapr knows about our application
and assigns a sidecar for its needs. As I mentioned, every
application using Dapr deserves a dedicated Dapr sidecar. While
you are still in the project directory, run the following command:
7. This tells Dapr that the application identifier (App ID) is hello-
service; it listens on port 8088 and can be started by executing
the command node app.js. You will see the following output;
however, I simplified it a bit by removing the redundant
information to make it easier for reading:
Starting Dapr with id hello-service. HTTP Port: 56677. gRPC Port: 56678
== APP == Listening on port 8088
40
Chapter 2 Introduction to Dapr
Now let’s examine the logs. As you may recall, each application has a unique ID,
which in this case is hello-service. This ID will be later used for making requests
to the application via the system of Dapr sidecars. According to the logs, the Dapr
API that is served from the sidecar process will be accessible via HTTP on port 56677
and port 56678 for gRPC. Those port numbers are auto-generated if not explicitly set
as arguments of dapr run. You can identify the logs coming from the Hello service
41
Chapter 2 Introduction to Dapr
application as they are prefixed with == APP == and Dapr logs follow a common
format that includes the timestamp, the message, and what Dapr application
generated it, among other things. You can see that Dapr does several initializations
of its internal parts. The two components we explored earlier were discovered by the
Dapr runtime – pubsub and statestore. This means that you can use the respective
building blocks, namely, Publish and Subscribe and State Management building
blocks. Furthermore, you can notice that actor runtime was also initialized. And on
top of that, since the Dapr configuration file that I mentioned earlier was loaded
successfully as stated by the logs, observability comes out of the box.
At this point, you may have noticed that the Dapr sidecar is running along with
the Hello service. Let’s open Process Explorer from Windows Sysinternals and search
for dapr.exe. The process tree appears as shown in Figure 2-3. Notice dapr run in a
PowerShell terminal. In addition, executing the Node.js application, it also launched a
daprd process by passing the following arguments to it:
Figure 2-3. The Dapr CLI run spawns up a daprd sidecar and starts the
application
As you can see from Figure 2-3, some of the values of those arguments passed to
daprd come from the arguments passed to dapr run. Let’s examine the available flags of
dapr run in Table 2-1.
42
Chapter 2 Introduction to Dapr
43
Chapter 2 Introduction to Dapr
You can see the list of all available flags from the Dapr CLI itself, by executing dapr
run --help.
Note Keep in mind that some commands in the Dapr CLI are meant to be used
for Self-hosted mode and others for Kubernetes and some work with both modes.
That is clearly stated in the output of dapr --help.
44
Chapter 2 Introduction to Dapr
3. Open a terminal in the project folder, run npm init, and follow the
steps to create a package.json file.
5. And finally, in the terminal you opened, execute dapr run --app-
id world-service --app-port 8089 -- node app.js. By now
you should have two Dapr applications running in two terminal
sessions. Both applications will be accessible on the ports they are
listening on; however, that’s not the way the Greeting service will
be accessing them.
45
Chapter 2 Introduction to Dapr
2. Next, create a new file named app.js inside the project directory. It
listens on port 8090 and has a single HTTP GET endpoint named
greet that calls the other two services and concatenates the output
from them so that it returns a proper Hello world greeting, as
shown in Listing 2-3.
console.log(`Sending: ${greeting}`);
res.send(greeting);
});
46
Chapter 2 Introduction to Dapr
As you can see from the code, the Greeting service does not know the ports the other
two applications are listening on. It just references them implicitly by specifying their
Dapr App IDs when calling its dapr sidecar process. Then Dapr figures out how to reach
out to the respective Dapr apps.
When we called dapr run, we specified the HTTP port number, but this was not
needed. The ports of the dapr sidecar process (the daprd binary) are injected as the
environment variables DAPR_HTTP_PORT and DAPR_GRPC_PORT so that the application
always knows how to access its dedicated sidecar.
47
Chapter 2 Introduction to Dapr
and what command was used to start them. That is a handy way to check the basic
information for the currently running Dapr applications.
But if you prefer exploring it visually, you can use the Dapr Dashboard, a web
application that visually shares information about the Dapr applications that are
running on your machine, what Dapr components are available, and the applicable Dapr
configurations, as seen in Figure 2-4. After you run dapr dashboard, in the output, you
will find the specific port on which the Dapr Dashboard is running.
48
Chapter 2 Introduction to Dapr
However, some developers prefer relying upon language-specific SDKs that are
strongly typed as opposed to making requests out in the wild. Instead of making a request
to http://localhost:${daprPort}/v1.0/invoke/hello-service/method/sayHello,
you may want to simply call the InvokeMethodAsync method of the DaprClient class
passing the Dapr App ID and the name of the method. For that reason, Dapr provides
several SDKs for a number of languages – C#, Java, Go, JavaScript, Python, PHP, Rust,
and C++. Being written in Go, Dapr exposes its API via gRPC; therefore, its interface is
specified in Protocol Buffers (Protobuf) format. This means it’s fairly easy to generate a
gRPC client stub based on the Protobuf definition for any language that doesn’t have an
official SDK. Actually, some Dapr SDKs are just wrapping the auto-generated client stubs
and providing some language-specific improvements on top.
Summary
In this chapter, you learned the basics of Dapr and how you can benefit from it when
implementing distributed applications. You were guided through the Dapr hosting
modes and learned how to initialize Dapr in Self-hosted mode. You were then shown
how to implement a simple yet distributed Hello World application using Node.js and
Dapr and how to visually inspect your Dapr applications using the Dapr Dashboard.
Running Dapr in Kubernetes mode requires some level of knowledge of both
Kubernetes and Docker, so in the next chapter, we’ll explore what Kubernetes is and
learn how to build Docker images that can run in it.
49
CHAPTER 3
Getting Up to Speed
with Kubernetes
Dapr can be hosted in one of two modes – Self-hosted and Kubernetes. Self-hosted is
handy for local development and gives you the freedom to host Dapr anywhere you
want, but the reality is that for production workloads, you will host it in Kubernetes. So,
to work with Dapr, you need to feel comfortable with Kubernetes. Kubernetes is worth
a whole book. However, I will try to guide you through the basic concepts in just one
chapter by covering the following topics:
• Container registries
51
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_3
Chapter 3 Getting Up to Speed with Kubernetes
52
Chapter 3 Getting Up to Speed with Kubernetes
53
Chapter 3 Getting Up to Speed with Kubernetes
The K8s API server does not only act as the main access point from the outside but
is also used internally within the cluster. As I mentioned, every object has a declarative
definition that Kubernetes tries to fulfill. The API server backs up the cluster state and
configuration into etcd. etcd is a distributed, consistent, and reliable key-value store.
The controller manager is responsible for reconciling the observed state with the
desired state. It runs several control loops that monitor the state of the cluster via the API
server and takes corrective actions when needed. There are several controller types, each
of them having a separate responsibility. For example, the Node controller watches the
state of the worker nodes and reacts if any of them goes down. The Replication controller
makes sure that the specified number of replicas of your workloads is always fulfilled.
As a sidenote, some controllers need to communicate directly with external systems in
order to achieve a certain outcome on the cluster, albeit indirectly, for example, Cluster
Autoscaler, which adjusts the size of a Kubernetes cluster by communicating with the
APIs of a specific cloud provider.
The Scheduler watches the API server for newly created Pods that are not yet
assigned to a node. Scheduling is an optimization problem where based on some
constraints of the Pods, the Scheduler identifies the particular nodes eligible for taking
them. A Pod can specify some restrictions and requirements. For example, a Pod may
be defined in such a way to require scheduling a node with a GPU. Or you may explicitly
state that two Pods of the same type should never be colocated into the same node.
N
ode Components
Worker nodes are the machines that run your Pods. They can be running Linux or
Windows as opposed to the master nodes, which only support Linux. You can have both
node types in your cluster as it gives you the flexibility to schedule workloads based on
Windows containers to Windows nodes and the traditional Linux containers to Linux
nodes. As shown in Figure 3-1, a worker node has three components running on it –
kubelet, kube-proxy, and container runtime.
The kubelet is the agent that works closely with the control plane. Whenever a Pod
gets assigned to a node, the kubelet of the node receives a definition of the Pod most
often via the API server; then it ensures the containers are up and running. The kubelet
also continuously monitors and collects the performance and health information for
the node, pods, and containers and sends it to the control plane so that it can make
informed decisions.
54
Chapter 3 Getting Up to Speed with Kubernetes
The kube-proxy is responsible for proxying the traffic to the nodes. A Pod has an
IP address that is shared by the containers running in it. However, if you have set up
multiple replicas of the same Pod, some load balancing should be happening to reach
a specific Pod on a specific node. The kube-proxy handles the networking aspect of that
communication, and it makes sure that the incoming traffic will reach a Pod.
As you already know, a Pod is an object that contains your application bits packaged
as one or multiple containers. The container runtime is what actually runs and manages
your containers. Kubernetes supports several container runtimes – containerd, CRI-O,
Docker, and in general any runtime that implements the Kubernetes Container Runtime
Interface (CRI). The container runtime in Kubernetes is pluggable. When it comes to
containers, Docker comes out as the most popular choice; however, it was not devised
to be used inside Kubernetes as it doesn’t support the Container Runtime Interface.
For that reason, to make it possible to use Docker inside Kubernetes, an adapter called
Dockershim had to be implemented to adapt the CRI to Docker internals and vice
versa. This architecture adds an extra hop in the communication between the kubelet
and the containers and therefore latency, and last but not least, the Dockershim needs
maintenance. This resulted in Docker being deprecated as a container runtime after
version 1.20 of Kubernetes, and at some point, it will be removed as a container runtime
from new versions of Kubernetes. The most popular choice for a container runtime is
containerd, which is actually a part of Docker.
Wait, but that is confusing! Let me clarify that we should look at containers from two
different perspectives – local development of container-based applications and having
those containers running inside Kubernetes. Until recently, almost everything around
containers used to be Docker, and probably that is why Docker means different things to
different people. However, after Kubernetes has deprecated and will eventually remove
Docker as a container runtime, you have to just switch to containerd, in case you haven’t.
And the images you build with Docker will still work. The reason behind this is that
Kubernetes works with Open Container Initiative (OCI) images, which all Docker images
are. Both containerd and CRI-O are able to work with OCI-compliant images no matter
how you build them. Standards are a good thing!
Nowadays you can run Kubernetes anywhere – on your laptop, on-premises, in
the cloud, or even on several Raspberry Pis. Each of the public cloud platforms has
a Kubernetes offering. In Microsoft Azure, it’s called Azure Kubernetes Service (AKS).
Google Cloud Platform named it Google Kubernetes Engine (GKE). You should have a
better idea of how Kubernetes works by knowing what each cluster component does.
55
Chapter 3 Getting Up to Speed with Kubernetes
If you have Kubernetes clusters running production workloads on-premises, you have to
manage all those machines. However, if you choose one of the cloud offerings, you will
find out that most of the infrastructure is being abstracted by the platform. For example,
you don’t have to do anything to ensure the high availability of your control plane in
AKS. Those have been abstracted away from you and managed by Microsoft. You just
allocate a couple of worker nodes, and your cluster is ready to go. The master nodes are
invisible and you don’t pay for them. The same applies to GKE as well – master nodes are
managed by the platform. Taking this further, Virtual Kubelet is an implementation of
the Kubernetes kubelet agent that enables Pods to be running on other technologies. For
example, you can use serverless container platforms like Azure Container Instances to
run some of the workloads in your Kubernetes cluster. Virtual Kubernetes is the enabler
for the so-called Nodeless Kubernetes. It alleviates the problems created by cluster
management, capacity planning, auto-scaling, and so on. Therefore, you will care only
about the application-related things, like building the container images properly and
describing them as Kubernetes objects.
C
ontainer Images
In Chapter 2: Introduction to Dapr, you saw that when we initialized Dapr in Self-hosted
mode by using the Dapr CLI, it started several containers – Redis, Zipkin, and Dapr
Placement service. They were somehow running locally, but I didn’t walk you through
to understand what happened behind the scenes. In order to run a container, you will
need a container image, which is a binary package of the program and its dependencies.
Container images can be stored locally or pulled from a container registry. There are two
types of container registries – public and private container registries. There are plenty of
images publicly available on Docker Hub, the world’s largest public container registry.
If you want to run your application as a container, first, you have to build an image and
push it to a container registry, so you can share it with your team or with the public. A
fully managed private container registry offering exists in every major public cloud. In
Microsoft Azure, for example, it is called Azure Container Registry. Alternatively, you can
even host one on your own by using some of the open source container registries.
56
Chapter 3 Getting Up to Speed with Kubernetes
Prerequisite You will need Docker to be installed on your machine. More info
here: https://docs.docker.com/get-docker/.
1. First, pull the latest version of the Ghost image locally by running
docker pull ghost.
2. Then, you should be able to see that the Ghost image is present
locally by running docker images.
3. Once you have downloaded the image, you can run a container
by using the following command: docker run -d --name my-
ghost -e url=http://localhost:3001 -p 3001:2368 ghost.
The command sets the name of the container to be my-ghost, sets
an environment variable for the URL of the blog, specifies that
the TCP port 2368 in the container is mapped to port 3001 on the
Docker host that is your machine, and at last provides the image
to be used.
57
Chapter 3 Getting Up to Speed with Kubernetes
FROM node:14
WORKDIR /usr/src/hello
COPY . .
RUN npm install
CMD ["node", "app.js"]
Create also a .dockerignore file (notice the dot) to specify what files to be ignored
by the COPY command, as shown in Listing 3-2. In this case, it doesn’t make sense to
pollute the container image by copying the NPM packages into it. To save some space,
the container itself will download them because after all, they are publicly accessible.
1. FROM specifies the parent image to build from. In this case, it is the
node image from Docker Hub that has version 14, the LTS (Long
Term Support) version of Node.js along with a lot of tools needed
for the development of Node.js applications.
58
Chapter 3 Getting Up to Speed with Kubernetes
2. WORKDIR creates if not existing and sets the working directory for
all successive commands
3. The COPY command copies the source code of the application into
the image except what you have specified to be ignored in the
.dockerignore file. In this case, it is the node_modules directory.
Open a terminal in the hello-service directory and run docker build -t hello-
service . that builds the image for you by following the steps in the Dockerfile. Great,
you have now built an image with your application! You can start a container by running
docker run --rm --name hello -p 8088:8088 hello-service. You can verify it
works by opening http://localhost:8088/sayHello in your browser. The --rm switch
indicates that the container should be deleted once it exits.
There are some best practices to follow when writing your Dockerfile in order to
optimize the work that is being done every time you build an image. They typically result
in faster builds and better reuse of underlying layers. However, they are not in the scope
of this chapter. You can find some of them described here: https://docs.docker.com/
develop/develop-images/dockerfile_best-practices/.
59
Chapter 3 Getting Up to Speed with Kubernetes
installs the NPM packages is just 2.32 MB and the layer that copies the application files is
only 15 KB. This explains the delta of ~2 MB between the node:14 image and the
hello-service image you just built.
Let’s switch the parent image to be node:14-alpine and run the docker build -t
hello-service . once again. This image is based on Alpine Linux, which is only 5 MB in
size. The result is that our hello-service image is about 118 MB in size, of which 5 MB is
for Alpine and about 103 MB for the node itself. Much better!
Finally, if you want to stop the hello container, execute docker kill hello. It will
not only stop the container but also delete it because of the --rm flag we applied upon
executing docker run.
There is also some room for further improvements in size. Multistage Docker
builds allow you to use intermediary steps during the image building process. With
multiple FROM statements in the same Dockerfile, you can use different parent images
during the build stages, and you can selectively copy files into further stages. By doing
this, the unneeded stuff is excluded from the final image. So you have one bigger
image containing all the tools needed to build an application, and then you copy only
the compiled version to another image that contains the minimum set of runtime
dependencies. Of course, that works best for compiled programming languages.
60
Chapter 3 Getting Up to Speed with Kubernetes
61
Chapter 3 Getting Up to Speed with Kubernetes
There is also a Kubernetes offering in each of the major public cloud platforms:
Whatever you choose to use, whether locally or in the cloud, you will need to have
the Kubernetes command-line tool (kubectl) installed on your machine. You will use it
to run commands against your Kubernetes cluster to deploy applications and manage
cluster objects.
If you use Docker Desktop, it will install kubectl for you and set its current context
to the Kubernetes cluster in Docker Desktop. Of course, Enable Kubernetes must be
switched on in the settings of Docker Desktop. For any other Kubernetes clusters,
you will most likely have to install kubectl if you don’t have it on your machine and
authenticate against the cluster. More info here: https://kubernetes.io/docs/tasks/
tools/install-kubectl/.
To validate that kubectl can access your Kubernetes cluster, run kubectl cluster-
info; and if it is correctly configured, the output will be similar to the following:
K
ubernetes Objects
By now you have some understanding of what the architecture of a Kubernetes cluster is
along with all the different components running in it. Also, you learned how to build and
publish container images. It’s time to start using Kubernetes to run some containerized
workloads on it. Let’s go through some of the most common types of objects that you will
likely use for every application:
• Pods
• Services
• Deployments
62
Chapter 3 Getting Up to Speed with Kubernetes
P
ods
I already mentioned that containers run in Pods. Pods are the smallest deployable unit
in a Kubernetes cluster scheduled on a single worker node. A Pod contains one or more
application containers that share the same execution environment, volumes, IP address,
and port space. Kubernetes objects are declared in manifest files using JSON or YAML
format. YAML is the most commonly used one.
Let’s create a new file and name it pod.yaml as it will contain the manifest in YAML,
as shown in Listing 3-3 for a Pod that has a container running the hello-service image
that you have already published to Docker Hub.
apiVersion: v1
kind: Pod
metadata:
name: hello-pod
labels:
app: hello
spec:
containers:
- image: <your-docker-hub-username>/hello-service:latest
name: hello-container
ports:
- containerPort: 8088
Note that we can add several containers to a Pod manifest; however, it is not needed
for the services we have implemented.
To deploy this manifest, you need to execute the following command, kubectl
apply -f pod.yaml, assuming the file pod.yaml is present in the current directory. This
will submit the Pod manifest to the API server, the Pod will be scheduled to a healthy
node, and the container will be started.
Then you will be able to see it by running kubectl get pods:
NAME READY STATUS RESTARTS AGE
hello-pod 1/1 Running 0 1m05s
63
Chapter 3 Getting Up to Speed with Kubernetes
Initially, the status that you see may be Pending, but with time it will transition to
Running, which means that the Pod is successfully created and the containers in it are
running.
This Pod is not currently exposed to the outside world. So if you want to access it, you
will have to set up port forwarding to it by using kubectl:
kubectl port-forward hello-pod 8088:8088
As long as the previous command is running, the Pod will be accessible on
localhost:8088; however, to reach the single endpoint of the Hello service, you need to go
to http://localhost:8088/sayHello.
You can also examine the logs of the Pod by running kubectl logs hello-pod.
There are many more things that you can do with Pods – setting up probes, managing
container resources, mounting volumes, and so on. However, Pods are ephemeral and
their state is usually not persisted. A Pod can be terminated at any time, or it can be
deleted and recreated on another node.
S
ervices
As I already explained, Pods are mortal – they come and go. You may want to replicate
your workload by creating several identical Pods. In order to make them accessible
from the outside, you will need to put a load balancer in front of this group of Pods. The
Service object distributes the traffic across a group of Pods based on a label that you
tagged your Pods with. Labels are key-value pairs that can be applied to Kubernetes
objects. If you go back to the Pod manifest, you can see that it has the app label applied.
If we want to create a service for our hello-service application, it will have the
definition shown in Listing 3-4.
apiVersion: v1
kind: Service
metadata:
name: hello-service
spec:
selector:
app: hello
ports:
64
Chapter 3 Getting Up to Speed with Kubernetes
- protocol: TCP
port: 80
targetPort: 8088
To create this service that represents all Pods labeled with app: hello, you have to run
Services are of several types depending on where you want to expose them. The default
one is ClusterIP, which makes your service reachable only within the cluster. To access a
service from outside of the cluster, you have to use either the NodePort or LoadBalancer
service type. You will typically use the LoadBalancer type whenever you want to expose a
publicly accessible Service from a Kubernetes cluster hosted in the cloud.
D
eployments
Although it’s entirely possible to define and create individual Pods, you most likely won’t
be doing that as managing them one by one is a tedious task.
A Deployment defines the application Pod and its containers. It also specifies how
many replicas of the Pod are needed. By creating a Deployment, you specify the desired
state of your application. Once you create it, Kubernetes makes sure that the actual state
matches the desired one. Depending on the way you configure the Deployment, it also
helps when transitioning your applications from one version to another by performing
rolling updates. Rolling update is a strategy where Kubernetes gradually creates one or more
Pods at a time with the new version and terminates one or more of the old Pods until all of
them are running the latest version. This results in no downtime for your application.
Let’s create a Deployment manifest for our Hello service, as shown in Listing 3-5.
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-deployment
labels:
65
Chapter 3 Getting Up to Speed with Kubernetes
app: hello
spec:
replicas: 3
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello-container
image: <your-docker-hub-username>/hello-service:latest
ports:
- containerPort: 8088
When you run kubectl get pods, you will find those three Pods that were created
by the Deployment. If we want to scale it out to five Pods, for example, we have to
update the spec.replicas field to 5 and reapply it with the same command. As you
can see, even the Deployment object works with labels and label selectors – the replicas’
definition searches for Pods having the app label with a particular value. The service
that we created previously will automatically discover all the new Pods created by this
Deployment and will send traffic to them as well. The reason for this is the new Pods will
also have the app: hello label that the service is looking for.
66
Chapter 3 Getting Up to Speed with Kubernetes
The first command adds the repository with all charts published by Bitnami where
the bitnami/mysql chart is provided. The second one simply installs it.
Summary
In this chapter, you learned the basics of Kubernetes – what components a Kubernetes
cluster consists of and what Kubernetes objects to use when deploying your applications
to Kubernetes. Since Kubernetes orchestrates containers, we explored how to build
Docker images, run them as containers, and push them to a container registry.
Now that you acquired some knowledge about Kubernetes, the next chapter will
focus on getting Dapr to work in Kubernetes mode.
67
CHAPTER 4
Running Dapr
in Kubernetes Mode
In Chapter 2: Introduction to Dapr, you learned how Dapr runs along with your
application code and provides added functionality that is accessible via HTTP and
gRPC. Then you got introduced to Kubernetes and its basic concepts.
The Self-hosted mode gives you the freedom and flexibility to run Dapr wherever
you want. It is just a process running on a machine. Starting, operating, and scaling all
pieces of your Microservices application are some of your responsibilities. In general,
using Dapr in Self-hosted mode is recommended for development and testing.
But in production, you will most likely utilize Kubernetes to ease out some of those
responsibilities by using its declarative manner. This chapter will be about using Dapr as
a first-level citizen of Kubernetes:
69
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_4
Chapter 4 Running Dapr in Kubernetes Mode
As a prerequisite, you need to have the dapr binary (Dapr CLI) at hand and have
established a kubectl context to a running Kubernetes cluster. Let’s get to work and
install Dapr in a Kubernetes cluster. You have to simply execute the following CLI
command:
You already know that a Kubernetes cluster has master nodes that host the control
plane components, that is, the brain of your cluster. Likewise, when Dapr is installed
inside Kubernetes, it also has a control plane that holds the Dapr system services. The
70
Chapter 4 Running Dapr in Kubernetes Mode
output of the dapr status command shows all components the Dapr control plane
consists of:
NAME NAMESPACE HEALTHY STATUS REPLICAS
dapr-sentry dapr-system True Running 1
dapr-dashboard dapr-system True Running 1
dapr-placement-server dapr-system True Running 1
dapr-operator dapr-system True Running 1
dapr-sidecar-injector dapr-system True Running 1
The Dapr Placement and Dapr Sentry services should sound familiar as they are also
applicable for Dapr when it is hosted in Self-hosted mode. As you may remember, the
Dapr Placement service is automatically started as a Docker container for convenience
when you use Dapr in Self-hosted mode. Actors can be distributed across nodes in your
Kubernetes cluster, and the Dapr Placement service keeps track of the distribution of the
actors. Whenever you use the Actors building block, this service will play a major role.
You will learn more about actors in Chapter 10: The Actor Model, which is dedicated to
the Actor model.
The Dapr Sentry service is the certificate authority for all Dapr applications. Dapr
ensures that the communication inside the system of Dapr sidecars and also between
Dapr sidecars and Dapr system services (Dapr Placement service is one such example)
is secured by using Mutual TLS (mTLS) authentication. To establish secure two-way
communication, at the time when a Dapr sidecar initializes, it generates an ECDSA
private key and a Certificate Signing Request (CSR), which is then sent to Dapr Sentry.
Dapr Sentry receives the CSR and turns it into a certificate by signing the CSR using the
private key from the trust bundle that is available only to the Dapr Sentry service. The
trust bundle contains two certificates – the root certificate and issuer certificate, the
latter being used for signing workload certificates. The root certificate stays on the top
of the certificate chain and serves as the trust anchor from which the chain of trust is
derived. This also means that the issuer certificate is practically signed with the private
key of the root certificate, which makes it an intermediate certificate. That is how trust is
established between two parties without revealing their private keys.
Dapr Sentry automatically generates self-signed root/issuer certificates if they
are not provided at the time of installation. Dapr also can handle the rotation of root
certificates and workload certificates without any downtime. The default period of
71
Chapter 4 Running Dapr in Kubernetes Mode
72
Chapter 4 Running Dapr in Kubernetes Mode
Kubernetes mode. Or in other words, they can be applied to any Dapr installation,
regardless of its hosting mode.
As you already know, components determine how the building blocks function – they
specify what end service to be used and how to connect to it. By modifying the definition
of a component, you effectively change the internal strategy of a building block. In
the same way, the Configuration object allows you to control the way Dapr works. For
example, by modifying the Configuration manifest, you can configure mTLS settings and
how long a workload certificate should be valid or change the way distributed tracing
works. When you use custom resources in Kubernetes, you will most likely have an
operator that is going to be the controller of those custom resources. In the same way,
you have human operators and site reliability engineers who are looking after some
applications and services. They have the knowledge of how a complex application works,
how to deploy it and manage it, and what specific actions to do in case of any problems.
By implementing an operator, you can incorporate the domain-specific knowledge into
an automation inside Kubernetes. For example, the operator (or controller) will run as
yet another Deployment in your cluster and can make application-specific decisions and
actions. It can allocate more replicas of some service if certain thresholds are reached or
conditions are met. Another example would be knowing when and how to do backups
or knowing what specific steps to take whenever the configuration of the application
changes. In case any Dapr Component object has been changed, the Dapr Operator will
notify all sidecars, and they will try to hot-reload the component.
Note At the time of writing, the hot-reload feature is halfway implemented. Being
a hot topic, it will most likely be implemented soon. Before that is implemented,
you have to restart (or recreate) your Pods to make them pick up the updated
versions of components and configurations by using kubectl rollout
restart deployments/<DEPLOYMENT-NAME>, for example.
Let’s have a look at the Kubernetes objects for the Dapr system services. By default,
the control plane resides in the namespace dapr-system. Of course, if you don’t want
to use the dapr-system, you can specify the name of the namespace to be used at the
time of running the dapr init command. Namespaces in Kubernetes are intended to
provide logical separation for groups of objects inside a cluster. If you have a variety
of resources belonging to different projects, teams, or environments, you will most
73
Chapter 4 Running Dapr in Kubernetes Mode
likely benefit from using namespaces. The names of the resources within a namespace
should be unique, but it is entirely possible to have a resource with the same name in a
different namespace. If you don’t specify a namespace when using kubectl, the default
namespace will be implied for all commands. Execute the following command, and you
will find out what resources were created for the Dapr control plane:
You will find out that a Deployment with one replica and a Service have been
created for each Dapr system service – Operator, Placement, Sentry, and Sidecar
Injector. Although the Dapr Dashboard is not really a control plane service, because of
convenience, it is present as well. To open the dashboard, run dapr dashboard -k, and
it will set up port forwarding for you and open the address in a browser. Don’t forget
the -k/--kubernetes flag; otherwise, it will open the Dapr Dashboard from your Dapr
installation in Self-hosted mode. If you have installed Dapr in a nondefault namespace,
you have to specify it via the -n/--namespace flag.
It’s very slick to install Dapr using the CLI, but there are some other options as well.
74
Chapter 4 Running Dapr in Kubernetes Mode
3. Now that you have all prerequisites, let’s deploy a highly available
Dapr control plane using the following command. Helm Charts
allow you to specify values at the time of installation by using
the --set flag and passing a custom value from the command
line. That’s how parameterization happens in Helm. It’s a good
idea to specify the version of Dapr you want to install (but make
sure to install the latest stable version as at the time of writing the
book it is 1.0.1) and also set the --create-namespace switch to
create the dapr-system namespace, in case it doesn’t exist:
The output of the last command will be something like this, the AGE column having
been omitted for readability:
NAME READY STATUS RESTARTS
dapr-dashboard-fccd6c8cc-cjm5f 1/1 Running 0
dapr-operator-886df997-79z4z 1/1 Running 0
dapr-operator-886df997-7f6nq 1/1 Running 0
dapr-operator-886df997-jflkg 1/1 Running 0
75
Chapter 4 Running Dapr in Kubernetes Mode
dapr-placement-server-0 1/1 Running 0
dapr-placement-server-1 1/1 Running 0
dapr-placement-server-2 1/1 Running 0
dapr-sentry-7767c445f8-44gbt 1/1 Running 0
dapr-sentry-7767c445f8-9bbzn 1/1 Running 0
dapr-sentry-7767c445f8-sd8ql 1/1 Running 0
dapr-sidecar-injector-5ccd6b5466-dk2dp 1/1 Running 0
dapr-sidecar-injector-5ccd6b5466-gql46 1/1 Running 0
dapr-sidecar-injector-5ccd6b5466-szcpm 1/1 Running 0
Notice that every Dapr system service consists of three Pods. The three Pods of
each service provide redundancy, and requests are load balanced between them. One
exception is the Placement service that tracks where actors are instantiated across the
cluster. It implements the Raft consensus algorithm, and therefore one of the Pods is
elected as a leader, while the others are followers. If the leader goes down, another Pod
will be appointed as a leader, and in the meantime, the failed Pod will come back as a
follower.
There is just one replica of the Dapr Dashboard as it is not mission-critical. You can
increase the number of replicas by specifying the dapr_dashboard.replicaCount chart
value. Please have in mind that the dapr init -k command from the Dapr CLI also
supports all chart options by specifying the --set flag, which expects a comma-separated
string array of values, for example, key1=val1,key2=val2.
Some time will go by after you have installed Dapr, and there will be newer versions
coming out. You need to know how to upgrade your Dapr control plane to a newer
version.
Zero-Downtime Upgrades
Upgrading Dapr without any downtime is almost as easy as its installation is. However,
a Helm release upgrade may ditch some objects depending on how the chart has been
implemented and what values were provided. CustomResourceDefinitions are one of
the Kubernetes objects that are tricky to be updated by Helm. This also applies to the
certificate bundle of Dapr that is needed for mTLS communication. Whenever you are
upgrading Dapr, you would want to reuse the old certificates so that it won’t cause an
interruption. But this is all handled by both Dapr CLI and the Helm Chart itself.
76
Chapter 4 Running Dapr in Kubernetes Mode
Now let’s upgrade the Dapr Helm release to a newer version. In Helm’s terms, a
release is an installation of a chart. But first, don’t forget to get the new version of the
Dapr Chart by running
Then simply run helm upgrade and specify the new version you want to upgrade the
Dapr release to:
At this point, the Dapr control plane will be running the newer version of Dapr. But if
you have any sidecars running at the time you do the upgrade, they will still use the old
version of the Dapr sidecar. You have to trigger a recreation of those Pods to pick up the new
version of the Dapr sidecar container. You can do it by performing a rolling restart for the
Deployment object: kubectl rollout restart deployments/<DEPLOYMENT-NAME>. This
command will create a new set of Pods defined by this deployment. The existing Pods will
be functional until the new Pods are up and running. After that, the old Pods will be deleted.
Uninstalling Dapr
Finally, let’s also cover the uninstallation of Dapr. I don’t recommend you to do this as
you are going to need Dapr to be installed to be able to run the distributed Hello World
application later in the chapter.
Dapr can be uninstalled by both the Dapr CLI and Helm. If you used Dapr CLI to
install it, you can uninstall it by running dapr uninstall -k. In case you installed the
Helm Chart, uninstall it by running helm uninstall dapr --namespace dapr-system.
Note It’s not recommended to try uninstalling Dapr manually. Always use the
Dapr CLI or Helm. By trying to do it manually, you will most likely forget to delete
some of the Dapr-related things, which will put your cluster in a very bad state,
which may cause issues when you later try to install Dapr.
Now that you know how Dapr runs in Kubernetes mode, let me show you how
applications are actually using it.
77
Chapter 4 Running Dapr in Kubernetes Mode
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-deployment
labels:
app: hello
spec:
replicas: 1
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "hello-service"
dapr.io/app-port: "8088"
spec:
containers:
- name: hello-container
78
Chapter 4 Running Dapr in Kubernetes Mode
image: <YOUR-DOCKER-HUB-USERNAME>/hello-service:latest
ports:
- containerPort: 8088
Since we deploy everything in Kubernetes, it is perfectly fine for all services to listen
on the same port as you won’t have any conflicts as opposed to being in Self-hosted
mode. But I decided to reuse the same code from Chapter 2: Introduction to Dapr and
the containers built and published to Docker Hub in Chapter 3: Getting Up to Speed with
Kubernetes, which means that all three services run on different ports although each one
of them is hosted in a separate container.
Have you noticed those annotations starting with dapr.io? The Admission controller
will catch every creation of a Pod and will pass the control to the Dapr Injector service,
which will inspect the Pod for any dapr.io annotations. The most important annotations
are dapr.io/enabled, dapr.io/app-id, dapr.io/app-port, dapr.io/protocol, and
dapr.io/config. Those may look familiar to the flags available when executing dapr run.
If dapr.io/enabled is true, the Sidecar Injector service will inject another container
for the Dapr runtime as a sidecar container inside that Pod. The dapr.io/app-id,
dapr.io/app-protocol, and dapr.io/port specify, respectively, the Dapr Application ID,
the protocol used by the application, and the port to be used when invoking the service.
Among those are annotations for configuring Dapr sidecar settings such as the log level,
memory and CPU limits, and liveness and readiness probes. The dapr.io/app-port
specifies the port that the application listens on. If you don’t expect any other services
calling into this application or if it doesn’t listen on a port, you simply don’t specify
this annotation. The dapr.io/config specifies which Configuration CRD to use for
the Dapr sidecar of this service. You can find the full list of all available annotations in
Dapr docs at https://docs.dapr.io/operations/hosting/kubernetes/kubernetes-
annotations/.
Please follow the same logic and specify the manifests for the World and Greeting
services and save them as world.yaml and greeting.yaml, respectively.
In the case of the distributed Hello World example, the Greeting service will be the
only service that is client-facing. This means that you don’t have to define dapr.io/app-
port because it will be directly accessed via a Kubernetes service from the outside. Add
the following definition of the Service object at the bottom of greeting.yaml, but make
79
Chapter 4 Running Dapr in Kubernetes Mode
sure to include the --- separator that allows you to have multiple resources in a single
manifest file:
---
kind: Service
apiVersion: v1
metadata:
name: greeting-service
labels:
app: greeting
spec:
selector:
app: greeting
ports:
- protocol: TCP
port: 80
targetPort: 8090
type: NodePort
It targets the Pods that are tagged with the app selector with value greeting, exposes
port 80, and targets port 8090 in the matching Pod(s). If your Kubernetes cluster
runs locally, you would be better off using the NodePort service type as opposed to
LoadBalancer, which requires a unique public IP per Service. The LoadBalancer type
comes in handy whenever you have a Kubernetes cluster in the cloud such as Azure
Kubernetes Service.
In order to apply the manifests of the services that the distributed Hello World
application consists of, navigate to the directory where they are located and execute the
kubectl apply -f . command. This will apply the manifests in the default Kubernetes
namespace. Once that has been done, run kubectl get service greeting-service to
get the NodePort on which the greeting-service listens on. In my case, it listens on port
31196, so opening http://localhost:31196/greet results in seeing the outputs from
both Hello service and World service that were combined by the Greeting service.
This is the experience of having your Dapr application running inside Kubernetes.
You don’t have to think about what is available on your machine and whether any
service dependencies will conflict with any others. Now let’s inspect the logs of the Dapr
sidecar running along with the Greeting service. We can find the name of the pod by
80
Chapter 4 Running Dapr in Kubernetes Mode
executing kubectl get pods -l 'app=greeting'. Now that you know the name of the
pod, have a look at its logs by running kubectl logs <greeting-pod-name> daprd.
You will notice that many things were initialized like mTLS, various middleware, name
resolution, actors, workload certificates generated, and others.
Have in mind that at this point, you cannot use the Actors building block. The reason is
that there isn’t any state component for persisting the state of actors defined. You saw how
Dapr in Self-hosted mode created a Redis container and had the default state component
configured to point to it just for convenience. That’s not the case with Kubernetes as it is
meant to be used as a production environment. You have to deploy Redis in Kubernetes
yourself, or you can use an external service to persist the state of all actors.
• If you are installing/upgrading Dapr using its Helm Chart, you can
disable mTLS by specifying --set global.mtls.enabled=false.
• If you are installing Dapr via the Dapr CLI, provide an additional
flag for mTLS configuration dapr init --kubernetes --enable-
mtls=false.
81
Chapter 4 Running Dapr in Kubernetes Mode
At the time of writing, Dapr is known to work side by side with service meshes like
Istio and Linkerd. However, we won’t be exploring how to use service meshes together
with Dapr in this book.
Summary
In this chapter, you learned how Dapr functions when hosted in Kubernetes mode.
The Dapr system services are building the so-called Dapr control plane, or the
brain of Dapr. Two of the system services applicable for Dapr in Self-hosted mode are
still present in Kubernetes – Dapr Sentry and Dapr Placement services. There are two
additional services needed because of the way Kubernetes works – the Dapr Sidecar
Injector and the Dapr Operator. The Sidecar Injector service makes sure there is a Dapr
sidecar container for each Dapr application. Dapr Operator deals with the updates of
any Component and Configuration CRDs, which control the way the Dapr sidecar and
Dapr building blocks work.
You also learned about the ways to install Dapr in Kubernetes and later on perform
zero-downtime upgrades. Once you had the Dapr control plane up and running in the
Kubernetes cluster, it was about time to show you how to deploy the distributed Hello
82
Chapter 4 Running Dapr in Kubernetes Mode
World application that was implemented in Chapter 2: Introduction to Dapr without any
code changes. In order to distinguish it as a Dapr application, the Dapr annotation had
to be applied to the Pods of the three services. Once that was done, it became clear that
once you have packaged your applications in container images, running them inside
Kubernetes is a lot easier compared with Self-hosted mode because you don’t have to
think about port conflicts, installing local dependencies, and starting all application
parts one at a time.
Later on, you learned how Dapr can work side by side with service meshes and
how to deal with the isolation of components and configuration when you have a lot of
applications and therefore have to meet various needs.
Now that you know how to develop and deploy Dapr applications, in the next
chapter, you will gain some experience debugging them.
83
CHAPTER 5
Debugging Dapr
Applications
Knowing how to configure your local development environment to be able to develop
and debug applications seamlessly using Dapr is very important for your productivity.
In this chapter, I am going to show you several different ways to run and debug Dapr
applications locally, in a Docker development container, or in Kubernetes.
D
apr CLI
Dapr CLI is the tool that I keep showing you throughout the book. It’s rather primitive
when you want to start multiple applications as you need to keep several terminals open.
Furthermore, you have to keep track of local ports and Dapr Application IDs.
However, this gives you the most control of the setup because you can easily change
pretty much everything – you can change Dapr ports, you can choose to use the default
components location, or you can specify another path that contains the components.
The command with which you start your application along with a Dapr sidecar is dapr
run. For example, the Hello service that is one of the services from the distributed Hello
World application that we implemented in Chapter 2: Introduction to Dapr is started
with the following command, given that you have navigated to the project directory:
However, you can use the Dapr CLI just to start a Dapr sidecar for you because, for
example, you want to start the application with Visual Studio 2019 and debug it there. In
order to do this, you have to omit the command that you otherwise specify in the end to
start your application:
85
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_5
Chapter 5 Debugging Dapr Applications
After you run the command, you have to start the application, for example, from
Visual Studio in Debug mode. Obviously, this method gives you the most control, but it’s
not the easiest one if you are going to do it repeatedly.
86
Chapter 5 Debugging Dapr Applications
Additionally, the scaffolding creates a folder that contains any Dapr components that
are used by the application. When you start debugging, it will make sure to process all
components from that folder.
You can find the already configured for debugging Hello service in the source code of
the book under the following path: Chapter 5/Dapr-extension/hello-service.
D
evelopment Container
Another option is to develop entirely inside a Docker container. This means that you don’t
need to have any of the development tools and dependencies on your local machine.
Instead, everything is going to happen in a development container that Visual Studio Code
is going to interact with. There are a couple of pre-built containers for Dapr development.
Dapr will be installed in the container and added to the path. It will also make sure to start
other dependencies like a container running Redis, Zipkin, and the Dapr Placement service.
In order to do this yourself, you need to install the Remote – Containers extension
in your Visual Studio Code. Then, open the command palette (Cmd/Ctrl+Shift+P) and
select Remote-Containers: Add Development Container Configuration Files. Then select
From a predefined container configuration definition, search for Dapr, and click Show All
Definitions. The Dapr predefined configurations will show up as depicted in Figure 5-2.
Choose the one that fits the language you used for implementing your application. At the
time of writing, the two definitions are for C# and Node.js.
87
Chapter 5 Debugging Dapr Applications
Then Visual Studio Code will prompt you to open the application workspace in a
container. The first time you open it in a container, it will be rather slow as it has to install
a lot of stuff as you can tell from the logs:
88
Chapter 5 Debugging Dapr Applications
Then pressing F5 will launch your application in Debug mode and set up port
forwarding to the development container so that you can access your application on
localhost.
You can find the already configured for running in a development container
Hello service in the source code of the book under the following path:
Chapter 5/Remote-containers/hello-service. When you open the project directory in
Visual Studio Code, it will detect the configuration and prompt you by suggesting to
reopen the folder in a container.
B
ridge to Kubernetes
Most of the time, you will have a Microservices application that consists of a number of
services. Some of them will have a dependency on a database or some other external
service. You can use multitarget debugging in Visual Studio Code for starting multiple
applications. However, having the application processes up and running is just a small
part of what you will likely need in a complex architecture.
Bridge to Kubernetes is a tool that enables local development against a Kubernetes
cluster. In simple terms, it sets up port forwarding to your local machine so that
each Kubernetes service appears as if it was hosted locally. This makes it possible to
substitute a certain service with a locally running application as a counterpart that you
can debug. A single Kubernetes cluster that has the entire application stack deployed
can be shared and utilized by multiple developers. To start using this, you will first
need to install the Bridge to Kubernetes extension for Visual Studio Code or Visual
Studio 2019.
However, a known limitation of Bridge to Kubernetes is that it doesn’t work well
with Pods that have multiple containers. Dapr utilizes the sidecar pattern for making
the Dapr runtime accessible to each service, as it injects a daprd container in each
Dapr-annotated Pod. This is somewhat problematic for Bridge to Kubernetes as you
cannot fully utilize it for any services that are invoked via the system of Dapr sidecars
since it cannot intercept the traffic between sidecars and applications. When you set up
Bridge to Kubernetes for debugging a particular service, you have to trigger it yourself by
sending an appropriate request to its ports on localhost.
89
Chapter 5 Debugging Dapr Applications
Like the other Visual Studio Code extensions, this one also provides a command
that sets the configuration up. It launches a wizard that will prompt you to select the
Kubernetes service that you want to redirect to your machine, the local port where this
service will be running, and the launch configuration.
As an example, I will show you how to debug the Greeting service of the distributed
Hello World application. Note that I have deployed the entire application to Azure
Kubernetes Service (AKS) because the idea is that several developers will share it. And
I have pointed my kubectl context to it so that any kubectl command will point to
AKS. There is one workaround before you will be able to debug a service locally. The
Greeting service depends on its Dapr sidecar to call the Hello and the World services.
That’s why you need to expose the daprd container of some service that you won’t be
debugging. In my case, this will be the Dapr sidecar of the Hello service. Create a new file
named dapr-sidecar-service.yaml that contains the manifest of the Kubernetes service
that exposes a Dapr sidecar:
apiVersion: v1
kind: Service
metadata:
name: dapr-sidecar
spec:
type: LoadBalancer
selector:
app: hello
ports:
- name: dapr-http
port: 80
protocol: TCP
targetPort: 3500
90
Chapter 5 Debugging Dapr Applications
of the dapr-sidecar service that will be locally accessible on 127.1.1.1. This means that
when you hit this IP address on port 80 via HTTP, it will actually go and call the remote
dapr-sidecar service that is running inside AKS. This is configured in the following way:
version: 0.1
env:
- name: DAPR_SIDECAR_HOST
value: $(services:dapr-sidecar)
Of course, this will require some changes to be made in the application itself. It is
not safe to assume that Dapr will be accessible on localhost at port 3500 as it is typically
in Kubernetes. Bridge to Kubernetes will set up port forwarding to some random local
address as I already mentioned. That’s why you have to get the whole address from the
environment variable that was made available by the configuration file:
Then when you press F5, Bridge to Kubernetes will set up your local debugging
environment. It will require some permissions as it is going to manage the ports on your
machine. If you have Docker Desktop running, make sure to quit it as it will prevent
Bridge to Kubernetes to initialize successfully. Then opening http://localhost:8090/
greet will let you debug the Greeting service while it is calling the other two services that
are actually running inside a Kubernetes cluster.
You can find the Greeting service project configured for debugging with Bridge
to Kubernetes in the source code of the book under the following path: Chapter 5/
Bridge-To-K8s. Make sure to change values of targetCluster and targetNamespace
in .vscode\tasks.json to reflect what is appropriate to your current context. As it comes
to AKS, after you configure your kubectl context to point to it, you have to deploy Dapr
and the manifests of the Hello World application just like in Chapter 4: Running Dapr in
Kubernetes Mode.
91
Chapter 5 Debugging Dapr Applications
Summary
In this chapter, I showed you several ways to debug your Dapr applications. You can
debug everything locally by either using the Dapr CLI or the Dapr Visual Studio Code
extension. Another option is to run and debug your applications inside a development
container. And for the most sophisticated architectures, you can use Bridge to
Kubernetes to debug a service locally while other parts of the application are running
remotely inside a Kubernetes cluster.
You learned about the nature of the Microservices architecture. Later on, you were
introduced to Kubernetes and how Dapr helps during the development of distributed
applications. I also covered the concepts of Dapr and its two hosting modes – Self-hosted
and Kubernetes – and what tricks to use for debugging during the development of Dapr
applications.
The next part of the book will be a deep dive into the details of each building block
that Dapr provides starting with the Service Invocation building block.
92
PART II
Service Invocation
Developing an application based on the Microservices architecture implies having many
smaller services that work as a whole. A significant part of their responsibilities goes to
being able to communicate with each other. Some of them will use a loosely coupled
means of communication by broadcasting some messages to all interested parties.
Others, instead, would want to invoke a particular service by making a direct request
to it. Service discovery and service invocation are the essentials that you will need
whenever implementing such a Microservices application.
Fortunately, Dapr provides a building block for invoking other Dapr applications.
In this chapter, you will learn what this building block does and how it works. Some
services may be exposing an HTTP endpoint, while others understand gRPC. Dapr
supports HTTP and gRPC both on the caller and the callee sides. Additionally, those two
services might be running in different security boundaries. Because that makes things
much more complex, I will show you how to make cross-protocol and cross-namespace
calls. You might be willing to control access to some services very carefully as they may
contain some very critical information or functionality that shouldn’t be available from
everywhere. We will explore how to leverage access policies in order to make certain
services accessible only by a particular set of services that are eligible for calling in.
O
verview
Imagine that there is Service A that performs several tasks and one of them is getting
some result from Service B. There are a couple of challenges coming from this need:
95
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_6
Chapter 6 Service Invocation
Service B. Maybe this won’t feel very natural for the implementation
of Service A if the majority of services are based on HTTP.
Now let’s see how those things will be if you use Dapr’s Service Invocation
building block. As you already know, each Dapr-ized service has a Dapr runtime
companion running beside it as either a sidecar process or a sidecar container. All of
the above-mentioned challenges are addressed when you call the /invoke endpoint
of the Dapr sidecar as shown in Figure 6-1:
2. Once the request reached the sidecar, it resolves the address of the
sidecar target service by using the name resolution component
that finds the particular location of the sidecar of Service B. There
is more information on name resolution further in the chapter.
3. The request is forwarded to the sidecar of Service B. Note that
sidecar-to-sidecar communication happens only via gRPC.
96
Chapter 6 Service Invocation
4. Dapr Sidecar B invokes the method of Service B. If you recall when
starting the application with dapr run, you have to specify the
port number that your application listens on. The same applies
in Kubernetes mode where you use the dapr.io/app-port
annotation. The actual URL for the request to Service B will be
http://localhost:<app-port>/<method-name>.
Dapr acts like a reverse proxy between the caller service and the callee service.
Furthermore, it will also try to resolve transient failures received from Service B by
retrying the request up to three times with a backoff time period between calls.
Since all the communications flow through the Dapr sidecar, it is easy for it to
provide a distributed tracing functionality. We are going to explore distributed tracing in
depth in Chapter 12: Observability: Logs, Metrics, and Traces. For the same reason,
Dapr can help you control the number of processed simultaneous calls on the
application side whether it is coming from service invocation, pub/sub, or bindings.
97
Chapter 6 Service Invocation
The rate limiting takes effect when you specify the number of concurrent requests in the
app-max-concurrency flag for Self-hosted mode or the dapr.io/app-max-concurrency
annotation in Kubernetes mode. In this way, you can assume that only a configured
number of requests at a time go through any method of the service without having to
implement complex logic for managing concurrency.
The distributed Hello World example we explored earlier already uses the Service
Invocation building block. However, let me show you how you can implement services
based on HTTP and gRPC and how it looks from the consumer side.
As a matter of fact, the functionality provided by the Service Invocation building
block is part of the foundation of Dapr as it needs to use direct service invocation as part
of other building blocks, for example, the Publish and Subscribe and the Actors building
blocks. You just don’t call the service invocation API directly because it is internally
utilized by the logic of the respective building blocks.
Note As a prerequisite, you will need to have installed Visual Studio 2019 with
.NET 5; or if you prefer to use a code editor like Visual Studio Code, install just the
.NET 5 SDK, which you can download from https://dotnet.microsoft.com/
download. The following steps will be applicable for the latter case as running CLI
commands is easier to follow than clicking through the project creation wizard of
Visual Studio that might get updated.
1. First, start by creating a new folder for the project and navigate to
it in a terminal.
98
Chapter 6 Service Invocation
[HttpGet]
public IEnumerable<WeatherForecast> Get(int daysCount)
{
var rng = new Random();
return Enumerable.Range(1, daysCount).Select(index => new
WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
99
Chapter 6 Service Invocation
Now let me show you how you can invoke the Weather Forecast service from another
service:
1. First, open a new terminal and start a Dapr sidecar for your caller
service: dapr run --dapr-http-port 3500 --. At this point,
you will have two Dapr sidecars running in different terminals.
Note that the single flag specified for dapr run specifies the port
number that we want the second sidecar to be listening on.
As you already know, the communication will flow from the caller sidecar to
the callee sidecar, which will invoke the target method at http://localhost:5000/
weatherforecast?daysCount=10. After that, the result will be propagated back to the
caller app by the Dapr sidecars.
To recap, the service invocation API has the following endpoint http://
localhost:<daprPort>/v1.0/invoke/<appId>/method/<method-name> that accepts
POST/GET/PUT/DELETE requests:
• daprPort is the port number where the Dapr sidecars listen on.
• appId is the Application ID under which you have started the target
application. Since cross-namespace calls are also supported, if you
want to call a Dapr application that resides in another namespace,
you have to specify it as <appId>.<namespace>.
Whatever HTTP request method your service accepts, the same HTTP request method
must be used when sending the request to the service invocation API. The body of the
request along with the headers will be sent to the target application. Service invocation
supports the following HTTP request methods – GET, POST, PUT, and DELETE.
100
Chapter 6 Service Invocation
Name Resolution
Making a request to another service is a matter of specifying the Dapr ID of that service
and the method name. But behind the scenes, Dapr has to locate the specific sidecar,
which is a representative of the target service. In Self-hosted mode, Dapr uses Multicast
DNS (mDNS), whereas in Kubernetes it uses the built-in Kubernetes DNS. Anyway,
the name resolution component is pluggable, and you can substitute it with a custom
implementation that fits any other needs albeit being rather unusual.
101
Chapter 6 Service Invocation
Let’s see how the name resolution works in Kubernetes mode once we have the
Weather Forecast service deployed as a Dapr application:
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /usr/src/app
COPY --from=build-env /usr/src/app/out .
ENTRYPOINT ["dotnet", "WeatherForecastHTTP.dll"]
102
Chapter 6 Service Invocation
2. To keep the image size small, you can define a .dockerignore file
to ignore some files and folders when copying files to the filesystem
of the image. For .NET projects, those folders are typically the bin
and the obj. But you can include any other files that should be
ignored. It won’t make a huge difference since this is a very tiny
project; however, you should know there is such an option.
3. Now it’s time to build the Docker image. Open a terminal and
navigate to the project folder where the files Dockerfile and
.dockerignore are located and run
apiVersion: apps/v1
kind: Deployment
metadata:
103
Chapter 6 Service Invocation
name: weather
spec:
replicas: 1
selector:
matchLabels:
app: weather
template:
metadata:
labels:
app: weather
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "weather"
dapr.io/app-port: "80"
spec:
containers:
- name: weatherforecast
image: <your-docker-hub-username>/
weatherforecast:latest
ports:
- containerPort: 80
Now that you have the Weather Forecast service deployed as a Dapr application in
Kubernetes mode, let’s find out how the name resolution works in Kubernetes.
In order for a request to /v1.0/invoke/<dapr-app-id>/method/<method-name> to
be resolved, the Dapr sidecar has to find the address of the other sidecar that represents
the dapr-app-id. As already specified in the Pod annotations, the Dapr ID is weather.
The Dapr sidecar is actually exposed to the other Dapr sidecars via a Kubernetes
service. When you run kubectl get services, you will be able to see the weather-dapr
104
Chapter 6 Service Invocation
service that was automatically created behind the scenes. You will be able to see how
it is configured, for example, what is the selector and what ports it targets, by running
kubectl describe service weather-dapr. Let’s play with the name resolution:
apiVersion: v1
kind: Pod
metadata:
name: dnsutils
namespace: default
spec:
containers:
- name: dnsutils
image: gcr.io/kubernetes-e2e-test-images/
dnsutils:1.3
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
restartPolicy: Always
2. Open a terminal in the folder where you created the file and run
kubectl apply -f .\dnsutils.yaml.
3. When the Pod is running, you can check how the name resolution
is configured by examining the resolv.conf file:
nameserver 10.96.0.10
105
Chapter 6 Service Invocation
5. Then let’s try to resolve the name of the service that exposes the
Dapr sidecar of the Weather application by using nslookup and
passing just the name of the service created by Dapr:
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: weather-dapr.default.svc.cluster.local
Address: 10.1.2.32
It seems like it was able to find the FQDN of the weather-dapr service that resides in
the default namespace and also its IP address.
Cross-Namespace Invocation
So far we have applied the manifests of the Dapr applications into the default
namespace. But in reality, you may end up using different namespaces for various
reasons – separation of environments, having different versions of the same application
running side by side, canary deployments, or just for keeping different software stacks
separate.
It’s entirely possible to invoke a Dapr application running in a different namespace
than the one of the calling party. In order to do that, you have to include the namespace
name to the Dapr App ID in /v1.0/invoke/<dapr-app-id>.<namespace-name>/
method/<method-name>. In fact, if you don’t specify a namespace, the way name
resolution works in Kubernetes is it assumes it is the default one.
Let me show you how it works. I will show you how to deploy the same Weather
Forecast service to a different namespace and call it from the sidecar of the Weather
Forecast service running in the default namespace. Sounds very interesting, isn’t it? I
assume you have already deployed the service into the default namespace by following
the previous examples.
106
Chapter 6 Service Invocation
2. Make sure your terminal is in the folder that contains the YAML file.
Apply the same manifest but this time into the new namespace:
4. Now open a browser and enter the following address that will
invoke the Weather Forecast service in the another-ns: http://
localhost:3500/v1.0/invoke/weather.another-ns/method/
weatherforecast?daysCount=5
5. You should be able to see the JSON response with the forecast for
the next 5 days. This means that the Dapr sidecar (from the default
namespace) that you were calling directly was able to locate the
weather-service in the another-ns namespace.
gRPC uses HTTP/2 as its transport protocol, so it inherently leverages its performance
optimizations such as binary framing that is a great fit for Protocol Buffers, the ability
to send multiple requests and responses over a single TCP connection, and the ability
to initiate pushes from the server. So HTTP/2 not only provides many performance
optimizations when compared to HTTP/1.1 but also enables gRPC to support streaming
scenarios like server streaming, client streaming, and bidirectional streaming.
gRPC is gaining more and more popularity among Microservices applications
because of its high performance. I’ve seen several projects substituting some of their
RESTful services with gRPC-based ones and realizing the performance gains. Dapr also
supports communication over gRPC. In fact, the communication between sidecars
happens only via gRPC. Applications can call the Dapr runtime via either HTTP or gRPC.
So far we have used only the HTTP API of the Dapr sidecar. Let’s see how to make use
of the gRPC support from both client and server perspectives.
108
Chapter 6 Service Invocation
5. Now that we have imported the proto for the Appcallback service,
it will generate an abstract class whose methods we need to
implement. Create a new file named AppCallbackService.
cs under the Services folder with the following contents and
implement the OnInvoke method, which is the entry point for
method invocation:
6. The code is deserializing the input data and serializes the result
into JSON. You have to install the Newtonsoft.Json package:
endpoints.MapGrpcService<AppCallbackService>();
110
Chapter 6 Service Invocation
The gRPC application is running now. Note that we don’t have a Protobuf definition
for our service; instead, the method name and the parameters come dynamically in the
InvokeRequest argument.
Most applications talk to their Dapr sidecars via an unencrypted channel because
they are a part of one security boundary and they should trust each other. But the gRPC
service template comes with TLS enabled on port 5001. If you want to make the sidecar
talk to port 5001 over TLS, you should also specify the switch --app-ssl:
The gRPC service is implemented in such a way that it doesn’t depend on any
specifics of HTTP. Therefore, you can call it from either HTTP or gRPC. The method
name and the data it accepts are used by the gRPC service because they remain
supported across both protocols. When the caller invokes its Dapr sidecar via HTTP, the
method name is in the URL, and the data comes from the HTTP request body. Of course,
you will have to follow the HTTP semantics and choose methods that support sending
data in the HTTP body, like POST or PUT.
Let’s see how it works. Start a Dapr sidecar that is going to be used only for calling the
gRPC service. Open another terminal and rundapr run --dapr-http-port 3500 --
Now that the Dapr sidecar is up and running, you can use cURL, Postman, or any
other tool to issue a POST request to the gRPC service. In this example, I will use cURL:
1. First, create a new folder for the client application and create a
new .NET console application by running
dotnet new console -f net5.0
112
Chapter 6 Service Invocation
class ResultMessage
{
public string Result { get; set; }
}
using System.Threading.Tasks;
using Dapr.Client;
5. Finally, let’s run it with Dapr and see the result displayed in the
console. Note that I don’t specify --app-id nor --app-port as this
will be just a client application: dapr run -- dotnet run.
113
Chapter 6 Service Invocation
6. In the logs, you should be able to find the output of the application
right after the Dapr logs:
You're up and running! Both Dapr and your app logs will appear here.
The result is that you used gRPC to call the service without really knowing what the
underlying code of the .NET Dapr SDK does.
114
Chapter 6 Service Invocation
The Sentry service generates and attaches a SPIFFE ID for each workload certificate
it issues for a Dapr sidecar. This is the reason mTLS must be enabled. The SPIFFE ID has
the following format: spiffe://<trustdomain>/ns/<namespace>/<appid>. Let’s explore
what a SPIFFE ID consists of. The trust domain is a way to create a higher-level grouping
of several applications. This is the first evaluation level used to match an ACL policy. If
there isn’t any trust domain specified for an application, the default one is called public.
The trust domain of a Dapr application can be specified in its configuration. You already
know what namespaces and Dapr App IDs are – the mandatory attributes of every Dapr
application. In the future, this mechanism may make it possible to securely consolidate
Dapr applications running on various hosting environments as long as they share the
same trust anchors used by their respective Sentry services.
Let’s implement ACLs for the distributed Hello World application that I showed
you how to implement in Chapter 2: Introduction to Dapr and later on we deployed to
Kubernetes in Chapter 4: Running Dapr in Kubernetes Mode. To recap, once you deploy
it to Kubernetes, all the three services are accessible by any other Dapr applications
that might be deployed into the cluster in the future. Let me show you how to specify
that the endpoints of the Hello service and the World service can be accessed only by
the Greeting service and all other requests to those two services be denied. You have to
create Configuration manifests for both Hello and World services. Let me show you how
it looks just for the Hello service; however, you can also find the Configuration manifest
for the World service in the source code of the book. The configuration assumes all three
services are in the public trust domain and in the default namespace:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: helloconfig
namespace: default
spec:
accessControl:
defaultAction: deny
trustDomain: public
policies:
- appId: greeting-service
defaultAction: deny
trustDomain: public
115
Chapter 6 Service Invocation
namespace: default
operations:
- name: /sayHello
httpVerb: ['GET']
action: allow
3. In case a match at an application level has not been found for the
current calling application, the global action is applied.
Let me explain the different options for the configuration of Access Control Lists:
116
Chapter 6 Service Invocation
117
Chapter 6 Service Invocation
sidecars won’t allow any request that doesn’t have a valid token. Alternatively, you can
utilize an OAuth 2.0 middleware, if you want to enable OAuth 2.0 authorization for your
Dapr sidecars. You will learn how to do it in Chapter 13: Plugging Middleware.
Some building blocks like the Service Invocation, Publish and Subscribe,
and Resource Bindings building blocks rely on being able to directly invoke your
applications. If for some reason you need to expose your applications publicly, you may
want to know whether a request came from a Dapr sidecar to allow it or something else
tries to invoke it and therefore reject it. You can instruct Dapr to send a predefined JWT
as a header in the HTTP requests to your application or as metadata in gRPC requests.
You will be responsible to generate the token initially and then rotate it. To enable this,
you have to set an environment variable named APP_API_TOKEN to hold the JWT for
Dapr in Self-hosted mode or use a Kubernetes Secret.
S
ummary
Being able to discover and call other services is one of the most important needs in the
world of distributed applications. In this chapter, you learned how the Service Invocation
building block eases direct cross-service communication. You saw it in action for both
HTTP- and gRPC-based services that are called via the Dapr system of sidecars via either
gRPC or HTTP. We explored how name resolution works and how Dapr applications can
be invoked across namespaces. By intertwining gRPC services with HTTP clients and
vice versa, you know how cross-protocol communication was made possible by Dapr.
You also learned how to control access to Dapr applications by defining Access Control
Lists and also require authentication for any request coming to a Dapr sidecar or an
application.
In the next chapter, we are going to explore how the Publish and Subscribe building
block makes asynchronous communication possible.
118
CHAPTER 7
Publish and Subscribe
Communication and collaboration are vital for Microservices applications – whether it is
synchronous or asynchronous. In this chapter, I am going to elaborate on what Publish
and Subscribe is and when it is useful. I will walk you through the Dapr capabilities for
implementing Publish and Subscribe and what are the supported messaging systems.
Note that I will collectively refer to message brokers and streaming platforms that are
supported by Dapr as messaging systems. Then I will show you how to build a system
of three services where one of them is producing events and the others are consuming
and processing them. At the end of the chapter, I will briefly touch on the future of the
Publish and Subscribe building block in Dapr.
What Is Publish/Subscribe?
Publish/Subscribe or Pub/Sub is a messaging pattern where there are two types of
parties involved – publishers and subscribers. The publishers send their messages to
a messaging system where those messages can be categorized, filtered, and routed.
Publishers don’t know anything about the subscribers; they just publish a message that
carries some type of payload. The messaging system allows keeping this data temporarily
until it is processed. Then subscribers are declaring their interest in receiving all
messages of some kind. Usually, messages are published against a topic that is defined
by the publisher. Depending on the messaging system, there are various options for
filtering messages based on their content.
There is a slight difference between messages and events. When the publisher
expects that the information will be delivered and processed and then a certain outcome
is achieved by some action, that is messaging. In a message-driven system, the message
is virtually addressed to one or more receivers that are obliged to process it. An event is
a signal emitted by some party in order to alert that a particular state was reached. In an
event-driven system, an event published to an event stream can be received by zero or
119
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_7
Chapter 7 Publish and Subscribe
more listeners that are interested in the events of this stream. There is a slight nuance
between event-driven and message-driven systems, as one focuses on ingesting into event
streams, while the other focuses on addressing recipients. However, for this chapter, I am
going to use both interchangeably.
I already mentioned topic as a term. While we are talking about messaging, there is
also another popular term – a message queue. At times, you might hear someone using
both of those terms interchangeably. However, they serve different purposes. The queue
provides an intermediary location for message producers to buffer messages until they
are picked up by consumers. A single message is being picked by a single consumer
to do some processing work. Until the consumer acknowledges that the message has
been processed, the message is then hidden from other consumers by entering in a
locked state. In case the consumer does not acknowledge the message to be processed,
after some timeout, it is returned to the queue and is ready to be picked up by other
consumers. If, at the time, there are no available consumers, the message will be kept for
some time so that it can be picked up at a later stage. You can also define an expiration
policy so that in case the message has not been picked up, it expires.
In contrast, topics are like a broadcasting station. Messages are emitted by various
publishers, and they land into various topics. This routing process can happen
dynamically according to some predefined rules, depending on the particular messaging
system. Those messages will be delivered to zero or many subscribers that are listening
for messages on a particular topic. This means that from each message, there might
be different actions that are taking place at the same time. This is where the Publish
and Subscribe pattern fits. Each of the subscribers may read the messages from a given
topic at its own pace. You may hear someone referring to this as the fan-out messaging
pattern because the message is being effectively spread across a number of subscribers
in parallel.
• Loose coupling
120
Chapter 7 Publish and Subscribe
doesn’t affect the publisher in any way. If you are using direct service
invocation, you have to update the code so that it knows how to call
some additional services that may want to process the incoming
updates of a particular service. Taking this even further, at the time
of publishing some message, some of its subscribers may not be
running.
• Scalability
• Dynamic targeting
121
Chapter 7 Publish and Subscribe
• No polling needed
The Publish and Subscribe pattern eliminates the need for doing
continuous polling. Have you ever seen an application initiating
some kind of operation and then repeatedly querying the state of this
operation every second until it returns a result? This is not needed
anymore because once something is done, the interested parties will
get notified about it.
• Large messages
122
Chapter 7 Publish and Subscribe
out a different messaging system, you have to start using its specific library and rework
the code to wire it up to your solution. The new messaging system will also try to impose
some of its concepts over your code.
Dapr provides a handy building block that eases the integration of such a messaging
system for loosely coupled communication across your services. The building block
represents an implementation of the adapter pattern that is wrapping the operations
supported by each particular messaging system and provides via the interface of the
Publish and Subscribe building block. This makes it easy for you to transparently use a
target system without knowing its particular details.
The key to this building block is once again the system of Dapr sidecars. As you can
see, all the communication flows through them as shown in Figure 7-1.
The service that sends information to other services by sending a message is called
a publisher. The publisher sends the message to its Dapr sidecar by stating which
messaging system is targeted and the name of the topic inside it. This can happen either
via HTTP by using the /publish endpoint of the Dapr API or via gRPC by invoking the
respective function. The particular messaging system is one of the “supported messaging
systems” as referred to in Figure 7-1. It is determined by the type of Pub/Sub component
that was chosen.
123
Chapter 7 Publish and Subscribe
Defining the Component
The component holds the information needed for connecting to the messaging system –
URL, credentials, and so on. The specifics of the communication with a certain type of
messaging system are implemented by the component.
For example, the default Pub/Sub component for Redis that you have in your Dapr
components folder when you initialize Dapr in Self-hosted mode has the following
definition:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
You can use this definition both in Self-hosted mode and in Kubernetes mode as it
is also a valid CustomResourceDefinition (CRD) manifest. In Self-hosted mode, the file
is parsed each time when you call dapr run, whereas in Kubernetes mode, components
have to be applied and thus the CRDs are getting materialized as objects in Kubernetes.
The value of metadata.name is what you have to specify as the name of the component,
and you provide it when invoking the /publish endpoint of the building block. The
topic gets auto-created if it doesn’t exist in the target messaging system. The value
specified in spec.type is the name of the particular component implementation that
handles the specifics of the messaging system. Then whatever you specify as a list of key-
value pairs under spec.metadata is basically being sent to the underlying component
implementation as input parameters. Those fields typically contain the information
needed to establish a connection and to configure the behavior of the component. Later
on, you can change the connection info, or you can even substitute the component type
to talk to a different type of messaging system. Your services won’t notice the difference
as long as the name of the Pub/Sub component remains the same and you restart the
124
Chapter 7 Publish and Subscribe
Dapr sidecar so that it picks up the new configuration. You don’t need to change any of
the existing code, and that is because you interact with the messaging system by using
the API of the Publish and Subscribe building block.
You can find the source code of all components in the dapr/components-contrib
repo at https://github.com/dapr/components-contrib. The components available to
the Publish and Subscribe building block reside under the pubsub folder.
You might have more than one pubsub component. You can create many pubsub
components that are pointing to different messaging systems. After all, whenever you
publish a message or subscribe to a topic, you always have to specify the name of the
pubsub component. Keep in mind that components are namespaced. This means that
the application will be able to discover any components that are deployed in the same
namespace. For any cases of a big application, the services of which are spread across
different namespaces need to communicate via the same message system, you have to
create the same component in all the namespaces of the application involved.
M
essage Format
Once the message is received by the underlying messaging system, all subscribers are
notified at their specific endpoints that are to be invoked whenever there is an incoming
message on a new topic. The way this is achieved is by leveraging the system of Dapr
sidecars. The Dapr sidecar initiates the request to its subscribed application, which
then receives and consumes the message. The message received by the subscribers had
been actually converted in a special format, right before it was ingested in the messaging
system. This format conforms to the CloudEvents specification. CloudEvents is a CNCF
(Cloud Native Computing Foundation, www.cncf.io) project that is a specification for
describing event data in a common way so that it can be defined and processed across
a variety of systems and services. Without having such a specification, loosely coupled
communication won’t be possible at all. The reason is that publishers tend to publish
events in diverse formats and hence consumers must know how to process them on a
case-by-case basis. The more a consumer knows about some custom proprietary format,
125
Chapter 7 Publish and Subscribe
the more tightly coupled the communication becomes. That’s why CloudEvents greatly
improves interoperability by setting the standards for describing event data.
Dapr only wraps the message data into CloudEvents in case if the data you are trying
to publish is not a CloudEvents event. The CloudEvents spec defines a set of attributes
that describe the data you are sending, or in other words metadata. This gives the
context to the consumers so that they know what the type of the message is, where it
originated from, what payload type it is, and how to serialize it. Let’s take a look at an
example message that contains the reading of a temperature sensor at a given moment.
It was sent via the Publisher and subscribe building block of Dapr:
{
"id":"e846390d-9bfe-4abd-a247-e2b42ad1d775",
"source":"temperature-sensor",
"type":"com.dapr.event.sent",
"specversion":"1.0",
"datacontenttype":"application/json",
"data":{
"eventTime":"2021-03-22T23:11:09.7134846Z",
"temperatureInCelsius":30.421799
},
"topic":"temperature",
"pubsubname":"sensors",
"traceid": "00-5dce22d5dbe3354d871cbb1648115864-e5b9623f85147fd4-01"
}
• id – An identifier for the event being sent. It is unique among all
events incoming from a given source.
• data – The event payload, serialized into the media format specified
by the datacontenttype attribute.
Now that you know what the format of the messages is, let me walk you through the
details of receiving a message.
R
eceiving a Message
Dapr follows the semantic of at-least-once delivery, which means that the message will
be delivered to each subscriber at least once. If the delivery turns out to be successful,
the subscriber has to acknowledge the message, which will be considered as delivered
for this particular subscriber. But there are times when the message could not be
processed by the subscriber or the subscriber is facing some intermittent issue at the
time. In such cases, the messages are attempted to be redelivered. The retry should be
expected by the subscriber, and so its logic must be implemented in an idempotent
way. This means that multiple full or partial processing of the message won’t cause any
side effect except what is expected from single processing. If the operation cannot be
implemented in an idempotent way, it should deal with any potential duplication that
can be caused by any message delivered multiple times.
127
Chapter 7 Publish and Subscribe
{
"status": "<value>"
}
128
Chapter 7 Publish and Subscribe
Of course, similar semantics apply for gRPC as well. If the gRPC request succeeded,
the message will be deemed successfully processed; otherwise, it will be retried.
Subscribing to a Topic
At this point, you might be wondering how to subscribe to a given topic. There are two
ways to accomplish this – programmatic and declarative.
Programmatic Subscription
The programmatic subscription is done via the consumer application. It states what
topics the consumer application is interested in and what endpoint it has for receiving
the incoming messages. Upon startup, each Dapr sidecar issues a GET request to the /
dapr/subscribe route of its application to collect all topics, if any, the application wants
to subscribe to. If it subscribes to any, it returns a JSON array of all subscriptions with the
following structure:
[
{
pubsubname: "pubsubComponent",
topic: "topicName",
route: "endpointName"
}
]
Each object in the array represents a single subscription where pubsubname is the
name of the pubsub component that targets a messaging system, the topic member
is the name of the topic within the messaging system, and the value of route is the
endpoint of the service that gets called whenever a new message is published.
129
Chapter 7 Publish and Subscribe
Declarative Subscription
A potential downside of the programmatic approach is that you have to rebuild, publish,
and deploy a new version of the Docker image of the service whenever you want to make
a change in the subscriptions. To prevent this from happening, you may want to utilize
the declarative way of specifying subscriptions, that is, the Subscription CRD. It has the
following structure:
apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
name: my-declarative-subscription
spec:
topic: topicName
route: /endpointName
pubsubname: pubsubComponent
scopes:
- app1
- app2
This component manifest defines a subscription to the same pubsub and topic
and listens on the same route as from the programmatic subscription example. The
difference is that the subscription is applied to two Dapr applications at once. This
means that whenever a new message is published to this topic, a post request to the /
endpointName endpoint of each application will be sent.
130
Chapter 7 Publish and Subscribe
TTL can also be configured at the topic level in the component manifest. But have in
mind that not all components support this feature.
• MQTT – Publish and subscribe to any broker that supports the MQTT
protocol.
131
Chapter 7 Publish and Subscribe
• Azure Event Hubs – A fully managed big data streaming platform and
event ingestion service.
132
Chapter 7 Publish and Subscribe
133
Chapter 7 Publish and Subscribe
First, let’s see the definition of the pubsub component that talks to a Redis instance
running in a container:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: sensors
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
It’s almost the same as the default Pub/Sub component that you get by initializing
Dapr in Self-hosted mode. Note that you may have multiple Pub/Sub components of
the same or different type, as long as their names are unique within a namespace. This
component is named sensors; hence, you are going to use this name later on whenever
publishing or consuming a message. The component file should be copied to the global
components directory. As a reminder, when you execute dapr run, Dapr by default picks
up all components in this folder; otherwise, you have to specify the path to the folder
containing the components. The Dapr components folder is %USERPROFILE%\.dapr\ on
Windows or $HOME/.dapr/bin on Linux and macOS.
Then let’s examine the Main method of the temperature sensor console application
that you can find in Program.cs:
134
Chapter 7 Publish and Subscribe
.UseJsonSerializationOptions(jsonOptions)
.Build();
while (true)
{
temperature = GetTemperature(temperature, MIN_TEMPERATURE,
MAX_TEMPERATURE);
var @event = new
{
EventTime = DateTime.UtcNow,
TemperatureInCelsius = temperature
};
await daprClient.PublishEventAsync("sensors",
"temperature", @event);
Console.WriteLine($"Published event {@event}, sleeping for
5 seconds.");
await Task.Delay(TimeSpan.FromSeconds(5));
}
}
The GetTemperature method was omitted to keep the snippet short, but it simulates
the reading of the current temperature from a hardware sensor. It will basically generate
a random value that is close to the last temperature reading, so we don’t see any
dramatic temperature fluctuations. What the Main method does is that it sends a new
message containing the current temperature along with a timestamp to the Publish and
Subscribe API of Dapr by using the DaprClient class, which comes from the Dapr .NET
SDK. The PublishEventAsync sends the event to the temperature topic of the sensors
pubsub component. Note that via a single pubsub component, we can use many topics,
for example, you may decide to start monitoring also the humidity by reading from
another sensor and sending events to the humidity topic.
As you can notice, no logic wraps the event into a CloudEvents envelope. This is
done once the event is received by the Dapr sidecar and it finds out that the payload is
not already in CloudEvents format.
135
Chapter 7 Publish and Subscribe
Next, let me run this application so that it starts ingesting events into Redis. A new
terminal must be opened into the temperature sensor project folder, and the following is
invoked:
If everything is fine, you should see the application logs as the application will be
producing an event every 5 seconds. Leave this terminal session working, and open
another one. Let’s explore how things look inside Redis. Run the following command
to start an interactive shell session in the Redis container. The container is named
dapr_redis and was created when you initialized Dapr in Self-hosted mode:
Then, launch the Redis CLI by entering redis-cli. The Redis CLI allows you to
run Redis commands. It is particularly useful for testing purposes when you want to
explore the data in Redis. Once you are inside the Redis CLI, you can run commands like
XLEN temperature, which will return the number of entries ingested into the particular
stream. A Redis Stream is equal to a topic of a particular Pub/Sub component pointing
to a Redis instance. You can further explore the information about a stream by using the
XINFO command:
It will show how many entries there are, the number of consumer groups
associated with the stream along with the first and the last entries, and some additional
information. When using Dapr, by default the consumer groups will be named after the
Dapr App IDs of the subscriber applications.
At this point, the temperature reading information is ingested into Redis, but there
isn’t anything consuming those events. Let’s explore the Small talker application, which
is a Node.js application. It serves on two endpoints – dapr/subscribe for announcing to
Dapr that it wants to subscribe to the temperature topic and /temperature-measurement,
which is the endpoint where new events are delivered:
136
Chapter 7 Publish and Subscribe
pubsubname: "sensors",
topic: "temperature",
route: "temperature-measurement"
}
]);
})
137
Chapter 7 Publish and Subscribe
Last but not least, let’s explore the AC Controller service that is implemented in ASP.
NET Core. The code of the controller that is receiving the message is very simple and can
be found in TemperatureMeasurementController.cs:
return Ok();
}
138
Chapter 7 Publish and Subscribe
input formatter in Startup.cs so that it can deserialize the body of the request into a
CloudEvents object and also suppress one validation that will otherwise cause your code
to fail because the default validation in .NET 5 has changed:
services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new
CloudEventJsonInputFormatter());
options.
SuppressImplicitRequiredAttributeForNonNullableReferenceTypes =
true;
});
Before starting the AC Controller application, let’s define its subscription by using
the declarative approach:
apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
name: temperature-subscription
spec:
pubsubname: sensors
topic: temperature
route: /TemperatureMeasurement
scopes:
- ac-controller
Under spec you basically provide the same info as in the programmatic approach –
the name of the pubsub component, a topic inside it, and a delivery endpoint where
new messages are sent. The difference here is that the subscription endpoint supports
defining multiple subscribers at once. In this case, under scopes you can find just the
ac-controller, which is the Dapr ID of the AC Controller service. Make sure to also copy
this component manifest file into the Dapr components folder. Finally, let’s run the AC
Controller service as a Dapr application:
139
Chapter 7 Publish and Subscribe
Then by reading the application logs, you will track how the state of the air
conditioner changes over time depending on the current temperature:
1. You have to either install Azure CLI on your machine, or you can
use the Azure Cloud Shell, which you can access at http://shell.
azure.com. If you use the Azure CLI locally, you have to first log in
by running az login. The Azure Cloud Shell will automatically do
that for you.
3. Next, let’s create the Azure Service Bus namespace in the resource
group that we just created. It has to use the Standard tier so that
Service Bus topics are supported. It’s a good idea to not use the
same name for the namespace as I do here because the name of
the namespace must be globally unique:
After you have done this final step, the temperature sensor application will start
sending the messages into a Service Bus topic named temperature. Dapr will create
it automatically for you. Then when the two consumer applications want to read the
messages in the topic, Dapr will create a new subscription for each of them. You can
explore all this visually by opening the Azure Portal and navigating to your Service Bus
namespace. There are also some useful charts that help you understand at what rate
messages are ingested and processed, as you can see from Figure 7-3.
141
Chapter 7 Publish and Subscribe
Additionally, you can pass some additional configuration that is specific to the
messaging system of your choice as additional key-value pairs in the metadata of the
component. The supported keys vary depending on the type of pubsub component that
is used. You can find all supported by consulting the source code of a given component
in the dapr/components-contrib repository on GitHub or in Dapr docs (docs.dapr.io).
For example, by default, the maximum number of delivery attempts for an Azure
Service Bus subscription is 10. After that, the message is moved to the dead-letter
subarea of the subscription. You can control this number among others supported by
specifying an item with the name maxDeliveryCount and the number as a value in the
component manifest.
Be aware that the specifics are up to the messaging system of choice. For example,
Apache Kafka offers persistent storage of data, whereas the message retention in Azure
Event Hubs can be no more than 7 days. It’s advisable to try not to rely on a specific
behavior offered by a given component as this will make your application less portable
across different messaging systems.
142
Chapter 7 Publish and Subscribe
Summary
In this chapter, you learned how Dapr helps you to easily use the Publish and Subscribe
pattern. After a brief overview of what Pub/Sub communication is and when it makes
sense to be used and when it isn’t a good idea, you explored how the Publish and
Subscribe building block of Dapr works. You learned what messaging systems are
supported by Dapr. Then, you put all this knowledge into practice by implementing a
temperature processing system consisting of three services. One of the services used a
programmatic subscription, while the other used a declarative one. Initially, the system
of three services was using Redis Streams as a messaging system, but later on, I guided
you to switch over to Azure Service Bus. The chapter wrapped up by touching upon
some of the possible limitations that you may hit if you have any special requirements
around Publish and Subscribe.
In the next chapter, you will learn how to use the State Management building block
to persist the state of your services.
144
CHAPTER 8
State Management
At some point, services need to work with state. The way for persisting the state marks
out how scalable they are going to be. In this chapter, I will walk you through a brief
introduction to what is the difference between stateful and stateless services and what
are the challenges for scaling them. As it typically happens, when you don’t want to
incorporate some functionality in your code, you use an external service that provides the
functionality you need out of the box. You will learn how Dapr helps you persist the state
of your services by leveraging external state stores. When you make it to the point to persist
data of a distributed application, you must think over two important traits – concurrency
and consistency. Once I cover how the State Management building block works, I will go
through the state stores that are supported by Dapr. I will also touch on how the Actors
building block that I am going to cover in Chapter 10: The Actor Model relies upon state
management.
145
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_8
Chapter 8 State Management
Based on the information it has collected, the service will make sure to grade and order
the returned products accordingly. As a developer, you will be tempted to save all
this information about the user locally and in memory. It’s very easy to create a static
concurrent dictionary that can keep the state locally among different requests. This
makes the service a stateful one, which means that its instances won’t be autonomous
and interchangeable anymore. Each instance holds its portion of the whole state so
that the load balancer has to examine every request and based on some condition to
direct it to the respective instance that keeps the state information about a user. In such
a scenario, the instances can effectively be called shards. Sharding is a technique used
to split a large volume of data into multiple chunks called shards that are persisted on
different servers. But the primary goal of sharding is to accommodate a volume of data
that is so big that doesn’t fit a single server. Sharding is very typical for databases, but
it also brings a lot of challenges. For example, each shard becomes a single point of
failure. You may want to replicate each shard to another server to make sure a failure or
corruption in one shard does not deteriorate the whole sharded service. Furthermore,
the load balancer needs a strategy to distribute requests to the available shards. This
mapping between a request and a shard is done by a sharding function whose job is to
resolve a request to a particular shard in a deterministic and uniform way. The sharding
function is pretty much like the hashing function that is used by hash tables. You have
to identify what part of the request to use as a sharding key – IP address, path, country,
and so on. The sharding function will use this key to consistently determine which shard
this request should be routed to. Let’s assume that the initial number of shards was set
up to be 5. With time those shards become full, and you need to add another shard to
make them 6. Depending on the sharding function that you use, this might result in a
change in the way all requests are being distributed across shards. Or you may need to
implement a resharding process. This becomes very complicated for implementation
unless you are building a new database technology, of course.
But there is a way to have a stateless service that still uses a state. I know it sounds
confusing at first! This can be achieved by offloading the state persistence to an external
service as shown in Figure 8-1. When you scale out such a stateless service, each replica
will always manage to find the ID for a particular session in the state store. And you
don’t care what replica will service your request in the end. However, calling an external
service will add some performance overhead; but in most cases, it will be negligible
if the service and the state store are in close proximity. The service can be scaled out
indefinitely; however, eventually, the bottleneck will become the common state store,
which will have to be scaled as well to keep up with the load.
146
Chapter 8 State Management
• As a result, swapping one state store for another doesn’t require any
code changes.
147
Chapter 8 State Management
Defining the Component
Before you can use the building block, a component that points to the target state store
must be created. The default state component that is created when you initialize Dapr in
Self-hosted mode looks like this:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
The value of metadata.name is the name of the state store. Multiple state store
components can be created and used by Dapr applications. Later on, when you want to
execute some operation via the API, you have to reference the particular state store by
specifying its name.
As with other components, the spec.metadata section contains the connection
details and any options that are supported by the particular state store implementation.
In the preceding example, actorStateStore is set to true, which means that this
component will be used for persisting the actors’ state. Only a single state store can be
used by the Actors building block.
Note Components are namespaced. This means that Dapr applications see all
the components that exist only in the namespace where they reside. If you want
to persist state to the same state store but from other namespaces, you have to
create the same component. You can use the same name because names should
be unique within a namespace.
148
Chapter 8 State Management
Controlling Behavior
Let me show you how you can control the behavior of the target state store. Some of the
state stores support specifying the options for concurrency and consistency. Dapr tries to
support them via the State Management building block on a best effort basis. You can
also specify some options that are supported by a particular implementation of a state
component. However, if you want to make your applications as universal as possible,
you should restrain from specifying any particular options or metadata and embrace
eventual consistency and last-write-wins as a strategy for concurrent changes. Let’s see
what this means.
Concurrency
To understand what the concurrency concerns are, let’s imagine that multiple instances
of a service have acquired the last state of an item from the state store. At some point,
they may try to update it almost at the same time. For example, imagine that each
instance counts the requests it receives getting the last count, increments it by one, and
saves it in the same place. You may expect in the end the sum of all requests to be equal
to the total requests received. But so many requests are coming that some of them are
even processed in parallel by different instances of the same service. For those parallel
requests, the last counter gathered from the state store will be equal to 10, for example.
All the instances that have picked up 10 will try to update it to 11, that is, incrementing
it by one. Because those instances don’t know if the particular item was updated right
before persisting the value, some calls may be missed by such a naïve counter.
But there is a way to prevent such conflicts from happening. Dapr implements
Optimistic Concurrency Control (OCC) by using ETag or (Entity Tag) for items stored in a
state store. ETag actually originates from HTTP where it also helps leverage the already
cached data without making new requests. ETag is an identifier assigned to a specific
149
Chapter 8 State Management
version of a resource, in this case an item persisted in a state store. Performing OCC by
using ETag happens in two steps:
ETag can be optionally provided only in mutating operations, which in Dapr are
saving and deleting state.
Dapr supports two modes of concurrency, which you choose depending on whether
you pass an ETag when performing a mutating operation:
For stores that don’t natively support OCC with ETags, Dapr will try simulating the
behavior as it stays in the middle between Dapr applications mutating state and the
target state store. The implementation is up to the state store component. It is best to
check whether the state store component you are about to use supports ETags or not.
Consistency
Dapr supports two consistency levels: strong and eventual. The consistency level can be
specified as a hint to get, save, and delete operations.
When using strong consistency, you ensure that reads will return the most recent
version of the data, and mutating operations will synchronously confirm the change with
a quorum before completing the request. Strong consistency is considered slow but safe
150
Chapter 8 State Management
as operations will most likely have to span multiple replicas depending on the target
state store. Note that not all state stores support strong consistent reads.
When using eventual consistency, the read operation can get data from any of the
replicas of the state store (again, depending on the state store). This means that some
read requests may return some obsolete value because replicas will converge to the
latest value as time passes. For mutating operations, the change will be asynchronously
reconciled with a quorum of replicas.
You have to decide which mode works best for your application depending on the
state data it uses. Taking such a decision imposes a trade-off between consistency on one
side and scalability on the other. The cost of strong consistency is a higher latency, as
you are going to wait for multiple replicas to acknowledge; however, your data is always
up-to-date. In contrast, eventual consistency performs faster but may return stale data
from time to time.
M
etadata
Different state stores support different features. For example, when using Azure Cosmos
DB, you can specify the partition key as a metadata property as a part of any request to
the Dapr State API. When you do so, you will be able to achieve better performance for
your queries because, otherwise, Cosmos DB will have to scan all the physical partitions
for a particular item. The partitionKey is a property of the metadata object that is
supported on all operations of the State Management building block.
That was just one example of a supported metadata property. Other state stores
support different properties. Don’t get confused with the store-specific metadata that is
configured in the component. The metadata of a component defines how it works (e.g.,
establishing a connection, controlling its behavior), whereas the metadata of a state
request is used just for the current operation that is executed in the target state store.
S
aving State
As is common, the State API is provided by the Dapr sidecar container or process (for
Self-hosted mode) that stays next to each service. As shown in Figure 8-2, to persist
some state you have to issue a POST request to the /state/<your-state-store-
name> endpoint where you specify the name of the component. In Figure 8-2, the state
component is named myStateStore.
151
Chapter 8 State Management
The body of the request is an array of one or more key/value pairs. Those items will
be processed individually while persisted in the state store. Note that both inserts and
updates can be performed via this endpoint. In distributed scenarios, it is more useful to
have an upsert endpoint as opposed to standalone insert and update endpoints.
Now let’s see how to persist some state items. You can optionally specify options and
metadata to be sent to the state store for each of the state items:
{
"key": "callerIp",
"value": "88.88.99.16",
"etag": "27",
"options": {
"concurrency": "first-write",
"consistency": "strong"
},
"metadata":{
"metadataKey": "metadataValue"
}
}
In the target state store, the items were persisted using keys derived by the Dapr App
ID and the original key of the state item following the convention <App ID>||<state-
key>. This makes it possible to use the same state store for persisting the state of
152
Chapter 8 State Management
multiple applications without having any key collisions. In case the state store is being
used for persisting state of actors, the keys will follow a slightly different format: <App
ID>||<Actor type>||<Actor id>||<state key>.
Having the Dapr App ID automatically inserted at the beginning of the state key
originally specified means that by default your Dapr applications won’t be able to share
state data. In other words, Service A cannot access Item1 that is persisted by Service B,
as the requests originating from Service A will have the following key Service-A||Item1
while the state item will be persisted under the Service-B|Item1 key.
For most applications, this will be useful. But Dapr allows you to configure the state
sharing behavior by specifying a state prefix strategy. A prefix is what is inserted in front
of the key you specify when calling any endpoint of the State API. By default, it is the
Dapr App ID.
But you can use the name of the state store component as a prefix. This makes it
possible for all applications regardless of their Dapr App IDs to work with the state data
persisted via the same state component.
The next and the most relaxed prefix strategy is to not use a prefix. In this way,
applications will be able to access any state data, regardless of whether it is persisted via
Dapr or by an external application.
To control the state prefix strategy, you have to specify a metadata item named
keyPrefix in the state component configuration, and the three possible values we just
explored are, respectively, appid, name, and none.
Note As a best practice when you use Dapr in Self-hosted mode and want to
use the State Management building block, make sure to always specify a Dapr App
ID. Otherwise, upon each execution of dapr run, a new App ID will be generated. If
you rely on the default prefix strategy that leverages the App ID, having auto-generated
IDs is not a good idea as you won’t be able to access the persisted state later on.
Getting State
There are two endpoints for retrieving state – one for gathering individual items and
another that supports bulk requests.
153
Chapter 8 State Management
{"name":"John"}
Note that I have specified the consistency mode as a parameter, which is optional.
The ETag of the item is returned as a response header. The single prerequisite if you
want to use the Optimistic Concurrency Control is you have to pass this ETag to any
consecutive requests that are mutating this item.
What Dapr will do behind the scenes is it will issue N number of requests to the state
store, where N is the number of keys that you specified. That’s why there is an optional
property that you can specify to control the number of requests that are executed in
parallel. The reason for providing a way to limit the parallel requests is that you can
overwhelm the target state store if you specify too many keys. For state stores like
Cosmos DB, you should make sure that it has enough capacity of request units so that it
performs all requests without any throttling.
154
Chapter 8 State Management
[
{
"key":"person",
"data":{
"name":"John"
},
"etag":"26"
},
{
"key":"callerIp",
"data":"90.186.223.193",
"etag":"24"
}
]
It’s entirely possible for some of the individual requests to fail for various reasons. In
such cases, the object will be present in the resulting payload, but it will have an error
property that holds the error message Dapr received at the time of sending the request to
the target state store. Also, if such a key doesn’t exist, it will still be returned but without a
data property.
You can pass some metadata property to the target state store by providing it as
a query parameter, as it is with the following example that targets Cosmos DB:
http://localhost:3500/v1.0/state/myStateStore/bulk?metadata.partitionKey
=mypartitionKey.
Deleting State
In order to delete state items, you have to issue individual DELETE requests for each
key you want to delete. You can optionally pass the ETag in the If-Match header of the
request.
Here you can see a request to delete a state item with the key that equals person. The
optional parameter consistency is also included:
155
Chapter 8 State Management
{
"operations": [
{
"operation": "upsert",
"request": {
"key": "person",
"value": {
"name": "James"
}
}
},
{
"operation": "delete",
"request": {
"key": "callerIp"
}
}
]
}
156
Chapter 8 State Management
As I already mentioned, the Actors building block uses a single state component to
persist the state of the actors. An important detail is that this state store must support
both transactions and ETags. In the next section, you will learn what stores are supported
and what their capabilities are.
S
upported Stores
Now that you know what the State Management building block offers, let me list all
the supported state stores in Table 8-1 at the time of writing the book. All of the stores
support the CRUD operations like upsert, get single, get bulk, and delete. And just a
handful of them provide transactional support. Those state stores that support both
transactions and ETags can be used to persist the state of actors, which will be covered in
detail in Chapter 10: The Actor Model.
Aerospike ✔ ❌ ✔
AWS DynamoDB ✔ ❌ ❌
Azure Blob Storage ✔ ❌ ✔
Azure Cosmos DB ✔ ✔ ✔
Azure SQL Database/SQL Server ✔ ✔ ✔
Azure Table Storage ✔ ❌ ✔
Cassandra ✔ ❌ ❌
Cloudstate ✔ ❌ ✔
Couchbase ✔ ❌ ✔
Google Cloud Firestore ✔ ❌ ❌
HashiCorp Consul ✔ ❌ ❌
Hazelcast ✔ ❌ ❌
Memcached ✔ ❌ ❌
MongoDB ✔ ✔ ✔
(continued)
157
Chapter 8 State Management
PostgreSQL ✔ ✔ ✔
Redis ✔ ✔ ✔
RethinkDB ✔ ✔ ✔
ZooKeeper ✔ ❌ ✔
Some of the state stores listed in the preceding require special attention. For
example, when you use the state component for the SQL Server store, you will most
likely provide the credentials of a user that is not part of the sysadmin or db_owner role.
That is usually considered as the best practice – you grant only the minimum set of
permissions that are needed. But in order for Dapr to work, it needs to set up a table
where the state is saved. Therefore, the credentials for the user that you specify in the
connection string must have the permissions for CREATE TABLE, CREATE TYPE, and
CREATE PROCEDURE granted.
Another example is when you use Azure Cosmos DB for transactions. A peculiarity of
Cosmos DB is that transactions can span items stored in a single partition. Hence, if you
want to execute a transaction, you have to pass the partition key as metadata.
Summary
Having the ability to store some state is crucial. We outlined what are the differences
between a stateless and a stateful service and the various considerations when it comes
to scaling both. Next, you learned how the State Management building block makes it
easier to use a target state store with the functionality it provides – persisting, getting,
and deleting state. After we explored how to use the basic operations, I introduced you to
the transactional support that some state stores have. During the course of the chapter,
you learned how to work with concurrency and consistency and how to pass metadata
items to the state store whenever executing a state operation.
In the next chapter, you are going to see how the Resource Bindings building block
can help you integrate your Dapr-based applications with various external systems.
158
CHAPTER 9
Resource Bindings
So far, I have shown you how to interface with other services within your
microservices-based application. This chapter will be about integrating with various
external services. The communication typically happens in two ways – either an external
service creates an event that a local Dapr service listens for or a local Dapr application
makes a request to an external system. Dapr has a building block for talking to external
systems, which logically can be split into two groups from a logical perspective – input
and output bindings. I will enumerate the various systems that the Resource Bindings
building block integrates with and whether it is for input or output or it is bidirectional.
Then I will explain what the API of the Resource Bindings building block looks like. To
demonstrate how things work, I will walk you through implementing a solution that
monitors an Azure Blob Storage container for newly uploaded photos using Azure Event
Grid; it performs object detection, then sends the recognized and tagged images to the
same storage account, and also persists the objects identified to a state store.
159
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_9
Chapter 9 Resource Bindings
protocols. You get the idea: there is a huge variety of services, each of them providing
various communication options – APIs, SDKs, libraries, and so on. But more holistically,
there are basically two reasons you want to do this – to receive some input from an
external system and process it or to send a request to an external system and receive the
result, if any. That’s why the Bindings building block aims to provide a universal way to
interact with external systems by generalizing the communication with them behind the
Dapr API.
The building block can be divided into two subparts – input bindings and output
bindings. Input binding is when an external system triggers an endpoint of a Dapr
application. It typically happens when some events happen in this external system and
your application must be notified in order to process the new events. The building block
relies on a component of a specific type. I will describe the component structure in
more detail later in this chapter. The trigger of the input binding is described inside the
component manifest. For example, it can connect and listen to some message queue
and get triggered whenever a new message is created. There are a lot of message queues
that are supported by the Bindings building block. Another example can be when you
want to receive and process tweets that match a particular query that was defined in the
component. Depending on the component type you choose, you can achieve different
things.
Output binding is when your Dapr application needs to gather some data from an
external system or execute some action in the external system. Therefore, it calls its Dapr
sidecar, which relies on the specific component that you have defined to talk with the
external system. By using an output binding, you can execute a SQL SELECT statement,
for example, and receive the result, or you can send a message to a queue, or you can
send an SMS. Some of the output bindings return results, while others do not.
Supported Bindings
I will intentionally start by enumerating all supported external systems at the time of
writing the book so that you get a better idea of what is possible by using this building
block. For each supported external system, there is an implementation of a component.
The components of the Bindings building block should implement at least one of the
interfaces – either for input or output. Some of the components support both directions.
Once you understand what Bindings components exist, I will walk you through the API
of the building block.
160
Chapter 9 Resource Bindings
I will categorize the supported components into several groups to make it easier to
absorb the information because there are a lot of supported systems.
G
eneric Components
Table 9-1 contains all the supported external systems that are not a part of any of the
public cloud platforms. Some of them can be hosted anywhere – on-premises or in the
cloud. Others like Apple Push Notification Service and Twilio SMS are proprietary and
therefore comply with the Software as a Service (SaaS) model. By looking at the table,
you can tell whether the component supports input, output, or bidirectional bindings.
Under the column Input triggers, I have listed the events that cause the input binding to
be triggered. Under the column Output operations, you can see what operations you can
execute via the output binding on a specific external system.
161
Chapter 9 Resource Bindings
162
Chapter 9 Resource Bindings
As you can see from Table 9-1, there is a broad range of components that Dapr
provides for the Bindings building block. Among them are message queues, common
protocols, databases, notification services, mailing services, and even social networks.
With time, the number of supported external systems will grow because you can
implement a Bindings component for every system that exposes some kind of API that
other applications could use.
You may have noticed that some of the external systems are supported by other types
of components as well. For example, there is a Redis component for State Management,
Publish and Subscribe, and Bindings. If you use all those building blocks, you can point
the component to the same Redis instance, and you won’t experience any conflicts.
That is because each component type uses a different Redis data structure and applies
different semantics. The Publish and Subscribe component uses Redis Streams, the State
Management component uses Redis Hashes, and the Bindings component uses Redis
Strings.
When it comes to Apache Kafka, there is also an implementation of a Publish and
Subscribe component. The difference is very slight – the Bindings component expects
a consumer group to be defined and reads only from it, whereas Publish and Subscribe
enables you to leverage the fan-out messaging pattern where the consumer group is by
default assigned to the App ID of each subscriber that consumes messages on its own
pace. The Bindings component applies the message queue semantics of all messaging
systems that are supported. This means that if you happen to have multiple services that
can be triggered by the same input binding, they will compete for messages. The first
service that takes a message and processes it successfully will acknowledge the message,
so it won’t be received by other services.
There is one outlier in Table 9-1. That is the Cron component. The Cron component
does not interface with any external system. Instead, it provides a way to schedule
the triggering of an endpoint. Let’s say that every 30 minutes your application should
check for new subscribers in your mailing list to send them a welcome email. In order
to achieve this, you have to create a Cron component, and you can either use one of the
shortcuts like “@every 30m” or the full CRON expression “0 30 * * * *”. This means that it
works the same way as the other input bindings; the difference is that it is not triggered
by an external system but on a schedule instead. Then you have to create the respective
endpoint that is going to be triggered. It should be named after the name of the
component. Note that the scheduling depends on the start time of the Dapr sidecar – no
matter if it is in Self-hosted or Kubernetes mode. Whenever a sidecar process/container
163
Chapter 9 Resource Bindings
is created, the clock of the Cron scheduler starts ticking away. As you know, Pods are
ephemeral, so don’t expect the Cron to happen consistently on the defined schedule. If
a Pod is being recreated, the schedule can be twofolds late at most, and this can happen
if the Pod was destroyed right before it was about to trigger the input binding. Then the
new Pod will start counting from zero.
164
Chapter 9 Resource Bindings
Next, you can find all the supported services within Amazon Web Services listed in
Table 9-3.
The supported services from Google Cloud Platform are enumerated in Table 9-4.
And there is one service supported in Alibaba Cloud that is listed in Table 9-5.
165
Chapter 9 Resource Bindings
• No SDKs or libraries are required – you specify the component for the
external service you want to use by putting in the metadata specific
to the particular system. From there on, you leave it to the API of the
building block.
• You can focus on the business logic instead of thinking about how to
implement an integration with some external system.
Binding Components
As usual, the component is what defines the specific behavior of the building block.
Once you have chosen the specific component type you want to use, then you have
to make sure to supply values for connection information properties and some other
metadata specific to the particular external system that this component interfaces with.
A single component can support both input and output bindings from/to an external
system.
For example, the following snippet is a definition of a binding component for Kafka:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mykafkabinding
namespace: default
spec:
type: bindings.kafka
version: v1
metadata:
- name: brokers
value: "http://localhost:9092"
166
Chapter 9 Resource Bindings
Let’s first focus on the metadata section, which in this case is specific to the Kafka
component. In order to connect to an external system, you have to specify its address
and some authentication information. In the preceding case, Kafka runs locally, and it
doesn’t require any credentials. Some properties can be used for both input and output
bindings, while others are specific to one of them. In the case of Kafka, the topics and
consumerGroup elements apply only for the input binding, which consumes events from
one or more topics by using a consumer group. The topic specified in publishTopic is
used whenever the output binding is invoked, and that is the topic that ends up receiving
the event.
The name of the component is being further referenced by any input and output
binding requests.
I nput Bindings
Input bindings are useful when a Dapr application needs to be triggered by an event
that happened in an external system. Once you have defined a binding component that
supports input bindings, Dapr will try to identify what applications can be triggered
by this input binding. Remember there are namespaces and scopes applicable for
components, so not all components can see all applications and vice versa. The Dapr
runtime will try to execute an OPTIONS request to a route with the name of the binding
component, and if it receives anything other than 404 Not Found, this will mean that the
Dapr application subscribes to the input binding.
When the application registration is done, the input binding works as shown in
Figure 9-1.
167
Chapter 9 Resource Bindings
On the left side, you can see that the flow is initiated by one of the supported external
systems. When the input binding detects a new event that occurred, Dapr calls into an
application endpoint with the same name as the binding component. The body of the
request will contain the payload coming from the external system. That’s where things
start to get specific. You cannot really swap one component for another because the
application processing the payload expects a payload with a specific structure.
When the service is done processing the payload, it should return a successful
response with status code 200. It means that the input binding was successful, and the
event should be marked as processed and removed. Otherwise, Dapr will assume that
the event wasn’t processed successfully and will try to redeliver it. As I just mentioned
redelivery, maybe you are wondering what the event delivery guarantees are. This
depends solely on the component implementation – it can be either exactly once or at
least once.
Returning just status code 200 is enough for Dapr to know the event was processed
successfully. There is more you can do just by providing a response body, although I
don’t recommend doing so for various reasons. However, this is currently supported,
and we should explore it. For example, you can directly invoke one or more output
bindings or persist some state in a state store. In the case of output binding, the limitation
is that you cannot receive the response, since the output binding was invoked by the
Dapr app response to the input binding trigger. It’s a fire-and-forget kind of action.
168
Chapter 9 Resource Bindings
The following JSON is an example of a response body that persists some data in a
state store and invokes output bindings:
{
"storeName": "stateStore",
"state": stateData,
This will instruct Dapr to do two things – persist some data into a state store named
stateStore and invoke output bindings named storagebinding and queuebinding by
passing them some payload. Here stateData is a placeholder for an array of JSON state
objects as state items will be applied in bulk. The outputBindingData is a placeholder
for the JSON object that the output bindings that you are targeting expect. Note that
you can specify more than one binding, but the data you pass to them is a single object.
You should have this in mind as some bindings might expect a different input format.
Currently, there is no way to pass metadata to those bindings. You will learn about what
metadata can be supplied when I explain how the output bindings work. If you have
specified multiple bindings, they can either be invoked in parallel or serial (one after
another), which is specified by the concurrency property.
Note The option to return a response with payload from a Dapr application upon
processing an input binding trigger will be deprecated in Dapr v1.1.0 and removed
after that. If you still need to persist some state or invoke an output binding, then
simply use the respective endpoints from the Dapr API.
O
utput Bindings
Output bindings allow you to call an external service and optionally receive some data
from it. After you have defined a component (or created the CRD in Kubernetes mode)
that supports output binding, you simply reference it by name in any requests to it as
shown in Figure 9-2.
169
Chapter 9 Resource Bindings
As you might have noticed, this time the components stay on the right side as the
initiator is the Dapr application itself. It issues a POST or PUT request to the /bindings
endpoint specifying the name of the binding that is targeted. This example shows how
to use the HTTP-based API of the building block, but keep in mind that you can also
achieve the same by calling the respective gRPC method.
The structure of the payload you are passing when invoking an output binding looks
like this:
{
"data": someJsonObject,
"metadata": {
"key1": "value1"
},
"operation": "create"
}
Although the payload has a generic structure, the values of the data and metadata
properties are determined by the specifics of the external system you target and the
binding component for it. The data property contains the data that the external system
expects. It can be the contents of a file, a message, and so on.
The operation property states what operation you want to execute. There are several
standard operations defined in Dapr – get, create, delete, and list. You can revisit the
tables with the supported bindings to check what operations each component supports
for output. Some components stick to the standard operations, while others define a set
of custom operations.
170
Chapter 9 Resource Bindings
The metadata property is a set of key-value pairs that specify certain properties that
are used when performing the operation in the target external system. This typically
contains information that is very specific to the component implementation and the
target external system. If certain metadata items are not specified with the request
payload, the component will usually try to get them from its metadata. Maybe you would
want to try invoking an output binding without any metadata for the sake of keeping
your code loosely coupled. But the reality is that you cannot fully invoke an output
binding without passing some metadata. For example, imagine you want to persist some
object in Redis by invoking the operation create. You pass the object value into the data
property of the payload, and you do not pass any metadata. The request will fail because
the Redis binding component doesn’t know what key to use to persist the value. The list
of examples goes on and on.
When implementing the endpoint to be triggered by an input binding or the code
invoking an output binding, you always have to keep in mind what is the target external
system you are invoking. You need to have a good grasp on what is supported, what is
the schema of the input payload, and what data and metadata you can pass to the output
binding.
O
verview
Since the interaction between various parties is quite wild, you can see what actions take
place in the process by looking at the sequence diagram in Figure 9-3.
171
Chapter 9 Resource Bindings
2. Azure Event Grid detects the uploaded image and creates a new
message in an Azure Storage Queue.
3. The Dapr sidecar notices the new message in the Azure Storage
Queue and triggers the service endpoint that is going to contain
the main logic. We are going to explore its implementation.
172
Chapter 9 Resource Bindings
The ONNX model that I use for object detection is YOLO version 4. This is a
pre-trained model that can recognize objects from 80 classes. It is fast enough that
it can be used for real-time object detection, for example, think about recognizing
objects from a web cam stream.
ONNX is an abbreviation of Open Neural Network Exchange. ONNX is an open
source format used to represent machine learning and deep learning models. The
model format was initially introduced by Facebook and Microsoft in 2017, but later
on, other companies joined the project as well. ONNX is a description of a model
made in Protobuf format, which aims to ease the interoperability between machine
learning frameworks and to allow hardware vendors to maximize the performance of
model inference. In other words, ONNX enables you to build and train your model in a
framework of choice and then convert it into ONNX and run it anywhere you want. For
example, the YOLO v4 model was originally a pre-trained TensorFlow model, which was
later converted into ONNX. The inference is the step that comes after training. During
training, the model relearns a new capability, whereas during inference it applies this
capability to some new data it has never seen. There are various runtimes that support
ONNX models; among them are ONNX Runtime, NVIDIA TensorRT, and Windows
ML. For this example, I will be using the support for ONNX Runtime in ML.NET. ML.NET
is a machine learning library for .NET. Since the service will be implemented in ASP.NET
Core, ML.NET is the perfect match for doing the inference of the ONNX model.
You might be wondering what the end result of a model inference will look like. As
shown in Figure 9-4, those will be the objects detected when you upload this photo of
two adults crossing a street and pushing a stroller with a very special dog riding it. As you
can see, several object types were detected: person, fire hydrant, and traffic lights.
173
Chapter 9 Resource Bindings
174
Chapter 9 Resource Bindings
that your machine is using. Your machine is connected to a network in which you have
a private IP address. This means that if a service like Azure Event Grid wants to directly
trigger an endpoint of a service running on your machine, this won’t be possible because
you don’t have a public IP that it can reach. You will end up using a tool like ngrok to
open a tunnel to your machine.
Before I show you any code from the service, let me first guide you through the
process of creating the Azure resources that will be needed. For the next script, you
will either need Azure PowerShell installed on your machine, or you can directly open
shell.azure.com and choose PowerShell. In the latter case, you have to comment out
the Connect-AzAccount cmdlet as you will be automatically authenticated. Please make
sure that before running the script, you have selected the correct subscription where
you want to provision the resources in. You can check what the current subscription is
by executing (Get-AzContext).Subscription. If you want to select another one, you
can use the Select-AzureSubscription cmdlet:
$location = 'westeurope'
$resourceGroupName = 'images-detect-rg'
$storageAccountName = 'imgdetectstor'
$storageContainerName = 'images'
$storageQueueName = 'images'
Connect-AzAccount
$newEventGridSubParams = @{
EventSubscriptionName = 'new-image'
ResourceId = $storageAccount.Id
175
Chapter 9 Resource Bindings
EndpointType = 'storagequeue'
Endpoint = "$($storageAccount.Id)/queueServices/
default/queues/$storageQueueName"
IncludedEventType = @('Microsoft.Storage.BlobCreated')
SubjectBeginsWith = '/blobServices/default/containers/images/
blobs/input'
SubjectEndsWith = '.jpg'
}
New-AzEventGridSubscription @newEventGridSubParams
Feel free to change the values of the variables at the beginning of the script as the
names of the storage accounts must be globally unique. Hence, make sure to change
the value of $storageAccountName. Also, depending on where you are, you may want to
choose another Azure region and set it to the $location variable. Note that I removed
some conditional statements that prevent the partial execution for the sake of placing
simpler code in the book. You can find the full version of the script that first checks
whether a resource exists and then attempts to create it in the source code of the book.
When this script finishes successfully, it will output the value of the first key of the
storage account, which will be needed when creating the component manifests.
By now, in the resource group that you have created with the script, you should have
a storage account with a single container named images and a single queue named
images. An Event Grid System Topic has also been created to serve the subscription. The
subscription is triggered for any file having the .jpg extension that is uploaded into the
input folder in the storage container. If you open the Azure Portal, the resource group
will look like as displayed in Figure 9-5. It’s a good idea to explore the storage account
resource to check whether a container and a queue were properly created.
176
Chapter 9 Resource Bindings
Figure 9-5. The resources and the resources created by the script
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azqueue
namespace: default
spec:
type: bindings.azure.storagequeues
version: v1
metadata:
- name: storageAccount
value: "<storage-account-name>"
- name: storageAccessKey
value: "<storage-account-key>"
177
Chapter 9 Resource Bindings
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azblob
namespace: default
spec:
type: bindings.azure.blobstorage
version: v1
metadata:
- name: storageAccount
value: "<storage-account-name>"
- name: storageAccessKey
value: "<storage-account-key>"
- name: "container"
value: "images"
- name: decodeBase64
value: "true"
Make sure to replace the storage account name and the storage account key with
values that are applicable to your resources. As you can see, the azqueue component
points to a queue named images, and the azblob component points to a container
named images. Both components work with base64-encoded values as a payload. I
have to point out that even if we are about to use the azblob component for performing
output bindings and the azqueue component for responding to input bindings, this is not
specified anywhere. If the component supports bidirectional bindings, it is up to your
service code to decide how to utilize the binding via the Dapr API.
178
Chapter 9 Resource Bindings
I mplementing the Service
The image processing service will be implemented using ASP.NET Core. This
time I won’t guide you through the process of creating a new project as it is
straightforward. The ONNX model inference is isolated in a class library project named
ObjectRecognition that is referenced by the ASP.NET Core project. This chapter doesn’t
cover topics like how to use ONNX, but feel free to explore the code on your own because
I know that using ready-made models feels very exciting. Just make sure that the ONNX
model is located in the following path: ObjectRecognition/Model/yolov4.onnx. If it
is not, you can download it from the ONNX Model Zoo: https://github.com/onnx/
models.
The service consists of a single controller named ImagesController, which has a
single Post action method that handles the input binding that is triggered by the Azure
Storage Queue. Note the default route for the controller will be /images, which doesn’t
have anything to do with azqueue. To make that possible, note that the RouteAttribute
was applied to the whole controller, which means it will resolve the requests coming
from the Azure Queue component at /azqueue. After the PostAsync method receives
the URL of the uploaded blob, the DownloadFileAsync method invokes the Blob Storage
output binding in order to download the photo. Note that the operation property is get
and the blobName is passed as metadata:
179
Chapter 9 Resource Bindings
Once the image is downloaded, it is saved to a temporary file that is later used
for inference. The next step is to perform the ONNX model inference. As I already
mentioned, the code that performs the inference is isolated in its own project. When
the inference is done, the model saves a tagged image that has all recognized objects
outlined, and a list with the counts of all recognized object classes is returned as well.
The next step is to upload the file to Azure Blob Storage by using the create operation
of the output binding. The image is being serialized into base64 format, and the relative
path to the blob inside the container is passed as metadata so that it knows where to
upload the new file:
180
Chapter 9 Resource Bindings
After all those steps are performed, the request that initially triggered the Dapr
application via an input binding calls ControllerBase.Ok() to produce an empty HTTP
status code 200 response. In this way, Dapr is getting acknowledged that the triggered
endpoint successfully processed the incoming request.
Alternatively, if you want to debug the whole application, you can start just the Dapr
sidecar and launch the debugger manually (from either Visual Studio 2019 or Visual
Studio Code). An important detail is to fixate the Dapr sidecar HTTP port by passing the
optional flag:
Also, make sure the logs returned by both the application and the Dapr sidecar look
good, that is, there are no errors logged.
Next, it is time to upload a new image to a folder named input in the storage
container. There are two options – Azure Storage Explorer, which is a cross-platform
desktop application, or its web counterpart in the Azure Portal – you can find Storage
Explorer in the blade of the storage account we created earlier.
For this, I will open Azure Storage Explorer, locate the subscription where I created the
storage account, and open the images container inside the storage account. Next, I will
create a new folder named input, and I will upload a new image as shown in Figure 9-6.
181
Chapter 9 Resource Bindings
Figure 9-6. Uploaded a new photo into the input folder under the images
container
After you have uploaded the photo, it’s time to go back to the terminal session where
the Dapr sidecar is running. If everything was successful, you should be able to see logs
for each step that we discussed earlier. Now to see the tagged image, go back to Azure
Storage Explorer and go one directory up the hierarchy. You should be able to see the
output folder, which was created automatically where tagged images will be uploaded to.
Your result image should already be here with all the recognized objects outlined. Lastly,
if you open the terminal where you started the application, you should be able to see the
operations performed by the application and also the recognized objects:
182
Chapter 9 Resource Bindings
Summary
In this chapter, you learned what the capabilities of the Resource Bindings building block
are. There are two types of bindings that can be identified – input and output. There are
ready-made components for external systems and services in several public clouds that
support input, output, or bidirectional mode. Next, you learned how components are
defined, and we explored the API that the Resource Bindings building block provides.
Finally, I showed you how to build an image processing application based on ASP.NET
Core that combines the integration capabilities of both input and output bindings. It
integrates with both Azure Blob Storage and Azure Queue Storage and utilizes an ONNX
object recognition model in the middle.
In the next chapter, you will learn about what the Actor model is and how Dapr
simplifies it by providing an Actors building block.
183
CHAPTER 10
185
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_10
Chapter 10 The Actor Model
However, there is one important difference between the two models. This is how
they deal with concurrency. If we compare the Actor model to OOP in a single-threaded
world, they will be almost identical on an abstract level, ignoring all technical details.
However, it is not reasonable to write single-threaded applications as this means that
at a given time, there will be only one task that is performed. Imagine your application
starts a blocking operation to read something from the disk; this will render your single-
threaded application unusable until it finishes processing the blocking operation. So
apparently, concurrency is a must for modern applications. Actors were devised to
be used in concurrent systems, whereas the OOP paradigm itself does not impose a
specific way to achieve concurrency. If you want to achieve concurrency in traditional
OOP languages, you have to spawn multiple threads that can do some work in parallel.
But multithreaded programming requires mastery as it is fairly easy to introduce race
conditions, for example, if your threads depend on some shared state. As a way to
solve this problem, one has to introduce locks or mutexes. But it really depends on the
developer’s skills to early identify the problem that may likely occur. On the contrary,
actors address the concurrency in a very simple manner. You can think about creating
an actor as spawning a new thread, although that’s not always the way it happens
technically. There are some restrictions that actors impose – they have some dedicated
memory to represent their state, but it’s not shared with any other actor. Unlike threads,
the only way to gain access to the actor’s state is by sending a message to it. Since actors
don’t depend on anything from the current machine, it makes them easy to distribute,
which means that they can run on different machines. Traditionally, concurrency
is achieved by using multiple cores of the CPU, whereas the Actor model blurs the
boundary of cores, CPUs, and even machines. As a result, this makes the implementation
of distributed and concurrent applications simpler, and you don’t have to care about
complex stuff.
To summarize, actors are independent units of computation that communicate by
sending or receiving messages and can make a decision based on their local persistent
state. Although multiple actors can perform some logic at the same time, it is very
important to highlight that each actor processes messages sequentially. This means that
if a single actor receives multiple messages at the same time, it will process them one at
a time. That’s why the definition of the Actor model states that each actor has a mailbox
that stores all messages. That’s pretty much like a message queue. The operations inside
every actor happen on turn-based concurrency, which creates the illusion of single-
threaded processing. Having the guarantee that only one operation will be performed
186
Chapter 10 The Actor Model
at a time protects against having the standard problems with concurrency. If you want
to perform actions in parallel, you have to create more actors. Whether this makes any
sense really depends on the problem you want to model, and I don’t really encourage
you to use actors if you solely need to achieve higher parallelism. Depending on the
domain, actors can be modeled after various objects – actors can perform mathematical
operations or models, for example, a finite-state machine, serve as digital twins of an IoT
system, represent game objects, or even model technical concepts, for example, the Saga
pattern that I described in Chapter 1: Introduction to Microservices.
If you were to model a parking system using the Actor model, you would most likely
try to identify what are the various classes that build the digital model of a parking
system. Let’s assume these are parking sensors, a parking space containing one or
more parking sensors, a parking lot that groups a number of parking spaces, parking
levels, parking ramps, and so on. As the example implies, there is a hierarchy of objects
because, for example, parking sensors are assigned to a single parking space, which is
part of the parking lot. This means there is a tiered control, where the top-level actors
assign subtasks to their child actors. The child actors are in fact supervised by the
top-level actors. The number of actor instances can reach millions, while the number of
actor types depends on the actual design. It follows pretty much the same logic as OOP;
objects are instances of classes. The turn-based concurrency applies to actor instances
and not actor types, as shown in Figure 10-1. The rule is that concurrent operations to
the same actor instance are not allowed. Instead, they are processed sequentially.
187
Chapter 10 The Actor Model
Each actor has a unique address that is assigned at the time of creation. When
you want to send a message to an actor, you send it to the actor’s address. Actors can
communicate only with actors whose addresses they know. An actor that creates other
actors inherently knows their addresses, so it can directly send some messages to them. As
actors are newly created, they have an initial state and behavior. With time, an actor will
process messages that will cause state and behavior to change for any upcoming messages.
Advantages and Disadvantages
Now that you’ve learned what the Actor model is, let me identify some of its benefits:
• Easy to scale – Actors don’t have any dependency on the local state
or anything on the machine. This means that you just create new
instances without trying to keep track of any boundaries. Actors can
be placed on the same machine or any other machine. The actor
runtime will decide what is optimal.
188
Chapter 10 The Actor Model
• Easy distribution – The actor you are trying to address can be running
locally or in a node in a remote data center. It doesn’t matter where it
is located as long as it receives the messages.
• Too many actor types and/or instances – The Actor model implies
that everything should be an actor, but the granularity depends
on your design. Creating too many actor types generates a lot of
overhead because they have to create and pass messages over the
network. Creating too many actor instances doesn’t necessarily mean
that your system will be faster, but for sure you will waste some of the
189
Chapter 10 The Actor Model
190
Chapter 10 The Actor Model
state, then you probably should revisit why you use the Actor model in the first place. On
the contrary, if you store too much state, upon each operation that the actor performs,
it will take more time to gather and persist the state. Therefore, this will prove to be a
bottleneck. This also applies to any blocking I/O operations.
Actor Implementations
There are a lot of frameworks that provide you with the Actor model. There are even
languages that provide out-of-the-box support for actors – the processes in Erlang are
equivalent to actors. One of the most notable frameworks that implement the Actor
model is Akka that runs on Java Virtual Machine (JVM) and therefore supports Java and
Scala. There is a port of Akka for JavaScript called Akka.js and one for .NET called Akka.
NET that supports C# and F#.
Other examples are Orleans that runs on .NET, Service Fabric Reliable Actors that
supports .NET and Java, and, of course, Dapr that is language-agnostic. Each of the
above-mentioned frameworks and languages imposes its own opinions about the Actor
model albeit using the same foundation. I grouped the last three examples intentionally,
as they have a common virtue. And it’s not only that Microsoft created them.
Virtual Actors
The virtual Actor model was introduced by Project Orleans that was created by Microsoft
Research. Virtual actors use the same analogy as virtual memory does – it allows clients
to invoke a virtual actor by calling its address no matter where the actor is physically
placed (if at all). This vastly simplifies the Actor model as callers don’t have to think
about where the actor is placed and how to balance the load and recover from failures.
The actor runtime itself takes care of those concerns. When a request to a given actor
arrives, the runtime checks whether such an instance exists somewhere in the cluster.
If it cannot find the actor, it is activated in some node of the cluster, which means that
an in-memory instance of the actor is being created. Over time, unused actors will be
garbage collected. Albeit being unallocated from memory, once a virtual actor is created,
it has a perpetual existence, and therefore its address remains the same. The reason
for this is that the state of an actor always outlives the lifetime of the actor in-memory
object. Whenever the next request to this address comes, the actor will be materialized in
memory with the same state as before it was garbage collected.
191
Chapter 10 The Actor Model
Now that you know the idea of the Actor model and the concept of virtual actors, let
me introduce you to the support the Actors building block in Dapr provides.
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
192
Chapter 10 The Actor Model
It is very useful to know how the state is persisted inside the chosen state store if you
intend to issue any queries to get the state of all actors. The naming convention of the
key for the state of each object follows the scheme <APP-ID>||<ACTOR-TYPE>||<ACTOR-
ID>||<KEY> where
• App ID – The Dapr App ID of the application that hosts the actor.
• Actor type – Represents the type of the actor.
• Actor ID – The unique ID of the actor that allows you to address
individual actor instances.
• Key – The name of the state key. The reason this exists is that actors
can persist their state using multiple keys.
As I already explained about virtual actors, actors are being activated upon their first
use, which means that an object representing the actor gets created in memory. After
some time passes, Dapr scans for any actors that are idle and deallocates them. The
exact address of an actor instance in Dapr is determined by its Actor ID and the actor
type. Dapr does the heavy lifting for managing actors across all available nodes – it has to
ensure that there is a single activation of a given Actor ID across the entire cluster, ensure
that a single client can access an actor at a time, and keep track of where actors are
placed. There is a system service inside Dapr called the Placement service that ensures
actors work properly. It is only used by the Actors building block for keeping track of
actors. If you don’t plan to use the Actors building block, you can choose not to deploy it.
The early versions of Dapr used to support only a single instance of the Placement
service. But since this used to be a single point of failure for the Actor model, something
had to be done. The Placement service keeps the placement tables that contain the
actor hosts and the actor instances that are spread across all actor hosts in the cluster.
It also establishes a communication channel with all the Dapr sidecars, each of which
keeps a local cache of the latest version of the placement tables. You can think of the
placement table as a hash table. Using a hash function, it maps the actor type and Actor
ID to a given actor host (represented by its sidecar), except that hashing is not so simple
in this scenario. For the next couple of paragraphs, I will use servers, nodes, and Pods
interchangeably while the meaning is actor host.
Kubernetes environments are of a very dynamic nature – Pods come and go, and even
nodes can go down, and that is why you have several of them. This means that the internal
address and number of Pods can vary at any given moment. Having a simple hash table
that is based on the existence of individual Pods won’t take you much further. The reason
is that with any change in the number of Pods (which may occur frequently), the whole
hash table will have changed. The naive modulo hashing function works well as long as the
list of buckets remains stable. When a bucket is either added to or removed from the list
(or an actor host Pod), the majority of requests will be resolved to another bucket, and only
a small part of the requests will still be resolved to the same host. For a live environment,
this means that actor instances must be reshuffled to keep up with the structure of the hash
table, or the hash table itself must be remapped to fit the new list of buckets.
Instead, Dapr uses a modern hashing approach named consistent hashing with
bounded loads. Consistent hashing is very often used whenever you have to shard a
cache across a set of nodes, which is also called a distributed cache. By using consistent
hashing, you will ensure that whenever a Pod is either added or removed, the majority
of keys will still map to the same bucket (or Pod). It operates independently of the
number of buckets by assigning the keys at a given position on an abstract circle called
a hash ring. The request is not directly mapped to a given bucket; instead by using
approximations, the hash is mapped to the closest bucket to it on the hash ring. But there
is still one problem – the classic consistent hashing does not deal with load balancing
across buckets. It’s pretty much a random assignment to some bucket, which sometimes
causes unlucky buckets to become overloaded. That is why the hashing algorithm is
called consistent hashing with bounded loads. It deals with both of the problems – have
a minimal number of moves whenever something in the environment changes and
address the problem with load balancing across servers by setting a tight guarantee on
194
Chapter 10 The Actor Model
the maximum load of each bucket. Eventually, the placement tables that use the hashing
approach described in the preceding will be distributed to all Dapr sidecars to serve as a
local cache.
Now that you know the concepts, let’s take a look at the place where the placement
tables are kept as a single source of truth – the Placement service. Having a state and
a distributed system to manage it is usually a tricky problem as well. You cannot solve
it by simply replicating the state manager because the state that the Placement service
persists is applicable for the whole cluster. Instead, all instances of the Placement
service have to agree on what the latest state is. There is a whole other category of
algorithms that lets multiple participants reach a complete consensus on a thing, called
consensus algorithms. The most famous one is Paxos while being also the foundation of
distributed consensus algorithms. However, Dapr uses Raft that was designed to be easy
to understand and also easy to apply because there are a lot of implementations of it that
can be easily reused.
Nodes in Raft are in one of three states – leader, follower, or candidate. While the
candidate is an intermediary state, there is one node appointed by quorum voting to be
a leader, and the other nodes become followers. The leader is the conductor of the whole
consensus-building process. Each server in the cluster has a log and a state machine.
The state machine is the component that has to be made fault-tolerant and is further
used by clients. In this case, the state machine, which is the application-specific part
of the algorithm, represents the Actor placement tables in Dapr. However, internally,
the changes are first made to a write-ahead log, and the consensus is actually reached
on it. The reason for this is that by following the log entries, you can recreate the most
recent state. All new state changes that come from clients are brokered by the leader at
first, which translates them into a log entry and replicates them to all followers. Once a
quorum of nodes (N/2+1) acknowledges that the log entry is persisted, it is considered as
committed and applied to the finite state machine.
The interaction between actor host sidecars, the Placement service, and the ordinary
Dapr sidecars is depicted in Figure 10-2. Let me walk you through what happens behind
the scenes. On the left-hand side, you can see there are a bunch of actor hosts that are
represented by their Dapr sidecars. Whenever a change in any of the actor hosts occurs,
the Placement service has to be informed, which internally executes the Raft algorithm,
and when consensus is reached eventually, all Dapr sidecars will receive the latest state.
Such a change may be when a new Pod that is hosting actors of some type joins the
cluster or maybe some Pod went down (in other words, leaves the cluster).
195
Chapter 10 The Actor Model
2. The leader converts the request into a log entry and appends it
to the local log. While doing so, it also sends the log entry to all
followers and waits for their responses.
3. Once a quorum of all nodes successfully persists the log entry, the
leader applies the log entry to the state machine or in this case the
Actor placement tables.
a. In the first phase, all Dapr sidecars are locked, which means that incoming
requests will be held for a while.
b. Then in the second phase, after all sidecars are locked, the update of the
placement tables commences in all sidecars, so that they update their local
copies of the placement tables.
c. In the third phase, when the sidecar update is done, all sidecars are
unlocked.
196
Chapter 10 The Actor Model
In this way, it is ensured that all Dapr sidecars use the same version of the Actor
placement tables. That was enough to get an idea of the Placement service. Let’s explore
how the building block works.
{
"entities": ["actorType1", "actorType2"],
"actorIdleTimeout": "1h",
"actorScanInterval": "30s",
"drainOngoingCallTimeout": "30s",
"drainRebalancedActors": true
}
197
Chapter 10 The Actor Model
For example, if you want to register a timer or reminder that is fired 3 seconds after
registration and then every 6 seconds, it will look like this:
{
"dueTime":"0h0m3s0ms",
"period":"0h0m6s0ms"
}
198
Chapter 10 The Actor Model
Concurrency
As you already know, only one operation can happen at a given time inside an actor
instance. Although, from a technical point of view, every method or timer/reminder
callback is asynchronous, those are not executed simultaneously. Instead, an operation
can place a lock on a given actor instance only in case if there isn’t any other active
operation at the moment, as shown in Figure 10-3.
As you can see, all operations on the actor with ID 1 happen sequentially, no matter
if they are methods, timers, or reminders. If currently there is an ongoing operation in a
given actor, at a time the new request comes, it waits asynchronously to take its turn. And
in parallel, before all pending operations on the actor with ID 1 finish, the same MethodA
executes on the actor with ID 2, but those are different instances of the same type.
199
Chapter 10 The Actor Model
http://localhost:<daprPort>/v1.0/actors/<actorType>/<actorId>/<method|state
|reminders|timers>/<name>
For example, if you want to invoke MethodA of an actor with ID ID1 and type
ActorTypeA, the request will look like the following:
POST http://localhost:<daprPort>/v1.0/actors/ActorTypeA/ID1/method/MethodA
Of course, you can pass some data in the body of the request. And to summarize, as a
client, you can do the following actions via the Actors API:
• State
• Reminders
• Create a reminder.
• Delete a reminder.
• Timers
• Create a timer.
• Delete a timer.
As you notice, the API gives you access to pretty much any property of an actor.
However, think twice before using any other endpoint than the one for method invocation
on an actor. The reason is that the Actor model was designed to have a self-sustaining
system of actors as the main unit of computation. Only an actor knows best how to
manage its own state and when it makes sense to register or delete a reminder.
That was all about the Actors API from a client perspective. Let’s take a look at how
actors can be implemented.
200
Chapter 10 The Actor Model
Implementing Actors
An actor is an ordinary web service. Actors must implement a certain set of API
endpoints via HTTP so that Dapr can interact with the actual actors on the host. It
doesn’t make a lot of sense to explain what these endpoints are because you will most
likely use a certain Dapr SDK for implementing actors. The SDK will automatically
implement those methods. It doesn’t make sense to try to implement the hosting from
the ground up.
I am going to show you how to use the .NET SDK to implement a set of actors
representing a room (a smart one) and some devices in it as shown in Figure 10-4. I
will also show you how to build a client application that will act like a person who just
got back from work and enters the living room, turns on the lights, and turns on the air
conditioner. The air conditioner is yet another actor type that is instantiated by the room
actor. It has a temperature sensor as a discrete actor that it pings every 10 seconds (using
a reminder) in order to know what the current room temperature is. When the room
temperature converges with the target temperature, the air conditioner will become idle,
to save on your electricity bill. What a smart room, huh?
Let’s get started! We will create three projects in total – one class library for the actor
interfaces, one for the ASP.NET Core API that is going to implement and host the actors,
and another one that will be a console application acting as a client of the actor system.
Since I am going to build a kind of home automation solution, every project name will be
prefixed with Home. Before everything, make sure you have defined a state component
that supports transactions to persist the state of your actors.
201
Chapter 10 The Actor Model
cd Home.Actors
dotnet add package Dapr.Actors --version 1.0.0
3. It’s time to define the interface for each actor. The interface simply
inherits the Dapr.Actors.IActor interface. Each actor method
returns a Task. Let’s start with the IRoomActor interface. It has
methods for switching the lights on or off and getting the ID of
the actor for the air conditioner and a Describe() method that
describes the current state of the room:
202
Chapter 10 The Actor Model
203
Chapter 10 The Actor Model
Implementing the Interfaces
Now that you have defined the interfaces, it is time to implement them in an ASP.NET
Core Web API project:
1. Navigate to the root folder of the solution and create a new ASP.
NET Core Web API project:
2. Navigate to the project folder and add the latest version of the
Dapr.Actors.AspNetCore NuGet package, which at the time of
writing is 1.0.0:
cd Home.ActorsHost
dotnet add package Dapr.Actors.AspNetCore --version 1.0.0
await StateManager.SetStateAsync<double>(STATE_NAME,
currentTemperature);
return currentTemperature;
}
205
Chapter 10 The Actor Model
206
Chapter 10 The Actor Model
if (!stateExists)
{
var data = new AirConData()
{
TemperatureSensorActorId = ActorId.
CreateRandom(),
IsTurnedOn = false,
Mode = AirConMode.Cool,
TargetTemperature = 20d
};
await StateManager.SetStateAsync(STATE_NAME, data);
}
}
await UnregisterReminderAsync("control-loop");
}
207
Chapter 10 The Actor Model
await RegisterReminderAsync("control-loop",
null, TimeSpan.FromSeconds(5), TimeSpan.
FromSeconds(10));
}
208
Chapter 10 The Actor Model
6. Create a new class RoomActor for the room actor. It manages the
state of the lights and keeps an instance of the air conditioner
actor. Please note that the following code snippet contains the
essential methods only:
var stateExists = await StateManager.
ContainsStateAsync(STATE_NAME);
if (!stateExists)
{
var data = new RoomData()
{
AreLightsOn = false,
AirConActorId = ActorId.CreateRandom()
};
a wait StateManager.SetStateAsync(STATE_NAME,
data);
}
}
209
Chapter 10 The Actor Model
7. Then register the actor runtime and all actor types in the
ConfigureServices method in Startup.cs. You can optionally
apply the actor configuration settings that I described earlier:
services.AddActors(actorRuntime =>
{
actorRuntime.ActorIdleTimeout = TimeSpan.FromHours(1);
actorRuntime.ActorScanInterval = TimeSpan.FromSeconds(30);
actorRuntime.DrainOngoingCallTimeout = TimeSpan.FromSeconds(40);
actorRuntime.DrainRebalancedActors = true;
actorRuntime.Actors.RegisterActor<RoomActor>();
actorRuntime.Actors.RegisterActor<AirConActor>();
actorRuntime.Actors.RegisterActor<TemperatureSensorActor>();
});
8. Last but not least, you have to map the endpoints that must be
implemented by actor hosts. Just add the following lines in the
Configure method:
app.UseEndpoints(endpoints =>
{
endpoints.MapActorsHandlers();
});
Implementing the Client
The actors are fully implemented. It’s time to use them. The next steps will guide you on
how to create a console application that invokes actor methods:
2. Navigate to the project folder and install the latest version of the
Dapr.Actors NuGet package, which at the time of writing is 1.0.0:
cd Home.Person
dotnet add package Dapr.Actors --version 1.0.0
210
Chapter 10 The Actor Model
await airConProxy.TurnOff();
Console.WriteLine(await roomProxy.Describe());
}
The preceding code creates a proxy to an actor of type RoomActor that has an ID
LivingRoom. The proxy gives you strongly typed support when calling the methods of an
actor’s interface. After the lights in the room are turned on, a proxy for the air conditioner
actor is created. By calling the methods of the proxy, the air conditioner is being turned
on and set to cooling mode with a target temperature of 24 degrees Celsius. Then the
state of the room is repeatedly queried every 15 seconds so that you can observe the
temperature changes.
211
Chapter 10 The Actor Model
Using the strongly typed interface of the actor proxy that provides a way to do RPC is
called Actor Service Remoting. However, that is not the only way to use actors. Another
way is to create an ActorProxy that is not strongly typed. You will have access only to
the InvokeAsync method for invoking a method of an actor. You will have to specify the
exact types of input and output data upon calling the method. Or you can directly query
the HTTP API of an actor that I explained earlier in the chapter. Of course, the preferred
approach is to leverage the actor interface with Actor Service Remoting as it is the
easiest.
3. The logs outputted by the actor client will look like the following:
== APP == The current room temperature is 23.1. The AC is turned on and
set to Cool mode. It is currently idle as the target temperature of 24
degrees Celsius has been reached. The lights in the room are: On.
Summary
The Actor model is a simple yet powerful model. In this chapter, you learned what the
Actor model is, what problems it aims to solve, in what cases it can be used, and some
common pitfalls that you may fall into when using actors. After that, you learned how
212
Chapter 10 The Actor Model
the virtual Actor model treats actors as perpetual objects whose lifetime is not tied to
their in-memory representation at the current moment. When we covered everything in
theory, it was time to make use of it. I walked you through the capabilities of the Actors
building block of Dapr, how it works, and how you can use it via the HTTP or gRPC
API. Then, we ran through the hands-on implementation of a simple home automation
system based on the .NET SDK of Dapr.
In the next chapter, you will learn how to leverage the Secrets building block to gain
easy access to various secret stores.
213
CHAPTER 11
Secrets
Sooner than later, a service will have to use one or more secrets to access other
systems – whether it is an API key, a password, or something sensitive. In this chapter,
you will learn what challenges the secrets impose on your applications and what are
the bad practices that you have to stay away from. After a short introduction about what
secret managers are and what benefits they bring to the table, you will learn how Dapr
simplifies the retrieval of secrets with the Secrets building block. Next, you will learn
what secret stores are supported and how you can access them by either referencing
secrets right in the Dapr component manifests or by calling the Secrets API from your
services. Later, you will see how easy it is to use Kubernetes Secrets and how to use Azure
Key Vault if you want to back away from using solely relying on Kubernetes. To wrap
up the chapter, I will show you how you can control the access of Dapr applications to
secrets.
215
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_11
Chapter 11 Secrets
however, in this chapter, we are going to explore the first side of the problem – the usage
of secrets. If a secret has leaked from either one of both involved sides, it is considered
a breach as parties that gain access to the secret can act on the user’s behalf and do
malicious actions.
Besides the technical implementation of secrets generation, storage, and retrieval,
there a lot of processes that can be established to achieve a better level of security. Some
typical questions that arise are as follows: What secrets are available? Who has access to
them? Who has accessed a particular secret and when? How are secrets secured at rest?
How is the rotation being performed? What policies apply to different types of secrets?
How to track versions? How to revoke a secret that has leaked? Who is responsible for
any of the prior topics?
216
Chapter 11 Secrets
lose your wallet, you will have lost all factors of authentication the bank card has. To be
fair, losing your card alone is a debacle in itself with contactless payments and magstripe
payments.
Deciding where to store the secrets can bring an avalanche of problems. If you
choose to keep secrets somewhere within the application or service, it makes it difficult
to know what secrets are used by each application whenever you try to get the bigger
picture. Furthermore, the rotation and revocation of secrets are far more difficult
because you have to identify all the services that use a particular secret. After that, you
need to know where the service stores the secret and how to update it. It’s a lot of work
because access to secrets most likely won’t be uniform across services, especially if they
are developed by different teams. Some may utilize environment variables, while others
read secrets from a configuration file.
So far, you have learned how Dapr building blocks utilize component definitions. For
example, when you want to interface with an external system in the Resource Bindings
building block, you have to specify the connection information like URL, password, or
token right in the component definition. As we already discussed, storing secrets in raw
format inside source code is a very bad practice. So the approach we followed so far
prevents you from being able to keep your components under source control. But when
you use Dapr, component manifests are one of the main artifacts that you need. Not
having the component manifests renders Dapr building blocks unusable. Later in this
chapter, you will learn how not to store secrets inside components.
217
Chapter 11 Secrets
• HashiCorp Vault
• Kubernetes Secrets
As you can see, Dapr supports the secret managers of the most used public cloud
platforms. Kubernetes Secrets come out of the box without requiring any configuration
at all when Dapr runs in Kubernetes mode. The name of the Kubernetes Secrets–based
secret store is simply kubernetes. This means that in Kubernetes, you take it for granted
218
Chapter 11 Secrets
and just reference it without defining a component for it. For development purposes,
when you don’t want to use a real secret store, you can either retrieve secrets stored in
either a JSON file or as environment variables.
For any secret stores other than Kubernetes, a component should be defined. For
example, the configuration of the state component that uses a local JSON file to retrieve
secrets from looks like this:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysecretstore
namespace: default
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: <PATH-TO-JSON-FILE>
- name: nestedSeparator
value: ":"
As usual, you have to specify the name of the component and the namespace where
it will be accessible. Then, you have to choose the particular type of component, which
in the preceding case is secretstores.local.file. Depending on the type you choose,
it will expect specific metadata. In this case, it is a path to the JSON file that stores the
secret values in plain text, and the nestedSeparator specifies how you will reference the
names of nested objects when retrieving the secrets. As you already know, the metadata
typically contains the connection information if you are using a cloud secret store.
Once you define the manifest for the secret store, there are two ways you can utilize
the Secrets building block.
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azblob
namespace: default
spec:
type: bindings.azure.blobstorage
version: v1
metadata:
- name: storageAccount
value: imgdetectstor
- name: storageAccessKey
secretKeyRef:
name: storageAccessKey
- name: "container"
value: "images"
- name: decodeBase64
value: "true"
auth:
secretStore: mysecretstore
As you can notice, the auth.secretStore section references the secret store by
name. If this is omitted, Dapr assumes you are trying to use the default Kubernetes
Secrets store, although it is not available in Self-hosted mode for obvious reasons. Then
each secret you want to retrieve from the secret store can be referenced by introducing
the secretKeyRef element, which has the properties name and key. In this case, only
name is used because I am using a secret store that reads from a local JSON file. Other
220
Chapter 11 Secrets
secret stores like Kubernetes can contain multiple keys inside a single secret; that’s why
you can reference the particular key by name. You can use the secret references in the
spec.metadata section of any Dapr component.
You can optionally specify metadata such as the version ID of the secret, in case
the secret store supports secrets versioning. However, have in mind that specifying
any metadata will most likely make your usage of a secret store tied to the specifics of
a particular secret store. For example, not all secret stores support retrieving a specific
version of a secret.
221
Chapter 11 Secrets
http://localhost:<daprPort>/v1.0/secrets/<secret-store-name>/bulk.
1. Log in to Azure. This is not needed if you are using Azure Cloud
Shell:
az login
3. Set the variables that are going to be used for further commands.
This is the reason why you need bash. Although Azure CLI works
on any platform, certain features vary across different shells as is
the case with variable assignment:
LOCATION=<location>
RESOURCE_GROUP=<your-resource-group>
KEY_VAULT_NAME=<your-keyvault-name>
SPN_NAME=<your-spn-name>
222
Chapter 11 Secrets
4. Create the resource group that will hold the key vault:
5. Create the key vault in the specified resource group and location:
223
Chapter 11 Secrets
11. If you use Azure Cloud Shell, you have to run one additional
command to download the file to your computer:
download spn_cert.pfx
Now that you have Azure Key Vault created and a service principal that can read
secrets from it, it’s time to define the secret store component named myazurekeyvault:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: myazurekeyvault
namespace: default
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: <your-keyvault-name>
- name: spnTenantId
value: "<your-tenant-id>"
- name: spnClientId
value: "<your-service-principal-appId>"
- name: spnCertificateFile
value : "<local-path-to-the-certificate.pfx>"
Make sure to substitute the placeholders in brackets. You should have saved the
Tenant ID and the App ID from step 9.
224
Chapter 11 Secrets
Note When you specify the path to the PFX certificate on Windows, make sure
to escape the backslashes, for example, “C:\\folder1\\spn_cert.pfx”. On Linux, you
can use single slashes, for example, “/folder1/spn_cert.pfx”.
Next, save the component in the default components folder (or alternatively specify
the --components-path flag). Then open a terminal and start a Dapr sidecar that will be
used for calling the Secrets API:
Make sure to inspect the logs and verify the secret store component has been
loaded successfully and leave the terminal session open. Let’s go to the Azure Portal
to create a secret that we are going to retrieve from the API. Find the key vault in the
resource group you created and select Secrets on the left side of the blade. Then click
the Generate/Import button, provide values for the fields Name and Value, and click
Create, as shown in Figure 11-2.
225
Chapter 11 Secrets
Once the secret is saved, you can open your browser and go to http://localhost:3500/v1.0/
secrets/myazurekeyvault/very-secret. If everything is configured properly, it should return
{"very-secret":"VALUE"} as a response.
226
Chapter 11 Secrets
Now you should realize why it is an especially bad idea to expose Dapr externally
when you have configured a secret store. This effectively makes it possible for everyone
to dump the secrets in your secret store by using the bulk endpoint of the Secrets
API. This could be somewhat alleviated by enabling the authentication on every request
coming to Dapr sidecars.
Next, the component manifest that references the secret you just created looks like this:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: myazurekeyvault
namespace: default
spec:
type: secretstores.azure.keyvault
metadata:
- name: vaultName
value: <your-keyvault-name>
- name: spnTenantId
value: "<your-tenant-id>"
227
Chapter 11 Secrets
You can keep the previous values that you used locally, except the changes in
spnCertificate and the auth section that was added to specify that referenced
secrets come from the Kubernetes Secrets store. Note the name of the metadata item
is spnCertificateFile when you specify a local file, but when you reference it from a
secret store, it is spnCertificate.
Let’s apply this component manifest by running
At this point, the Azure Key Vault secret store has been deployed and can be
used by Dapr applications and components. To verify it works, let’s deploy a Dapr
application in order to use its Dapr sidecar. Let’s use the busybox container image
although this actually doesn’t matter because you just need a Pod with the dapr.io
annotations applied:
Then find the container that starts with busybox-* from the output of kubectl get
pods. After that, let’s set up port forwarding from the local 3500 port to the same port that
the Dapr sidecar container listens on inside the pod:
228
Chapter 11 Secrets
229
Chapter 11 Secrets
secrets:
scopes:
- storeName: kubernetes
defaultAccess: deny
allowedSecrets: ["secret1"]
- storeName: localstore
defaultAccess: allow
deniedSecrets: ["secret1"]
In the preceding fragment, you can see that the secrets are enlisted per secret store.
For each secret store, you can specify the following properties:
230
Chapter 11 Secrets
The lists of allowed and denied secrets always take precedence over the value of
defaultAccess. The value of defaultAccess is applicable when no specific lists are
defined, jointly or separately. In the preceding fragment, for the kubernetes secret
store only secret1 can be accessed and for the localstore all secrets except secret1
can be accessed. Although the secrets access lists offer a quite flexible configuration,
I encourage you to deny access to all secrets by default by setting defaultAccess to
deny and whitelist all secrets that can be accessed by the particular application in
allowedSecrets.
Summary
Secrets must be handled with special care. That’s why Dapr provides a Secrets building
block that allows you to get all secrets from a central place for storing all security
information – a secret store. Dapr supports the secret managers of the major public
cloud providers and also Kubernetes Secrets for Dapr in K8s mode. The building block
can be used in two ways – when other Dapr components need to reference secrets
in their manifests and by calling the Secrets API. The Secrets API is useful when
applications need runtime access to some secrets – a good example is a database
connection string. Later on, you learned how to set up Azure Key Vault to be used
as a secret store for both Self-hosted and Kubernetes modes. You also learned how
Kubernetes Secrets can play a supplementary role when Azure Key Vault is used
as the main secret store. Although Kubernetes Secrets come in handy without any
configuration on the Dapr side, I outlined what are the trade-offs and what you can
do to secure it further. Finally, we explored how access to secrets can be controlled by
specifying the specific secrets that a Dapr application can access from each secret store.
In the next chapter, you are going to learn why observability is important and how
you can monitor Dapr applications.
231
CHAPTER 12
Observability: Logs,
Metrics, and Traces
Knowing what happens inside a system while it is running is beneficial for debugging,
maintenance, and analysis purposes. In this chapter, we will explore the three pillars of
observability – logs, metrics, and traces. You will learn what they are, how they are used,
and what are the differences between them. After that, we will explore how Dapr allows
you to put the telemetry data into practice by mostly leveraging the open standards such
as the OpenTelemetry project. Then you will understand how to use various monitoring
systems to collect, analyze, and visualize the information coming from Dapr and your
applications.
L ogs
Logs are likely the simplest of the three. Simply put, they represent some insight about
an event that happened at a given time; therefore, they carry a timestamp and some
payload. There is support for logging in most of the frameworks and libraries that you
will ever use. Sometimes it’s as simple as outputting a line of text. Logs generally come in
three forms:
233
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_12
Chapter 12 Observability: Logs, Metrics, and Traces
• Plain text – The most common form you will likely see emitted from
any running process.
• Binary – Logs are stored in binary format. It’s not likely for your
application to have binary logs. An example of such type is the logs of
some RDBMS, for example, MySQL. Binary logs are generally useful
only for the system that is generating them.
What is the value of logs? Logs can give you some contextual information from a
particular request that has been handled by a service. Depending on the granularity of
your logs, you may be able to identify the root cause of some performance issues or to
debug a problem without attaching any debugger whatsoever. This may sound like you
should log each and every individual action your services happen to perform. However,
you should think about the performance implications of this as logs are typically stored
on disk. This means that depending on the logging system you use, generating too many
logs can also degrade the performance of your services. Apart from that, too many logs
can incur a huge operational cost for your whole application because logs are typically
sourced to external systems in order to be consolidated, stored, and further analyzed.
Dapr Logs
So far, you should have already seen some logs coming from the Dapr system services
and sidecars. Whether you run Dapr in Self-hosted mode or in Kubernetes mode, its
system services emit logs that are as verbose as the log level you choose. These are levels
of log verbosity:
• debug
• info
• warn
• error
The error level reports only errors, while debug is the most verbose level. By default, it
uses the info level. Besides that, you can also specify the log format to be used – plain text
or JSON. If you are planning to collect the logs into an external system for further analysis,
you should configure Dapr to emit logs in JSON format; otherwise, the default is plain text.
234
Chapter 12 Observability: Logs, Metrics, and Traces
For Self-hosted mode, you can specify the verbosity level by setting the --log-level
flag whenever you start any system service or a Dapr sidecar (daprd). When starting
Dapr applications with a sidecar, the flag is specified in the following way:
If you want to change the logging format in Self-hosted mode, you have to directly
start the daprd binary as you cannot set the format with Dapr CLI, at the time of writing
the book.
In Kubernetes mode, it’s crucial what you select when you install Dapr. For example,
here is how to specify both the verbosity level of each Dapr system service and the
format of the Dapr system services when installing the Helm package:
Then for each Dapr application, you control the logging behavior by specifying the
following annotations:
dapr.io/log-as-json: "true"
dapr.io/log-level: "debug"
As you already know, the good practice is to have structured logs that allow you to
better navigate between the properties. Here is an example of a log item in JSON format
emitted by a Dapr sidecar:
{"app_id":"hello-service","instance":"hello-deployment-7bd95f6647-kh
q69","level":"info","msg":"internal gRPC server is running on port
50002","scope":"dapr.runtime","time":"2021-03-28T13:41:33.317200855Z","type
":"log","ver":"1.0.1"}
While logs can give you a lot of details about all events that happened in a system,
you will hardly be able to notice a problem in a distributed system by solely inspecting
the logs of every service. That’s why you need a higher-level view – metrics and traces
can be viewed as generalized derivatives of logs.
235
Chapter 12 Observability: Logs, Metrics, and Traces
Metrics
Metrics are time-phased numeric information measured over time. Since it is a numeric
representation, it is very convenient for any type of aggregation, summarization, and
correlation that you will typically see in dashboards where you can explore some
historical trends about your application. Metrics are accompanied by a set of key-value
pairs called labels that provide details about where the particular metric was measured.
Such a label in Dapr is the Dapr Application ID and the name of a component, to name a
few. Labels give a multidimensional view of the metrics.
Time-series databases are well suited to collect metrics. The performance overhead
is constant for the metric-emitting side. Unlike logs, the amount of metrics does not
depend on how many operations the service is performing. Since metrics describe
some state of a system, alerts can be triggered when a metric reaches a particular value.
Metrics can also drive additional capabilities like auto-scaling in a Kubernetes cluster.
Dapr Metrics
Metrics are enabled by default in Dapr. Examples of metrics include the number of
sidecar injection failures, the number of successfully loaded components, and latency of
calls between Dapr sidecars and applications to name a few. Dapr metrics are exposed
by a Prometheus endpoint that by default listens on port 9090 on both Dapr system
services and application sidecars. Prometheus, apart from being the name of a Titan god
from Greek mythology, is the de facto standard for a monitoring system in the cloud-
native world. Prometheus collects metrics by scraping a metrics endpoint at regular
intervals. At the time of writing, Dapr leverages the OpenCensus Stats exporter for the
Prometheus endpoint. OpenCensus is a set of libraries for various languages that help
you for collecting metrics and distributed tracing. OpenCensus and OpenTracing have
merged to form OpenTelemetry, which aims to deal with the full set of telemetry data –
logs, metrics, and traces. At some point, when OpenTelemetry reaches maturity, Dapr
will fully leverage its capabilities.
There are different ways to scrape metrics data from this endpoint. However, to make
it possible for all scrapers to know where to find the Prometheus metrics endpoint, you
must specify the following Prometheus annotations on each Pod:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/"
236
Chapter 12 Observability: Logs, Metrics, and Traces
Tracing
Traces are the representation of the communication flow between services and other
dependencies in a distributed system. It’s also called distributed tracing because it
captures trace information spanning across processes, nodes, networks, and security
boundaries. If properly set up, a trace provides visibility over which services were
involved from the start to the end of a request and how long it took for each of them to
finish. You can also easily pinpoint where errors are happening.
The unit of work in distributed tracing is a span that represents the work done by a
single service as part of the end-to-end workflow. A span constitutes a single operation
happening in the service, for example, a request to another service or a database query.
A span contains metadata information about the operation such as the operation name,
the start and the end timestamps, and some attributes. A trace is a tree of spans united
by a root span that encompasses the lifetime of all child spans. By inspecting the root
span, you can tell how long the duration of the whole operation took. Child spans are
united by a context that they have to provide. The W3C Trace Context specification
defines how the context is populated in HTTP by different systems by leveraging Trace
ID and the mechanism for sharing this context in a vendor-agnostic way.
237
Chapter 12 Observability: Logs, Metrics, and Traces
may contain some vendor-specific information, which can be set and incrementally
augmented by all parties involved in a trace. For gRPC, the header is grpc-trace-bin,
which is the binary equivalent of traceparent, albeit not being a part of the W3C spec.
Those headers are all generated and propagated by Dapr. It’s very handy that the whole
communication flows through the system of Dapr sidecars.
By default, if Dapr detects that the trace context is present, it will simply propagate it
to any further service calls. In case there isn’t any trace context, Dapr will generate one
and will propagate it further. Keep in mind that trace context might be missing if this is at
the beginning of the request flow, and Dapr will generate a new one in this case.
Configuring Tracing
It’s very important to have tracing data in production. But the volume of the tracing data
can grow exponentially for an application that receives many requests as is the case with
logs. However, in most cases, you would want to have at hand all generated logs because
logs point to specific events that happened at a given time. Instead, you can benefit from
traces even if you don’t collect 100% of them. Of course, you will likely miss collecting
some of the outliers; however, the general patterns that are applicable for the majority of
requests will be captured. In this case, you can save some space and processing costs.
The ability to not persist each span is called sampling. The term sampling comes from
statistics, where sampling is the process of taking a subset of individual observations from
a statistical population in order to make a conclusion about the whole population by using
extrapolation. The default sampling rate in Dapr is 0.0001, which means that 1 out of every
10,000 spans is sampled. You can specify the sampling rate in the configuration file of Dapr
in Self-hosted mode or by applying a Configuration CRD in Kubernetes mode. It is a number
between 0 and 1, where 0 means tracing is disabled and 1 means that every span is sampled:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
spec:
tracing:
samplingRate: "1"
zipkin:
endpointAddress: http://localhost:9411/api/v2/spans
238
Chapter 12 Observability: Logs, Metrics, and Traces
Now that traces are sampled at a given ratio, you need to export them to a
tracing system to be able to realize any benefit from having them – trace collection,
visualizations, analytics, and so on. By default in Self-hosted mode, Dapr sends the trace
data to Zipkin, which is a distributed tracing system with a simplistic yet powerful UI. In
Self-hosted mode, Dapr spins up a container running Zipkin, whereas in Kubernetes
mode you have to deal with deploying it.
As you can see from the preceding code snippet, the address of the HTTP endpoint
of Zipkin that collects spans is specified. Because Dapr is in Self-hosted mode, Zipkin
runs in a container. In the case of Kubernetes, you have to change the address of the
Zipkin collector endpoint to point to the FQDN of the Service that stays in front of the
Zipkin Pod. Alternatively, you can use an OpenTelemetry collector to send the traces to
various systems. More on that is covered later in this chapter.
In the preceding case, when Service A calls Service B, a context will be generated. Then
along with its response, Service B will return the context as response headers back to the code
of Service A. However, in this case, it is the responsibility of the caller to further propagate
the returned context. If you don’t do it, Dapr will generate a new context for each sequential
service invocation. You have to explicitly instruct Dapr to link those calls in a single trace tree.
Let’s revisit the distributed Hello World application that we implemented in Chapter 2:
Introduction to Dapr once again. There were three services – Greeting, Hello, and World.
The Greeting service is what produced the “Hello world” output by sequentially invoking
the Hello service and World service and combining their responses.
Now, follow the same steps as outlined in Chapter 2: Introduction to Dapr to run
the three services, and make a couple of requests to the Greeting service. Then open
Zipkin UI, at http://localhost:9411/zipkin/. It has two pages – Find a trace and
Dependencies. The first page enables you to see the recent traces and execute some
queries against all the available traces. For example, you can filter by service name,
duration, or timeframe. The Dependencies tab displays a graphical simulation of how
requests flow logically from one service to others. Let’s focus on the Find a trace page
that displays the recent requests issued by the Greeting service as shown in Figure 12-1.
Did you notice anything? If you click each individual result, you will be able to
explore more info about the particular trace. The problem with this is that the Greeting
service seems to be invoking the other two services somewhat separately although you
know this happens during the lifetime of a single request in the Greeting Service. Let’s
revisit the code that invokes the two requests:
res.send(greeting);
});
Do you see any trace of the traceparent header? In this case, Dapr thinks the
request to the World service has nothing to do with the request to the Hello service
and generates a trace context for both. Now, let’s make sure we propagate the context
to the World service appropriately by retrieving and propagating the headers on any
consecutive calls:
res.send(greeting);
});
241
Chapter 12 Observability: Logs, Metrics, and Traces
After saving the new version of the Greeting service, stop the dapr run command by
pressing Ctrl+C and rerun it so that it picks up the new changes. Then, do some requests
to the Greeting service endpoint and refresh the Zipkin UI discover page. By observing
the results, it should be clear that the Greeting service now invokes two other services
in the lifetime of a single request; and by clicking one of the results, it will display the
timeline of the request as shown in Figure 12-2.
By now, you have learned what the capabilities of Dapr are when it comes to
observability. With its system of sidecars, Dapr collects all the telemetry information.
You don’t have to make decisions about how to collect and where to store telemetry data
per service, which otherwise may lead to having different practices across the services.
Instead, all comes out of the box. However, Dapr doesn’t provide any options to call, for
example, a tracing middleware for any custom tracing needs.
To get the job done, Dapr relies on industry-standard projects like OpenCensus,
OpenTracing, and OpenTelemetry. OpenCensus and OpenTracing were merged
into OpenTelemetry, but at the time of writing, the underlying code still leverages
OpenCensus. From another perspective, OpenTelemetry can also be seen as the next
242
Chapter 12 Observability: Logs, Metrics, and Traces
major version of both of the aforementioned projects. With time OpenTelemetry will
become the de facto cloud-native telemetry standard covering all three aspects of
observability – tracing, metrics, and logs. Be prepared for some changes around Dapr
observability until OpenTelemetry becomes stable and fully adopted.
So far, I have shown you how to export tracing data in Zipkin, which comes by
default for Dapr in Self-hosted mode. In the next paragraphs, you are going to learn how
to export all the telemetry data to various monitoring tools and systems. Because what is
the value of telemetry if you are not able to see, analyze, and react?
A
zure Monitor
Azure Monitor is a comprehensive solution in Microsoft Azure that allows you to
collect, store, analyze, and react on telemetry data coming from the cloud and from
on-premises environments. When it comes to collecting telemetry from Kubernetes and
any applications running on it, there are several approaches for collecting the data. The
easiest is if you use Azure Kubernetes Service, as most of the things come out of the box.
2. Then create an AKS cluster if you don’t have one. The AKS cluster
should have Azure Monitor for containers (Container Insights)
enabled. You can do it at the time of the creation of the cluster
by checking the checkbox or later using Azure CLI. To find the
different methods to enable it on existing AKS clusters, consult
the Microsoft Docs: https://docs.microsoft.com/en-us/azure/
azure-monitor/containers/container-insights-enable-
existing-clusters.
243
Chapter 12 Observability: Logs, Metrics, and Traces
After you verified the resources of the agent are present and healthy, you should
be able to see the health and resource utilization of your AKS cluster by opening the
Insights tab in your AKS blade in Azure Portal. The container logs are also going to be
available.
However, this setup won’t collect any deep telemetry that is application-specific like
the one available from the Prometheus metrics endpoint of Dapr. To collect them, you
will usually need to have a Prometheus server with a store. But by using Azure Monitor
for Kubernetes, you don’t need a server because it’s capable of collecting metrics by
itself. Let’s configure the Log Analytics agents to scrape Prometheus metrics:
prometheus-data-collection-settings: |-
[prometheus_data_collection_settings.cluster]
interval = "1m"
monitor_kubernetes_pods = true
monitor_kubernetes_pods_namespaces = ["dapr-system", "default"]
4. Next, you have to annotate your Dapr application pods so that the
Log Analytics agents can find the Prometheus metrics endpoint.
You can do so right in the manifests of the distributed Hello World
application we defined in Chapter 4: Running Dapr in Kubernetes
Mode. And don’t forget to instruct Dapr to log in JSON format:
244
Chapter 12 Observability: Logs, Metrics, and Traces
dapr.io/log-as-json: "true"
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/"
If you have gone through those configuration steps, your applications will
leverage the maximum support of Azure Monitor for containers. Then you can start
running queries in Log Analytics or incorporate the collected data into Azure Monitor
Workbooks, which provide visual reports and a lot of opportunities for customizations.
Queries against Azure Log Analytics are being written in Kusto Query Language
(KQL). Using KQL, there is a sample query that retrieves the Dapr logs:
ContainerLog
| extend parsed=parse_json(LogEntry)
| project Time=todatetime(parsed['time']), app_id=parsed['app_id'],
scope=parsed['scope'],level=parsed['level'], msg=parsed['msg'],
type=parsed['type'], ver=parsed['ver'], instance=parsed['instance']
| where level != ""
| sort by Time desc
The query displays a chart showing the change of used memory of both Dapr system
services and Dapr sidecars, as you can see from Figure 12-3:
InsightsMetrics
| where Namespace == "prometheus" and Name == "process_resident_memory_bytes"
| extend tags=parse_json(Tags)
| project TimeGenerated, Name, Val, app=tostring(tags['app'])
| summarize memInBytes=percentile(Val, 99) by bin(TimeGenerated, 1m), app
| render timechart
245
Chapter 12 Observability: Logs, Metrics, and Traces
Figure 12-3. Running a Kusto query that displays used memory by Dapr system
services and Dapr applications
1. First, you have to deploy the Container Insights solution for your
Azure Log Analytics workspace.
I won’t guide you through those steps because they are out of scope for this chapter.
You can find more info in the Azure Monitor for containers documentation at Microsoft
Docs: https://docs.microsoft.com/en-us/azure/azure-monitor/containers/
container-insights-hybrid-setup.
Another alternative just for the logs is to use Fluentd for collecting them, and then
you can utilize an output plugin for Fluentd to transfer them to Azure Log Analytics.
246
Chapter 12 Observability: Logs, Metrics, and Traces
247
Chapter 12 Observability: Logs, Metrics, and Traces
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
namespace: default
spec:
tracing:
samplingRate: "1"
zipkin:
endpointAddress: http://otel-collector.default.svc.cluster.
local:9411/api/v2/spans
6. All your Dapr applications must use the configuration you just
created. Add the following Dapr annotation that references the
previously created configuration to their manifests and apply it:
dapr.io/config: "appconfig"
After all those steps, opening the Application Map of your Application Insights
instance should display the collected information about the interdependencies of the
services in your application as shown in Figure 12-4.
248
Chapter 12 Observability: Logs, Metrics, and Traces
In case nothing appears in the Application Map, validate you have supplied a correct
Instrumentation Key and consult the logs of the OpenTelemetry collector. Find out
what is the name of the OpenTelemetry pod and run kubectl logs otel-collector-
556c98b76b-nrwrq. If it receives anything, you should be able to see the spans as they
come.
Note At the time of writing, Zipkin is the preferred format for traces. However,
at some point in time when OpenTelemetry becomes more stable, it will also be
supported. When this happens, most likely Zipkin will become deprecated as Zipkin
is just one of the many supported receivers by the OpenTelemetry collector.
Grafana
Grafana is a very popular open source data analysis and visualization solution. It is
capable of integrating with a large number of data sources – Prometheus, InfluxDB,
Elasticsearch, Azure Monitor, and many others. Although Azure Monitor is supported,
in a typical stack that uses Grafana, you will most likely use the Prometheus server to
collect metrics, instead of Azure Monitor for containers.
249
Chapter 12 Observability: Logs, Metrics, and Traces
Furthermore, with every Dapr release, a couple of Grafana dashboards are shipped
as well. Let’s see how to use them.
Installing Prometheus
Follow the steps to install Prometheus:
Setting Up Grafana
Now let’s deploy and set up Grafana:
250
Chapter 12 Observability: Logs, Metrics, and Traces
10. Click Save & Test to save your Prometheus data source.
2. Click the + icon from the left menu and then Import.
3. Import the dashboard templates.
Having those dashboards gives you a lot of insights about what happens with Dapr as
shown in Figure 12-5.
251
Chapter 12 Observability: Logs, Metrics, and Traces
Summary
Being able to tell what is going on inside a given system while looking from the outside
is crucial. By collecting, storing, and analyzing the three pillars of observability – logs,
metrics, and traces – you can very productively spot any issues or investigate some
corner case scenarios. In this chapter, you learned how Dapr supports each one of those
telemetry types. Then you learned how to ship that information to various monitoring
systems like Azure Monitor and Grafana and do further analysis.
This was the last chapter of Part 2, which covers all Dapr building blocks. Part 3 aims
to explore how Dapr can integrate and work side by side with other technologies. In the
next chapter, you are going to learn how to plug in middleware to execute additional
actions during the request pipeline.
252
PART III
Integrations
CHAPTER 13
Plugging Middleware
Being able to change the behavior or to extend the functionality of a piece of software,
without changing it from the inside, is pretty neat. In this chapter, you will learn how to
plug middleware in the request pipeline of Dapr to deal with things like rate limiting,
authentication, and authorization.
M
iddleware in Dapr
Using middleware is a very popular technique for request-based pipelines. It is
essentially a system for chaining plugins that do some kind of processing on a request,
one after another. Every middleware is a layer that performs a specific function.
Dapr is very much a request-based runtime, and that’s why it is very handy to
define a custom middleware pipeline as shown in Figure 13-1. As a matter of fact, Dapr
internally utilizes a built-in chain of middleware for certain functions. For example,
there is a tracing middleware, a metrics middleware, and a Cross-Origin Resource Sharing
(CORS) middleware. The distributed tracing and the Dapr metrics that were covered in
Chapter 12: Observability: Logs, Metrics, and Traces rely heavily on the tracing and the
metrics middleware. The CORS middleware is what enables you to whitelist the URLs
that will be allowed to access the sidecar by the browser. The URLs can be configured by
specifying the allowed-origins flag on the sidecar.
255
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_13
Chapter 13 Plugging Middleware
Every request that comes into a Dapr sidecar gets processed sequentially by each
middleware until it reaches the endpoint of a service, which returns some payload as a
response, which is then processed by the same middleware but in reverse order. It is up
to the implementation of the middleware to choose whether to implement its logic on
the ingress, the egress, or both sides.
At the time of writing, Dapr only supports HTTP-based middleware. However, keep
in mind that the middleware is not only applicable to service invocation but also publish
and subscribe, state management, resource bindings, actors, and pretty much anything
that hits an endpoint of the Dapr API.
256
Chapter 13 Plugging Middleware
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: ratelimit
spec:
type: middleware.http.ratelimit
version: v1
metadata:
- name: maxRequestsPerSecond
value: 2
The way it is configured in the preceding, the rate-limiting middleware will make
sure that no more than five requests are processed per second. Whenever the limit is
reached, it will respond with status code 429: Too Many Requests. Don’t get rate limiting
confused with the max-concurrency flag for Dapr in Self-hosted mode and the dapr.
io/app-max-concurrency annotation in Kubernetes mode. The rate limiting counts and
limits the requests per second, while the concurrency setting makes sure that no more
than the configured number of requests is being executed at any single point of time.
Now that you have defined all the middleware components you are about to
include in a pipeline, let me show you how to configure it. The pipeline is defined
in the httpPipeline section of a Dapr configuration. It is up to you to configure the
number of middleware and their order. In this example, I am going to define a pipeline
that consists of two middleware – the uppercase and the ratelimit – in a file named
pipelineconfig.yaml:
257
Chapter 13 Plugging Middleware
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: pipelineconfig
namespace: default
spec:
httpPipeline:
handlers:
- name: ratelimit
type: middleware.http.ratelimit
- name: uppercase
type: middleware.http.uppercase
Before running this example, let me show you a simple web service based on ASP.
NET Core that I have built that will serve for debugging all requests in this chapter. It
has a single endpoint that returns all information about the HTTP request – the HTTP
method, the query string, the HTTP headers, and the body:
[Route("/test")]
[HttpGet, HttpPut, HttpPost, HttpDelete]
public async Task<string> Test()
{
var response = $"HTTP Method: {Request.Method}\n" +
$"Query: {Request.QueryString} \n" +
"Headers\n";
foreach (var header in Request.Headers)
{
response += $"\t{header.Key}: {header.Value}\n";
}
return response;
}
Now, it is time to run and test our pipeline with Dapr in Self-hosted mode:
258
Chapter 13 Plugging Middleware
3. Issue a POST request. You can use your favorite tool for making
HTTP requests. I will use cURL:
259
Chapter 13 Plugging Middleware
that is requesting access to some portion of the user’s data. Once the Client obtains an
access token, it can perform operations in the Resource Server on behalf of the Resource
Owner (the user). The general flow of the protocol is shown in Figure 13-2.
But depending on the way the client application obtains the access token, there are
several different grant types that can be used. Two of the most widely used authorization
grants are the Authorization Code grant and the Client Credentials grant. They both
obtain an access token in the end; however, they serve different user types. In the
Authorization Code grant, the user is being redirected to the Authorization Server by
the client application. After the user enters their credentials, the Authorization Server
redirects the user back to the client application along with the Authorization Code. Once
the client application receives the Authorization Code, it is exchanged for an access
token by making a call to the token endpoint of the Authorization Server. In contrast, the
Client Credentials grant is applicable whenever a system wants to access its own data
by exchanging its own credentials to obtain an access token. In this case, it means that
the Resource Owner and the Client are basically the same thing – an application. This is
enough for you to have a basic understanding, and it’s beyond the scope of this chapter
to discuss the OAuth 2.0 protocol in further detail.
To see how widely used the OAuth 2.0 protocol is, here are some of the most popular
technologies that can be used as an Authorization Server. Some of them are pure Identity
260
Chapter 13 Plugging Middleware
and Access Management (IAM) solutions, while others are social networks. Most of
them follow the SaaS model, while others like IdentityServer provide a good room for
customization and various hosting options:
• Google APIs
• Twitter
• Okta
• IdentityServer
Dapr provides out-of-the-box middleware for both the Authorization Code grant and the
Client Credentials grant. I will show you how to use them with Azure Active Directory (AAD).
However, they work with any server that supports OAuth 2.0. The main difference will be in
the way that you register a client application using the respective user interface.
For some reason, you may need to publicly expose the Dapr sidecars to the Internet.
This will require you to make sure that only authenticated users are allowed to use the Dapr
API. Dapr supports out-of-the-box API token–based authentication, although the token for
all clients is the same. In Self-hosted mode, the token is in the DAPR_API_TOKEN environment
variable, whereas in Kubernetes mode it must be stored as a Kubernetes Secret and
then referenced by name with the dapr.io/api-token-secret annotation. You will be
responsible for rotating the secret frequently. However, in production you will most likely
use some kind of an Identity Provider and a protocol like the OAuth 2.0 authorization grant
or OpenID Connect, depending on whether the client is a user or another application.
261
Chapter 13 Plugging Middleware
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: oauth2
namespace: default
spec:
type: middleware.http.oauth2
version: v1
metadata:
- name: clientId
value: "<Client-ID>"
- name: clientSecret
value: "<Client-Secret>"
- name: scopes
value: "User.Read"
- name: authURL
value: "https://login.microsoftonline.com/<Tenant-ID>/oauth2/v2.0/
authorize"
262
Chapter 13 Plugging Middleware
After you have defined the component, you have to reference it in the httpPipeline
section of the Dapr configuration. Then this configuration should be referenced by the
Tester application that you already have upon calling dapr run. If you are still unsure
how to do those steps, please revisit the previous paragraphs that explain them. If you
still have the previous middleware in the pipeline, it is a good idea to remove them.
Now, after you started the application with a Dapr sidecar, you can open http://
localhost:3500/v1.0/invoke/tester/method/test in your browser, and you will be
redirected to Azure Active Directory that will prompt for your consent to logging into
this application that you just created. Once you give your consent, you will be redirected
back to http://localhost:3500/v1.0/invoke/tester/method/test, which will output
the access token that is received as a value of the Dapr-Access-Code header.
This means that all further requests that your service receives will carry the access
token via the header you specified. Of course, the service must use a Dapr configuration
that incorporates the middleware. Depending on the permissions you choose in the
application settings in AAD, the application will be able to call various APIs on the user’s
behalf using the provided token. By default, the application will only have the User.Read
permission, which grants privileges to read the profile of the signed-in user and some
basic company information.
263
Chapter 13 Plugging Middleware
Let me show how you can use the Client Credentials flow in order to enable your
Dapr applications to easily obtain an access token, which then can be used for calling
any other APIs, for example, getting all Azure AD users from the Microsoft Graph API:
1. Firstly, although you can use the same application that you
created for the Authorization Code grant, I encourage you
to register a new one as this is the best practice for different
applications. Note that for the Client Credentials grant, you don’t
have to configure a platform, as there won’t be any redirects
happening before you receive the token.
2. Since you will query the Microsoft Graph API to get all users,
appropriate permissions must be granted to the Azure AD
application. Go to API Permissions, click Microsoft Graph to
display all available permissions, select Application permissions
as these permissions will be applicable for the Dapr application
itself, and add User.Read.All.
264
Chapter 13 Plugging Middleware
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: oauth2clientcreds
namespace: default
spec:
type: middleware.http.oauth2clientcredentials
version: v1
metadata:
- name: clientId
value: "<Client-ID>"
- name: clientSecret
value: "<Client-Secret>"
- name: scopes
value: "https://graph.microsoft.com/.default"
- name: tokenURL
value: "https://login.microsoftonline.com/
<Tenant-ID>/oauth2/v2.0/token"
265
Chapter 13 Plugging Middleware
8. Whenever you invoke any endpoint of the Dapr API, it will check
whether it has a cached token and if not it will obtain one before
proceeding further. Since this application has just one method,
open http://localhost:3500/v1.0/invoke/tester/method/
test, and you should be able to see the access token that was
passed in the Dapr-Access-Code header. Copy the value of the
token as you will need it for calling the Microsoft Graph API.
Now, to verify the token that Dapr got for you can be used, let’s get all users by calling
the /users endpoint of the Microsoft Graph API. You can use a tool like Postman or
cURL according to your preference. The request with cURL looks like this – of course, the
access token is truncated:
266
Chapter 13 Plugging Middleware
Before I show you how to utilize this middleware, let me make a few important
points on the Azure AD application configuration. But of course, you can use just about
any other Identity Provider that supports OpenID Connect:
• The option for issuing ID tokens must be enabled for the AAD
application
If you have properly configured it, it should look exactly as shown in Figure 13-4.
267
Chapter 13 Plugging Middleware
Now, let’s configure the middleware component by replacing the Client ID of the
application and the Tenant ID in the respective placeholders:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: bearer
spec:
type: middleware.http.bearer
version: v1
metadata:
- name: issuerURL
value: "https://login.microsoftonline.com/<Tenant-ID>/v2.0"
- name: clientID
value: "<Client-ID>"
Next, you have to refer to this middleware inside your Dapr configuration, after
which you can start the app. To obtain an OpenID token, you have to open a browser and
go to the following address, but before that make sure to replace the placeholders:
https://login.microsoftonline.com/<Tenant-ID>/oauth2/v2.0/authorize?
client_id=<Client-ID>
&response_type=id_token
&redirect_uri=http%3A%2F%2Flocalhost:5000%2Ftest
&response_mode=form_post
&scope=openid
&state=12345
&nonce=678910
After you log in successfully, you will be redirected with a POST request to http://
localhost:5000/test, and the ID token will be delivered as URL-encoded form data in
the body of the HTTP request. This means that the ID token and some other fields are
encoded as key-value tuples joined with an ampersand just as you are used to having
any GET parameters in the URL. Make sure to copy only the value of the id_token key.
268
Chapter 13 Plugging Middleware
If you try accessing an endpoint of the Dapr API without a valid bearer token in the
Authorization header, Dapr will reject the request and return HTTP status code 401
Unauthorized. Now open your favorite application for issuing HTTP requests and make
the following request, but make sure to include the full value of the ID token that you
obtained because I have trimmed it here:
2. OPA evaluates the Rego policies on the input data that was
received. Then OPA sends the final decision to the calling service
or system. The output is in a structured format (JSON).
3. The service receives the output from OPA and enforces the
decision.
By using the OPA middleware in Dapr, you can apply some conditions to the
incoming requests to the Dapr API. When you define the OPA policy for Dapr, the end
result will be whether or not to allow the request, what HTTP status code to return,
and what additional headers to inject. All those policies can be described in the Rego
language. Dapr provides the properties of the incoming HTTP requests as an input
269
Chapter 13 Plugging Middleware
to OPA. Such properties are the HTTP method, the request path, the query string,
HTTP headers, and so on. You can learn more about Open Policy Agent in the official
documentation: www.openpolicyagent.org/docs/latest/.
S
ummary
Injecting a middleware is a great idea for request-based workloads. Multiple middleware
can be chained together to control the ins and outs of a request to Dapr. In this chapter,
you learned about the OAuth 2.0 middleware for Authorization Code grant and Client
Credentials grant. You also learned how to verify incoming OpenID Connect tokens
using the bearer middleware. Lastly, we touched on what Open Policy Agent is and how
the OPA middleware can be used.
In the next chapter, you are going to learn about how the Dapr .NET SDK makes ASP.
NET Core development with Dapr simpler.
270
CHAPTER 14
Using Dapr in
ASP.NET Core
In this chapter, you will learn about the integration between Dapr and ASP.NET Core.
If you made it so far in the book, you have already seen a lot of examples implemented
in ASP.NET. Dapr does not need really anything special to be used with any web
framework. However, when it comes to ASP.NET Core, Dapr has some syntactic sugar
that makes it even easier. There is support for using the State Management and the
Publish and Subscribe building blocks natively inside a controller. There is also a way to
expose a secret store as an ASP.NET Core configuration. Furthermore, implementing a
gRPC service doesn’t mean that you have to import the Protobuf files of Dapr in order to
call its gRPC API. It comes pre-packaged inside the support for ASP.NET Core.
O
verview
A lot of the examples in the book were based on .NET Core, and in some of them I even
used the Dapr .NET SDK. You might be wondering what else you can see for Dapr in the
context of .NET Core given that I have covered all in terms of the Dapr functionality. The
Dapr .NET SDK covers a somewhat broad area of use cases. It is split into a couple of
NuGet packages, some of which you have already seen:
271
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_14
Chapter 14 Using Dapr in ASP.NET Core
app.UseCloudEvents();
272
Chapter 14 Using Dapr in ASP.NET Core
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
endpoints.MapControllers();
});
The last two steps are only required if you use the Publish and Subscribe building
block and you want to leverage the support for ASP.NET controllers that comes with the
Dapr .NET SDK.
To further demonstrate the capabilities inside a controller, I will implement a very
made-up application (yet simple) that has one endpoint that processes incoming
messages in a topic and saves them into a state store. There is also another endpoint
that gathers the information about a given key by taking it from the state store. You can
find this project in the source code of the book; it is named ControllersExample. There
is a DummyData class that contains just two properties – an Id property of type string and
a Data property of type string. It is located in a Common project that is referenced by all
example projects in this chapter. There is also a Constants class that keeps the names of
the state component, the pubsub component, and the topic in it. Having such a class is
not a good practice; however, it simplifies a lot the examples we are going to explore.
S
ubscribing to a Topic
Subscribing to a topic is as easy as applying the TopicAttribute and providing the name
of the pubsub component and the name of the topic:
[Topic(Constants.PubSubName, Constants.TopicName)]
[HttpPost(Constants.TopicName)]
273
Chapter 14 Using Dapr in ASP.NET Core
return Ok();
}
[HttpGet("/data/{id}")]
public ActionResult<DummyData> GetDataFromStateStore([FromState(Constants.
StateStoreName, "id")] StateEntry<DummyData> data)
{
if (data.Value is null)
{
return this.NotFound();
}
return data.Value;
}
274
Chapter 14 Using Dapr in ASP.NET Core
In the preceding code, the key of the state entry is provided in the URL as you can
see from the route template /data/{id}. Then the name of the route parameter is
specified in the FromStateAttribute. Then the state entry is obtained from the state
store, and you can even call some methods that change it, for example, DeleteAsync and
SaveAsync.
You can test this example locally. You can publish a message by calling the /
publish/<pubsub-name>/<topic> endpoint of the Dapr sidecar. Then you can retrieve
the state item by calling http://localhost:<app-port>/data/<state-item-key>.
Note DaprClient needs to know the ports of both the HTTP and gRPC APIs of
Dapr. It expects the port numbers to be defined as the environment variables
DAPR_HTTP_PORT and DAPR_GRPC_PORT; otherwise, it uses port numbers 3500
for HTTP and 50001 for gRPC by default. When you use the dapr run command
of Dapr CLI, to start your application with a command like dotnet run, it
automatically injects the environment variables into your application.
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
275
Chapter 14 Using Dapr in ASP.NET Core
endpoints.MapGet("/data/{id}", GetDataFromStateStore);
endpoints.MapPost(Constants.TopicName, ReceiveMessageFromTopic).
WithTopic(Constants.PubSubName, Constants.TopicName);
});
context.Response.StatusCode = 200;
}
As you notice, it simply retrieves the body of the request that contains the payload
and deserializes it to an instance of the DummyData class. Then it persists the object in
the state store by using the instance of the DaprClient and later acknowledges that the
message was successfully processed by returning HTTP status code 200. That was a lot of
work that otherwise you take for granted in ASP.NET controllers.
276
Chapter 14 Using Dapr in ASP.NET Core
Let’s build the same made-up example with DummyData, this time with a
gRPC service. The base class AppCallback.AppCallbackBase that your gRPC
service must extend has three methods that you have to override – OnInvoke,
ListTopicSubscriptions, and OnTopicEvent. Let’s take a look at each of them.
Firstly, the OnInvoke method gets called whenever a method has been invoked via
the Service Invocation building block. In contrast with HTTP-based services, there is a
single method that accepts all method invocations. You will typically have a switch-case
statement for each of the methods your service supports:
Then, let’s override the ListTopicSubscriptions method that returns all topic
subscriptions:
277
Chapter 14 Using Dapr in ASP.NET Core
And finally, let’s override the OnTopicEvent method that processes all incoming
messages:
278
Chapter 14 Using Dapr in ASP.NET Core
Note When running the gRPC service, don’t forget to specify to Dapr that the
protocol of the application is gRPC along with the port it listens on. The Dapr CLI
accepts the --app-protocol flag for this purpose. In Kubernetes mode, the
annotation is dapr.io/app-protocol.
Retrieving Secrets
Secrets can be retrieved by calling the Dapr API or using the DaprClient class in .NET,
for example. But if you want a more elegant solution, the Dapr SDK for .NET ships also
a configuration provider that loads the secrets your application will need. Then by
registering the IConfiguration instance in the dependency injection container, you can
use it even inside controllers.
The Dapr configuration provider comes from the Dapr.Extensions.Configuration
NuGet package. Once you have installed the latest version of it, go to the Program.cs in
your ASP.NET Core project to configure the provider:
return Host.CreateDefaultBuilder(args)
.ConfigureServices((services) =>
{
services.AddSingleton<DaprClient>(client);
})
.ConfigureAppConfiguration((configBuilder) =>
{
var secretDescriptors = new DaprSecretDescriptor[]
{
new DaprSecretDescriptor("very-secret")
};
configBuilder.AddDaprSecretStore("mysecrets",
secretDescriptors, client);
})
279
Chapter 14 Using Dapr in ASP.NET Core
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
To validate that it works, after you run the Dapr application with dapr run, open
http://localhost:5000/secret, and it should return the value of the secret coming
right from the JSON file.
Note Keep in mind that this example requires you to have a secret store
component. I have included one that keeps all secrets in a JSON file as it is useful
for local testing purposes. It is very important to consider that the location of the
JSON file is addressed depending on the local directory where you have called
dapr run. Make sure to call dapr run --app-id secrets --components-
path components -- dotnet run in the project directory; otherwise, it won’t
be able to locate the secrets file.
280
Chapter 14 Using Dapr in ASP.NET Core
Summary
In this chapter, you learned what goodies the .NET SDK for Dapr brings. You can
leverage certain attributes to subscribe to topics and to retrieve items from a state store.
The support for those two operations is also available when you don’t want to use ASP.
NET Core controllers for some reason but routing endpoints, instead. You also learned
how to implement a gRPC service by overriding the methods of the AppCallback.
AppCallbackBase base class provided by the Dapr .NET SDK. Then you learned how
to leverage the Dapr Secrets configuration provider in order to load secrets into the
application configuration.
In the next chapter, you will learn how Dapr integrates with Azure Functions.
281
CHAPTER 15
283
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_15
Chapter 15 Using Dapr with Azure Functions
By default, the Functions runtime includes only HTTP and timer triggers. If you
want to use any of the number of supported triggers and bindings, you have to install
the respective NuGet package. Those packages are called extensions. For example,
the extension for Azure Blob Storage is Microsoft.Azure.WebJobs.Extensions.
Storage. Likewise, the Dapr APIs are wrapped as triggers and bindings inside the Dapr.
AzureFunctions.Extension NuGet package. Extensions are implemented in .NET but
can be used by any of the supported languages inside Azure Functions – C#, F#, Java,
JavaScript, TypeScript, Python, PowerShell, and Go.
Functions are one of the enablers of serverless computing. The popular cloud
computing model Function as a Service (FaaS) is powered by those small and modular
pieces of code – the functions. Because of the fact that the underlying infrastructure is
abstracted away, FaaS in theory provides limitless scaling of function-based applications.
FaaS also has an intrinsically simpler cost model that is based on the number of
executions, duration of execution, and memory consumption. Keep in mind that the
limitless nature of scaling of FaaS also carries limitless charges. It might make sense to
introduce some throttling of client requests or allocate a dedicated capacity to safeguard
against a huge cloud bill.
There are several hosting plans for Azure Functions inside Microsoft Azure. First, the
Consumption plan starts at the lowest cost possible (almost zero), and as the demand
for function executions changes, it dynamically adds or removes Azure Functions host
instances on the fly. Second, the Premium plan builds upon the Consumption plan by
adding some advanced features like perpetually warm instances, VNET connectivity,
unlimited duration of execution, and others. The third hosting plan is to reuse a
Dedicated App Service plan to host your Functions-based application alongside any
web applications that might be running. That’s a great option when you have some spare
capacity left in the App Service Plan that you otherwise pay for. This is also helpful if you
want to avoid any unexpected charges incurred by the otherwise limitless scalability.
However, at the time of writing the book, the integration between Dapr and Azure
Functions provided by the extension does not support any of the cloud-based Azure
Functions hosting plans. Fortunately, the Azure Functions runtime is open source and
can be deployed to any Self-hosted environment, for example, Kubernetes and IoT Edge.
284
Chapter 15 Using Dapr with Azure Functions
Once triggered, a function can do a number of actions and optionally integrate with
some external resources by leveraging bindings. Note that the binding types we are
about to explore represent Azure Functions bindings and not the Dapr Bindings building
block. Input bindings supply input payload to your functions by gathering data from
various places. The following are the supported input binding types:
During execution, a function may output data to zero or more external services by
leveraging output bindings. At the time of writing, the following output binding types are
supported:
285
Chapter 15 Using Dapr with Azure Functions
Note The latest version of the Azure Functions runtime that is generally available
for production usage is version 3.x, which supports .NET Core 3.1. There is a
preview of .NET 5 support for Azure Functions.
286
Chapter 15 Using Dapr with Azure Functions
sure to only define the application port when you have at least one Dapr trigger in your
functions. Otherwise, the extension will not expose it, and it may cause issues during the
initialization phase of the Dapr sidecar.
In order for the function application to reach out to Dapr, it needs to know the port
on which the Dapr sidecar listens for HTTP. To achieve this, the value of DAPR_HTTP_PORT
is inspected; and in case it is empty, the function application uses port 3500 by default.
If you start the function application by invoking dapr run, it will automatically set the
HTTP port of the Dapr sidecar as an environment variable. In any case, when you want
to debug a function, you may start just the Dapr sidecar, but don’t forget to explicitly
set the port to 3500 by specifying the --dapr-http-port flag. Otherwise, the Functions
runtime won’t be able to access the Dapr sidecar.
Implementing an Application
Let’s build a simple application to put all this knowledge into practice. There will be four
functions. The first function is triggered by the Dapr Service Invocation building block
and pushes a message to a Pub/Sub topic. This results in a message-based workflow of
functions. The next function will be triggered by the incoming message from one topic
and will output the same message to another topic. Then the message in the other topic
will trigger another function that will persist the message in a state store. And to verify
that the data is persisted in a state store, there is one function that provides an HTTP
endpoint to retrieve the data from the state store. All that is achieved by only using Dapr
triggers and bindings that come from the Dapr extension for Azure Functions. You don’t
have to deal with HTTP requests, web frameworks, or any clients.
To get this started, open your favorite code editor or IDE. Visual Studio Code with
the Azure Functions extension and Visual Studio 2019 can guide you through the
project creation. Make sure to create a Functions project based on .NET with functions
implemented in C#. Then install the latest version of the Dapr.AzureFunctions.
Extension NuGet package. You are all set for writing the code of the functions!
The first function that is triggered by Dapr service invocation looks like this:
[FunctionName("Ingest")]
public static void Run(
[DaprServiceInvocationTrigger] JObject payload,
287
Chapter 15 Using Dapr with Azure Functions
outputEvent = payload;
}
The Ingest function has two important parameters with some attributes applied. As
you can notice, the DaprServiceInvocationTrigger attribute is applied to the payload
parameter, which contains the input data. This has the same effect as implementing an
endpoint with our favorite web framework to be called by the Dapr Service Invocation
building block. As the name of the other parameter outputEvent suggests, it is used for
pushing messages into a topic by leveraging the Publish and Subscribe building block.
In the function body, the value of the payload parameter is assigned to the binding
parameter that accepts the outgoing messages. The name of the pubsub component and
the topic can be specified directly in code, or you can reference an application setting. In
the preceding case, the percentage notation is used to represent a key in app settings so
that values are retrieved from the local.settings.json.
When the message is pushed to a topic, another function takes it from there and
sends it to another topic in the same underlying messaging system as configured by the
pubsub component. It uses a topic trigger and a Pub/Sub output binding:
[FunctionName("ChainTopic")]
public static void Run(
[DaprTopicTrigger("%PubSubName%", Topic = "%SourceTopicName%")]
CloudEvent inputEvent,
[DaprPublish(PubSubName = "%PubSubName%", Topic =
"%TargetTopicName%")] out object outputEvent,
ILogger log)
{
log.LogInformation("C# function received an event by a topic
trigger from " + "Dapr with payload: " + inputEvent.Data);
288
Chapter 15 Using Dapr with Azure Functions
Once received by the target topic, another function is triggered to persist the
incoming message into a state store by using a key that is specified in application settings
as well. Multiple incoming messages will result in replacing the value that is stored in the
state store:
[FunctionName("PersistState")]
public static void Run(
[DaprTopicTrigger("%PubSubName%", Topic = "%TargetTopicName%")]
CloudEvent inputEvent,
[DaprState("%StateStoreName%", Key = "%StateItemKey%")] out object
state, ILogger log)
{
log.LogInformation("C# function received an event by a topic
trigger from" + " Dapr Runtime with payload: " + inputEvent.Data);
log.LogInformation($"Persisting the payload into a state store");
state = inputEvent.Data;
}
And let’s see the last function that exposes an endpoint that you can use to
retrieve the latest version of the data stored in the state store. It is accessible at
http://localhost:7071/api/state/<key> where the key is a placeholder for the key
of the state item:
[FunctionName("GetState")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "state/
{key}")] HttpRequest req,
[DaprState("%StateStoreName%", Key = "{key}")] string state,
ILogger log)
289
Chapter 15 Using Dapr with Azure Functions
{
log.LogInformation("C# HTTP trigger function processed a
request.");
It’s time to see it in action. Navigate to the directory of the project and execute the
following command:
If you want to debug the Functions app, with say Visual Studio 2019, you can omit the
func host start in order to start just a Dapr sidecar. When you have both the Functions
app and the Dapr sidecar up and running, you can issue a POST request to http://
localhost:3500/v1.0/invoke/functionapp/method/ingest using, for example,
Postman or cURL. Make sure to send some payload in the body of the HTTP request.
Then a chain of functions will be triggered that will finish with persisting the payload
from the initial request into the state store. To check whether this payload made it into
the state store, open http://localhost:7071/api/state/stateitem1; and if everything
is good, it will return the payload back to you.
Summary
In this chapter, you learned how to unleash the power of Azure Functions and use them
alongside Dapr. Firstly, we touched upon the basics of Azure Functions, its hosting
models in the cloud, and how you can you can host it yourself. I also covered what
features the Dapr extension for Azure Functions provides and showed you how to
leverage them in a simple function-based application.
In the next chapter, you are going to learn about the integration between Dapr and
the low-code development experience provided by Azure Logic Apps.
290
CHAPTER 16
291
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5_16
Chapter 16 Using Dapr with the Azure Logic Apps Runtime
that are deployed and managed by Microsoft and that are used to access cloud services
or on-premises systems. For example, by using them, you can access data in SQL
Server, Office 365, Salesforce, and others. Of course, to access any on-premises services,
you will need to have the on-premises data gateway installed. It establishes a secure
communication channel between the cloud and on-premises.
Like Azure Functions, Azure Logic Apps is no longer a cloud-only offering. Azure Logic
Apps started shipping a set of NuGet packages that enable you to deploy and use Logic
Apps–based workflows in any environment you have – on-premises, in any cloud, or in
Kubernetes or not. This means that the engine behind Azure Logic Apps does not lock you
to a specific vendor. The release at the time of writing the book is a preview, and one of its
restrictions is that managed connectors cannot be used at this point. However, you can use
the built-in triggers and actions. Another limitation is that you will still have to provide the
credentials to an Azure Storage Account where the state of the workflows will be saved.
http://localhost:3500/v1.0/invoke/<app-name>/method/<workflow-name>
292
Chapter 16 Using Dapr with the Azure Logic Apps Runtime
Workflows can also be triggered by Dapr input bindings. The name of the binding
component should be the same as the name of the workflow that should be triggered by it.
You can call the entire Dapr API inside a workflow. It is just a simple HTTP action.
Have in mind that at the time of writing, there isn’t any custom connector for Dapr that
provides Dapr-specific triggers and actions. The triggers are possible because of the
gRPC service that implements the AppCallback interface needed for communication
with Dapr and by raw HTTP requests.
Designing a Workflow
Let me show you how to build a very simple workflow that gets triggered by Dapr service
invocation and persists the HTTP body of the request into a state store.
First of all, I should mention the tooling that you can use. Both Visual Studio
Code and Visual Studio 2019 have an extension for Azure Logic Apps that makes it
easy to visually design a workflow or declaratively edit its JSON definition. Keep in
mind that there are slight differences in the support in both places. For example,
Visual Studio 2019 expects you to have a fully fledged ARM template containing the
Logic Apps workflow in order to be able to open it in the designer. However, for this
example, I am going to use the Azure Logic Apps extension for Visual Studio Code,
which is currently in preview.
293
Chapter 16 Using Dapr with the Azure Logic Apps Runtime
Two projects are required for running workflows with Dapr. You can find them in the
source code of the book. The Dapr.Workflows project represents the Logic Apps host
that has the gRPC server and loads all workflows. The other project is named Workflows,
and it contains the actual JSON definitions of your workflows. Each workflow is placed in
a folder under the workflows folder. When you open the Visual Studio Code Explorer, you
can open each workflow using the designer that is provided by the extension. Consult the
extension documentation as it has a few prerequisites such as having .NET installed and
the Azure Storage Emulator to be running. Right-clicking a workflow.json file opens a
context menu that should include the Open in Designer option as shown in Figure 16-2.
The workflow itself is very simple. It contains a single HTTP trigger that is actually
triggered by the Dapr AppCallback implementation of the Logic Apps host. Once this
workflow is triggered, it persists the body of the HTTP request in a state store under the
key workflow-state. Opened in the designer, the request to the State API is depicted in
Figure 16-3. Then, as a response, the workflow just returns the body that was received by
the trigger.
It’s time to see this in action. But before starting the project, make sure to update
the values of the environment variables that point to an Azure Storage Account for the
Dapr.Workflows project. Those environment variables are defined in launchSettings.
json. This is currently needed because the state of the workflow is saved in an Azure
294
Chapter 16 Using Dapr with the Azure Logic Apps Runtime
Storage Account. This dependency will likely be removed in future versions of the Logic
Apps NuGet packages. Note that it won’t be possible to use the Azure Storage Emulator;
instead, use a real Azure Storage Account.
You have to make sure that there is a state store component named statestore
in the components folder. Now that everything is ready, let’s start the Logic Apps host
alongside a Dapr sidecar. Make sure the local storage emulator is running and also the
environment variables inside launchSettings.json point to a real Azure Storage Account.
Open a terminal and navigate to the Dapr.Workflows project directory and run the
following command that also includes a flag that points to the folder where the JSON
definitions of the workflows can be found and loaded from:
Next, you can trigger the workflow via the Service Invocation building block. You can
use your favorite tool for issuing HTTP requests like Postman or cURL. With cURL the
request that also passes some JSON content in the HTTP body looks like this:
295
Chapter 16 Using Dapr with the Azure Logic Apps Runtime
It will return HTTP status code 200 if everything ran properly and the body of the
HTTP request was persisted in the state store. To verify this, open your browser at http://
localhost:3500/v1.0/state/statestore/workflow-state and you will find out.
Summary
Azure Logic Apps offers a powerful way to develop complex workflows that automate
business processes. In this chapter, you learned how Dapr and Logic Apps can work
together in any environment, except in the cloud as the integration is still in preview.
The integration with Dapr makes it possible to define workflows that are triggered by
Dapr building blocks such as Service Invocation and Resource Bindings. Additionally,
you can call just about any endpoint of the Dapr API. You also learned how to design
workflows by using the Logic Apps extension for Visual Studio Code.
296
Index
A Azure Container Registry, 56
Azure Functions, 283, 291
AAD Pod identity, 229
application, 287, 289, 290
Access Control Lists (ACLs), 114, 116, 118
development, 286
Actor model
ports of Dapr, 286, 287
advantages/disadvantages, 188, 189
Azure Key Vault, 222, 226–229
use, 190
Azure Kubernetes Service (AKS), 55, 90
API Gateway, 17
Azure Monitor, 243
aspects, 18
AKS, 243–245
backends for frontends, 18
K8s, 246
choreography, 20, 21
traces from K8s, 247–249
functions, 17s
Azure PowerShell, 175
orchestrator, 19, 20
Azure Storage Account, 172, 292, 294, 295
Saga pattern, 19
Azure Storage Blob container, 171
sidecar, 21, 22
Azure Storage Queue/Azure Blob Storage,
Application frameworks, 7, 26
164, 172, 177, 179, 220, 283
Application Programming
Interface (API), 9
ASP.NET Core controllers, 272 B
endpoint routing, 275, 276 Bindings, 28, 118, 285
gRPC service, 276–278 Bounded Contexts, 10
publish/subscribe, 273 Bridge to Kubernetes, 89, 91
state store, 274, 275 Building blocks, 27, 28
subscribe topic, 273 Business capability, 11
Asynchronous communication, 13, 118
Atomicity, Consistency, Isolation,
Durability (ACID), 14 C
Authorization Code grant, 261–263 Caching, 17, 18
azqueue component, 178 Cart microservice, 11
Azure Active Directory (AAD), 261 Central Processing Unit (CPU), 5
297
© Radoslav Gatev 2021
R. Gatev, Introducing Distributed Application Runtime (Dapr), https://doi.org/10.1007/978-1-4842-6998-5
Index
298
Index
299
Index
300
Index
301
Index
302
Index
303