Chef to OpsWorks Workshop Part 1 - Hands on Chef

Objective

This workshop prepared to fill the Chef knowledge gap before moving into Amazon OpsWorks.

In the next ~2 hours, we will learn Chef concepts, create a local development environment and configure two identical web servers.

Chef Terminology

Chef is a powerful automation platform that transforms infrastructure into code. Whether you’re operating in the cloud, on-premises, or in a hybrid environment, Chef automates how infrastructure is configured, deployed, and managed across your network, no matter its size.

chef-client

A command-line tool that that runs Chef.

chef-zero

Chef Zero is a simple, easy-install, in-memory Chef server that can be useful for Chef Client testing and chef-solo-like tasks that require a full Chef Server.

Node

A node is any physical, virtual, or EC2 instance that is configured to be maintained by a chef-client.

Desired State

The state we want a node to be in.

Template

A cookbook template is an Embedded Ruby (ERB) template that is used to dynamically generate static text files. Templates may contain Ruby expressions and statements, and are a great way to manage configuration files.

Template Example

<html>
  Hello AWS World! <br>
  This is <%= node['hostname'] %><br>
</html>

Recipe

A recipe is a collection of resources that tells the chef-client how to configure a node.

Recipe Example

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

Cookbook

A cookbook is the fundamental unit of configuration and policy distribution. It contains everything required to configure nodes. (Recepies, attribute files, files, templates, custom resources and libraries)

Cookbook Example

Layout for demo cookbook.

demo/
├── attributes
│   └── default.rb
├── Berksfile
├── Berksfile.lock
├── chefignore
├── data_bags
│   └── demo_bag
│       └── config.json
├── metadata.rb
├── README.md
├── recipes
│   ├── default.rb
│   ├── init.rb
│   ├── service_user.rb
│   ├── update.rb
│   └── web_server.rb
├── spec
│   ├── spec_helper.rb
│   └── unit
│       └── recipes
│           ├── configure_spec.rb
│           ├── default_spec.rb
│           ├── init_spec.rb
│           ├── service_user_spec.rb
│           ├── update_spec.rb
│           └── web_server_spec.rb
├── templates
│   ├── default
│   ├── hello.conf.erb
│   ├── hello.html.erb
│   └── test.txt.erb
├── test
│   └── recipes
│       ├── configure.rb
│       ├── default_test.rb
│       ├── init.rb
│       ├── service_user.rb
│       ├── update.rb
│       └── web_server.rb
└── upload.sh (custom script to upload cookbook to S3 bucket)

Community Cookbooks

These are publicly available cookbooks hosted on Chef Supermarket.

Data bag

A data_bag is a global variable that is stored as JSON data and is accessible from a Chef server.

Data bag Example

{
    "name": "demo application"
}

Resource

A resource is a statement of configuration policy that describes the desired state of an piece within your infrastructure, along with the steps needed to bring that item to the desired state.

Resource Examples

directory resource

directory '/tmp/folder' do
  owner 'root'
  group 'root'
  mode '0755'
  action :create
end

package service

service "tomcat" do
  action :start
end

Knife

A command-line tool that provides an interface between a local chef-repo and the Chef server.

Knife Example

Using knife to find a community cookbook.

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

Role

A role is a way to group nodes for a single function.

Run-list

A run-list defines all of the configuration settings that are necessary for a node that is under management by Chef to be put into the desired state and the order in which these configuration settings are applied.

Run-list Example

$ knife node run_list set test-node 'recipe[iptables]'

Kitchen

Kitchen is an integration framework that is used to automatically test cookbook data across any combination of platforms and test suites. Kitchen is packaged in the Chef development kit. We will use kitchen to provision EC2 instances and run Chef recipes on them.

Chef DK

A collection of tools to help development of Chef and Chef resources, distributed as a single installable package.

Create Required AWS Resources

Before you move forward be sure to have an IAM user that can create resources on AWS. With this user you can login to AWS Console or use aws-cli in order to create the required resources. We will also use this IAM user on chef development instance.

For OS specific installation instructions check out AWS CLI Installation document.

  • We will create two EC2 Key Pairs
    • We will name first key EC2 Key Pair as chef-dev01-key.
    • Second key is named chef-user which is going to be used by Chef to login to EC2 instances it manages
  • We will create a security group to allow ingress access for ports 80 and 22
  • Finally we will be creating an EC2 instance named chef-dev01 as our Chef development machine.

Create EC2 Key Pairs

Chef will need an EC2 key pair in order to login to EC2 instances that it will provison.

Switch to the folder where you keep your key pairs and create an EC2 Key pair named chef-dev01-key.

$ cd ~/Workdocs/keys # this is the folder where I keep key pairs
$ aws ec2 create-key-pair --key-name chef-dev01-key --query 'KeyMaterial' --output text --region us-east-1 > chef-dev01-key.pem
$ aws ec2 create-key-pair --key-name chef-user --query 'KeyMaterial' --output text --region us-east-1 > chef-user.pem

Now we have two .pem files.

$ ls -1 chef*.pem
chef-dev01-key.pem  # we use this to login to `chef-dev01`
chef-user.pem       # `chef-dev01` will use this to login to other EC2 instances

Change permission on these two .pem files.

$ chmod 600 chef-*

Create Security Group

To create a security group, we need to pick the VPC.

$ aws ec2 describe-vpcs --region us-east-1
{
    "Vpcs": [
        {
            "VpcId": "vpc-6fe5000b",
            "InstanceTenancy": "default",
            "Tags": [
                {
                    "Value": "Default",
                    "Key": "Name"
                }
            ],
            "State": "available",
            "DhcpOptionsId": "dopt-928173f7",
            "CidrBlock": "172.31.0.0/16",
            "IsDefault": true
        }
    ]
}

I chose vpc-6fe5000b which is the default VPC. You can choose any VPC.

Now, we can create the security group.

$ aws ec2 create-security-group --group-name chef-workshop-sg --description "Chef workshop security group" --vpc-id vpc-6fe5000b --region us-east-1
{
    "GroupId": "sg-8604b7fa"
}

Note the GroupId value as we are using it in the following commands to add ingress rules to the security group.

$ aws ec2 authorize-security-group-ingress --group-id sg-8604b7fa --protocol tcp --port 80 --cidr 0.0.0.0/0 --region us-east-1
$ aws ec2 authorize-security-group-ingress --group-id sg-8604b7fa --protocol tcp --port 22 --cidr 0.0.0.0/0 --region us-east-1

Let’s describe the security group to check the rules we’ve added.

$ aws ec2 describe-security-groups --group-names chef-workshop-sg --region us-east-1
{
    "SecurityGroups": [
        {
            # ...
            "Description": "Chef workshop security group",
            "IpPermissions": [
                {
                    "PrefixListIds": [],
                    "FromPort": 80,
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "ToPort": 80,
                    "IpProtocol": "tcp",
                    "UserIdGroupPairs": [],
                    "Ipv6Ranges": []
                },
                {
                    "PrefixListIds": [],
                    "FromPort": 22,
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "ToPort": 22,
                    "IpProtocol": "tcp",
                    "UserIdGroupPairs": [],
                    "Ipv6Ranges": []
                }
            ],
        "GroupName": "chef-workshop-sg",
            "VpcId": "vpc-6fe5000b",
            "OwnerId": "676452272092",
            "GroupId": "sg-8604b7fa"
        }
    ]
}

Create Chef Development Instance

List the subnet IDs available in your vpc (vpc-6fe5000b is the one I chose earlier).

$ aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-6fe5000b" --region us-east-1
{
    "Subnets": [
        {
            "VpcId": "vpc-6fe5000b",
            "AvailableIpAddressCount": 4079,
            "MapPublicIpOnLaunch": true,
            "DefaultForAz": true,
            "Ipv6CidrBlockAssociationSet": [],
            "State": "available",
            "AvailabilityZone": "us-east-1c",
            "SubnetId": "subnet-da8afdf1",
            "CidrBlock": "172.31.48.0/20",
            "AssignIpv6AddressOnCreation": false
        },
        {
            "VpcId": "vpc-6fe5000b",
            "AvailableIpAddressCount": 4075,
            "MapPublicIpOnLaunch": true,
            "DefaultForAz": true,
            "Ipv6CidrBlockAssociationSet": [],
            "State": "available",
            "AvailabilityZone": "us-east-1a",
            "SubnetId": "subnet-897032d0",
            "CidrBlock": "172.31.16.0/20",
            "AssignIpv6AddressOnCreation": false
        },
        ....
        ....

I chose subnet-897032d0 from the list.

I am creating chef-dev01 instance in subnet subnet-897032d0 and using security group sg-8604b7fa.

$ aws ec2 run-instances --image-id ami-6869aa05 --count 1 --instance-type t2.micro --key-name chef-dev01-key --security-group-ids sg-8604b7fa --subnet-id subnet-897032d0 --region us-east-1 --query 'Instances[0].{ID:InstanceId}' --output text --region us-east-1
i-0567786b739c97874

Add a name tag to the instance.

$ aws ec2 create-tags --resources i-0567786b739c97874 --tags Key=Name,Value=chef-dev01

Find public dns name for chef-dev01

$ aws ec2 describe-instances --instance-ids i-0567786b739c97874 --query 'Reservations[0].Instances[0].PublicDnsName' --output text --region us-east-1
ec2-52-90-80-237.compute-1.amazonaws.com

Copy chef-user.pem from your computer to Chef development instance. You can use scp or just copy paste the chef-user.pem file.

# on your laptop
$ scp -i ~/WorkDocs/keys/chef-dev01-key.pem chef-user.pem ec2-user@ec2-52-90-80-237.compute-1.amazonaws.com:~/.ssh/
chef-user.pem

Login to the chef-dev01 instance using public DNS name.

$ ssh ec2-user@ec2-52-90-80-237.compute-1.amazonaws.com -i ~/WorkDocs/keys/chef-dev01-key.pem

    __|  __|_  )
    _|  (     /   Amazon Linux AMI
    ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2016.03-release-notes/
21 package(s) needed for security, out of 81 available
Run "sudo yum update" to apply all updates.
Amazon Linux version 2016.09 is available.

Do the updates and configure ssh timeout.

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

Change permissions of chef-user.pem file.

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

Finally create ~/.aws/credentials file and put credentials for an IAM user that can create EC2 instance on chef instance.

[default]
aws_access_key_id = AKIAJGBFBGOJJJJMKYXQ
aws_secret_access_key = EnlAfEjw4S4JWfX9ABCAAAAAAAAAABC

Configure Chef Development Instance

Install ChefDK

You can use screen to keep ssh session.

$ # optional
$ # create a screen session named CHEF
$ screen -S CHEF
$ # screen -dr CHEF - to resume the session

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 workshop cookbook

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

Configure Kitchen

Modify ~/chef/cookbooks/workshop/.kitchen.yml to look like below.

Don’t forget to update security_group_ids with your own security group value. You may change region and AZ if you were creating resources in a different region.

---
driver:
  name: ec2
  aws_ssh_key_id: chef-user
  region: us-east-1
  availability_zone: us-east-1c
  require_chef_omnibus: true
  instance_type: t2.small
  security_group_ids: ["sg-8604b7fa"]

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

provisioner:
  name: chef_zero

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

suites:
  - name: Web1
    run_list:
      - recipe[workshop::default]
  - name: Web2
    run_list:
      - recipe[workshop::default]

We will modify this file later on.

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 ~/chef/cookbooks
$ chef generate template workshop test.txt
Recipe: code_generator::template
* directory[./workshop/templates/default] action create
    - create new directory ./workshop/templates/default
* template[./workshop/templates/test.txt.erb] action create
    - create new file ./workshop/templates/test.txt.erb
    - update content in file ./workshop/templates/test.txt.erb from none to e3b0c4
    (diff output suppressed by config)

Paste the text below into ~/chef/cookbooks/workshop/templates/test.txt.erb

This is a test file created by Chef.
# end with new line

Create a recipe

$ cd ~/chef/cookbooks
$ chef generate recipe workshop init
$ cd ~/chef/cookbooks/workshop/recipes

Modify ~/chef/cookbooks/workshop/recipes/init.rb to look like below.

#
# Cookbook Name:: workshop
# 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 ~/chef/cookbooks/workshop/recipes/default.rb to look like below.

include_recipe 'workshop::init'

Create the instance using kitchen.

$ # switch to the folder where we have `.kitchen.yml` file.
$ cd ~/chef/cookbooks/workshop
$ kitchen converge Web1-amazon
-----> Starting Kitchen (v1.10.2)
-----> Creating <Web1-amazon>...
    If you are not using an account that qualifies under the AWS
free-tier, you may be charged to run these suites. The charge
should be minimal, but neither Test Kitchen nor its maintainers
are responsible for your incurred costs.

    Instance <i-0c8f2de1607aac250> requested.
    Polling AWS for existence, attempt 0...
    Attempting to tag the instance, 0 retries
    EC2 instance <i-0c8f2de1607aac250> created.
    Waited 0/300s for instance <i-0c8f2de1607aac250> to become ready.
    EC2 instance <i-0c8f2de1607aac250> ready.
    Waiting for SSH service on ec2-54-210-168-160.compute-1.amazonaws.com:22, retrying in 3 seconds
    [SSH] Established
    Finished creating <Web1-amazon> (0m46.58s).
...
...
Transferring files to <Web1-amazon>
Starting Chef Client, version 12.18.31
Creating a new client identity for Web1-amazon using the validator key.
resolving cookbooks for run list: ["workshop::default"]
Synchronizing Cookbooks:
    - workshop (0.1.0)
Installing Cookbook Gems:
Compiling Cookbooks...
Converging 0 resources

Running handlers:
Running handlers complete
Chef Client finished, 0/0 resources updated in 01 seconds
Finished converging <Web1-amazon> (0m10.93s).

If you see an error message as seen below, just run the converge command again.

>>>>>> ------Exception-------
>>>>>> Class: Kitchen::ActionFailed
>>>>>> Message: 1 actions failed.
>>>>>>     Failed to complete #converge action: [SCP did not finish successfully (): ] on Web1-amazon
>>>>>> ----------------------
>>>>>> Please see .kitchen/logs/kitchen.log for more details
>>>>>> Also try running `kitchen diagnose --all` for configuration

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.
$ exit

Configure Web Servers

Create a User for Web Service

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

$ cd ~/chef/cookbooks
$ chef generate recipe workshop service_user

As all recipes for workshop cookbook, this recipe is created under ~/chef/cookbooks/workshop/recipes/ folder.

Add the lines below to ~/chef/cookbooks/workshop/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. Under cookbooks folder, run this command.

$ cd ~/chef/cookbooks
$ chef generate attribute workshop default

Edit ~/chef/cookbooks/workshop/attributes/default.rb to have the code below.

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

Modify service_user recipe to use custom attributes. ~/chef/cookbooks/workshop/recipes/service_user.rb should looks like below. As you will notice we changed static values with attributes.

group node['workshop']['group']

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

Include service_user recipe in the default recipe. Edit ~/chef/cookbooks/workshop/recipes/default.rb to look like below.

include_recipe 'workshop::init'
include_recipe 'workshop::service_user'

Run the cookbook again

$ cd ~/chef/cookbooks/workshop
$ kitchen converge Web1-amazon
$ kitchen converge Web1-amazon
-----> Starting Kitchen (v1.10.2)
-----> Converging <Web1-amazon>...
    Preparing files for transfer
    Preparing dna.json
    Resolving cookbook dependencies with Berkshelf 4.3.5...
    Removing non-cookbook files before transfer
    Preparing data_bags
    Preparing validation.pem
    Preparing client.rb
-----> Chef Omnibus installation detected (install only if missing)
    Transferring files to <Web1-amazon>
    Starting Chef Client, version 12.18.31
    resolving cookbooks for run list: ["workshop::default"]
    Synchronizing Cookbooks:
        - workshop (0.1.0)
    Installing Cookbook Gems:
    Compiling Cookbooks...
    Converging 3 resources
    Recipe: workshop::init
        * template[/tmp/test.txt] action create (up to date)
    Recipe: workshop::service_user
        * group[service_admin] action create
        - create group service_admin
        * linux_user[service_admin] action create
        - create user service_admin

    Running handlers:
    Running handlers complete
    Chef Client finished, 2/3 resources updated in 01 seconds
    Finished converging <Web1-amazon> (0m3.22s).

This was a quicker run as chef didn’t provision the instance again but just configured what service_user recipe had.

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:
$ exit

Configure SELinux with a Community Cookbook

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

Let’s check what selinux cookbook looks like.

Go to Chef Supermarket and check for the recipes that selinux cookbook provides.

To use a community cookbook, we have to include it in the metadata file which is located at ~/chef/cookbooks/workshop/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 workshop 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 ~/chef/cookbooks/workshop/recipes/default.rb.

include_recipe 'selinux::permissive'

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

$ kitchen converge Web1-amazon --debug
-----> Starting Kitchen (v1.10.2)
-----> Converging <Web1-amazon>...
    Preparing files for transfer
    Preparing dna.json
    Resolving cookbook dependencies with Berkshelf 4.3.5...
    Removing non-cookbook files before transfer
    Preparing data_bags
    Preparing validation.pem
    Preparing client.rb
-----> Chef Omnibus installation detected (install only if missing)
    Transferring files to <Web1-amazon>
    Starting Chef Client, version 12.18.31
    resolving cookbooks for run list: ["workshop::default"]
    Synchronizing Cookbooks:
        - workshop (0.1.0)
        - selinux (0.9.0)
    Installing Cookbook Gems:
    Compiling Cookbooks...
    Converging 6 resources
    Recipe: workshop::init
        * template[/tmp/test.txt] action create (up to date)
    Recipe: workshop::service_user
        * group[service_admin] action create (up to date)
        * linux_user[service_admin] action create (up to date)
    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)

    Running handlers:
    Running handlers complete
    Chef Client finished, 0/6 resources updated in 02 seconds
    Finished converging <Web1-amazon> (0m4.51s).
-----> Kitchen is finished. (0m4.87s)

Check if selinux is disabled

$ kitchen login Web1-amazon
$ sestatus
SELinux status:                 disabled

Configure Apache

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

$ cd ~/chef/cookbooks
$ chef generate recipe workshop web_server

Modify web_server recipe to look like below. File located at ~/chef/cookbooks/workshop/recipes/web_server.rb.

#
# Cookbook Name:: workshop
# Recipe:: web_server
# Copyright (c) 2017 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['workshop']['user']
  group node['workshop']['group']
  notifies :restart, 'service[httpd]'
end

template '/var/www/html/hello.html' do
  source 'hello.html.erb'
  mode '0644'
  owner node['workshop']['user']
  group node['workshop']['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.

$ cd ~/chef/cookbooks
$ chef generate template workshop hello.conf

Put the code below into hello.conf.erb. File is located at ~/chef/cookbooks/workshop/templates/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 might have noticed, we changed the DirectoryIndex to hello.html from default index.html.

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

$ cd ~/chef/cookbooks
$ chef generate template workshop hello.html

Put the code below into hello.html.erb. File is located at ~/chef/cookbooks/workshop/templates/hello.html.erb.

<html>
    Hello AWS World!
</html>

Finally add the web_server recipe to default recipe. ~/chef/cookbooks/workshop/recipes/default.rb must look like below.

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

Run the cookbook

$ cd ~/chef/cookbooks/workshop
$ kitchen converge Web1-amazon
...
...
* template[/etc/httpd/conf.d/hello.conf] action create
        - create new file /etc/httpd/conf.d/hello.conf
        - update content in file /etc/httpd/conf.d/hello.conf from none to 5637db
        --- /etc/httpd/conf.d/hello.conf     2017-01-27 16:36:06.399912841 +0000
        +++ /etc/httpd/conf.d/.chef-hello20170127-25450-19gt7tn.conf 2017-01-27 16:36:06.399912841 +0000
        @@ -1 +1,30 @@
        +<VirtualHost *:80>
        +    ServerAdmin webmaster@localhost
        +
        +    DocumentRoot /var/www/html
        +
        +    DirectoryIndex hello.html
        +
        +    <Directory />
        +        Options FollowSymLinks
    ...
    ...
       +    <html>
       +        Hello AWS World!
       +    </html>
       - change mode from '' to '0644'
       - change owner from '' to 'service_admin'
       - change group from '' to 'service_admin'
     * service[httpd] action enable
       - enable service service[httpd]
     * service[httpd] action start
       - start service service[httpd]
     * service[httpd] action restart
       - restart service service[httpd]

   Running handlers:
   Running handlers complete
   Chef Client finished, 6/13 resources updated in 05 seconds
   Finished converging <Web1-amazon> (0m7.95s).

Find the public IP address of the instance and try on a browser.

Use Data Bag and Node Attributes 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.

$ cd ~/chef/cookbooks/workshop/
$ mkdir -p data_bags/workshop_bag
$ touch data_bags/workshop_bag/config.json

Put the code below into conifg.json

{
    "name": "workshop application"
}

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

It looks like this

  • workshop_bag
    • config
      • name -> workshop application

Let’s modify web_server recipe to use the data bag. Recipe is located at ~/chef/cookbooks/workshop/recipes/web_server.rb.

config = data_bag_item('workshop_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['workshop']['user']
  group node['workshop']['group']
  notifies :restart, 'service[httpd]'
end

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

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

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

Modify ~/chef/cookbooks/workshop/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. File is located at ~/chef/cookbooks/workshop/.kitchen.yml.

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

Provision Apache on Both EC2 Instances

Let’s check instance status

$ cd ~/chef/cookbooks/workshop/
$ kitchen list
Instance     Driver  Provisioner  Verifier  Transport  Last Action
Web1-amazon  Ec2     ChefZero     Busser    Ssh        Converged
Web2-amazon  Ec2     ChefZero     Busser    Ssh        <Not Created>

Run the command below.

$ kitchen converge Web1
...
...
    * template[/var/www/html/hello.html] action create
    - update content in file /var/www/html/hello.html from 6bbe7b to 800c69
    --- /var/www/html/hello.html 2017-01-27 16:36:06.435912564 +0000
    +++ /var/www/html/.chef-hello20170127-26434-2bu8bn.html      2017-01-27 16:56:16.182581442 +0000
    @@ -1,4 +1,6 @@
    -    <html>
    -        Hello AWS World!
    -    </html>
    +<html>
    +  Hello AWS World! <br>
    +  Application: workshop application (-> comes from data bag) <br>
    +  This is ip-172-31-62-107 (-> comes from node attributes) <br>
    +</html>
    * service[httpd] action enable (up to date)
    * service[httpd] action start (up to date)

Running handlers:
Running handlers complete
Chef Client finished, 1/12 resources updated in 02 seconds
Finished converging <Web1-amazon> (0m4.70s).

This will update Web1-amazon instance using data bag and node attributes.

If you look at the web page again you will see instance’s IP address.

Now lets do all for both instance. This command will take a little longer as it will provision second instance for the first time.

$ cd ~/chef/cookbooks/workshop/
$ kitchen converge

We can see both instances as Converged

$ cd ~/chef/cookbooks/workshop/
$ kitchen list
Instance     Driver  Provisioner  Verifier  Transport  Last Action
Web1-amazon  Ec2     ChefZero     Busser    Ssh        Converged
Web2-amazon  Ec2     ChefZero     Busser    Ssh        Converged

Finally we have two instances exactly at the same desired state. Let’s check second instance.

Further reading

Data Bags vs Node Attributes

  • If it is global across all of your infrastructure, and you think you might need to change that item en-masse at some point. Examples: An external service API key which does not vary per environment; an office gateway’s external IP address; a license key.
  • If it needs to be encrypted. Data bag items can not only be encrypted, but each item can have a different encryption key if desired. Encrypted data bag items give you the ability to secure sensitive information on the Chef server, so that no intruder could reveal your secrets even if they gained access to the Chef server. Also, no man-in-the-middle attack could reveal sensitive information by sniffing the traffic between the Chef client and the server; ciphertext is decoded only on the client. (This is less of a concern given that client-server communication is performed over SSL).
  • If it needs to be written to by another system and we want to isolate the scope of the data that system can write to. Example: application release information which could eventually be written by a continuous integration pipeline.
  • If an external team needs to update limited pieces of information and that team does not normally write Chef recipes. Example: the DBA that needs to occasionally modify a database password or connection string.

source: Data bags vs Node attributes

comments powered by Disqus