METE ATAMEL: Hello, everyone.
My name is Mete Atamel.
I'm a Developer Advocate.
And today, I want to talk about an app modernization story
with Cloud Run.
Back in early 2016, a friend of mine and I--
we wanted to build a news aggregator app.
In this app, we would go and fetch news articles
from various news sources, and we
would display them in a mobile app in a clean timeline.
My friend worked on the mobile front end
while I worked on the back end.
In this presentation, I want to talk about the modernisation
journey or the back end.
The back end was initially written with ASP.NET 4.6.
It was a Windows app, and it was deployed on an IIS host.
There were three main services.
The reader would fetch the right size feeds.
The transformer would convert them into a common format
that we defined.
And the web API would serve the feeds to the mobile front end.
The initial prototype, we worked on, on a couple of weekends,
and it worked.
It was very easy to understand, and it was very easier
to deploy because everything was bundled together.
Also, the IIS host was quite inexpensive.
At the same time, the app had issues from the beginning.
First of all, there was too much coupling between services.
All three services there were all bundled together.
The developer experience of the IIS host was not great.
Sometimes the machine where our application was running on
would go down, and we wouldn't know why.
Or if we had to look at the logs of the application,
I had to FTP into the machine and download the logs.
We didn't have any kind of redundancy,
any kind of persistence, or any kind of resilience.
So if there were any issues with any of the services,
it could potentially bring down our whole application.
But this was the initial prototype,
and it was a good start.
And it served us well for about a year.
But in 2017, we started looking into moving our application
to a better place.
That's when we came across Compute Engine.
Compute Engine-- they are virtual machines
on Google Cloud where you can have
as Linux or Windows-based virtual machines.
So we took our app and we deployed to a Windows Server
on Compute Engine.
Then we configured it the way we liked.
And then, we created what's called an instance template out
of our VM.
Instance template is kind of like a snapshot of your VM.
And then you can use it to create instance groups, which
are identical VMs load balanced behind the Cloud Load Balancer.
With this setup, we got redundancy.
We had two VMs running our application,
we got load balancing with Cloud Load Balancer,
and we also got possibility of autoscaling
because with instance groups, you
can set metrics and use those metrics to autoscale
if you need to.
It was a very easy move.
We didn't have to change our code.
We just had to deploy it to do Windows Server on Compute
Engine.
And the developer experience with Google Cloud
was much better.
We had integrated logging with Stackdriver,
we had VM snapshots for backup and recovery,
and we had easy access to our VMs with remote RDP.
At the same time, things were getting more expensive.
Instead of running on a single machine shared
by other applications, we were running on two dedicated VMs,
and we had Cloud Load Balancer as well.
Even though it wasn't that expensive,
it was more than we wanted for a hobby application.
But this setup served us well for a couple of years.
From 2017 to 2019, there were a few interesting things
happening in the tech world, especially in .NET world.
First of all, there was this new version of the net called .NET
Core.
And the most interesting thing about .NET Core is that it can
run on multiple platforms--
Linux, Mac, or Windows.
Secondly, there was a lot of hype around containers.
Everyone was talking about how to take a single monolith
application running on a VM and how
to break it down into smaller pieces
and run them in containers in a much more efficient way.
Third, we had our windows dependency.
Our application was a Windows application.
And that limited our deployment options,
and we wanted to get rid of that at some point.
And last, it cost our application.
Even though it wasn't that much, it was still something
that we wanted to bring down.
So all of these forces came into play, and in early 2019,
we started looking into how to containerize our application.
Now, before containerization, we had to first convert
our application from .NET to .NET Core.
So we rewrote our application as .NET Core 2.2,
the latest version at the time.
And this took a weekend.
We were pleasantly surprised because we
were expecting at least couple of weeks to do the conversion.
But we had all the libraries that we needed,
and everything that we were using on .NET,
they were already available on .NET Core.
Now, once we rewrote our application on .NET Core,
we freed ourselves from Windows, and we could run on Linux.
That's when we got into containerization.
We containerized our application with Docker
using some base images provided by Microsoft.
And then, we could deploy to multiple places
in Google Cloud.
There were two good options--
first, Google Kubernetes Engine, and second, App Engine.
We chose App Engine because all we needed was a web front end.
And everything was bundled together.
So App Engine served us well.
Google Kubernetes Engine could have
been better because it provided infrastructure
for more complex architectures.
But App Engine was enough for us.
So that's why we went App Engine.
With this setup, the Windows license fees were out.
So our application was running on Linux,
and it was cheaper to run.
We got hassle-free autoscaling.
Because in App Engine, you get autoscaling without having
to do anything special.
And we got the nice features of App Engine,
such as revision management and traffic splitting.
But we were still running on virtual machines.
App Engine does a good job of hiding the fact that you are
running on virtual machines.
But behind the scenes, on App Engine Flex,
you are running on virtual machines,
and you need to pay those virtual machines per second.
This meant that our application, when it was not
used-- especially during the evenings-- we
were still paying for those VMs at all times.
Also, the deployments of App Engine--
there were kind of slow.
It could take anywhere from 5 to 10 minutes.
Even though it wasn't a big problem,
it slowed us down during our development cycle.
Around mid-2019, something new came along called Cloud Run.
And the whole premise of Cloud Run
is to bring serverless features to containers.
This was very exciting for us, because our application
was already containerized.
And we were already looking into running it
in a serverless way, especially when it comes to billing.
So in mid-2000-- also, I should mention that Cloud Run,
it has a couple of use cases.
First one is public endpoints.
Cloud Run is very good for that.
And also, the second use case is private endpoints
where you need to do some kind of background task.
Cloud Run is a good fit for that as well.
And both of these use cases were the use cases or application.
We had the web front end that was public.
And we had internal services that
could be private endpoints on Cloud Run.
And also, the best part of Cloud Run was its billing time.
In Cloud Run, even though your container
might be running for a while, you only pay for the time
that the requests are being handled.
This meant that our application, when
it wasn't being used in the evenings,
it was scaled down to zero and we paid nothing.
So with this in mind, we tried to deploy our application
to Cloud Run in mid-2019.
To do that, first, we upgraded our application
to the latest version of ASP.NET Core 3.0.
And then, deploying to Cloud Run was a single command--
gcloud app run deployed, and it was there.
You didn't have to change anything.
Now with this, we got serverless billing.
And this really brought down our billing considerably.
Because we weren't paying for anything when our application
wasn't being used.
We also got awesome developer experience with Cloud Run.
We had fast deploys, so we could deploy within seconds
instead of minutes.
And we got integrated logging with Stackdriver.
And we got all the awesome features of Cloud Run
with traffic management, and revision management, and so on.
At the same time, we were still running
as a single monolithic application.
And we had monolith issues.
Those monolith issues were, first, scaling.
We could only scale all or nothing.
If we had to scale the web front end because there
were more users, we had to scale the reader and the converter
as well.
And this wasn't ideal for us because we
had to scale individual services as we needed.
But we couldn't do that in a monolith application.
Second, we had an issue with cold starts.
So when our application wasn't being used,
it was scaled down to zero.
And this was great for billing.
But at the same time, when the first user came along,
our application would wake up.
It didn't have any kind of persistence.
So it had to go and fetch the feeds,
then it had to transform them, and it
had to serve to the user.
So this meant that the first user could
wait for a while for the data to show up,
and this was becoming a big issue.
Third, all of the state was in memory.
We didn't have any kind of backup state.
And we wanted to have something more persistent.
And lastly, we didn't have a way to update individual services.
Most of the times, we didn't have
to update our reader or our web API,
but we had to change rules about how
to transform their RSS feeds.
Even though we only had to change
the small piece or our location, we
had to update our whole application
because it was a monolith.
So with these issues in mind, in stage four,
we started looking into how to break down monolith
into smaller microservices.
This was early 2020.
And this is what we came up with.
First, we had this new service called Reader.
This is a private Cloud Run service.
It's private, meaning it doesn't have a public endpoint.
And all it does is, it goes and fetches
RSS feeds and brings them back.
Now, this Reader is being called by Cloud Scheduler, which
is a scheduling service on Google Cloud.
And Scheduler would call Reader once an hour, every hour,
and Reader would go and fetch the feeds.
And it would save the feeds into Cloud Storage,
which is a cloud-based object storage service.
Now, once the fees are saved into Cloud Storage,
Cloud Storage can generate notifications
to a Pub/Sub topic.
And these notifications, they can
be served to another Cloud Run service called Converter.
This Converter, it's another private Cloud Run service.
And it will look at the list of rules that we defined,
and it will transform these RSS feeds from different formats
into a common format that we defined.
Once they're put into the common format,
they're saved into Cloud Firestore.
This is a fully managed, NoSQL database.
And it's perfect for JSON-like data.
And then, we had another public Cloud Run service called Web.
This was the public front end of our application.
The users-- the end users, the mobile app,
would go and ask for the feeds from the web service.
And the web would just look at Firestore
and fetch the latest data from there.
As you can see, we brought our monolith
into three separate microservices.
Two of them are private services because they
don't need to be public.
One of them is public.
They are totally independent from each other.
So the reader doesn't have to know about the Converter,
and the Web doesn't need to care about the Converter
or the Reader.
They are all event-driven, so they
don't need to call each other in a synchronous way.
And they can be individually scaled.
So if we have more users, we can only up-scale web front end.
We don't have to scale the Reader or the Converter.
And they can be individually updated as well.
Usually, they'll need to update Reader and Web.
Because the way we read and the way we serve
is always the same.
But we have to update Converter quite often.
Because the rules around RSS feeds, they change quite a lot.
And this setup allows us to do that.
So with this setup, even though it's a little bit more
complicated, we so all the issues
that we had with the previous application.
Now, the lessons learned in this transformation journey--
first of all, transformation does not
have to be all or nothing.
As you can see from our journey, it
was a step-by-step transformation
where each step added something new to our application,
removed some legacy, and opened the way for new technologies.
Secondly, sometimes you need to use
non-optimal optimal solutions to get ready
for optimal solutions.
For example, in our case, when you look at it,
App Engine Flex was probably not the most optimal solution
because we were still running on VMs.
Cloud Run was the optimal solution
because we needed an event-driven microservices
kind of environment.
And that's what Cloud Run allowed us to do.
But to get from our VM to Cloud Run,
we had to use App Engine in the middle.
And by doing that, we went from a .NET Windows app to a .NET
Core app running on App Engine.
It wasn't ideal, but it served us well
because in the next iteration, we
could much easier move from App Engine Flex to Cloud Run.
Third, even simple things like lift and shift to the cloud
can have huge benefits.
In our application, by moving from IAS to Google Cloud,
we didn't have to really change any of our code.
All we had to change was our deployment and then
where we deployed.
But by doing that, we had instance tablets, instance
groups, and load balancing.
And with those, we got more resilience and redundancy.
So, to get benefits, you don't always
need to change your code.
You can just change your environment
and still get those benefits.
And at some point, you should expect some kind of rewrite.
Especially if you want to use the benefits
of the new environment that you're running in.
For example, in our case, when we deployed to Cloud Run,
initially, we deployed as a single monolith.
We got some benefits, but it wasn't
all the benefits of Cloud Run.
To truly use the benefits of Cloud Run,
we had to actually decompose our application into microservices.
And we had to make them event driven.
And for that, we had to restructure our code
and had to break it down.
So it was a lot of work that was needed
in order to use Cloud Run in a way that made sense for us.
And we all talk about monolith applications,
and how they're bad, and how we should break them down
into smaller pieces.
And it's usually true.
But at the same time, this is a hard process.
It involves refactoring and restructuring your code.
And then it involves figuring out
how these new services will communicate with each other,
whether it's going to be event driven,
or whether it's going to be synchronous,
and what kind of storage they're going to use.
So there's all these questions that you need to answer.
Monolith decomposition-- it's something
that you should take it very seriously.
And you should have a really good reason
to do it if you want to do it.
So that's all I have for today.
Hopefully, this was useful.
And if you want to read more about this application,
I wrote a blog post series where I
explain in more detail what this application is about
and give you more details about it.
And the code is, as well, on GitHub.
So if you want to take a look at the back end code,
it's out there.
And thanks very much for listening.
Thank you.