Containerization has transformed the way new applications are developed and implemented. However, many organizations retain the latest catalog of older systems that require a different approach. This disconnect between the new and the old does not have to continue: you can also pack older systems as containers, which makes it easier to continue their development with the help of more modern development methods.
In this article, we will look at the process you can use to start storing “legacy” software. Although the two products will not be the same, and the term “heritage” is subjective, we will focus on the widely applicable steps for packaging tightly coupled systems that are currently tied to individual environments.
1. Identify candidate systems
It is worth first preparing an inventory of the system that allows you to identify good candidates for containerization. In some cases, you may conclude that a particular application simply cannot be stored in a container. This will usually be when it has deep-rooted hardware requirements or relies on outdated kernel features and programming languages.
The best candidates are frequently used systems that will immediately benefit from accelerated future development. Look for apps that are already fairly standalone if you’re brand new to containerization. Choosing a system that is well used but not critical to the mission will give you freedom in case things go wrong, while allowing you to recognize the benefits of a successful migration.
2. System components
You could containerize your candidate system by writing Dockerfile, including all application dependencies, and calling it up for the day. While this is a valid way to quickly put the system in a container, it should not be the ultimate goal of your efforts. A monolithic container will result in long construction, huge image sizes and poor scalability.
Instead, you should look for opportunities to divide each of your systems into individual components. These components should end up in their own containers, preventing any single piece from becoming too large. You will be able to scale the components individually by creating additional replicas of resource-limited containers.
This step is also important for establishing overall modularity and encouraging further adoption of containers. As you separate multiple systems into their components, you’ll begin to find overlaps that allow you to reuse container images you’ve already created. You will notice that it gradually becomes easier to proceed with the containers.
Deciding where to divide the components should not be too burdensome. Start by identifying where the system relies on services that are already external to its source code. Database connections, message queues, email servers, proxies, and gateways should be independent of the component they are increasing. You will separate them into their own containers located next to the instance that runs your code.
It is also worth looking for opportunities to remake what is left. Does your service have too many responsibilities that could be shared as separate functional units? You may have a user profile API that accepts photo uploads; a service that resizes these photos could be a good candidate to run autonomously in its own container.
3. Prepare your components
After separating the components, you must prepare them for operation in a container environment. Containers have several key differences compared to traditional VMs. Permanent storage, configuration, and connections between components are the three most important to consider in advance.
Containers are ephemeral environments. File system modifications are lost when your containers stop. You are responsible for managing the persistent data of your application using the mechanisms provided by your container runtime.
In the case of Docker, volumes are used to keep data outside of your container instances. Volumes are mounted on certain tracks inside the container. To avoid the need to mount dozens of volumes, it’s best to concentrate your application’s data within a few top-level directories. Mounting the volume to these locations will guarantee the stability of the files that your application stores.
It’s important to review your application’s file system interactions to understand what volumes you need and what issues you’ll encounter. Ignoring this step could be costly if the data you assume is persistent is lost each time the container is restarted.
Many older applications are configured using static configuration files. They can be in a dedicated format, such as XML, JSON or INI, or encoded using the system programming language.
Containers are typically configured using external environment variables. Variables are defined when containers are created, using mechanisms such as Docker
-e flag with
docker run. They are injected into the working container environment.
Using this system ensures that you can rely on the container tool chain to set and change configuration parameters. You may need to refactor your application first to support reading settings from environment variables. One common way to facilitate the transition is to place a small script inside the entry point of the container. This can enumerate the environment variables after creating the container and write them to the configuration file for your application.
Links between services
Containerization also leads you to think about networking between services. The services are generally not exposed to each other except by explicit configuration. You can set up an automatic connection in Docker by associating multiple containers with the same Docker network. This offers a service discovery feature that allows containers to reach each other by name.
Other containerization technologies use different approaches to networking and discovery services. Once you have split your systems into individual components, you need to reconnect them using the capabilities offered by your runtime. The nature of container implementations means that there is often more complexity than networking between VMs or physical hosts. Traffic needs to be routed and load balance between all of your container replicas and their dependencies so you should acknowledge these requirements early on.
4. Write your Dockerfiles
Once you have planned your architecture, you can start the physical work associated with containerization. The first step is to write Docker files for the components of your application. They define a sequence of commands and actions that create a file system that contains everything a component needs to run.
Docker files begin with the corresponding base image referenced by a
FROM statement. This is usually the operating system (
alpine:3) or a pre-built programming language environment (
node:16). You can choose the image that best suits your application’s existing environment. Booting from an empty file system is possible, but usually not necessary unless you need extremely detailed control.
Additional content is layered on the basic image according to instructions like
RUN. They allow you to copy files from your host and run commands against a temporary file system. Once you have written your Dockerfile, you can make it using
docker build -t my-image:latest . command.
5. Set up the orchestration
Assuming you have componentized your system, you will eventually get one image of the container for each part. Now you need a way to run all the containers at once so you can easily run a functional instance of the application.
Larger production plants usually use Kubernetes for this purpose. It is a dedicated orchestration system that adds its own higher-level concepts to create replicated container implementations. Smaller systems and development environments are often well served by Docker Compose, a tool that relies on simpler YAML files to run a “slop” of several containers:
version: "3" app: image: my-web-app:latest ports: - 80:80 database: image: mysql:8.0 ports: - 3306:3306
docker-compose.yml the file allows you to run all its services using
docker-compose up -d
Setting up some form of orchestration makes your container fleet easier to manage and makes it easier to scale through replication. Both Kubernetes and Docker Compose can run multiple instances of your services, which cannot be achieved with legacy applications formed from tightly connected components.
6. After relocation: Monitor and expand your container fleet
Containerization does not end with running an instance of your application. To get the most out of technology, you need to properly monitor your containers to be informed about errors and resource usage.
Larger systems are best served by a dedicated monitoring platform that can aggregate records and metrics from across your fleet. You may already be using a similar solution with your legacy application implementations, but this is even more important for containers. Good visibility will allow you to track down problems up to the instance of the container from which they arose, revealing insights that are important when you have hundreds or thousands of replicas.
To continue expanding your fleet, double your documentation and standardization. We have already seen how the division of the system into components helps future reuse. However, this only works effectively if you have documented what you have and how each piece fits. Taking the time to write about your system and the process you have gone through will simplify future work. It will also help new team members understand the decisions you have made.
Is it worth it?
Containerization is worth the effort when you feel that the development of the system is being slowed down by its current processes. The possibility of implementation as a set of containers simplifies the development experience and gives you greater versatility in implementation. You can now run the service wherever container runtime is available, whether it’s a single instance on your laptop or 1000 on a public cloud provider.
Incorporating into containers makes it easier to harness the power of the cloud, consolidate your implementations, and reduce local infrastructure costs. However, these obvious victories can be balanced by the need to retrain engineers, hire new specialized talent, and maintain your containers over time.
The decision to container a legacy system must take into account the value of that system to your business, the current time spent on its maintenance, and the likely reduction as a result of using the container. It may be best to leave low-priority services alone if the processes associated with them do not cause current problems.
It should be recognized that not all legacy applications will need or be able to take advantage of any of the pronounced benefits of containerization. Adoption is a spectrum, from running a system in a single monolithic container, to full componentization, orchestration, and integration with observation packages. The latter model is an ideal target for business-critical applications that engineers develop on a daily basis; conversely, the former may be adequate for services that rarely touch on where the main obstacle is time spent in providing new VM-based development environments.
Migrating legacy applications to container workflows can look challenging on the surface. Breaking the process into different steps usually helps define where you are and where you want to be. In this article, we looked at six granular phases that you can use to access containerization of existing systems. We also discussed some considerations to consider when deciding whether to proceed.
From a conceptual point of view, containerizing a legacy application is little different from working with a new one. You apply the same principles of componenting, related services, and configurations that are inserted from the external environment. Most systems are relatively easy to store when viewed from this perspective. Focusing on these aspects will help you separate your applications, create scalable components, and devise an efficient containerization methodology.