Automate Umbraco deployment with Powershell

Nowadays, it’s inconceivable to deploy web sites manually which is partly why some solutions such as “Umbraco Cloud” exist. However, these options are not always the best. Indeed, for instance, I’m currently working in a governmental company that does not want to have its data hosted elsewhere that on-premise. In that case, it’s important to have procedures to install and upgrade websites in an automated way.

“Umbraco” is doing a great job in its installation/upgrade procedure. There is really few steps and it’s pretty quick, however, some steps still require manual interventions. I’m mainly thinking about the page displayed after each installation or upgrade (the one asking database configuration data or asking you to click on “Upgrade” to upgrade “Umbraco”).

In this blog post, we are going to create a small script (less than 400 lines) to deploy a new “Umbraco” web site without any manual intervention. Note that I’m going to explain most of this script step by step so if you don’t really care about how it’s done, you can jump to the “usage” part to see how to use it.

I created this script in one day to see how to automate deployments so it can probably be improved a lot. Moreover, this script is only handling one server, it is not usable as such to install “Umbraco” in a multi front end environment.

Prerequisites

This script does not create the database. It’s a good practice to create the “Umbraco” database with a specific user, then grant privileges to the “Umbraco” user on this database. We don’t want this user to be able to create databases on the server. Therefore, the script assumes that an empty database is created an accessible.

You also need a “zip” file containing your website. Most of the time, you’ll use the “Publish” function of “Visual Studio” but if you don’t plan on deploying any custom code, you can directly download a copy of “Umbraco”. If  The “web.config” file of the site must follow some rules for reasons that I’m going to explain right now.

I did a lot of researches on how to handle the “web.config” for “Umbraco” web sites and the most common answer I found consists in using “web.config” transformations. Basically, there is a “Debug” transformation for the development environment, then there are other transformations such as “Test”, “Acceptance” and “Production”. This is nice but does not satisfy me for two reasons:

  • It allows the developer to have access to the credentials of the database as the “Test”, “Acceptance” and “Production” transformation specify them. Most of the time, passwords must not be accessible for developers, especially the ones of production database.
  • It forces the developer to maintain one transformation by environment and as it should be possible to have as many environment as required, this can become pretty cumbersome to maintain.

This is why the script assumes that:

  • The value of the “umbracoConfigurationStatus” element must be empty.
  • The attributes “connectionString” and “providerName” of the “umbracoDbDSN” connection string must be empty.
  • The “machineKey” element is not present

The “web.config” transformation to do so is the following one:

Let’s write some Powershell

This script relies on some features introduced in Powershell 5 so don’t try to execute it with a lower version.

First thing to do is defining the parameters that can be passed to the script. This parameters are configuration values used by the script to configure IIS, connect to the SQL database and configure “Umbraco”.

These parameters are explained in the “usage” section of this blog post.

The script relies on the “WebAdministration” module used to control “IIS”, so we need to import it:

We also need the following enumerations:

  • DatabaseType” defines the type of the database used by “Umbraco”.
  • ResultType” defines the type of the value returned by the functions declared in the script.

The “ResultType” enumeration is used by utility functions to display the status of the script. Let’s create them:

The three first functions simply returns an hashtable with a property “Type” and a property “Message”. These values are “pipelined” to the “HandleResult” function to display a messages with a color that depends on the type of the result. Don’t pay too much attention to these functions as they are only used for the user interface.

There are also these three other utility functions that we’ll use later in the script.

  • GetSiteName” generates the name of the IIS site based on the host headers and the port. Basically, the name of the site is the result of the concatenation of these two values.
  • GetSiteUrl” returns the URL of the site based on the host headers and the port.
  • ParseResponse” is used when configuring “Umbraco”. Basically, to do so, we need to call some Web API that return responses as JSON. However, some characters are inserted at the beginning of the response to prevent JSON vulnerability.

Now that we have our utility functions, we can start with the really interesting ones. We are going to review all of them, then we’ll call them to tie everything together.

Create the web application pool

We start by creating the web application pool that is going to run your IIS site.

As we set the location of the execution to “IIS:\AppPools” to perform some operations on web application pools, we use the “Begin” and “End” block to push and pop the location. This ensure that the execution location is the same before and after the function execution. For the rest, it simply checks if a web application pool with the specified name exists and creates it if it does not. Note that we are using the “Warning” and “Success” functions to display information about the script execution.

Create the web site

This function uses the same mechanism as the previous one and is used to create the IIS web site. Now that the site is created, we need to unzip “Umbraco” in its physical directory but first, we need to ensure that the site and the web application pool are stopped. Indeed, for a first installation, it wouldn’t create any issue but in case of an upgrade, the existing files need to be replaced and if these ones are locked by the IIS process, the unzipping will fail, so we stop the web application pool and the site.

Stop the site

Stop the web application

Extract the site

This function cannot simply unzip the “zip” file in the physical directory of the IIS site. Indeed, for a first installation, it would be OK but for an upgrade, it would create a big issue: the “web.config” file would override the existing one and as we saw earlier, the one of the archive has no connection string nor configuration status.

Therefore, this method does more than unzipping the file. It checks if the target directory already exists. If so, it means that it is an upgrade and not an installation and in that case, the function:

  • Backs up the current “web.config” file under another name.
  • Reads the current “web.config” and save the value of the connection string, the configuration status and the machine key in variables.
  • Extract the zip file in the target directly with the “force” flag to override files.
  • Update the new “web.config” with the previously saved value.

This process ensures a merge between the existing “web.config” file and the new one.

Note that after the backup of the “web.config” file, the script waits for a minute before continuing. This comes from the fact that when the site is stopped, “Umbraco” still uses some files during several seconds. For example, it logs the fact that the application is stopped so if we try to override the file directly after the site is stopped, we can get some exceptions stating that some files cannot be replaced as they are currently in used.

Setting permissions

Some permissions have to be applied for “Umbraco” to run. This function just gives the necessary permissions to the following accounts:

  • The “ApplicationIdentity” one.
  • IUSR
  • IIS_ISRS

Update hosts file

This function updates the hosts file to ensure that the FQDN of the site can be resolved on the server. Basically, it just reads all the lines from the file that does not contain the host name, then adds the host name at the end of the file. This way, if the host header is already present, it does not get duplicated.

Start the web application

Start the site

Configure “Umbraco”

Even though the script is pretty straightforward so far, this step is a bit more complicated. Indeed, there is no “out of the box” way to automate the configuration of “Umbraco”, which is why I had to take a look at how the wizard was doing it to mimic it in “Powershell”. Before checking the code, let’s briefly explain how the wizard does it.

When you filled in all the information required by the wizard (database and user configuration), the page makes an AJAX call to “GetSetup”. This Web API initializes a new deployment by creating an ID an retrieving all the steps required for the installation.

Once this ID is retrieved by the Angular application, this one can start the installation. Basically, it calls the Web API “PostPerformInstall” with the data you entered in the wizard. This route simply executes the next step (the ones retrieved by “GetSetup”) and then send a response to the wizard. This response actually contains some information about the status of the deployment, the next step to execute and some information for Angular. Basically, as long as this response does not state that the deployment is completed, the wizard keeps on calling this Web API.

Even though this mechanism seems a bit tricky, it makes sense. Indeed, some installation steps induce the restart of the application pool. For example, the step configuring the connection string updates the “web.config” inducing a web application recycle. So if the installation process was executed in a single AJAX call, the execution context would never get refreshed (meaning that for example, the changes to the “web.config” would never be taken into account during the process). As the wizard makes an AJAX call after every step to starts the next one, if a recycle of the web application occurred, then the AJAX call will get the new context and everything will go as expected. That’s the behavior that the script mimics.

This is not really a supported way of doing things, we are just using the same behavior as the wizard. This means that if tomorrow, the “Umbraco” team decides to change the way it’s done, this script won’t work anymore.

Moreover, I created a pull request to create a Web API to automate deployment. The outcome of this discussion might lead to the support of automated deployments but it’s not sure that it will be in the way used by this script.

So all this process is performed by these two functions.

Even though there is a lot of code, it is pretty simple. Basically, the script:

  • Calls “GetSetup” to initialize a new deployment.
  • Analyzes the response to get the installation ID and check if this is a new installation or an upgrade.
  • If it is an new installation
    • It generates the payload with the configuration data.
    • It calls “StartUmbracoProcess” to call the “PostPerformInstall” as long as the installation is not done.
  • If it is an upgrade
    • It calls the “PostLogin” route to authenticate the user in the back office (an upgrade cannot start if the user is not authenticated).
    • It generates the payload with the upgrade data.
    • It calls “StartUmbracoProcess” to call the “PostPerformInstall” as long as the upgrade is not done.

For the upgrade, the payloads needs to contain the information about the current version and the target one. These information are returned by the “GetSetup” call, which is why we retrieve the property “model” of the first step at line 49 and use it at line 63.

Putting everything together

Now it’s time to put everything together by calling all these methods.

Usage

Let’s review all the parameters of the script.

Package (required)

Path (relative or absolute) to the archive containing the “Umbraco” web site.

Hostname (required)

Host headers used for the name of the site and the its URL.

Port (required)

Port used for the site URL. The default value is “80”.

Username (required)

Name of the “Umbraco” default administrator.

Email (required)

Email of the “Umbraco” default administrator used to login in the back office.

Password (required)

Password of the “Umbraco” default administrator.

DatabaseType (required)

Type of the of database used to contain “Umbraco” data. It can be one of the following values:

  • SqlCe” (default value)
  • SqlServer
  • MySql
  • SqlAzure

SqlServer

Name of the SQL server (only applies if “DatabaseType” is not set to “SqlCe”).

DatabaseName

Name of the “Umbraco” database (only applies if “DatabaseType” is not set to “SqlCe”). The default value is “umbraco-cms”.

SqlUseIntegratedAuthentication

Defines if the integrated authentication is used to connect to the database (only applies if “DatabaseType” is not set to “SqlCe”).

SqlUsername

User name used to connect to the “Umbraco” database (only applies if “DatabaseType” is not set to “SqlCe”).

SqlPassword

Password used to connect to the “Umbraco” database (only applies if “DatabaseType” is not set to “SqlCe”).

Example

Install or upgrade a new site using a SQL Compact Edition database

Install or upgrade a new site using SQL Server

Can I use it?

As I said in the introduction, this script works but it’s not bullet proof for several reasons:

  • It’s tightly coupled with the way “Umbraco” supports installation and upgrade. If this mechanism changes in the future, the script will no longer work.
  • It is not yet parameterized enough. For example, it would be cool to add a parameter to enable the newsletter subscription or to disable the machine key generation.
  • It does not really support multi server environment. So far, I never had to configure such an installation, but off the top of my head, I’d say that the only thing to do is to add a parameter to enable or disable the “Umbraco” configuration as it has to be done only once. However, I never tested it, so I’m not 100% sure.
  • Error handling is probably not optimal.

However, I’m using it for some days now and it seems to be pretty stable and very useful. This script has been developed because I needed it for my current project so it’s very likely that I’ll update it in a near future, so don’t hesitate coming back on this blog post to follow updates.

If this script has a bit of success, I might create a github project for this.

If you use it and have some issues or ideas to improve it, don’t hesitate to leave a comment.

Final script

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.