 So hi, everyone. Thank you for joining me today in app developer con. I will be talking about application modernization. I'll try to pair theory with a few of the lessons learned either through our journey to modernize our cloud stack at MongoDB or the work my team and I were doing at our time at Citrix before that. And hopefully, you're going to find this useful and you're going to find valuable insights that are going to help you with what you are building. I'm George Sanjaras currently working as a director of engineering at MongoDB. I'm looking after our customers who deploy and manage MongoDB on Kubernetes and also our enterprise advanced customers. A big part of what we'll see today comes from a larger body of work that we've been doing for just over a year now, which will come out, hopefully, in May. It's the upcoming book Practical Platform Engineering by PACT. And you can connect with me to get updates on that. So a good way to start is to make sure that we're all on the same page as application modernization has been one of those terms that's been misused quite often. So application modernization is about updating legacy software in order to adopt newer software paradigms. That is done by adopting new frameworks, architectural patterns, infrastructure and components, and even new languages altogether at some cases. So what is interesting here is that even in this really nice definition, the interesting part is with the words updating and legacy, which are two words, again, quite broad and quite misused. So let's dive into those a bit deeper. So legacy has always been one of those concepts that we really love to hate. Legacy software is used to describe applications that have a very bad code base, very complex spaghetti code, don't work properly, aren't reliable, and in many more cases where kind of legacy is this catch-all term about kind of bad code. But legacy also means mission-critical, and that's often where the problems start. The concept of if it's working, we shouldn't touch it. It's hard to update, hard to scale, and changing legacy systems often is complicated in terms of stakeholder management and business decisions. So if it's so hard why are we trying so much to do it? And most importantly, we often reach critical points when organizations are growing fast or growing a lot that we just can't avoid doing it. Another big driver is reducing the complexity and the cost of running services, making deployments easier and more frequent rather than something that happens once a quarter and is kind of the major company event, the release, and over to improve the quality of our services, so improving uptime, improving resilience, and so on. The 7Rs is a great way to start exploring the options we have as to how we can modernize a service. Retiring, replacing, and repurchasing is used more to address third-party services, so we're not gonna be addressing that a lot. Retaining means doing nothing about it, so obviously I couldn't do a whole presentation about doing nothing about it. So we'll be focusing more on refactoring, replatforming, and re-hosting. So refactoring has the biggest impact but also requires the biggest effort and carries the largest risks. It's about re-architecting the code base and even rewriting big parts of it. The re-hosting strategy, we also call that lift and shift, involves lifting parts of an application from on-prem or a different cloud environment to a new cloud environment to a new infrastructure. Shifting is done with very little or no modification in the lift and shift strategy. It's a kind of a beginner's approach to start kind of application modernization. Now it's very easy to implement from a code perspective, but alone you might not be getting the full benefit of the underlying infrastructure, so a good example there is kind of taking a monolith that's running on a VM and putting it into Kubernetes without doing any refactoring. You are gonna have some benefits, but definitely you're not gonna see all the benefits there. Finally, replatform migration is the middle ground between those two. It's similar to re-hosting, but it does involve some modification of the application to take advantage of the underlying infrastructure. So let's start by taking a closer look into refactoring. So what we want to do in refactoring is to go from the monolith on the left and start adopting architectures further on the right, like microservices and microservices. Now a monolithic architecture is kind of a traditional architecture. The entire system is built as a single unit, the components are tightly coupled. It's simple, it's easy to implement, but it's kind of hard to maintain as the organization scales. The monolithic architecture is not as bad as we kind of present it to be nowadays. It's good in some cases, kind of small projects, new greenfield projects and so on. Microservices focus on building small independent services that can be deployed and managed independently. They have a very specific business function. They interact through well-defined APIs. And they're more complex to implement and manage and kind of there is this risk that we can over-complicate things and go to very small services that are not self-sustained and they don't have business meaning. Now finally, microservices are kind of the new kid on the block. So a few descriptions include kind of that they're large microservices, they're so-apart of a kind of partial monolith. Another definition which is good says that there are a few big loosely coupled services, kind of a broad definition there. So basically they're stripped down and streamlined monoliths. So they have more business meaning than just a microservice that does one very small tasks. And it's kind of easier than managing hundreds of microservices with very narrow contexts. So as we start to modernize the architecture, we have a few architectural tools. First off, service-oriented architectures. It's based on the services context. So the system is divided into services. Each performs a specific functions. Those services communicate with each other using well-defined interfaces. Service-oriented architectures is highly scalable, flexible, and it's a very good choice for large, complicated projects. However, it is more complex to implement and maintain. So this kind of sounds a lot like microservices, right? So microservices are considered the variant of service-oriented architectures. The second tool is domain-driven design. Now, DDD focuses on modeling the domain of a software system. So we divide the system into domains. Each domain has its own set of models and business logic. Again, this is a highly scalable architecture. It is a great bridge between the gap, between engineering and business functions. We have a common language between business and engineering. But the modeling process, again, here can be a bit time-consuming. Lastly, we have event-driven architectures, EDA. So here we focus on the flow of events between components in the system. So each component communicates with each other by sending and receiving events. That's a core principle in EDA. The architecture is highly decoupled and allows for kind of creating highly scalable systems. And how do these three concepts come together? So, SOA and EDA help us design develop-driven micro and macro services, while DDD helps us define those services and their functionalities. So, well-designed services are the ones whose name is familiar to everyone, from the developer to the CEO, and the executives actually care how the services are doing. So let's take an example. So we have a monolith with all of the modules of building an e-commerce website. So using a DDD approach, we can start by defining different bounded contexts in the e-commerce domain. And as a second step, we'll be choosing one of those contexts as our first micro service. We can start by thinking about the module that is used by many other services. It's quite independent and would make sense to kind of be a standalone service. No surprises. The authentication module is always a great first candidate. This was kind of also the starting point in Mongo's SOA journey. This technique is called strangling the monolith. So essentially, you build a strangling facade that is an API gateway that makes this transition invisible to the outside world. So after a module has been carved out of the monolith and is deployed as a new service, now we also have to think of what we call the backing services. So according to the 12-factor app methodology, a backing service is a service that the app consumes over the network as part of its kind of normal operation. It can include data stores, queuing systems, SMTPs, casting systems, anything like that. So to our new service, the backing services are attached to the service. So the final step each time is to give the new service its own backing services and full ownership of it. So a service has full ownership over its database. So after doing that for one service, we repeat the process for each of the bounded contexts until the monolith has completely disappeared. Although this seems pretty simple, a lot of kind of new complexities arise throughout the process. As we're not talking about services instead of tightly coupled modules, we have the new issue of service discovery. Asynchronous communication brings the complexity of interacting with pub-sub services, managing state, and last but not least, we now have two managed secrets used by multiple different services. Now, a great tool that I really like that solves those problems is DAPR. So DAPR provides simplified interfaces to kind of implement these functionalities. It makes it easy to build stateless and stateful applications and kind of embrace diversity when it comes to different languages, developer frameworks, and even underlying infrastructure. So in a nutshell, it makes it easier to manage state to orchestrate communication between services and many more of the problems we are facing. And it's very easy to implement. So we can see from top to bottom, we can see how easy it is to do service invocation. We can see how easy we can publish messages in a new pub-sub system. And we can also manage states, manage secrets. The added benefit here is that you don't care about the underlying infrastructure component. So in our state store example, the code would be exactly the same irrespective of whether you're using Redis or MongoDB or anything to manage your state. So this was a very quick journey of the how. And kind of the question that always follows this is the when. When should we do it? Is this approach something that we should be doing in all of our projects in every case? And that's been one of the issues with these architectures. They've often been treated as kind of catch-all solutions where we want everything to move to microservices, everything to move to Kubernetes, and obviously that's not the solution for everything. So a great example is when we're starting a new project, a Greenfield project, or we have a very small team start-up, managing the complexity of microservices might not be something that we should be doing. So a very important tool there is the modular monolith. Essentially, we use, again, DDD to define the bounded context and build each module in a tightly coupled monolithic architecture. So every module represents a bounded context. This gives the code base that is kind of easy to deploy, easy to manage initially, but is also ready to be split very easily when we want to scale. Each module represents kind of a future micro-service, and it's going to be very easier to carve out when the time comes. So although modules are tightly coupled, they use well-defined interfaces between them and this is going to be used as the interface when we break down the monolith. Another great tool I really like for this is Backstage. Specifically, the templating functionality of Backstage. You can define custom templates and create custom scaffolding functionality, and this can help you reduce the time to build a new service, but most importantly, it can help you maintain architectural consistency when building one. So it's much easier to kind of define a template, and when the time comes to launch a new service, you can base it on the templates to follow this architecture. So now that we've done the refactoring part, let's very quickly see kind of how a platform approach can help us with re-platforming and re-hosting. So what we're looking to achieve here is a very simple way to provide another service experience for engineers. We want to underline... to abstract the underlying platform complexities as we adopt new tools to match those new architecture that we created. And of course, as we've been sifting a lot of things left lately, it's very important to make it self-service while also greatly simplifying infrastructure code. We see now that we've been kind of shifting everything left. We see teams that they have to manage their code base, but they also have to manage HCL files for Terraform, Yaml files for Kubernetes, and a bunch of other tools. So this is a very important approach in platform. So to build an internal developer platform, there is kind of a very vibrant community, many great advancements. There's a great working group in CNCF that you can follow about this. A good first approach to this is Cloud Native Operational Excellence, which is the architecture you're seeing here. This kind of provides an open source first approach to building an internal developer platform. Similarly, there is the Backstack, which is kind of a composable internal developer platform, which uses four widely adopted projects. Backstage, which is a self-service developer portal. Argo CD, which is a GitOps-based continuous delivery tool. And Cross Plane, which is kind of a universal control plane. And finally, Keyverno, which is a policy as code tool to practically detect and prevent misconfigurations. So the core underlying infrastructure in most IDPs is Kubernetes. Kubernetes has evolved to being much more than just an orchestration platform. Nowadays, we call it the platform of platforms because of its extensibility and the interfaces it provides for us to kind of build more functionality in it. So we'll be leveraging Kubernetes' controller management and its reconciliation loop in order through operators to manage all our resources, internal and external, as Kubernetes resources. So operators extend Kubernetes' core functionality by automating tasks that human operations team would be doing. So they can be used to manage resources running both in Kubernetes or to orchestrate third-party cloud services. And this is a great way we can provide a common interface for development teams. So treating everything as Kubernetes resources enables them to manage everything through YAML files and through a declarative configuration. Before we close, a quick example of how we do this for MongoDB users. So we have three Kubernetes operators, the enterprise operator community and the Atlas one. So whether you're running MongoDB community or enterprise yourself or you're using the Atlas developer data platform, all of your database definitions go through Kubernetes and our operators are responsible of picking those resources up and reconciling and making sure that your infrastructure provisioning runs smoothly. So they interact either they create the relevant stateful sets and volume claims in your cluster to manage community and enterprise or they interact with the Atlas API in order to deploy the databases in the cloud.