Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hi everyone, I'm Lernak McJolo. I'm a senior solutions architect
with AWS. I have 18 years background in technology and financial services
industry. I started with centralized web infrastructure
such as authentication systems, single sign on federated authentication.
I also work with high performance distributed caching as well
as multilegion cloud native deployments using infrastructure,
AWS code and pipelines to name a few.
And in those specific talk I'm using to go over
why and how to move from monolithic applications
towards microservices in a practical example with a
demo, and we are going to be chipping away at the monolith using
the strangler fig pattern. So let's jump in.
So first of all, why would we want to do this?
So if you think about monolithic applications, what we are referring
to is an application that needs to be deployed in its entirety
together with all of its components.
And we have multiple layers inside such an application,
such as presentation, application logic, data access
layer, and the application has one
database for the entire app.
You have one technology stack for the entire app,
and also it may be running as a single process.
You may have many developers that are all pushing changes through a
shared release pipeline, and that may cause frictions at different
points in software development lifecycle.
For example, if you were to want to upgrade a shared library to take
advantage of a new feature, you need to convince
everybody else such that they upgrade at the same time.
And if you were to quickly push an important fix for
a feature, you still need to merge everybody else's
changes alongside yours.
So over time, what you may see is
the challenges of an overgrown monolith. So what are those challenges?
Let's take a look at that.
So one of the challenges is a list of feature requests that we
are not able to deliver in a timely manner to our users.
So we have long lead time to delivering features.
Another challenge is frequency of releases where we have releases
maybe a couple of times in a year, and because we're
not deploying as frequently to production, each release contain
a lot of changes for the entire application.
That then creates a high risk deployment. We have
many changes bundled together. We're not really exercising
our muscle to deploy to production because we're not releasing to
production enough. So we have high risk deployments,
but also if we run into any issues during the release,
we also have high risk rollbacks.
So you may typically see in this scenario teams prefer to fix
forward, which is pretty high risk as well, but it's
easier. So I've been in a situation
worked in a team where I faced each one of these challenges.
So I know of the pain of each one of these challenges
from personal experience. So you may now ask,
how about microservices? So let's take a look at
the microservices. What are they?
So microservices are minimal function services
and they're each organized around business capabilities.
You can deploy each one independently and
each can scale independently as well.
Each has its own independent technology stack
and its own data store.
They can talk to each other over APIs and
you can have different teams aligned to each microservice such
that the teams independently architect, design, develop,
deploy, maintain each of these microservices.
So all of that is good, but how does all of these things tie
in together? So we need to evolve
this monolith. It has the challenges that we just talked about.
Now, when we talk about evolving the
overgrown monolith, we are not thinking about a big bang approach
because that just is not realistic. Instead, what we
need to do is an iterative incremental approach.
We can explore different new technologies and
refactor components from this monolith
where we are looking to gain some speed, to gain some agility,
or we're looking to be able to independently scale
specifically to meet the demands of the business.
So we are iterating modernizing incrementally
for a business purpose. So this is where we are
going to start.
Now, why should we refactor the service for
business value?
Independent deployment is the heart of microservices and
with that we are going to gain reduction of the
blessed radius for deployments.
Each microservice is focused on a specific business
function with a single responsibility and
the teams aligned to a microservice can decide their
own architecture and their own tech stack. This brings
functional autonomy.
With that we have minimized the dependencies
across and that will maximize the development
velocity for the teams working on the microservice.
Now with each service focused on a single responsibility,
we have the opportunity to focus scaling the
high demand functions as opposed to scaling
the whole app in its entirety, including the lesser used
parts, lesser used functions alongside
it. So let's take a look at our sample
bonnet. We have an ecommerce shop.
It's called the Unicorn shop. This is where we are selling
unicorn related items such as a
unicorn pencil, unicorn notebook and
the application is a spring boot Java application.
It has a MySQL database and
on the front end it is using bootstrap.
The application is deployed on EC two within a dedicated
VPC in a public subnet for the purposes
of this sample demo. And this is basically
the tech stack for the entire monolithic ecommerce shop application.
So our monolithic application has a number of controllers on
top. So you see the inventory
management in the unicorn controller. You see the
shopping cart or the basket management in the basket controller,
and the user controller that deals with user management with
registrations and logins. We also have a health
controller that performs basic health checks that I didn't include.
Over there at the bottom, we have the database tables.
We have each one focused on a specific domain.
So we have three tables. We have
the unicorns table. That is our unicorn teams
inventory all the way on the right. We have the users table. Those are
the users in our system. And in those middle, we have
an association table that associates the users
and their selections of unicorn items in their basket.
So how do we go about evolving this monolith?
We will use the Strangler fig pattern that's named by Martin
Fowler from Thoughtworks and alongside him Sam Newman,
previous thoughtworker. Both have really useful content on the topic.
The idea is that our new services, the microservices,
are similar to the strangler fig that's slowly growing around
the trunk of the old tree, the system, the monolithic application.
Now, if we go back to the sample in our ecommerce
application, those basket functionality is critical and
it needs to be highly available, durable and scalable to meet
the ongoing sales demand, as well as spiky
demand for sales such as Black Friday surges.
So one good first step to evolve the ecommerce app is
to move the basket functionality that's critical to the
business out of the monolith and allow it to scale
independently.
So when thinking about breaking a monolith, we need to consider a
few points. What is the best technology stack that
is going to help us to implement the new microservice?
And what would scale best based on the usage of
this, in this case, shopping cart service, the basket service.
There is existing data in the tables in the MySQL
database. So how do we move the data from the database
of the monolith to the database of the new microservice?
There will be internal data exchange between monolith, microservices, AWS,
the monolith. So when we are breaking those monolith to smaller
chunks, we are introducing those chatter to the network. So we need
to be thinking about the performance impact of
that.
How do we enable seamless switchover between
the monolith and microservices? How do we make sure
that the current customers, current consumers of our APIs
won't break.
So let's focus on this. How can we seamlessly chip away
at the monolith? First of all, there is an
existing service contract. That's a REstful API that is exposed by
our monolithic application, and we need to abide by
this contract such that we're not breaking any of our existing
application consumers existing clients. So how do we
achieve that? First of all, what's in this restful
API? For example, if you want to create a user,
you'll be showing an HTTP post to user
Uripad. If you wanted to log in, that's a post
HTTP post to user login Uripad to
get all of the inventory, all of the unicorns, such that we
can show it in the homepage. It's a get HTTP,
get on unicorns and so on.
So we are interested in replacing the monolith's implementation of the
shopping cart functionality with the new microservice and
all of the shopping cart. The based related API
actions like adding, removing items from the basket,
getting the contents of the basket, they're all at a specific
URI pad, so they're hanging off of the base URL,
unicorns and basket. So that's those URI pad.
So we will first introduce a reverse proxy in between the
apps consumers and the monolith. And for
now, the reverse proxy is taking the requests
from the consumers, the application consumers, and routing them
all to the monolith.
We can then introduce the refactored microservice behind the reverse
proxy, and we will configure it to route the requests
mapping to the unicorn's basket URI Pat
such that it all goes. Those requests go to
the refactored shopping cart microservice. So one
way to achieve this already today, like on AWS, is to use
AWS API gateway. API Gateway allows you to define resources
and methods. So first you would create all of the
unicorn resources, and then you
would add methods on top of that. You would configure,
create and configure HTTP methods on top of those.
But is there a faster and less manual approach?
We can leverage AWS migration hub refactor spaces to
accelerate the evolution of our monolith. It is going
to help us with managing the iterative refactoring process
while operating in production using the strangler
fig pattern. This way we can focus on developing the applications
for the new microservices and not lose time and effort
in creating and managing those underlying infrastructure that
makes refactoring possible. So let's dive into how
refactoring the monolith looks like using those approach.
So let's walk through the steps for also going to be
used for our demo of refactor spaces.
First you are going to create the refactor spaces
environment in an account that becomes the environment
owner. This account is special because it
is going to get cross account visibility into the other accounts
that are hosting the other services, such as the monolithic application
and the shopping cart service. Now,
refactor spaces configures transit gateway in this account
on our behalf when we create the environment, and because
the transit gateway is in our own account, we can
customize it as needed. Next we are
going to add the other accounts to be used for refactoring
to the environment. So in this example
we have two accounts. One is the existing monolith
account that's already available and
then the other is for the new microservice.
So in real world you likely have the monolith
app first. So you would be adding that account first and eventually when
you create the shopping cart service or the second microservice
for example, then you would add that later.
Now in our demo we are actually using one account for both of these,
but as a best practice in terms of following the multi account strategy,
the recommendation is to use an account
per app for isolation of resources, which I'll dive into
in a little bit. Next we are going
to create an application proxy in the environment owner
account. Remember that reverse proxy? So that's what we are doing here.
And AWS part of this refactor spaces on
our behalf is configuring the API gateway VPC link
and network load balancer such that it can allow external
HTTP access to the services in our
refactor spaces environment. So the monolithic application
and also the shopping cart service in those example.
Next we are going to add a service with its VPC
or you can also create this service from the specific service
account. For example from the shopping cart account you could create the service
and refactor spaces configures the transit gateway to
bridge the vpcs. So traffic is now permitted
between the service vpcs in each of the accounts. So the monolith
application VPC and the shopping Cart VPC
now have cross account network
bridge and now
we are ready to add a route for the external access to
our application. So we will likely first add
the default route to send all of the application traffic to
the monolith. And all traffic
initially is using to services by the monolith and API
gateway is just acting as our front door to the application right now.
And then once the monolith microservices AWS ready, it's available,
we will route a specific set of the URI
pads to the monolith microservices AWS
external app users don't know that traffic is now being handled by
the new shopping cart microservice for the shopping cart
requires so also want to deep dive
into the architecture here. One of the major benefits of moving towards
microservices architecture is that we can develop each microservice
using different technology stacks that is most suitable for the use case.
So here we decided to use lambda as the compute and Dynamodb
as a database for the shopping cart functionality given
their ability to run and scale based on usage with high
availability. Think about Black Friday
or Cyber Monday. So our unicorn shop is not going to have any
issues with scaling for those spiky sales days
in this architecture. And Lambda and DynamoDB are
serverless. That means that we don't need to worry about
provisioning and managing servers. We can focus on
application development for this shopping cart application and also we
will only pay for what we use now
another thing that I had mentioned is that refactor spaces is enabling us
to follow best practices in having account level isolation
for these services. So we're following the
multi account strategy for each application here,
monolith app and the shopping cart app we have a separate account and
this is helping us in multiple ways. We are
going to minimize the blessed radius with this approach.
If there is an issue in one of these accounts with one of
these apps that is scoped
to the account in which the application is,
we are isolating resources that belong together
based on perhaps their security profile or data profile.
For example, different business units, different environments will
have different security profiles, different data access profiles
and we are able to isolate those resources if
we use separate accounts.
We are also ensuring that each workload gets a well defined
individual quota for its resource limits because we
are using a separate account.
So here we're using refactor spaces. It is orchestrating the networking and traffic
routing requirements for us. It's creating that network fabric that
we talked about earlier with the reverse proxy.
And this way the monolith and the new microservice
are able to communicate directly across different AWS accounts and our
users are not impacted of any changes that are happening as we are refactoring
with chip innovate this monolith.
So let's dive into our app modernizing workshop.
So here is the workshop link. If you'd like to follow along
in your own account, in your free accounts.
And of course, after you're done with the workshop, make sure to clean up your
resources.
So, brief overview of the steps we just already discussed.
So it's the same steps. We will be creating refactor spaces environments.
We will be adding the accounts. In our case, we only have one
account, but best practice is to have multiple
accounts, one account per application per environment.
Ideally we will create the app proxy
in refactor spaces. We are not creating these resources individually.
Refactor spaces is taking care of the creation of the API
gateway Transit gateway VPC link network load balancer.
Underneath the covers we'll be creating
defining those services, the monolithic application
and the shopping cart service. In reality, in real world,
you would likely first have the monolith and then you would add the new
microservices as you create them. And then we'll be adding the
routes such that we can route the appropriate requests
based on the URI pads to the appropriate service that is
in changes of those requests. So in this case, Unicorn's basket
URi pad is going to be routed to the shopping cart microservice,
the new one and everything else. The default route for
all those other requests will be routed to the monolithic app.
So let's jump into our demo. So as
a first step, we create those refactor spaces environments.
Here at the bottom you see a brand new one that was
created and when we do this, what happens is that
transit gateway is also created on our behalf by refactor
spaces. After this step, if we have
our monolithic application in a different account, then we can share this
refactor spaces environment to that account. We can also
do that once we chip away at the monolith.
If we have our new microservice in a different account, we can do
the same. We can share those refactor spaces environment to the
microservice account. For the purposes of the workshop.
We have a simple use case, so we have all of the
services in the same account. But as a best practice,
you already know that in real world you'll be following
multi account strategy. After you create the environment,
the next step is to create the application.
And when you create the application, it will also create
all of the application proxy resources. Remember that's the
API gateway, the VPC link, the network load balancer,
and this way you are going to have a proxy endpoint URL
that is going to be used by the front
end, in this case for the unicorn shop, the front
end that is bootstrap JS deployed
in public facing s. Those bucket is going to be
using that proxy endpoint URL as its destination
as soon as we put the monolith behind that
proxy endpoint URL. So here you're seeing the
front end, the application that
we'll be creating in refactor spaces. And first
we are going to configure the
service, the monolith service behind it. We are going to
route all of those requests by default to the
monolith. So that's what you would typically do as well in the real world.
And then AWS, you apply the strangler fig pattern to
chip away at the monolith. In the next stage, you would
add another service and you would define all of the routes to that
service. In our case, that's a shopping cart. So I'll be going
over those routes and how
we would configure that as well in a minute.
So as you can see, as part of creation
of the application, we are telling it where to put the
proxy VPC and where to deploy those resources,
which is the API gateway reST API, the VPC links
network load balancer and the resource policies resources.
So here I already have an application inside this
environment, refactor spaces environment. That's the unistore app
that already has the API gateway endpoint.
It has the network load balancer that it created on its
own. I didn't have to create those on my own. So that is what refactor
spaces is giving me.
I don't need to focus on creating
all the network layer resources that are required
to make refactoring easy.
And after I'm done creating the application, then I came
here and I created those services. First I
added the monolith. I named it Monolith and
I gave it the endpoint, which is this is the
EC two URL endpoint from
inside those VPC. And after
that, once I chipped away at the monolith and I
created three lambdas, using the workshop to add
teams to the shopping cart, to remove items from the shopping
cart, and to get teams from the shopping cart. Each one of
these is a separate lambda. Remember with the DynamoDB
database for
those services.
Here is how I'm doing the routing. So first of all,
it is Pat based URI pattern
match. So unicorns basket. That's the pattern match on
the Uri pad. And also it's coupled with HTTP
verbs because this is a restful API to
get the items from the cart we're using HTTP get.
So that is mapping to the get cart service lambda to delete teams
from the cart we are using HTTP delete
and that is mapping to the remove cart service lambda.
So any requires that come in as HTTP deletes on
those unicorns basket is going to be routed to this lambda and
also addition of items to the cart using
to get routed to the add to cart service lambda.
So it's as easy as that. I'm able to come
here in the future if I want to add another microservice.
If I decided that over time that
there's a need to chip away further to this monolith,
I create that microservice in a different account,
I'll share my refactor spaces environment to that account and
I'll define the service inside this application, the uni store
unicorn shop application inside this
environment. And then I will also define the routes,
those Uri pads coupled with the
HTTP verbs that I'm
going to use to route to this new microservice.
And over time you can iteratively,
incrementally chip away at that monolith using refactor spaces
without having to worry boot the network load balancer,
API, gateway, VPC link Transit gateway, all that
configuration because it is all taken care on your behalf.
So here is how that unicorn shop looks
like this is using the monolith
for everything but the shopping cart. So if I'm
doing actions inside the shopping cart,
then those actions are going to my
lambda. But if I am actually doing other
things here, if I am browsing,
clicking on other things, if I'm logging in, et cetera,
then those are going to the monolithic
app. So with that concludes
those demo. So here are our key takeaways
from this talk refactor. When it provides business value,
iterate incrementally. If it still provides business value,
use an app proxy such that you honor the service contract to
the users so that all of these changes are happening seamlessly to
them and follow multi account strategy.
For any new microservices that you're chipping away from
that monolith min minimize that blessed radius and
all the good things that come with multi account
strategy benefit from them. Thank you.