Multiple deployments using a single code base

Emre Teoman
Borda Technology

--

DevOps is the model that is created to deliver applications and services faster than traditional software development processes. With the spread of the DevOps culture, continuous delivery has become an indispensable part of software development. The ability to automatically deploy applications to staging and production environments has improved software development processes. With DevOps technologies, automatic and continuous deployment can be made in methods that produce outputs in short periods, such as scrum. In this way, companies have had the opportunity to serve their customers better and faster.

While continuous delivery can be easily implemented in a single deployment case, operations are more difficult for multiple deployments. Although the test environment is usually single, the production environment can be various. The use of the software by more than one customer requires customization in our production deployment methods. If only cloud service is provided, multi-tenant application development would be a proper solution. But what if it is necessary to deploy on-premise for some of these customers? There are a lot of challenges in deploying the single code base to multiple environments. This article explains how we got over these challenges. Let’s start by defining the concept of deployment:

We define the concept of deployment with the formula above. It means that we make our software special for customers by adding different configurations on the single code base. As an example, the database connection string is one of the possible configuration parameters.

In summary, we can deploy the single code base to more than one customer by creating configurations for each customer.

Development

Let’s take a look at how development has progressed before explaining how the deployment is handled. This section shows a simplified version of the continuous integration architecture in our company. We have two branches named main and dev. The main is production-ready, and the dev is the development branch. A developer has to have a task (on the issue tracking software, such as Jira) related to the development and create a branch with the task’s ID. For example, if the ID of the task is 3422, the development branch will be dev_3422. After the development is complete, a pull request is created from dev_3422 to the dev branch. Then the code is reviewed and the merge is completed. After that, the application gets an alpha version and is deployed to the development environment. After the tests are complete, it’s time to merge the main branch and get the release version. However, no deployment is triggered because there is more than one production environment as on-premise. In the following sections, how the deployment is done is explained.

Configuration

The first part of the deployment, which is application development, is explained above. In this section, the second part, the configuration, will be described. Configuration files in DotnetCore are JSON files called appsettings.json. Depending on the environment, multiple appsettings.json files can be written, such as appsettings.dev.json and appsettings.production.json. Dev and production parts represent the environment. We use three settings files in our projects.

The first one is appsettings.Development.json and is used in the local development environment.

{
“ConnectionStrings”: {
“DefaultConnection”: “Host=localhost;Port=5432;Database=localdb;Username=admin;Password=admin”
}
}

The other is appsettings.dev.json and has a configuration for the development environment on the server.

{
“ConnectionStrings”: {
“DefaultConnection”: “Host=10.0.100.21;Port=5432;Database=devdb;Username=admin;Password=admin”
}
}

The last is different than these configurations. appsettings.production.template is the template of the file that will be used during that deployment.

{{- with $config := key "deployment/{client}/sample-application" | parseJSON -}}
{{- with $secret := secret "deployment/{client}/sample-application" -}}
{
"ConnectionStrings": {
"DefaultConnection": "Host={{$config.dbHost}};Port={{$config.dbPort}};Database={{$config.dbName}};Username={{$secret.Data.data.dbUsername}};Password={{$secret.Data.data.dbPassword}}"
}
}
{{- end -}}
{{- end -}}

Let’s explain each section used in the template file.

Consul

Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. It provides several key features: multi-datacenter, service mesh, service discovery, health checking and key/value storage.

We use key/value storage of consul the store template files and non-secret configurations. Main CI creates apsettings.production.template file to consul with the path template/sample-application/1.5.4 . The path contains the application name and version.

Template file in consul.

As shown below, we store non-secret configurations as JSON in the /deployment/client1/sample-application directory. The first line of the template file points the path in Consul. The path contains the client and application name.

{{- with $config := key "deployment/{client}/sample-application" | parseJSON -}}
Non-secret configuration file in consul.

Vault

Vault is a tool for securely accessing secrets. It provides several key features: secure secret storage, dynamic secrets, data encryption, leasing and renewal, revocation.

As shown below, we store secrets as JSON in the same directory breakdown with the Consul. The second line of the template file points the path in Vault. The path contains the client and application name.

{{- with $secret := secret "deployment/{client}/sample-application" -}}
Secrets in vault.

Consul-Template

It provides a convenient way to populate values from Consul and Vault into the file system using the consul-template daemon.

Consul-template agent turns template file to appsettings.production.json file. Therefore, the environment name can be used as production in all client servers. The content of the file produced according to the above configurations will be as follows:

{
"ConnectionStrings": {
"DefaultConnection": "Host=10.5.20.46;Port=5432;Database=sampleapplicationdb;Username=admin;Password=admin"
}
}

ConfigMaps

ConfigMap is a concept in Kubernetes for setting configuration separately from the codebase.

A ConfigMap allows you to decouple environment-specific configuration from your container images , so that your applications are easily portable.

ConfigMap is versionable, just like docker images, making it easier to associate it with the image running in the container during the deployment phase. The point to be noted is that ConfigMap is not suitable for storing sensitive information as it does not provide any privacy or encryption.

Deployment

Deployments are made by executing Ansible playbooks.

An Ansible® playbook is a blueprint of automation tasks — which are complex IT actions executed with limited or no human involvement.

Let’s explain basically how Ansible apply deployment for sample-application with version 1.5.4 .

  • Start deployment for docker image sample-application:1.5.4
  • Read template file from Consul path template/sample-application/1.5.4
  • Generate appsettings.production.json using Consul and Vault with path /deployment/client1/sample-application
  • Create sample-application-config-1.5.4 as configMap
  • Pull docker image sample-application:1.5.4
  • Apply deployment for configMap and docker image

Conclusion

Environment-independent application development is essential in multi-deployment scenarios. Configuration files should be used to deploy a single application code to more than one environment. In this article, we talked about how to prepare the configuration files to be given as ConfigMap. Although ConfigMap can be used as a configuration file in Kubernetes, Secret should be used for confidential information.

Thanks for reading.

--

--