Introduction to OpsWorks Workshop

Objective

We will create a local Chef development environment and a demo cookbook. We will use this cookbook to create an OpsWorks stack and will also take the advantage of OpsWorks data bags to gather information about the environment.

Create Chef Development Instance

Create an EC2 key pair named chefuser and download pem file chefuser.pem. This is used in by chef to login to EC2 instances.

Create another EC2 key pair, or use existing one, to login to Chef development instance. (Mine happens to be named awsplus.pem.)

Create an instance to use as Chef development instance. In this workshop, we use the latest Amazon AMI (ami-6869aa05). m4.large or a smaller instance type would be enough. Use awsplus as the key.

Login to the instance.

$ echo "ServerAliveInterval 50" > ~/.ssh/config
$ chmod 644 ~/.ssh/config
$ sudo yum update -y
$ sudo yum install tree -y #optional
$ sudo yum install emacs -y #optional

Create an IAM user that can create EC2 instances and enter it’s credentials into ~/.aws/credentials on chef instance.

[default]
aws_access_key_id = AKIAJGBFBGOJJJJMKYXQ
aws_secret_access_key = EnlAfEjw4S4JWfX9ABCAAAAAAAAAABC

Create a security group that will allow inbound connections to TCP port 80. We will use this security group in .kitchen.yml configuration file. (In the example below there are two security groups. One of them allows ssh in, other one allows all connections to my IP address so I can try TCP port 80)

Configure emacs (http://cloudway.io/post/emacs-tips/)

Logout, login.

Configure Chef Instance

Install ChefDK

You can use screen to keep ssh session

$ screen -S OZ

Find the latest chefdk package on https://downloads.chef.io/chef-dk/ and download it.

$ wget https://packages.chef.io/stable/el/6/chefdk-0.16.28-1.el6.x86_64.rpm

Install the chefdk.

$ sudo rpm -iUvh chefdk-0.16.28-1.el6.x86_64.rpm

Create demo cookbook

$ mkdir -p ~/chef/cookbooks; cd ~/chef/cookbooks
$ chef generate cookbook demo
$ cd demo

Configure Kitchen

Modify ~/cookbooks/demo/.kitchen.yml to look like below.

---
driver:
  name: ec2
  aws_ssh_key_id: chefuser
  region: us-east-1
  availability_zone: us-east-1c
  require_chef_omnibus: true
  instance_type: t2.small
  security_group_ids: ["sg-2385c744", "sg-2e48f255"]

transport:
  ssh_key: /home/ec2-user/.ssh/chefuser.pem
  connection_timeout: 10
  connection_retries: 5
  username: ec2-user

provisioner:
  name: chef_zero

platforms:
  - name: amazon
    driver:
      image_id: ami-6869aa05
      #image_id: ami-f5f41398 - older
    transport:
      username: ec2-user

suites:
  - name: Web1
    data_bags_path: data_bags
    run_list:
      - recipe[demo::default]
  - name: Web2
    data_bags_path: data_bags
    run_list:
      - recipe[demo::default]

We will modify this file later on.

Copy chefuser.pem from your computer to Chef development instance, and change permissions.

$ chmod 600 /home/ec2-user/.ssh/chefuser.pem

Try kitchen list

$ kitchen list
Instance          Driver  Provisioner  Verifier  Transport  Last Action
Web1-amazon  Ec2     ChefZero     Busser    Ssh        <Not Created>
Web2-amazon  Ec2     ChefZero     Busser    Ssh        <Not Created>

Create A Simple Recipe with A Template

Create a template

$ cd cookbooks
$ chef generate template demo test.txt
Recipe: code_generator::template
  * directory[./demo/templates/default] action create
    - create new directory ./demo/templates/default
  * template[./demo/templates/test.txt.erb] action create
    - create new file ./demo/templates/test.txt.erb
    - update content in file ./demo/templates/test.txt.erb from none to e3b0c4
    (diff output suppressed by config)

Content of ~/demo/templates/test.txt.erb

This is a test file created by Chef.

Create a recipe

$ cd cookbooks
$ chef generate recipe demo init
$ cd cookbooks/demo/recipes

Modify init.rb to look like below.

#
# Cookbook Name:: demo
# Recipe:: init
#
# Copyright (c) 2016 The Authors, All Rights Reserved.

template '/tmp/test.txt' do
  source 'test.txt.erb'
  owner 'root'
  group 'root'
  mode '0766'
end

Include init recipe in the default recipe. Edit demo/recipes/default.rb to look like below.

include_recipe 'demo::init'

Create the instance using kitchen.

$ kitchen converge Web1
...
...
Running handlers:
Running handlers complete
Chef Client finished, 1/4 resources updated in 05 seconds
Finished converging <Web1-amazon> (0m21.95s).

Login to newly created instance and check test.txt file.

$ kitchen login Web1-amazon
$ cat /tmp/test.txt
This is a test file created by Chef.

Configure Web Servers

Create a User for Web Service

Under cookbooks folder run the command below to create service_user recipe.

$ chef generate recipe demo service_user

As all recipes for demo cookbook, this recipe is created under cookbooks/demo/recipes/ folder.

Add the line below to cookbooks/demo/recipes/service_user.rb.

group 'service_admin'

user 'service_admin' do
  group 'service_admin'
  system true
  shell '/bin/bash'
end

Create custom attributes file to define our own attributes instead of prividing them in the recipes.

$ chef generate attribute demo default

Edit ./demo/attributes/default.rb to have the code below.

default['demo']['user'] = 'service_admin'
default['demo']['group'] = 'service_admin'

Modify service_user recipe to use custom attributes.

group node['demo']['group']

user node['demo']['user'] do
  group node['demo']['group']
  system true
  shell '/bin/bash'
end

Include service_user recipe in the default recipe. Edit demo/recipes/default.rb to look like below.

include_recipe 'demo::init'
include_recipe 'demo::service_user'

Run the cookbook again

$ kitchen converge Web1-amazon

Login and check if service_admin user and groups are created.

$ kitchen login Web1-amazon
$ grep service_admin /etc/passwd
service_admin:x:498:501::/home/service_admin:/bin/bash
$ grep service_admin /etc/group
service_admin:x:501:

Configure SELinux

To configure SELinux we will use a community cookbook. You can find community cookbooks at https://supermarket.chef.io/. If you know the name of the cookbook, you can use knife command as shown below.

$ knife cookbook site show selinux|grep latest_version
latest_version:       https://supermarket.chef.io/api/v1/cookbooks/selinux/versions/0.9.0

To use a community cookbook, we have to include it in the cookbooks/demo/metadata.rb file. Append the line below to metadata.rb

depends 'selinux', '~> 0.9.0'

Now we can refer to selinux cookbook and the resources it provides in from our demo cookbook. selinux cookbook is relatively easy to use and we will only call a recipe to disable selinux on the linux instance.

Append the code below to default recipe located at cookbooks/demo/recipes/default.rb

include_recipe 'selinux::permissive'

Run the cookbook and you will see selinux recipe is applied.

$ kitchen converge
...
   Recipe: selinux::_common
     * yum_package[libselinux-utils] action install (up to date)
     * directory[/etc/selinux] action create (up to date)
   Recipe: selinux::permissive
     * selinux_state[SELinux Permissive] action permissive (up to date)
   Recipe: demo::service_user
...

Configure Apache

To create web_server recipe, under cookbooks folder run the command below.

$ chef generate recipe demo web_server

Modify web_server recipe to look like below.

#
# Cookbook Name:: demo
# Recipe:: web_server
#
# Copyright (c) 2016 The Authors, All Rights Reserved.

package 'httpd' do
  action :install
end

directory '/var/www/html' do
  recursive true
end

template '/etc/httpd/conf.d/hello.conf' do
  source 'hello.conf.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  notifies :restart, 'service[httpd]'
end

template '/var/www/html/hello.html' do
  source 'hello.html.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
end

service 'httpd' do
  action [:enable, :start]
end

We will create two templates.

  • hello.conf: apache virtual host configuration
  • hello.html: to be used as index page

Create hello.conf template that is referred in the web_server recipe.

$ chef generate template demo hello.conf

Put the code below into hello.conf.erb.

<VirtualHost *:80>
    ServerAdmin webmaster@localhost

    DocumentRoot /var/www/html

    DirectoryIndex hello.html

    <Directory />
        Options FollowSymLinks
        AllowOverride None
    </Directory>

    <Directory /var/www/html>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride None
        Order allow,deny
        allow from all
    </Directory>

    ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
    <Directory "/usr/lib/cgi-bin">
        AllowOverride None
        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
        Order allow,deny
        Allow from all
    </Directory>

    LogLevel warn
</VirtualHost>

As you can notice, we change the DirectoryIndex to hello.html from default index.html.

Create hello.html template that is referred in the web_server recipe.

$ chef generate template demo hello.html

Put the code below into hello.html.erb

<html>
    Hello AWS World!
</html>

Finally add the recipe to default recipe.

cookbooks/demo/recipes/default.rb must look like below.

include_recipe 'demo::init'
include_recipe 'selinux::permissive'
include_recipe 'demo::service_user'
include_recipe 'demo::web_server'

Run the cookbook

$ kitchen converge Web1-amazon

Find the public IP address of the instance and try on a browser (You need a security group that will let you access TCP port 80).

Use Variables in The Template

We will modify hello.html template in order to use data bags, node attributes.

Kitchen lets you pass a data_bag_path. Lets create a data bag.

$ mkdir -p data_bags/demo_bag
$ touch data_bags/demo_bag/config.json

Put the code below into conifg.json

{
    "name": "demo application"
}

Now we have a data bag named demo_bag with an item named config. It has a key named name with a value of demo application.

Let’s modify web_server recipe to use the data bag.

config = data_bag_item('demo_bag', 'config')

package 'httpd' do
  action :install
end

directory '/var/www/html' do
  recursive true
end

template '/etc/httpd/conf.d/hello.conf' do
  source 'hello.conf.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  notifies :restart, 'service[httpd]'
end

template '/var/www/html/hello.html' do
  source 'hello.html.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  variables(:name => config['name'])
end

service 'httpd' do
  action [:enable, :start]
end

We used config item from demo_bag data bag and used name key.

Modify cookbooks/demo/templates/hello.html.erb template so it will look like below.

<html>
  Hello AWS World! <br>
  Application: <%= @name %> (-> comes from data bag) <br>
  This is <%= node['hostname'] %> (-> comes from node attributes) <br>
</html>

Modify suites section in .kitchen.yml so it will look like below.

suites:
  - name: Web1
    data_bags_path: data_bags
    run_list:
      - recipe[demo::default]
  - name: Web2
    data_bags_path: data_bags
    run_list:
      - recipe[demo::default]

Run the command below.

$ kitchen converge

This will update Web1-amazon instance and also will create the second instance identical to first one.

Moving the Cookbook to OpsWorks

Preparation for OpsWorks Stack

OpsWorks accepts data bag data in a custom JSON. Details here

Below is the JSON we will pass to OpsWorks.

{
  "opsworks": {
    "data_bags": {
      "demo_bag": {
        "config": {
          "name": "demo application"
        }
      }
    }
  }
}

Create a package of the cookbook and upload to your S3 bucket. (oz-opsworks is the name of the bucket in this demo)

$ berks package demo-cookbook.tar.gz; aws s3 cp demo-cookbook.tar.gz s3://oz-opsworks/demo-cookbook.tar.gz

Create elb demo-elb with port 80 and health check on /hello.html.

Create the Stack

Go to OpsWork console and click on Chef 12 Stack. Give a name (demo) to your stack. Choose the region, VPC as you want. Choose Linux and latest Amazon AMI, the SSH key you want to use.

As seen below check yes on Use custom Chef cookbooks and provide S3 information. This is where OpsWorks will look for the cookbook.

In the Advanced Section make the selections as seen before. Remember the JSON we mentioned earlier, paste that into Custom JSON.

Click Add Stack.

At this point we have a stack and we will create a Layer for Web Service.

Create the Layer

On the following page, click Add a Layer. Give a long and a short name to the layer and click Add Layer.

On the next page, click on Network.

Choose the ELB you created earlier and click Save.

Now we have associated an ELB with our layer and each instance we will create under this layer will automatically join to ELB.

Click recipes and enter the recipe names and save. It will look like below.

Use OpsWorks Data Bags

Before going forward and creating instances, we will modify our demo cookbook in order to use aws_opsworks_instance data bag which provides information about the instances in the stack.(More here)

We will make changes to web_server recipe and hello.html template in order to find all EC2 instances in the layer using OpsWorks aws_opsworks_instance data bag. These data bags are automatically populated by OpsWorks and are especially useful in dynamic environments where you add/remove instances based on the load.

Edit web_server.rb recipe so it will look like below.

#
# Recipe:: web_server
#
# Copyright (c) 2016 The Authors, All Rights Reserved.

config = data_bag_item('demo_bag', 'config')
instances_data_bag = data_bag('aws_opsworks_instance')

package 'httpd' do
  action :install
end

directory '/var/www/html' do
  recursive true
end

template '/etc/httpd/conf.d/hello.conf' do
  source 'hello.conf.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  notifies :restart, 'service[httpd]'
end

template '/var/www/html/hello.html' do
  source 'hello.html.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  variables({
              :name => config['name'],
              :instances => instances_data_bag
            })
end

service 'httpd' do
  action [:enable, :start]
end

Create update recipe.

$ cd cookbooks
$ chef generate recipe demo update
$ cd cookbooks/demo/recipes

Add the code below to update.rb. It is actually a section of web_server recipe. We could include update recipe in web_server recipe in order not to repeat the code. For the sake of this example, we are just copying it.

config = data_bag_item('demo_bag', 'config')
instances_data_bag = data_bag('aws_opsworks_instance')

template '/var/www/html/hello.html' do
  source 'hello.html.erb'
  mode '0644'
  owner node['demo']['user']
  group node['demo']['group']
  variables({
              :name => config['name'],
              :instances => instances_data_bag
            })
end

Edit hello.html.erb template so it will look like below.

<html>
  Hello AWS World! <br>
  Application: <%= @name %> (-> comes from data bag) <br>
  This is <%= node['hostname'] %> (-> comes from node attributes) <br>
  <b>All Instances:</b> (-> comes from OpsWorks data bag)<br>
  <% @instances.each do |instance| %>
  <%= "#{instance} <br>" %>
  <% end %>
</html>

Package and upload the cookbook to S3.

$ berks package demo-cookbook.tar.gz; aws s3 cp demo-cookbook.tar.gz s3://oz-opsworks/demo-cookbook.tar.gz

Add Instances

Click Instances from the left navigation, click Add Instance and choose size and subnet for the instance. You can use the proposed name or enter a new one.

Click + Instance to add another one. Instances sizes can be different if you like.

Once you see two instances there, click Start All Instances.

After several minutes, both instances will come online. It will take longer than a plain EC2 instance boot as OpsWorks will run Chef recipes on these instances. The more you do with the Chef recipes, the longer it will take for instances to boot up.

Check the Web Site

Click Layer from the left navigation bar. As seen in the screenshot below, you will find the public DNS record for the ELB. Click the link.

Check ELB public URL on the browser. Your http request will go through ELB and hit one of the web servers OpsWorks provisioned using the demo cookbook.

First request hits Web2

When I reload the page, this time request goes to Web1

If we add another instance, for example Web3, OpsWorks would automatically run configure lifecycle event on the existing instances. This event would call update recipe, which would then update hello.html template and include all the instances in the layer using aws_opsworks_instance data bag. This mechanism helps creating dynamic recipes that can take environment changes into account.

Going a step further, you can check the state of the instance and print it in the hello.html only if it is in running state. The recipe right now, just checks the instances in the layer and prints them even if they are not running.

Summary

In this workshop, we created a cookbook that uses attributes, data bags and a community cookbook as we probably do most of the time in a chef environment while developing cookbooks. While we could use this cookbook without any modification, to get most out of OpsWorks we used OpsWorks data bag named aws_opsworks_instance to find out the instances in the stack. Using lifecycle events, instances in the environment automatically changed their configuration (hello.html) when an instance is added to the layer.

comments powered by Disqus