As Cloudflare Workers, and other Serverless platforms, continue to drive down costs while making it easier for developers to stand up globally scaled applications, the migration of legacy applications is becoming increasingly common. In this post, I want to show how easy it is to migrate such an application onto Workers. To demonstrate, I’m going to use a common migration scenario: moving a legacy application — on an old compute platform behind a VPN or in a private cloud — to a serverless compute platform behind zero-trust security.
Wait but why?
Before we dive further into the technical work, however, let me just address up front: why would someone want to do this? What benefits would they get from such a migration? In my experience, there are two sets of reasons: (1) factors that are “pushing” off legacy platforms, or the constraints and problems of the legacy approach; and (2) factors that are “pulling” onto serverless platforms like Workers, which speaks to the many benefits of this new approach. In terms of the push factors, we often see three core ones:
Legacy compute resources are not flexible and must be constantly maintained, often leading to capacity constraints or excess cost;
Maintaining VPN credentials is cumbersome, and can introduce security risks if not done properly;
VPN client software can be challenging for non-technical users to operate.
Similarly, there are some very key benefits “pulling” folks onto Serverless applications and zero-trust security:
Instant scaling, up or down, depending on usage. No capacity constraints, and no excess cost;
No separate credentials to maintain, users can use Single Sign On (SSO) across many applications;
VPN hardware / private cloud; and existing compute, can be retired to simplify operations and reduce cost
While the benefits to this more modern end-state are clear, there’s one other thing that causes organizations to pause: the costs in time and migration effort seem daunting. Often what organizations find is that migration is not as difficult as they fear. In the rest of this post, I will show you how Cloudflare Workers, and the rest of the Cloudflare platform, can greatly simplify migrations and help you modernize all of your applications.
Getting Started
To take you through this, we will use a contrived application I’ve written in Node.js to illustrate the steps we would take with a real, and far more complex, example. The goal is to show the different tools and features you can use at each step; and how our platform design supports development and cutover of an application. We’ll use four key Cloudflare technologies, as we see how to move this Application off of my Laptop and into the Cloud:
Serverless Compute through Workers
Robust Developer-focused Tooling for Workers via Wrangler
Zero-Trust security through Access
Instant, Secure Origin Tunnels through Argo Tunnels
Our example application for today is called Post Process, and it performs business logic on input provided in an HTTP POST body. It takes the input data from authenticated clients, performs a processing task, and responds with the result in the body of an HTTP response. The server runs in Node.js on my laptop.
Since the example application is written in Node.js; we will be able to directly copy some of the JavaScript assets for our new application. You could follow this “direct port” method not only for JavaScript applications, but even applications in our other WASM-supported languages. For other languages, you first need to rewrite or transpile into one with WASM support.
Into our ApplicationOur basic example will perform only simple text processing, so that we can focus on the broad features of the migration. I’ve set up an unauthenticated copy (using Workers, to give us a scalable and reliable place to host it) at https://postprocess-workers.kirk.workers.dev/postprocess where you can see how it operates. Here is an example cURL:
curl -X POST https://postprocess-workers.kirk.workers.dev/postprocess --data '{"operation":"2","data":"Data-Gram!"}'
The relevant takeaways from the code itself are pretty simple:
There are two code modules, which conveniently split the application logic completely from the Preprocessing / HTTP interface.
The application logic module exposes one function postProcess(object) where object is the parsed JSON of the POST body. It returns a JavaScript object, ready to be encoded into a string in the JSON HTTP response. This module can be run on Workers JavaScript, with no changes. It only needs a new preprocessing / HTTP interface.
The Preprocessing / HTTP interface runs on raw Node.js; and exposes a local HTTPS server. The server does not directly take inbound traffic from the Internet, but sits behind a gateway which controls access to the service.
Code snippet from Node.js HTTP module
const server = http.createServer((req, res) => {
if (req.url == '/postprocess') {
if(req.method == 'POST') {
gatherPost(req, data => {
try{
jsonData = JSON.parse(data)
} catch (e) {
res.end('Invalid JSON payload! \n')
return
}
result = postProcess(jsonData)
res.write(JSON.stringify(result) + '\n');
res.end();
})
} else {
res.end('Invalid Method, only POST is supported! \nPlease send a POST with data in format {"Operation":1","data","Data-Gram!" }
} else {
res.end('Invalid request. Did you mean to POST to /postprocess? \n');
}
});
Code snippet from Node.js logic module
function postProcess (postJson) {
const ServerVersion = "2.5.17"
if(postJson != null && 'operation' in postJson && 'data' in postJson){
var output
var operation = postJson['operation']
var data = postJson['data']
switch(operation){
case "1":
output = String(data).toLowerCase()
break
case "2":
d = data + "\n"
output = d + d + d
break
case "3":
output = ServerVersion
break
default:
output = "Invalid Operation"
}
return {'Output': output}
}
else{
return {'Error':'Invalid request, invalid JSON format'}
}
Current State Application Architecture
Design Decisions
With all this information in hand, we can arrive at at the details of our new Cloudflare-based design:
Keep the business logic completely intact, and specifically use the same .js asset
Build a new preprocessing layer in Workers to replace the Node.js module
Use Cloudflare Access to authenticate users to our application
Target State Application Architecture
Finding the first win
One good way to make a migration successful is to find a quick win early on; a useful task which can be executed while other work is still ongoing. It is even better if the quick win also benefits the eventual cutover. We can find a quick win here, if we solve the zero-trust security problem ahead of the compute problem by putting Cloudflare’s security in front of the existing application.
We will do this by using cloudflare’s Argo Tunnel feature to securely connect to the existing application, and Access for zero-trust authentication. Below, you can see how easy this process is for any command-line user, with our cloudflared tool.
I open up a terminal and use cloudflared tunnel login
, which presents me with an authentication flow. I then use the cloudflared tunnel --hostname postprocess.kschwenkler.com --url localhost:8080
command to connect an Argo Tunnel between the “url” (my local server) and the “hostname” (the new, public address we will use on my Cloudflare zone).
Next I flip over to my Cloudflare dashboard, and attach an Access Policy to the “hostname” I specified before. We will be using the Service Token mode of Access; which generates a client-specific security token which that client can attach to each HTTP POST. Other modes are better suited to interactive browser use cases.
Now, without using the VPN, I can send a POST to the service, still running on Node.js on my laptop, from any Internet-connected device which has the correct token! It has taken only a few minutes to add zero-trust security to this application; and safely expose it to the Internet while still running on a legacy compute platform (my laptop!).
“Quick Win” Architecture
Beyond the direct benefit of a huge security upgrade; we’ve also made our eventual application migration much easier, by putting the traffic through the target-state API gateway already. We will see later how we can surgically move traffic to the new application for testing, in this state.
Lift to the Cloud
With our zero-trust security benefits in hand, and our traffic running through Cloudflare; we can now proceed with the migration of the application itself to Workers. We’ll be using the Wrangler tooling to make this process very easy.
As noted when we first looked at the code, this contrived application exposes a very clean interface between the Node.js-specific HTTP module, which we need to replace, and the business logic postprocess module which we can use as is with Workers. We’ll first need to re-write the HTTP module, and then bundle it with the existing business logic into a new Workers application.
Here is a handwritten example of the basic pattern we’ll use for the HTTP module. We can see how the Service Workers API makes it very easy to grab the POST body with await, and how the JSON interface lets us easily pass the data to the postprocess module we took directly from the initial Node.js app.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
try{
requestData = await request.json()
} catch (e) {
return new Response("Invalid JSON", {status:500})
}
const response = new Response(JSON.stringify(postProcess (requestData)))
return response
}
For our work on the mock application, we will go a slightly different route; more in line with a real application which would be more complex. Instead of writing this by hand, we will use Wrangler and our Router template, to build the new front end from a robust framework.
We’ll run wrangler generate post-process-workers https://github.com/cloudflare/worker-template-router
to initialize a new Wrangler project with the Router template. Most of the configurations for this template will work as is; and we just have to update account_id in our wrangler.toml and make a few small edits to the code in index.js.
Below is our index.js
after my edits, Note the line const postProcess = require('./postProcess.js')
at the start of the new http module - this will tell Wrangler to include the original business logic, from the legacy app’s postProcess.js
module which I will copy to our working directory.
const Router = require('./router')
const postProcess = require('./postProcess.js')
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handler(request) {
const init = {
headers: { 'content-type': 'application/json' },
}
const body = JSON.stringify(postProcess(await request.json()))
return new Response(body, init)
}
async function handleRequest(request) {
const r = new Router()
r.post('.*/postprocess*', request => handler(request))
r.get('/', () => new Response('Hello worker!')) // return a default message for the root route
const resp = await r.route(request)
return resp
}
Now we can simply run wrangler publish, to put our application on workers.dev for testing! The Router template’s defaults; and the small edits made above, are all we need. Since Wrangler automatically exposes the test application to the Internet (note that we can *also* put the test application behind Access, with a slightly modified method), we can easily send test traffic from any device.
Shift, Safely!
With our application up for testing on workers.dev, we finally come to the last and most daunting migration step: cutting over traffic from the legacy application to the new one without any service interruption.
Luckily, we had our quick win earlier and are already routing our production traffic through the Cloudflare network (to the legacy application via Argo Tunnels). This provides huge benefits now that we are at the cutover step. Without changing our IP address, SSL configuration, or any other client-facing properties, we can route traffic to the new application with just one wrangler command.
Seamless cutover from Transition to Target state
We simply modify wrangler.toml
to indicate the production domain / route we’d like the application to operate on; and wrangler publish
. As soon as Cloudflare receives this update; it will send production traffic to our new application instead of the Argo Tunnel. We have configured the application to send a ‘version’ header which lets us verify this easily using curl.
Rollback, if it is needed, is also very easy. We can either set the wrangler.toml
back to the workers.dev only mode, and wrangler publish
again; or delete our route manually. Either will send traffic back to the Argo Tunnel.
In Conclusion
Clearly, a real application will be more complex than our example above. It may have multiple components, with complex interactions, which must each be handled in turn. Argo Tunnel might remain in use, to connect to a data store or other application outside of our network. We might use WASM to support modules written in other languages. In any of these scenarios, Cloudflare’s Wrangler tooling and Serverless capabilities will help us work through the complexities and achieve success.
I hope that this simple example has helped you to see how Wrangler, cloudflared, Workers, and our entire global network can work together to make migrations as quick and hassle-free as possible. Whether for this case of an old application behind a VPN, or another application that has outgrown its current home - our Workers platform, Wrangler tooling, and underlying platform will scale to meet your business needs.