How We Replaced Classic ASP with Go on Windows Server
Two decades ago, around the same time as the first release of PHP, Microsoft introduced Active Server Pages (ASP). ASP was included with Internet Information Services (IIS) and had a relatively low learning curve to do server-side scripting, positioning it well to gain popularity. This began a new era in web technology where it was easy to gain access to server resources and commands while serving pages client-side. This novel concept expanded what a web page could be, particularly in the days before XMLHttpRequest had materialized and allowed for new experiences in the browser.
These benefits, along with Microsoft's dominance in enterprise helped push ASP adoption for both internal and external tools, with our company being no different.
Twenty years later it's still powering many web based applications, even though official support for ASP ends in a couple years. Due to its wide usage, the tech community have developed libraries to greatly expand its functionality over the years such as adding support for JSON or improving how database connections are handled.
Almost all of our web-based applications at work use ASP and still function well, although there are some persistant issues:
Performance, particularly with some pages taking upwards of several minutes to generate a simple list
Setting up an ASP environment on modern versions of Windows Server is not straightforward or a default option as support wanes
Limitations in ASP make implementing certain new features troublesome
Windows-only support which contributes to vendor lock-in
We knew many of these problems could be fixed, although there would also be new challenges using a different language:
Support Windows-based authentication
Reliably connect to and interact with Microsoft SQL Server
Running server-side sites using Windows Server-based tools and smoothly integrating alongside existing websites and services
These are certainly achievable requirements, however if using a non-Microsoft solution then they may require some extra attention to get working with our existing infrastructure. Another goal that we had was to make everything as portable and as OS-agnostic as possible, such that we no longer had to rely on a very specific server setup and we had freedom in choosing how to run and maintain our servers. This has the added benefit of being able to free up server resources and allow us to get more value out of existing hardware.
After extensive reading on what language is the most performant, yet easy enough to quickly build powerful and stable applications we opted to use Go. There are countless articles as to why you should or should not use Go, however our choice was largely based on these few bits:
Go is fast and efficient. Being compiled, allowing for easy concurrency, and with low memory usage, there is a lot of power and speed here
Networking is easy in Go. There are a lot of parts simply built in such as creating a web server that make setting up a quick server in Go a breeze
Go is easy to learn and read, which allows for quick adoption by our developers
Built in unit testing helps ensure our code will perform as expected
Our company opted to try Go for a new web-based project that involved user authentication, loading large lists of complex data, and needed to be robust since any errors from this project would directly impact our finances. We also had to make the promise that if the minimum viable product didn't work out we would continue this particular project in Classic ASP, or pivot to a newer implementation of ASP such as ASP.NET or Core.
The first task was to set up a simple web server. Go makes this very easy by allowing you to import "net/http", set up some handlers, and then the listener. With a few lines of code we have a basic web server. This can then be expanded to serve static files as well.
Now that a web server was created, we had to figure out how we incorporate Go into our web pages, since right now either we have static HTML/JS/CSS pages, or we can run Go code, but not really both together in a development-friendly way. Yes, we could have done a print statement and thrown all the HTML into that function interspersed with Golang, but this felt messy. As with many things in Go, the community is full of potential options, and most suggestions were to use a template engine. This is how some other languages accomplish this same task, so it seemed reasonable to try out.
Go itself provides a template package, although we had trouble getting the functionality out of this that we wanted. There are alternatives such as Hero, and while these were closer to our ideal system, there was a tremendous learning curve. So now on top of having to learn Go, developers would also need to understand the template engine, and this seemed like it just added a significant cognitive burden. This burden was exaserbated by the fact that for ASP all you had to do was know a supported script language and HTML. We decided there was a better method instead of being forced to use templates.
Taking inspiration from how both ASP and PHP allow you to seamlessly incorporate server-side and client-side code, we decided to build a "website compiler" which would take source files written in a mixture of Go and HTML/JS/CSS, parse out the Go, and then compile everything into a single web server executable that contained the entire website. This allowed us to write Go for logic and HTML for the display all in one file, very similarly to ASP or PHP. By investing time to build this tool future applications could be developed in the same way. Since this project new developers have been brought on and find themsleves able to work with this system as it already feels familiar. Benefits of our method include:
Only requiring knowledge of Go and HTML, as there is no templating engine to learn
Having Go and HTML altogether means there can be logic done on the server-side, such as assessing user permissions, and then only the HTML that a given user is allowed to see is even ever sent to the user
You get to code the same way and organize files the same way you would when working with ASP, PHP, or even static HTML
By limiting the learning curve while retaining the functionality, we could now get started on the application itself. And that's where the next roadblock happened: working with SQL.
Go has support for working with SQL databases, but it also needs a database driver. Thankfully, there are plenty of community-supported Microsoft SQL Server database drivers to choose from. This eliminates a concern about using Go with our existing environment. We load up the driver and start querying the database and notice something odd: unlike any other time we've ever worked with database results, we were unable to reference a given column by name.
Being unable to reference columns by name was a major concern because it required working with data very differently, and at least for this application, not in an intuitive way. With the company's heavy reliance on databases, this would be a dealbreaker if working with SQL was a nightmare.
Then we got clever: with Go being fast, we could create a map out of our results which would allow us to specify columns we wanted by name and row number. We wrote a function to query the database and return results as a map, which then we could use to easily interact with our data. This implementation ended up being more performant than ASP on the same hardware, which we think is due to how SQL results are handled in ASP and Go. Then, by creating a map we were able to achieve fast lookups as well. Go was already showing a significant advantage over ASP in an area that plagued our applications as datasets had grown.
Now we had an application that we could develop in a way that felt familiar and work with SQL. Next, we needed to figure out how to support authentication. Our existing web applications support using Windows credentials as defined in Active Directory, which is easy for users to remember and allows us to control permissions in a standard way across applications. We needed to interface with Active Directory in our Go application to allow users to log on.
Once again, the Golang community comes to our rescue as there are several lightweight directory access protocol (LDAP) packages to choose from. After some trial and error on how exactly to connect to our Active Directory server and query it, we had a working way to authenticate users with their same credentials they have always used against Active Directory just like in any other application. This does not support passthrough authentication however, so users may be required to log into web pages individually. Although, moving to a post-Windows world and making sure things are more open, users would eventually have to do this anyway. And while we are largely Windows-based, our mobile devices are varied across Android, iOS, and Windows tablets, so this is not a terrible sacrifice to make.
Seeing time and time again that Go, either defaultly or through community packages, had many tools already in existance to work with different workplace solutions gave us confidence that we wouldn't have to constantly reinvent the wheel. We could build upon existing solutions to customize them for our systems, saving a lot of time. Package management in Go is also extremely simple, with a quick command to grab a package, and then you can import it into your project and you are good to go. As a bonus, since Go is compiled they get added to your final binary too. There is no weirdness of dealing with a bunch of different package versions and attempting to figure out how to distribute modules with your code or anything like that; instead it just works.
Another major advantage of Go development, which we didn't realize until we really started getting into it, was that local development environments were much easier to create. All that was needed was a Go installation, and since the server and everything else was just part of the program it worked wherever you ran it. Setting up an ASP development environment was limited to Windows-based hardware and required the installation of IIS along with ASP, which is a lot to install and run. As a result, we had set up various virtual machines for testing, but then that even required a lot of copying and pasting of files, or required a lot of resources for multiple developers. Now developers could easily use their local computer hardware.
Demoing and Deployment
We now had a fully featured application and were ready to show it to the department that had requested it. All of our internal testing had gone well and we were pleased with it, but the real assessment would happen by the end users. Immediately, the speed of the application was praised. While we expected Go to show performance increases over ASP, we didn't know that this could be minutes faster when loading and displaying the same data, thereby saving the end users a lot of time and frustration. In fact, due to the performance that now the end users knew was possible, it changed what they thought the application even needed to be. We got comments saying that since they could access data so much faster, they would have been happy with only the fast search tools. This was great praise and validation of our new technology choice, and meant we were ready to deploy.
Except, how do we make this work on a Windows Server? This entire application has been coded to include its own server since we wanted it to be self contained, with the benefit of not even needing Golang to be installed on the server. However, this now meant that we didn't really have a way to hook into how Windows Server wanted to serve websites, and forcing users to add a port to a URL didn't seem like a great option either.
Some searching on how to effectively serve a server from a server sent us here, which provided instructions on how to do exactly what we wanted. The overall idea is that the client browser makes a connection to a public URL pointed to IIS. Then, by setting up IIS as a reverse proxy, data is passed through on a private network to a backend server (our Go application). The backend server sends a response which IIS sends to the public URL so the end user only sees https://appname.com. However, IIS is actually reverse proxying requests to http://privateserver:8293 instead of directly serving the application. Thanks to this functionality, we were still able to use Windows Server to manage our Golang applications the same was as other applications we had. It seamlessly worked with our current infrastructure, and as time goes on and we upgrade applications one at a time, we now know they can be simple drop in replacements.
Big Chances Yield Big Successes
We were able to meet all requirements in Go that were our minimum criteria for an ASP replacement. Beyond those goals, we also solved every problem we were having with ASP, and end users loved the result! Changing to Go did require us to write some custom tools in order to make it work for us, but after doing so it is a pleasure to work with. We also had to change how we deployed applications to Windows Server, which requires a little more setup than if ASP was just installed, but now our applications are also truly portable to any operating system and require far fewer resources to run. Moving to Go has been so successful for us that it is now our go-to for new applications.