When we think of environments from infrastructure point of view, we normally think in terms of its use case - Production, Test, and so forth. It's probably more useful to think in terms of application, with collection of its components, flowing thru different environments.
That is, if you have several applications, each with its own collection of components. And if we can have declarative representation of this collection, there are several benefits to it, which I will get to shortly. Please note I am not talking about Google or Netflix scale here, I am talking about 99% of rest of use cases.
In so far as dealing with environments is concerned, it's always important to think in terms of applications first, then environment next. Environments exist for application/s and not vice-versa. What good is an environment if the application is not functional?
It's the application that moves between environments yet that relationship between its collection of dependencies pretty much remains the same - number of servers an application needs may change depending on if it is a test environment or production. In a perfect world every application component would be a service that can be developed and tested fully without needing anything else.. But we are talking reality that exist in most companies, most of the time. And not theoretical state only possible at Google or Amazon scale - and even then there is always dependency on core services. And although possible to have some level of testing done using feature toggle but when it comes to functional and integration testing required to satisfy business functionality, it typically requires other dependent services.
Now there are plethora of ways to move just ONE application component from one environment to another - for example specifying it as docker image which moves from a laptop, to Kubernetes test cluster to production cluster. And of course CI/CD has been quite effective at this for more than a decade. This write-up is not about what is best way to implement CI/CD for a specific application component. Nor this is about next Kubernetes setup. This also is not another write-up on how we can use service mesh like Istio to solve this problem.
This is about why and how to define application and infrastructure dependencies declaratively. And how this collection, with its dependencies can be brought up and down seamlessly where application actually works. This is the key - a working application satisfying business functionality, when that collection, with its dependencies are brought up. This is true even when your entire application is serverless. And yes, you can try and specify all of an application dependencies via docker file to an extent -but it gets messy as dependencies increase.
Even in a fully decoupled, totally independent microservices world, there always exists that relationship between application and core services it needs. Or to put it in another way, it’s that collection of services that make particular business function work. Assuming these services are fully decoupled (whether you have gone crazy making everything as service or your services are based on domains). Most of the time, you will have collection of services which depend on base core services or on a monolith - this is the reality for 99% of us. Even there, application will have dependency on some core services like databases, Redis/Cache, Kafka, Gateway etc(these need to be up first, for the application to be functional).
Granted database may not be brought up and down each time application collection is brought up/down but it can be tested that it is available before application is brought up. And this can only happen if that dependency is explicit somewhere.
From infrastructure point of view, it is important to have this relationship defined explicitly, declaratively. Else it is in someone's head.. and probably in many heads. And each time you deal with the application, as a collection in an environment, the same heads will be needed to make that application functional.. Environments do not exist in isolation. Meaning it is better to think of Application X in an environment (production, development etc) instead of just environment. This has several benefits:
That is, if you have several applications, each with its own collection of components. And if we can have declarative representation of this collection, there are several benefits to it, which I will get to shortly. Please note I am not talking about Google or Netflix scale here, I am talking about 99% of rest of use cases.
In so far as dealing with environments is concerned, it's always important to think in terms of applications first, then environment next. Environments exist for application/s and not vice-versa. What good is an environment if the application is not functional?
It's the application that moves between environments yet that relationship between its collection of dependencies pretty much remains the same - number of servers an application needs may change depending on if it is a test environment or production. In a perfect world every application component would be a service that can be developed and tested fully without needing anything else.. But we are talking reality that exist in most companies, most of the time. And not theoretical state only possible at Google or Amazon scale - and even then there is always dependency on core services. And although possible to have some level of testing done using feature toggle but when it comes to functional and integration testing required to satisfy business functionality, it typically requires other dependent services.
Now there are plethora of ways to move just ONE application component from one environment to another - for example specifying it as docker image which moves from a laptop, to Kubernetes test cluster to production cluster. And of course CI/CD has been quite effective at this for more than a decade. This write-up is not about what is best way to implement CI/CD for a specific application component. Nor this is about next Kubernetes setup. This also is not another write-up on how we can use service mesh like Istio to solve this problem.
This is about why and how to define application and infrastructure dependencies declaratively. And how this collection, with its dependencies can be brought up and down seamlessly where application actually works. This is the key - a working application satisfying business functionality, when that collection, with its dependencies are brought up. This is true even when your entire application is serverless. And yes, you can try and specify all of an application dependencies via docker file to an extent -but it gets messy as dependencies increase.
Even in a fully decoupled, totally independent microservices world, there always exists that relationship between application and core services it needs. Or to put it in another way, it’s that collection of services that make particular business function work. Assuming these services are fully decoupled (whether you have gone crazy making everything as service or your services are based on domains). Most of the time, you will have collection of services which depend on base core services or on a monolith - this is the reality for 99% of us. Even there, application will have dependency on some core services like databases, Redis/Cache, Kafka, Gateway etc(these need to be up first, for the application to be functional).
Granted database may not be brought up and down each time application collection is brought up/down but it can be tested that it is available before application is brought up. And this can only happen if that dependency is explicit somewhere.
From infrastructure point of view, it is important to have this relationship defined explicitly, declaratively. Else it is in someone's head.. and probably in many heads. And each time you deal with the application, as a collection in an environment, the same heads will be needed to make that application functional.. Environments do not exist in isolation. Meaning it is better to think of Application X in an environment (production, development etc) instead of just environment. This has several benefits:
- When you bring up(or down) environment, you can work with a specific application(and its collection) instead of all applications in your environment. Think of this like having "decoupled application sets or verticals"
- This saves cost - you don't need to keep entire environment up if there is need to test only specific application, only its collection can be brought up as needed, else kept down
- This saves time - since you are not bringing up/down entire environment, less troubleshooting, faster turnaround time - it is the troubleshooting and testing that takes most time, not actual startup of components. Less moving parts you have in an environment, less troubleshooting. I should put this also in cost bucket - since time is money
- You provide ability for developers to bring up/down application, with collection of components(or just a subset of it) on as need basis - as opposed to having these up all the time. Very useful in a project or test environment
- Testing and keeping your disaster recovery site up-to-date and functional(instead of being surprised when you need one)
- Institutional knowledge is made explicit in a simple format, easily understandable by new team member... or people unfamiliar with the system
The benefits are quite obvious. Next question is how best to capture and maintain this state with its collection and its dependent order. I really like the Wikidata repository format(https://www.wikidata.org/wiki/Wikidata:Introduction ), and eventually this collection can evolve to something similar to this format. Also Gitlab's Mermaid markdown looks promising too. To begin with, it is best to start this in a very simple format which is both human readable and machine readable. Which can be stored in Git.
For example, there can be one file per application. Each line in this file can indicate the application component name and its repos location. That is it. This can be a simple text file and dependency order (if any, can be reflected via a number or level or place in the file).
It will be very tempting to go down the YAML, JSON or XML or markup rabbit hole. Since readability for humans is equally important, it is probably better to represent this in simplest of text format. Even a markup language may be too much here...
For example, let us say we have SALES application, which can be represented in a text file called SALES like so:
#Level1#
postgres:salesdb
registry:main-registry
#End Level1#
#Level2#
gateway:main-gateway
engine:sales-target-engine
backend:sales-backend
#End Level2#
#Level3#
frontend1:sales-frontend1
frontend2:saled-fronend2
#End Level3#
Here first field is application name and second field is Git repos name - this repos holds all information to bring up this component, along with its infrastructure. And Levels are dependency order - Level1 needs to be up first, for Level2 to be up and functional, and so on.
It's quite obvious that to bring up entire environment for an application, along with its infrastructure, and dependencies down to every single resource, is too simplistic to be defined in simple text files in Git. Here we are talking about bringing all these up(down) in specific order: Front-end and Backend components, Cache/Redis, Gateway, Database etc, along with its infrastructure(hosts, containers etc).
And to represent all those relationships, and every resource they use in a particular dependency order, is probably too simplistic to store via text files in Git. All those resource level details, and relationships, can indeed be stored in a text file via Git. But that will make it practically useless for humans.
Better to keep the format in Git text file real simple - something like shown above. And use that the as the seed data to represent all the details and relationship details in a database, then do higher order automated operations using that data in the database. Make an API available to access this relationship in database (seeded from Git) to operate at environment level.
Our eventual goal is this – command level ability to spin up(or tear down) desired application, with all its dependencies. Something like this:
">service_application_suite SALES TEST UP" <- this brings up entire SALES application, along with its entire dependent application components and infrastructure in TEST environment
">service_application_suite CART QA DOWN" <- this brings down entire CART application, along with its entire dependent application components and infrastructure in QA environment
It is implicit that a production version of this command would never /rarely be. used. But test, qa and project version of this would be very beneficial - where subset of application and its components can be brought up/down quickly - as explained in the benefits above.