Autodeploying Angular Applications to AWS OpsWorks

OCT 11, 2016 • Written by Don Denoncourt

Hosting on AWS OpsWorks

Amazon OpsWorks is an excellent, low-cost option for Platform-as-a-Service hosting. OpsWorks provides relatively easy-to-use UI mechanisms to manage, deploy and host applications on AWS EC2. Corgibytes uses OpsWorks to host both Rails-based and Angular-based servers for one of our clients. Configuring the Rails-based OpsWorks hosts was easy – mostly because there are tons of blog posts on how to do it. But setting up the Angular server was a bit more problematic for me, as my good friend, Google, provided little help.

Initially, we got the Angular app up quite easily on OpsWorks. But, we did it by manually configuring Nginx on the EC2 Linux box and using scp to copy the gulp-compiled version of the application from our Macs directly to a specific EC2 instance. As a rule, you should never manually configure OpsWorks applications. All of your OpsWorks-based applications should be completely rebuildable and deployable with a button click. You are able to pull that philosophy off by using OpsWorks Chef Cookbooks and Recipes and deployment hooks.

Let me show you how I built the AWS OpsWorks server, configured and customized Chef, configured GitHub retrieval of the application, and wrote a Ruby-based deployment hook to automate the configuration of Nginx and run gulp build to compile the Angular application.

Create a Stack

OpsWorks is set up with a stacked architecture known as: Stack, Layers, Instances, and Apps.

0. Stack overview

  1. Stack: contains your configuration for things like the Linux operating system, SSH access, and the location for custom Chef cookbooks.
  2. Layers: configuration for your application deployment type (such as Rails, PHP, or Java) and the specification for what custom Chef recipes to run and when.
  3. Instances: are actual Linux machines that are either running on AWS EC2 or in a stopped state.
  4. Apps: is where you configure the Git or Subversion location of your application and any environment variables.

Deployments, Monitoring, Resources, and Permissions are not so much part of the stack as they are ways to monitor and configure the application.

Now, we need to create a Stack. From OpsWorks Home, click the Add stack button. Give it a meaningful Stack name and accept the defaults for all but “Use custom Chef cookbooks”. For that option we need to select Yes, as we will be specifying some custom recipes.

Custom Cookbook

When you select Yes for cookbooks, the page will expand to show the options that are displayed in the image below. You will need to fork the AWS OpsWorks repository (Here’s help on how to do GitHub forks).

Note: You might want to set up SSH keys so you can access your Linux instances from a local Bash session. This GitHub help guide will walk you through that process. After you have forked opsworks-cookbooks, update the following on the Add Stack page:

  1. Repository Type: Select Git from the drop-down.
  2. Repository URL: git@github.com:<yourgithubaccountname>/opsworks-cookbooks.git
  3. Repository SSH: Paste in a key that you have configured in GitHub’s settings and keys page.
  4. Repository Branch: Key: release-chef-11.10

And, finally, click Add stack.

1. Create stack

Add a Layer

With the Stack build, we next need to create a layer:

  1. Click Layers and then Add layer.
  2. Select Node.js App Server from the drop-down.
  3. Click the Add layer button.

Our application is not directly using Node.js, but I found the OpsWorks Node.js layer type to be configured in a way that made adding an Angular application relatively easy.

If you haven’t already done so, click Add layer to create the layer.

2. add layer

Add Custom Recipes

With the layer built, we need to next customize it with recipes. Click the Recipes link on the Layers page.

3. use default recipes

Then, on the page shown below, we need to add two custom recipes and the Nginx OS package. Put key nginx in Setup and deploy::web in the Deploy prompt. Then, we need to add the Nginx package. The Node.js layer will not, by default, install Nginx. We can easily add it by keying the “ngin” into the “Package name” prompt and then selecting nginx from the auto-complete drop-down. Note that, if a Linux package is available for the Linux operating system specified for your Layer, it will show up in a drop-down. If the package is not found, you will have to configure the installation in a cookbook recipe or manually add it in a Bash session after the Linux instance is started (note, manually adding packages in an instance is not recommended).

Finally, click the Save button.

4. add an instance

Add the Application

Before we add a Linux instance to our Stack, we need to first specify the application. Enter your application’s GitHub location, the repository SSH key, and the branch.

6. start the instance

Add an AWS OpsWorks Chef Deployment Hook

We’re almost ready to add a Linux instance and launch the site. But first, we need to add a Ruby-based callback. Create a directory in your Angular application root called deploy and add a file called after_restart.rb. The full 50 lines of Ruby code is shown below and it is broken down into six steps:

  1. npm install
  2. build dist with gulp
  3. ensure config directory exists
  4. set the config file
  5. copy gulp generated dist directory to nginx
  6. restart nginx

Step 1 asks the Node Package Manager to install all the packages listing in package.json. One of those packages is gulp – which we need to build/compile the Angular application.

Step 2 runs gulp build.

With the app built, Step 3 makes sure that the configuration directory exists by running the mkdir with the -p option so it won’t error out if the directory already exists.

Step 4 copies in an Nginx configuration file that specifies the location of the application.

Step 5 then copies the gulp-compiled distribution directory to the appropriate Nginx location.

And, finally, Step 6 restarts Nginx.

Note that “pulse2” is the name of my application’s Git repository, so then the name of the Nginx-enabled site is /etc/nginx/sites-enabled/pulse2. And “pulse” is the name that I gave when I created the OpsWorks App entry, so that name is then used by OpsWorks to create a directory called /srv/www/pulse, and that’s where the gulp binary – that Node Package Manager installed – exists: /srv/www/pulse/current/node_modules/.bin/gulp. It may have been easier to not have confused things with the pulse2 name, but it’s good to know that the name of the /srv/www/pulse directory comes from the OpsWorks application you created, and the pulse2 directories were named to match the Git repository name.

# deploy/after_restart.rb
Chef::Log.info("Pulse on OpsWorks running after deployment")

execute "npm install" do
  cwd release_path
  user "deploy"
  environment "NODE_ENV" => 'production'
  command "npm install"
end

execute "build dist with gulp" do
  cwd release_path
  user "deploy"
  environment "NODE_ENV" => 'production'
  command "/srv/www/pulse/current/node_modules/.bin/gulp build"
end

execute "ensure config directory exists" do
  cwd release_path
  user "root"
  command "mkdir -p /var/www/pulse2"
end

execute "set the config file" do
  cwd release_path
  user "root"

  enable_site = <<-enable_site
server {
  listen   80 default_server;
  server_name  127.0.0.1;
  access_log  /var/log/nginx/localhost.access.log;
  location / {
    root   /var/www/pulse2;
    index  index.html index.htm;
  }
}
  enable_site

  command "echo '#{enable_site}' > /etc/nginx/sites-enabled/pulse2"
end

execute "copy gulp generated dist directory to nginx" do
  cwd release_path
  user "root"
  command "cp -Rf /srv/www/pulse/current/dist/* /var/www/pulse2"
end

execute "restart nginx" do
  cwd release_path
  command "/etc/init.d/nginx restart"
end

Add and Start an Instance

Now, with the Application configured and a callback added, let’s add a Linux machine by clicking on the Add Instance link. Warning: On Add Instance, be sure to change Size as it defaults to c3.large. As they say: “That’s how they get you!” OpsWorks uses EC2 instances and OpsWorks is very inexpensive – if you select small EC2 instances.

5. add an app (git)

Now start the instance by clicking the start link. It takes maybe 5-8 minutes as it builds the Linux machine, runs the recipes associated in the Layer, and then retrieves the application from Git. If it fails, click on the failure log link on the Apps page.

7. start instance

Deploy or Rebuild with a Button Click

Now, we can launch a new instance or deploy our latest changes with a button click. Also, we can easily create a new instance and change the GitHub branch to test a branch on a non-production server. This strategy is working well for us, but if you know of a better mechanism, please let us and our blog readers know.