CloudFormation + Chef-soloで環境構築する

はじめに

@smokeymonkeyです。AWS re:Invent 2013、盛り上がってますね!毎朝目が覚めてニュースをチェックするのがとても楽しいです。

さて、CloudFormationはAWSのプロビジョニングツールとして非常に強力ですが、複雑な処理を組み込むとすぐにTemplateが肥大化してしまい、またJSONで記述することからコメントが書けない(参考:JSONにはコメント行が付けられない?ネットで見つけた方法の有用性を試してみた | Developers.IO)という難点があります。

そのため、CloudFormationとChefPuppetなどのプロビジョニングツールを組み合わせて、AWSリソースの構成はCloudFormationから行い、EC2内部でのミドルウェアのインストールや設定はChefやPuppet等から行うことが多いと思います。そこで今回はCloudFormationとChef-soloを組み合わせて環境構築するTemplateを作ってみました。

Chefのレシピ

レシピはS3に配置する想定です。必要なのはレシピ自体のtar.gzファイルとJSONファイル。

tar.gzファイルは以下構成である必要があります。cookbooksというフォルダの中にレシピ名のフォルダ(例ではmyrecipe)があり、その中に各ファイルが配置されます。

レシピの構成

JSONファイルはこんな感じで、レシピをrun_listに渡しているだけです。

{
  "run_list": [ "recipe[myrecipe]" ] 
}

Template

Templateの中身は以下。長いのでgistにも張ってます

{
  "AWSTemplateFormatVersion" : "2010-09-09",
 
  "Parameters" : {
    "KeyName" : {
      "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instance",
      "Type" : "String"
    },
    "RecipeURL" : { 
      "Description" : "The location of the recipe tarball", 
      "Type": "String" 
    },
    "JsonURL" : { 
      "Description" : "The location of the node.json file", 
      "Type": "String" 
    },
    "EC2InstanceType": { 
      "Type" : "String",
      "Default" : "t1.micro",
      "AllowedValues" : ["t1.micro", "m1.small", "m1.large"],
      "Description" : "Enter t1.micro, m1.small, or m1.large. Default is t1.micro."
    }
  },
 
  "Mappings" : {
    "RegionMap" : {
      "us-east-1"      : { "AMI" : "ami-35792c5c" },
      "us-west-1"      : { "AMI" : "ami-687b4f2d" },
      "us-west-2"      : { "AMI" : "ami-d03ea1e0" },
      "eu-west-1"      : { "AMI" : "ami-149f7863" },
      "sa-east-1"      : { "AMI" : "ami-9f6ec982" },
      "ap-southeast-1" : { "AMI" : "ami-14f2b946" },
      "ap-southeast-2" : { "AMI" : "ami-a148d59b" },
      "ap-northeast-1" : { "AMI" : "ami-3561fe34" }
    }
  },
 
  "Resources" : {
    "PowerUserRole" : {
      "Type" : "AWS::IAM::Role",
      "Properties" : {
        "AssumeRolePolicyDocument" : {
          "Statement": [ {
            "Effect": "Allow",
              "Principal": {
                "Service": [ "ec2.amazonaws.com" ]
              },
              "Action": [ "sts:AssumeRole" ]
          } ]
        },
        "Path" : "/",
        "Policies" :[ {
          "PolicyName" : "PowerUserPolicy",
          "PolicyDocument" : {
            "Statement": [ {
              "Sid": "PowerUserStmt",
              "Effect": "Allow",
              "NotAction": "iam:*",
              "Resource": "*"
            }]
          }
        }]
      }
    },
    "PowerUserProfile" : {
      "Type" : "AWS::IAM::InstanceProfile",
      "Properties" : {
        "Roles" : [ { "Ref" : "PowerUserRole" } ]
      }
    },
    "EC2Servers" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable SSH access via port 22",
        "SecurityGroupIngress": [{
          "IpProtocol" : "tcp",
          "CidrIp" : "0.0.0.0/0",
          "FromPort" : "22",
          "ToPort" : "22"
        }]
      }
    },
    "Ec2Instance" : {
      "Type" : "AWS::EC2::Instance",
      "Metadata" : { 
        "AWS::CloudFormation::Init" : { 
          "configSets" : {
             "default" : [ "config1" , "config2", "config3" ]
          },
          "config1" : { 
            "packages" : {
              "yum" : {
                "gcc-c++"        : [],
                "ruby19"         : [],
                "ruby19-devel"   : [],
                "rubygems"       : []
              }
            }
          },
          "config2" : { 
            "commands" : {
              "alternatives" : {
                "command" : "alternatives --set ruby /usr/bin/ruby1.9"
              },
              "install-chef" : {
                "command" : "gem install --no-ri --no-rdoc chef"
              },
              "mkdir-chef" : {
                "command" : "mkdir /var/chef-solo"
              }
            }
          },
          "config3" : { 
            "files" : { 
              "/etc/chef/solo.rb" : { 
                "content" : { "Fn::Join" : ["", [ 
                  "log_level :info\n", 
                  "log_location \"/var/chef-solo/result.log\"\n", 
                  "file_cache_path \"/var/chef-solo\"\n", 
                  "cookbook_path \"/var/chef-solo/cookbooks\"\n", 
                  "json_attribs \"", { "Ref" : "JsonURL" }, "\"\n",
                  "recipe_url \"", { "Ref" : "RecipeURL" }, "\"\n" 
                ]] }, 
                "mode" : "000644", 
                "owner" : "root", 
                "group" : "root" 
              }
            }
          }
        } 
      }, 
      "Properties" : {
        "KeyName" : { "Ref" : "KeyName" },
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
        "InstanceType" : { "Ref": "EC2InstanceType" }, 
        "SecurityGroupIds" : [
          { "Ref" : "EC2Servers" }
        ],
        "IamInstanceProfile": { "Ref" : "PowerUserProfile" },
        "Tags": [
          { "Key" : "Name", "Value" : "host1" }
        ],
        "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
          "#! /bin/bash -v\n",
          "yum update -y\n",
          "function error_exit\n",
          "{\n",
          "  /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", { "Ref" : "WaitHandle" }, "'\n",
          "  exit 1\n",
          "}\n",
          "/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackId" }, " -r Ec2Instance ",
          "    --region ", { "Ref" : "AWS::Region" }, " || error_exit 'Failed to run cfn-init'\n",
          "/usr/local/bin/chef-solo\n",
          "/opt/aws/bin/cfn-signal -e $? '", { "Ref" : "WaitHandle" }, "'\n"
        ]]}}
      }
    },
    "WaitHandle" : { 
      "Type" : "AWS::CloudFormation::WaitConditionHandle" 
    }, 
    "WaitCondition" : { 
      "Type" : "AWS::CloudFormation::WaitCondition", 
      "DependsOn" : "Ec2Instance", 
      "Properties" : { 
        "Handle" : { "Ref" : "WaitHandle" }, 
        "Timeout" : "900" 
      }
    }
  },
 
  "Outputs" : {
    "InstanceId" : {
      "Description" : "InstanceId of the newly created EC2 instance",
      "Value" : { "Ref" : "Ec2Instance" }
    },
    "AZ" : {
      "Description" : "Availability Zone of the newly created EC2 instance",
      "Value" : { "Fn::GetAtt" : [ "Ec2Instance", "AvailabilityZone" ] }
    },
    "PublicIP" : {
      "Description" : "Public IP address of the newly created EC2 instance",
      "Value" : { "Fn::GetAtt" : [ "Ec2Instance", "PublicIp" ] }
    },
    "PrivateIP" : {
      "Description" : "Private IP address of the newly created EC2 instance",
      "Value" : { "Fn::GetAtt" : [ "Ec2Instance", "PrivateIp" ] }
    }
  }
}

Cloud-Initからcfn-initを叩き、以下のような動作が行われます。

  1. yumで各パッケージをインストール(config1)した後、
  2. alternativesでRubyを1.9に切り替えてgemでchefをインストール(config2)、
  3. その後Chef-soloで使うsolo.rbを作成(config3)し、
  4. chef-soloコマンドでrecipeを読み込んで実行。

Parameters

このTemplateをCloudFormationのStackで指定すると、以下のようなParameters画面が表示されます。

chef1

  • EC2InstanceType... EC2インスタンスのタイプを設定します。選択肢は"t1.micro", "m1.small", "m1.large"の3つで、Defaultではt1.microが入力されています。
  • RecipeURL ... 実行するChef recipe(tar.gzファイル)のURLを指定します。
  • JsonURL ... Chef-soloに渡すJSONファイルのURLを指定します。
  • KeyName ... 認証鍵名を指定します。

実行結果

Opscode Communityで公開されているbuild-essentialというRecipeを実行してみました。

/var/log/cfn-init.logにcfn-initの実行ログがあります。

2013-11-14 08:18:22,892 [INFO] Running configSets: default
2013-11-14 08:18:22,893 [INFO] Running configSet default
2013-11-14 08:18:22,939 [INFO] Running config config1
2013-11-14 08:18:49,925 [INFO] Yum installed [u'gcc-c++', u'ruby19', u'rubygems', u'ruby19-devel']
2013-11-14 08:18:49,946 [INFO] Running config config2
2013-11-14 08:18:49,960 [INFO] Command alternatives succeeded
2013-11-14 08:19:44,386 [INFO] Command install-chef succeeded
2013-11-14 08:19:44,413 [INFO] Command mkdir-chef succeeded
2013-11-14 08:19:44,422 [INFO] Running config config3
2013-11-14 08:19:44,427 [INFO] ConfigSets completed
2013-11-14 08:20:08,241 [INFO] Starting new HTTP connection (1): 169.254.XXX.XXX
2013-11-14 08:20:08,248 [INFO] Starting new HTTPS connection (1): cloudformation-waitcondition-us-west-2.s3-us-west-2.amazonaws.com

chef-soloの実行ログは/var/chef-solo/result.logにあります。

[2013-11-14T08:19:45+00:00] INFO: Forking chef instance to converge...
[2013-11-14T08:19:45+00:00] INFO: *** Chef 11.8.0 ***
[2013-11-14T08:19:45+00:00] INFO: Chef-client pid: 4493
[2013-11-14T08:19:47+00:00] INFO: Setting the run_list to ["recipe[build-essential]"] from JSON
[2013-11-14T08:19:47+00:00] INFO: Run List is [recipe[build-essential]]
[2013-11-14T08:19:47+00:00] INFO: Run List expands to [build-essential]
[2013-11-14T08:19:47+00:00] INFO: Starting Chef Run for ip-XXX-XXX-XXX-XXX.us-west-2.compute.internal
[2013-11-14T08:19:47+00:00] INFO: Running start handlers
[2013-11-14T08:19:47+00:00] INFO: Start handlers complete.
[2013-11-14T08:19:47+00:00] INFO: Processing package[autoconf] action install (build-essential::rhel line 38)
[2013-11-14T08:19:48+00:00] INFO: package[autoconf] installing autoconf-2.63-5.1.7.amzn1 from amzn-main repository
[2013-11-14T08:19:51+00:00] INFO: Processing package[bison] action install (build-essential::rhel line 38)
[2013-11-14T08:19:52+00:00] INFO: package[bison] installing bison-2.4.1-5.7.amzn1 from amzn-main repository
[2013-11-14T08:19:54+00:00] INFO: Processing package[flex] action install (build-essential::rhel line 38)
[2013-11-14T08:19:56+00:00] INFO: package[flex] installing flex-2.5.36-1.8.amzn1 from amzn-main repository
[2013-11-14T08:19:58+00:00] INFO: Processing package[gcc] action install (build-essential::rhel line 38)
[2013-11-14T08:19:59+00:00] INFO: Processing package[gcc-c++] action install (build-essential::rhel line 38)
[2013-11-14T08:19:59+00:00] INFO: Processing package[kernel-devel] action install (build-essential::rhel line 38)
[2013-11-14T08:19:59+00:00] INFO: package[kernel-devel] installing kernel-devel-3.4.68-59.97.amzn1 from amzn-updates repository
[2013-11-14T08:20:05+00:00] INFO: Processing package[make] action install (build-essential::rhel line 38)
[2013-11-14T08:20:07+00:00] INFO: Processing package[m4] action install (build-essential::rhel line 38)
[2013-11-14T08:20:07+00:00] INFO: Chef Run complete in 19.951180673 seconds
[2013-11-14T08:20:07+00:00] INFO: Running report handlers
[2013-11-14T08:20:07+00:00] INFO: Report handlers complete

ちゃんと実行されてますね!

まとめ

Parameterで使いたいレシピを指定するだけで簡単に必要な環境の構築が出来ると便利だなぁと思って作ってみました。CloudFormationもChefもとても便利なツールなので、それぞれの特性を活かして使っていきたいですね。