At BizStream, we follow a development lifecycle (also called DevOps) that’s built to increase efficiency, provide continuous delivery, and improve software quality. Software deployments are only a small part of this overall lifecycle, but they’re the main focus of this blog post. First, I’ll briefly cover our development process from a high level so you can see where deployments fit into the big picture, and then we’ll dig into the technical details of deployments.
I’ll first lay out our development process from a very high level, starting with sprint planning using Agile and then ending with our deployment process.
- Agile Scrum Project Management
- New tasks are created by business analysts with product owner input.
- Tasks are estimated and reviewed in regular grooming meetings.
- Sprint Kickoff meetings are used to determine the priority of each task, so developers know what to work on next.
- Development Process
- Developers work on the next available task in the sprint and assign it to QA when finished.
- For large features, developers demonstrate the new functionality to the team and receive feedback.
- QA personnel test completed tasks according to acceptance criteria.
- Tasks are marked ready to deploy if no issues are found.
- If documentation is needed, it will be completed by business analysts or product owners for tasks that are ready to deploy.
- Release Announcement
- A release date is announced to all clients.
- A developer (typically the technical lead), or an automated process, will release the new software to a production environment. This is the main topic of this blog post that we’ll be covering in more detail.
- The entire team and product owners test the new software on the production environment.
- Fix Issues
- If issues are found on the live site, we create new tasks and prioritize their development and deployment, starting the process over again.
- If no more issues are found, we begin the next sprint and repeat this process until the project is complete.
Detailed Deployment Steps
First, I’ll start by discussing a development step that helps reduce the pain of deploying database changes. Database changes need to happen in a particular order, and it’s easy to make mistakes if the steps aren’t tested and performed carefully. The best way to handle database changes is with automation such as Entity Framework (EF) or Xperience by Kentico.
- For non-Xperience sites, we use EF Migrations
- EF Migrations automate database changes to new environments. Mistakes can still happen but not nearly as often as when manually deploying these changes.
- A migration is essentially a mini-snapshot of a “point-in-time” of a database model.
- EF compares the current database model against a snapshot of the old database model to determine the differences and generates migration source files to help migrate from the old model to the new model.
- We add an Entity Framework step to our release/deployment process that runs new migration code against the deployment server’s database to automate the changes. This process is faster and safer than the manual steps we used to follow in the past.
- Kentico Database Changes
- For older Kentico sites, every database change between a downstream site and an upstream site is recorded in a synchronization log. To deploy these changes, we select each item/change in the sync log and click a “synchronize changes button” to deploy them to an upstream site.
- For newer Kentico sites, there’s a continuous integration solution that handles the serialization of data of most objects from the database into XML files on the file system. When these files are deployed to the next upstream site, the continuous integration process reads them and makes the necessary changes. However, there are a few database objects that, when changed, are not handled automatically by Kentico’s continuous integration. The database objects that aren’t supported are database indexes, functions, stored procedures, and custom views. For these special cases, migration scripts are used for automatic synchronization between sites.
Software Version Control
Version Control systems are essential to all automated deployment processes. They record all software changes made by developers and help us determine what code goes where and at what times by utilizing a branching strategy. At BizStream, we use the Git Flow branching strategy. Put simply, it's a process that determines how finished code moves through the system before it gets merged into its final destination, the main code branch that is deployed to production servers at the end of an Agile sprint.
Gitflow can be used for projects that have scheduled release cycles, like most projects at BizStream, and for the DevOps best practice of continuous delivery. Gitflow assigns very specific roles to different branches and defines how and when they should interact (atlassian.com).
Here are the specific branches we use at BizStream and their roles:
- Feature Branches
- Developers create feature branches by branching off the develop branch before starting development. When finished updating or adding code, they create a pull request for Peer Review, which is essentially a request to merge their updates back into the develop branch.
- The entire development team reviews this code. When the team is satisfied with the code, it is merged into the develop branch and automatically deployed to an internal dev site for testing (we’ll cover the automation of deployments shortly).
- Develop Branch
- When a peer review of a feature branch is complete, it is merged into the develop branch.
- There’s only one develop branch, and it always exists. It contains the code that is currently in development.
- When do QA personnel test the updated code? There are two main options:
- Some projects choose to perform QA before any updated code is merged back into the develop branch by deploying the code to a testing environment. The main benefit of this strategy is that buggy code isn’t merged back into the develop branch and accidentally deployed to a live site before QA has had the chance to test it.
- Other projects may choose to perform QA after the updated code is merged into the develop branch. The main benefit of testing code after it is merged into the develop branch is code is merged more frequently, reducing merge conflicts. The code is also available to other developers sooner because they aren’t waiting for QA to finish testing it, which can sometimes take days. However, a drawback is that untested code can be deployed to production sites if care isn’t taken to implement release branches and code freezes correctly.
- Release Branches
- Release branches are created by branching off the develop branch. The purpose is to create a code freeze, which is essentially code that is locked in time for release testing. This code may be deployed to an environment for extended testing and documentation. New feature code is never merged into release branches. Release branches are only updated if new bugs are found.
- When documentation and testing are complete, a release date is announced, and this branch is merged into the develop branch and the main branch to keep all branches in sync.
- Main Branch
- There is only one main branch, and it always exists.
- Release branches are merged into the main branch during deployments. At BizStream, we like to kick off an automated build of the Main branch when it changes, like when we merge a release branch into it. Once the build is complete, we set up Azure or Octopus (discussed in the next section) to create a release for us automatically. However, we usually require a developer to click the actual deploy button to ensure that an automated deployment to a production site never occurs without our supervision.
- The only branch type besides release branches that are allowed to be merged into the main branch are Hotfix branches.
- Hotfix Branches
- These branches are created to specifically fix issues that exist on the Main branch (which usually means the issue also exists on a live site). These bugs need to be fixed as soon as possible.
- Hotfixes are peer-reviewed and tested just like feature branches, but we don’t wait until the end of a sprint to merge them into the main branch. They can be thought of as mini-release branches.
- As work is constantly being completed and tested on the develop branch, we usually aren’t in a position to deploy it right away just to fix a small bug. Therefore, we create hotfix branches to circumvent the long feature-based development cycle.
In the past, we used a manual “copy and paste” process to perform deployments. We built the code on our local machine and copied the compiled code and web files over to the live server as fast as we could. Of course, we ran into issues, some changes were forgotten, deployments took up a lot of our time, and they were stressful. We don’t like taking our client's sites down for extended periods of time either. So, we decided to automate these manual deployment steps using Azure DevOps and Octopus.
Build triggers are a signal to Azure DevOps to build the code in a specific repository branch. These are the same branches we discussed above in this article. At BizStream, we typically utilize the following branch build triggers:
- Develop and Main Branches
- When these two branches are changed, that is, code is pushed to them, usually when a peer review is completed (the develop branch changes) or a release branch is completed (the master branch changes) a build is triggered that compiles and packages up a release artifact that can be deployed to a specific server.
- PRs (Peer Reviews)
- When a PR is created in our BitBucket repository, we also trigger an automated build of that code to make sure the changes build before they are reviewed by the team. If a PR doesn’t build and throws an error, this is visible to everyone in BitBucket. The developer fixes the issue, and the reviewers wait to review the code until they see that the build is successful.
- We create PRs for every feature, bug, and hotfix branch, so there’s no need to set up individual triggers for these specific branch types as we don’t want to waste server resources automatically building these branches every time they change. We only build them automatically when the work is done, and a PR is submitted.
So, what actually happens when a build is triggered? Here’s a list of our most used automated build steps:
- Send a Slack notification to the team that a build has started.
- Restore all NuGet packages the C#/.NET code relies on.
- Package the code into build artifacts. This could be a single .zip file containing the compiled DLLs or a set of NuGet packages built from the source files.
- Run NPM to install any node modules needed for the front-end code.
- Compile and minify the front-end code using a build tool like Gulp, Webpack, or NPM.
- Run all unit tests and verify they pass.
- Push the build artifacts to a staging directory that can be accessed by the release automation process.
The second thing we automated was our release process. We don’t want to pass around server credentials between each other, have developers logging into production sites late at night, or manually copying over files and database changes by hand anymore.
After the deployment package is created and sent to the staging directory by the automated build process, we utilize Azure DevOps or Octopus to automatically copy over the packaged code to specific servers. Below is a list of the most used steps in our automated release processes:
- Run the Entity Framework migration scripts to update the target server’s database with any schema and data changes.
- Deploy the compiled code to a server directory that is accessed by an IIS site or an Azure App Service.
- Slack notifies the team of a successful or failed deployment.
There isn’t a one-size-fits-all deployment solution that works for every team or project, but the deployment process outlined above usually works very well for us and our clients. If you’d like more information about the best DevOps and Continuous Integration practices, I highly recommend taking a look at the book Accelerate by Nicole Forsgren, Jez Humble, and Gene Kim.