Deploying Containerized Applications on Google App Engine
Take a look at this Node.js application and its deployment to Google App Engine for a better understanding of customizing container infrastructures.
Join the DZone community and get the full member experience.
Join For FreeThis article is featured in the new DZone Guide to Containers: Development and Management. Get your free copy for more insightful articles, industry statistics, and more!
Google App Engine
Platform-as-a-Service (PaaS) has become popular to build and deploy applications without having to worry about the underlying infrastructure that runs the application. When using PaaS, however, we need to make tradeoffs. Most importantly, when we give up control of the underlying infrastructure, we lose some ability to configure that infrastructure.
In this environment, a containerized PaaS such as Google App Engine (GAE) can provide a hybrid approach between letting the PaaS provider control the application runtime and having to build the runtime ourselves. Because applications in GAE run in Docker containers, we have the flexibility to customize the infrastructure while still being able to think in terms of deploying applications as opposed to deploying infrastructure. At the same time, there's a challenge in working with a PaaS like GAE because its application deployment behavior can be opaque. Sometimes, this means that you try a deployment before you know whether it will work.
To see how deployment to Google App Engine works and to understand some of the capabilities and challenges of customizing the infrastructure, let's look at a Node.js application deployed to GAE. This is very similar to an application that I built recently, but simplified for clarity. The example application provides a REST API that performs server-side rendering of Mermaid diagrams; this is challenging because it requires us to run a headless Chromium instance, which puts some unique demands on the application infrastructure.
To follow along, you can access the complete source code here. You'll need a GAE account and Google SDK installation. The installation instructions discuss running gcloud init
, which will enable you to provide your account information and register your first application.
Note: While I'm describing customization in GAE, similar logic applies to customizing deployments in other common PaaS environments such as Heroku.
NODE.js GAE Application Structure
For a standard Node.js application, there is very little we need to deploy it to GAE. Most importantly, we need an app.yaml
file at the top level. This can be very simple:
runtime: nodejs
api_version: '1.0'
env: flexible
threadsafe: true
manual_scaling:
instances: 1
The two important lines in this file are env
and runtime
. The env
line tells GAE to run this in the "flexible" environment, which means the runtime will use Docker. The runtime
line specifies the Node.js runtime.
Other than app.yaml
, the repository is a standard Node.js server using Express. The only concession we must make to GAE is to accept a PORT
environment variable telling us what port to listen on; this allows GAE to put a load balancer in front of our application. We handle this inside the server.js startup script for the application, defaulting to 3000 if PORT
is not set:
...
const port = process.env.PORT 3000;
...
The rest of server.js
configures Express to route traffic to REST API endpoints. We start by configuring the body_ parser
middleware:
...
app.use(bodyParser.text())
...
This tells Express to pass the request body through the text()
parser. This parser reads the request body in as plain text. By default, however, it only handles HTTP requests where Content-Type
is set to text/plain
, so our client requests will need to match that expectation.
We next create the API endpoint /render
:
...
app.post('/render', function(req, res){
render(req.body, function(content) {
res.send(content);
});
});
...
We provide a function that runs on a POST request to a specific URL path. By the time the function is called, the middleware has already read the request body, so we can access it as req.body
.
Finally, we listen on the configured port:
...
app.listen(port, () => console.log(`Mermaid Server on port
${port}`));
...
The provided lambda function is called once the server is established.
Now that we have our Node.js application and our app.yaml
file, we gcloud app deploy
run to deploy the application to GAE. GAE will build a Docker container with our application files inside, run npm install
, and then run npm start
to run our application. It provides a load balancer that accepts traffic at the hostname identified when we created the application and performs HTTP/S termination for us so that we don't have to configure our own SSL certificate.
For our example application, since it exports a REST API, we can use httpie to send it a Mermaid text and get a diagram back.
File mermaid.txt
:
graph TB
c1-->a2 subgraph one a1-->a2
end
subgraph two b1-->b2
end
subgraph three c1-->c2
end
Command:
cat mermaid.txt |
http https://mermaid-server.appspot.com/render
'Content-Type:text/plain' > render.png
This results in a PNG file with our rendered graphic:
Customizing the Container
As you can see, deploying a Node.js application to GAE is very simple. What makes this example a little more challenging is that it uses Mermaid, a JavaScript library that renders diagrams from a text description. Mermaid relies on having a browser available with SVG support. Fortunately, we can provide this on the server side in our Node.js application using Puppeteer. Puppeteer downloads and runs Chromium and allows us to interact with a Chromium instance directly from Node.js. This includes opening tabs, loading pages, running JavaScript, and taking screenshots of the rendered content.
When using PaaS, however, we need to make tradeoffs. Most importantly, when we give up control of the underlying infrastructure, we lose some ability to configure that infrastructure.
To get Chromium to work, we need some libraries that aren't present in the default Node.js container that GAE uses. We, therefore, need some way to get these libraries installed in our Docker container before Puppeteer will work as expected. One option would be to use runtime: custom
in our app.yaml
. This would tell GAE to look for a Dockerfile
in the same directory as app.yaml
. You can see an example of this in the "try-puppeteer" repository.
There is an alternative, however, that avoids the need for a Dockerfile. We can take advantage of Node.js preinstall to run commands when our container is set up. We include this in our package.json:
"scripts": {
"preinstall": "node preinstall.js"
},
This allows us to put any content we want into preinstall.js
, and it will be executed prior to npm install
.
In preinstall.js, we first check to see if we're running in production. If so, we use the child_process
module to run the necessary apt-get
commands to install Chromium dependencies.
const child_process = require('child_process');
const ne = process.env.NODE_ENV;
if (ne !== undefined && ne == 'production') {
console.log("Production environment…");
child_process.execSync('apt-get update',
stdio:'inherit'});
child_process.execSync('apt-get -y install libxss1
ibgconf-2-4 libatk-bridge2.0-0 libgtk-3-0 libx11-xcb1
ibnss3 libasound2', {stdio:'inherit'});
} else {
console.log("Production not detected, nothing to do");
}
Because we're in a Docker container and already running as root, sudo
is not necessary to run apt-get
. The exact list of dependencies is, of course, based on what is needed for Chromium; I started with this post but tested the exact set of libraries needed so I could cut down the list somewhat.
The {stdio:'inherit'}
configuration for execSync
was essential in developing this; it causes Node.js to send the output of the apt-get
commands to stdout
, which means it shows up in the GAE logs for the application. This got me past a couple cases where package names had changed. The logs are available in the GAE console or using the gcloud app logs read
command.
Conclusion
Platform-as-a-Service has made application deployment fast and easy by hiding the work of configuring the lower-level infrastructure. Sometimes, however, we need access to that infrastructure to configure it the way we want. For containerized PaaS environments such as Google App Engine, we can leverage the capabilities of the underlying Docker container to customize our application's infrastructure as needed without losing our simplified development and deployment model.
This article is featured in the new DZone Guide to Containers: Development and Management. Get your free copy for more insightful articles, industry statistics, and more!
Opinions expressed by DZone contributors are their own.
Trending
-
How To Use Geo-Partitioning to Comply With Data Regulations and Deliver Low Latency Globally
-
Manifold vs. Lombok: Enhancing Java With Property Support
-
Hiding Data in Cassandra
-
Docker Compose vs. Kubernetes: The Top 4 Main Differences
Comments