In my previous post I demonstrated about how to develop and deploy a Node.js application on Windows Azure Web Site (a.k.a. WAWS). WAWS is a new feature in Windows Azure platform. Since it’s low-cost, and it provides IIS and IISNode components so that we can host our Node.js application though Git, FTP and WebMatrix without any configuration and component installation. But sometimes we need to use the Windows Azure Cloud Service (a.k.a. WACS) and host our Node.js on worker role. Below are some benefits of using worker role. - WAWS leverages IIS and IISNode to host Node.js application, which runs in x86 WOW mode. It reduces the performance comparing with x64 in some cases. - WACS worker role does not need IIS, hence there’s no restriction of IIS, such as 8000 concurrent requests limitation. - WACS provides more flexibility and controls to the developers. For example, we can RDP to the virtual machines of our worker role instances. - WACS provides the service configuration features which can be changed when the role is running. - WACS provides more scaling capability than WAWS. In WAWS we can have at most 3 reserved instances per web site while in WACS we can have up to 20 instances in a subscription. - Since when using WACS worker role we starts the node by ourselves in a process, we can control the input, output and error stream. We can also control the version of Node.js. Run Node.js in Worker Role Node.js can be started by just having its execution file. This means in Windows Azure, we can have a worker role with the “node.exe” and the Node.js source files, then start it in Run method of the worker role entry class. Let’s create a new windows azure project in Visual Studio and add a new worker role. Since we need our worker role execute the “node.exe” with our application code we need to add the “node.exe” into our project. Right click on the worker role project and add an existing item. By default the Node.js will be installed in the “Program Files\nodejs” folder so we can navigate there and add the “node.exe”. Then we need to create the entry code of Node.js. In WAWS the entry file must be named “server.js”, which is because it’s hosted by IIS and IISNode and IISNode only accept “server.js”. But here as we control everything we can choose any files as the entry code. For example, I created a new JavaScript file named “index.js” in project root. Since we created a C# Windows Azure project we cannot create a JavaScript file from the context menu “Add new item”. We have to create a text file, and then rename it to JavaScript extension. After we added these two files we should set their “Copy to Output Directory” property to “Copy Always”, or “Copy if Newer”. Otherwise they will not be involved in the package when deployed. Let’s paste a very simple Node.js code in the “index.js” as below. As you can see I created a web server listening at port 12345. 1: var http = require("http");
2: var port = 12345;
3:
4: http.createServer(function (req, res) {
5: res.writeHead(200, { "Content-Type": "text/plain" });
6: res.end("Hello World\n");
7: }).listen(port);
8:
9: console.log("Server running at port %d", port);
Then we need to start “node.exe” with this file when our worker role was started. This can be done in its Run method. I found the Node.js and entry JavaScript file name, and then create a new process to run it. Our worker role will wait for the process to be exited. If everything is OK once our web server was opened the process will be there listening for incoming requests, and should not be terminated. The code in worker role would be like this.
1: public override void Run()
2: {
3: // This is a sample worker implementation. Replace with your logic.
4: Trace.WriteLine("NodejsHost entry point called", "Information");
5:
6: // retrieve the node.exe and entry node.js source code file name.
7: var node = Environment.ExpandEnvironmentVariables(@"%RoleRoot%\approot\node.exe");
8: var js = "index.js";
9:
10: // prepare the process starting of node.exe
11: var info = new ProcessStartInfo(node, js)
12: {
13: CreateNoWindow = false,
14: ErrorDialog = true,
15: WindowStyle = ProcessWindowStyle.Normal,
16: UseShellExecute = false,
17: WorkingDirectory = Environment.ExpandEnvironmentVariables(@"%RoleRoot%\approot")
18: };
19: Trace.WriteLine(string.Format("{0} {1}", node, js), "Information");
20:
21: // start the node.exe with entry code and wait for exit
22: var process = Process.Start(info);
23: process.WaitForExit();
24: }
Then we can run it locally. In the computer emulator UI the worker role started and it executed the Node.js, then Node.js windows appeared.
Open the browser to verify the website hosted by our worker role.
Next let’s deploy it to azure. But we need some additional steps. First, we need to create an input endpoint. By default there’s no endpoint defined in a worker role. So we will open the role property window in Visual Studio, create a new input TCP endpoint to the port we want our website to use. In this case I will use 80.
Even though we created a web server we should add a TCP endpoint of the worker role, since Node.js always listen on TCP instead of HTTP.
And then changed the “index.js”, let our web server listen on 80.
1: var http = require("http");
2: var port = 80;
3:
4: http.createServer(function (req, res) {
5: res.writeHead(200, { "Content-Type": "text/plain" });
6: res.end("Hello World\n");
7: }).listen(port);
8:
9: console.log("Server running at port %d", port);
Then publish it to Windows Azure.
And then in browser we can see our Node.js website was running on WACS worker role.
We may encounter an error if we tried to run our Node.js website on 80 port at local emulator. This is because the compute emulator registered 80 and map the 80 endpoint to 81. But our Node.js cannot detect this operation. So when it tried to listen on 80 it will failed since 80 have been used.
Use NPM Modules
When we are using WAWS to host Node.js, we can simply install modules we need, and then just publish or upload all files to WAWS. But if we are using WACS worker role, we have to do some extra steps to make the modules work.
Assuming that we plan to use “express” in our application. Firstly of all we should download and install this module through NPM command. But after the install finished, they are just in the disk but not included in the worker role project. If we deploy the worker role right now the module will not be packaged and uploaded to azure. Hence we need to add them to the project. On solution explorer window click the “Show all files” button, select the “node_modules” folder and in the context menu select “Include In Project”.
But that not enough. We also need to make all files in this module to “Copy always” or “Copy if newer”, so that they can be uploaded to azure with the “node.exe” and “index.js”. This is painful step since there might be many files in a module. So I created a small tool which can update a C# project file, make its all items as “Copy always”. The code is very simple.
1: static void Main(string[] args)
2: {
3: if (args.Length < 1)
4: {
5: Console.WriteLine("Usage: copyallalways [project file]");
6: return;
7: }
8:
9: var proj = args[0];
10: File.Copy(proj, string.Format("{0}.bak", proj));
11:
12: var xml = new XmlDocument();
13: xml.Load(proj);
14: var nsManager = new XmlNamespaceManager(xml.NameTable);
15: nsManager.AddNamespace("pf", "http://schemas.microsoft.com/developer/msbuild/2003");
16:
17: // add the output setting to copy always
18: var contentNodes = xml.SelectNodes("//pf:Project/pf:ItemGroup/pf:Content", nsManager);
19: UpdateNodes(contentNodes, xml, nsManager);
20: var noneNodes = xml.SelectNodes("//pf:Project/pf:ItemGroup/pf:None", nsManager);
21: UpdateNodes(noneNodes, xml, nsManager);
22: xml.Save(proj);
23:
24: // remove the namespace attributes
25: var content = xml.InnerXml.Replace("<CopyToOutputDirectory xmlns=\"\">", "<CopyToOutputDirectory>");
26: xml.LoadXml(content);
27: xml.Save(proj);
28: }
29:
30: static void UpdateNodes(XmlNodeList nodes, XmlDocument xml, XmlNamespaceManager nsManager)
31: {
32: foreach (XmlNode node in nodes)
33: {
34: var copyToOutputDirectoryNode = node.SelectSingleNode("pf:CopyToOutputDirectory", nsManager);
35: if (copyToOutputDirectoryNode == null)
36: {
37: var n = xml.CreateNode(XmlNodeType.Element, "CopyToOutputDirectory", null);
38: n.InnerText = "Always";
39: node.AppendChild(n);
40: }
41: else
42: {
43: if (string.Compare(copyToOutputDirectoryNode.InnerText, "Always", true) != 0)
44: {
45: copyToOutputDirectoryNode.InnerText = "Always";
46: }
47: }
48: }
49: }
Please be careful when use this tool. I created only for demo so do not use it directly in a production environment.
Unload the worker role project, execute this tool with the worker role project file name as the command line argument, it will set all items as “Copy always”. Then reload this worker role project.
Now let’s change the “index.js” to use express.
1: var express = require("express");
2: var app = express();
3:
4: var port = 80;
5:
6: app.configure(function () {
7: });
8:
9: app.get("/", function (req, res) {
10: res.send("Hello Node.js!");
11: });
12:
13: app.get("/User/:id", function (req, res) {
14: var id = req.params.id;
15: res.json({
16: "id": id,
17: "name": "user " + id,
18: "company": "IGT"
19: });
20: });
21:
22: app.listen(port);
Finally let’s publish it and have a look in browser.
Use Windows Azure SQL Database
We can use Windows Azure SQL Database (a.k.a. WACD) from Node.js as well on worker role hosting. Since we can control the version of Node.js, here we can use x64 version of “node-sqlserver” now. This is better than if we host Node.js on WAWS since it only support x86.
Just install the “node-sqlserver” module from NPM, copy the “sqlserver.node” from “Build\Release” folder to “Lib” folder. Include them in worker role project and run my tool to make them to “Copy always”. Finally update the “index.js” to use WASD.
1: var express = require("express");
2: var sql = require("node-sqlserver");
3:
4: var connectionString = "Driver={SQL Server Native Client 10.0};Server=tcp:{SERVER NAME}.database.windows.net,1433;Database={DATABASE NAME};Uid={LOGIN}@{SERVER NAME};Pwd={PASSWORD};Encrypt=yes;Connection Timeout=30;";
5: var port = 80;
6:
7: var app = express();
8:
9: app.configure(function () {
10: app.use(express.bodyParser());
11: });
12:
13: app.get("/", function (req, res) {
14: sql.open(connectionString, function (err, conn) {
15: if (err) {
16: console.log(err);
17: res.send(500, "Cannot open connection.");
18: }
19: else {
20: conn.queryRaw("SELECT * FROM [Resource]", function (err, results) {
21: if (err) {
22: console.log(err);
23: res.send(500, "Cannot retrieve records.");
24: }
25: else {
26: res.json(results);
27: }
28: });
29: }
30: });
31: });
32:
33: app.get("/text/:key/:culture", function (req, res) {
34: sql.open(connectionString, function (err, conn) {
35: if (err) {
36: console.log(err);
37: res.send(500, "Cannot open connection.");
38: }
39: else {
40: var key = req.params.key;
41: var culture = req.params.culture;
42: var command = "SELECT * FROM [Resource] WHERE [Key] = '" + key + "' AND [Culture] = '" + culture + "'";
43: conn.queryRaw(command, function (err, results) {
44: if (err) {
45: console.log(err);
46: res.send(500, "Cannot retrieve records.");
47: }
48: else {
49: res.json(results);
50: }
51: });
52: }
53: });
54: });
55:
56: app.get("/sproc/:key/:culture", function (req, res) {
57: sql.open(connectionString, function (err, conn) {
58: if (err) {
59: console.log(err);
60: res.send(500, "Cannot open connection.");
61: }
62: else {
63: var key = req.params.key;
64: var culture = req.params.culture;
65: var command = "EXEC GetItem '" + key + "', '" + culture + "'";
66: conn.queryRaw(command, function (err, results) {
67: if (err) {
68: console.log(err);
69: res.send(500, "Cannot retrieve records.");
70: }
71: else {
72: res.json(results);
73: }
74: });
75: }
76: });
77: });
78:
79: app.post("/new", function (req, res) {
80: var key = req.body.key;
81: var culture = req.body.culture;
82: var val = req.body.val;
83:
84: sql.open(connectionString, function (err, conn) {
85: if (err) {
86: console.log(err);
87: res.send(500, "Cannot open connection.");
88: }
89: else {
90: var command = "INSERT INTO [Resource] VALUES ('" + key + "', '" + culture + "', N'" + val + "')";
91: conn.queryRaw(command, function (err, results) {
92: if (err) {
93: console.log(err);
94: res.send(500, "Cannot retrieve records.");
95: }
96: else {
97: res.send(200, "Inserted Successful");
98: }
99: });
100: }
101: });
102: });
103:
104: app.listen(port);
Publish to azure and now we can see our Node.js is working with WASD through x64 version “node-sqlserver”.
Summary
In this post I demonstrated how to host our Node.js in Windows Azure Cloud Service worker role. By using worker role we can control the version of Node.js, as well as the entry code. And it’s possible to do some pre jobs before the Node.js application started. It also removed the IIS and IISNode limitation. I personally recommended to use worker role as our Node.js hosting.
But there are some problem if you use the approach I mentioned here. The first one is, we need to set all JavaScript files and module files as “Copy always” or “Copy if newer” manually. The second one is, in this way we cannot retrieve the cloud service configuration information. For example, we defined the endpoint in worker role property but we also specified the listening port in Node.js hardcoded. It should be changed that our Node.js can retrieve the endpoint. But I can tell you it won’t be working here.
In the next post I will describe another way to execute the “node.exe” and Node.js application, so that we can get the cloud service configuration in Node.js. I will also demonstrate how to use Windows Azure Storage from Node.js by using the Windows Azure Node.js SDK.
Hope this helps,
Shaun
All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.