CloudFormationでfluentdサーバを一発構築する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。

ハイ! AWSチームなのにSpring Frameworkネタばっか書いてんじゃねえよ、ということで、都元の愛するCloudFromationを使ってfluentdサーバを立ててみました。

CloudFormationの流儀

CloudFormationのテンプレートは汎用的に越した事はありません。その前提で。

EC2において、「何かをインストールしたサーバ」をCloudFormationで構築したい場合、ぱっと思いつくのは「構築済みのAMIを用意する」ことでしょうか。しかし、独自AMIのメンテナンスというのは意外と大変なので、出来れば「素のAmazon Linuxから、自動的にサーバが構築される」といった仕組みを用意したいものです。素のAmazon Linuxは公開されたAMIですので、アカウントに依存しないポータビリティが実現できる、というメリットも大きいです。

そんな望みに応えるCloudFormationの機能として、以前CloudFormationのヘルパースクリプトcfn-initによるインスタンスの初期化というものをご紹介しました。今回、この仕組みを使って、VPC環境内にfluentdサーバを立ててみましょう。

fluentdのテンプレート

テンプレート全体はgistに置いておきましたので、必要に応じて参照してください。ここでは重要な部分を抜粋して解説します。

ログの格納先S3バケット

fluentdに送られて来たログデータは、fluent-plugin-s3を使ってS3バケットに書き出すことにしましょう。その辺りの詳細は、Amazon LinuxにFluentdをインストールしてS3とMongoDB連携するというエントリで紹介されています。

CloudFormationで、まずログを出力するためのS3バケットを定義しましょう。あまりにも簡単な記述ですが、"DeletionPolicy" : "Retain"となっているのがキモです。というのも、CloudFormationは、スタックを削除する際に、基本的に関連リソースを全て削除してしまいます。つまり、ログが豊富に溜まったS3バケットも、デフォルトでは削除されてしまうのです。ログ用のバケットはそのようなことにならないように、スタックの撤収時であっても削除しない、というのがこの設定です。

    "LogBucket" : {
      "Type" : "AWS::S3::Bucket",
      "DeletionPolicy" : "Retain"
    },

2つのconfigSet

続いて、FluentdInstanceのcfn-init設定を見てください。configSetsという見慣れないプロパティがあります。

通常、AWS::CloudFormation::Initの直下には1つのconfigを配置し、その下にfiles, commands, packages, services*1の設定を記述していきます。

これらの設定は、通常packages, files, servicesの順で処理されます *2。しかし今回の場合、files/etc/yum.repos.d/td.repoファイルを配置してからpackagesによるtd-agentのインストールを行い、その後でまた、files/etc/td-agent/td-agent.confを書き換える、ということがしたいのです。

というわけで、今回はconfig1/etc/yum.repos.d/td.repoファイルを配置し、config2td-agentのインストールと設定等を行う、という二段構えのconfigをconfigSetとして用意しました。

      "Metadata" : {
        "AWS::CloudFormation::Init" : {
          "configSets" : {
            "default" : [ "config1" , "config2" ]
          },
          "config1" : {
            "files" : {
...
            }
          },
          "config2" : {
            "packages" : {
              "yum" : {
                "td-agent" : []
              }
            },
...
          }
        }
      }

td-agent.confの設定

td-agent.confでは、port 24224でTCPの口を開けておくと共に、自分自身の/var/log/messagesをsourceとして設定しておきます。

また、fluentdが受け付けたログデータは全て、S3に投げるようになっています。{ "Ref" : "LogBucket" }という記述によって、上で作成したS3バケット名を動的に設定するようになっています。

              "/etc/td-agent/td-agent.conf" : {
                "content" : { "Fn::Join" : ["", [
                  "<source>\n",
                  "  type forward\n",
                  "  port 24224\n",
                  "</source>\n",
                  "\n",
                  "<source>\n",
                  "  type config_expander\n",
                  "  <config>\n",
                  "    type tail\n",
                  "    format syslog\n",
                  "    path /var/log/messages\n",
                  "    tag ${hostname}/syslog.messages\n",
                  "  </config>\n",
                  "</source>\n",
                  "\n",
                  "<match *.**>\n",
                  "  type forest\n",
                  "  subtype s3\n",
                  "\n",
                  "  <template>\n",
                  "    s3_bucket ", { "Ref" : "LogBucket" },"\n",
                  "    s3_endpoint ", { "Fn::FindInMap": [ "AWSAPIEndpoint", { "Ref": "AWS::Region" }, "S3" ]}, "\n",
                  "\n",
                  "    path ${tag}/\n",
                  "    buffer_path /var/log/td-agent/buffer/${tag}\n",
                  "\n",
                  "    time_slice_format %Y/%m/%d/ec2-%Y-%m-%d-%H\n",
                  "    flush_interval 1m\n",
                  "  </template>\n",
                  "</match>\n"
                ]]},
                "mode"   : "000644",
                "owner"  : "root",
                "group"  : "root"
              }

また、s3_endpointにS3のエンドポイントホスト名が必要ですので、下記のようなMappingsを用意しておき、各リージョンに対応します。

  "Mappings": {
...
    "AWSAPIEndpoint": {
      "us-east-1":      { "S3": "s3.amazonaws.com" },
      "us-west-2":      { "S3": "s3-us-west-2.amazonaws.com" },
      "us-west-1":      { "S3": "s3-us-west-1.amazonaws.com" },
      "eu-west-1":      { "S3": "s3-eu-west-1.amazonaws.com" },
      "ap-southeast-1": { "S3": "s3-ap-southeast-1.amazonaws.com" },
      "ap-southeast-2": { "S3": "s3-ap-southeast-2.amazonaws.com" },
      "ap-northeast-1": { "S3": "s3-ap-northeast-1.amazonaws.com" },
      "sa-east-1":      { "S3": "s3-sa-east-1.amazonaws.com" }
    }, 
...
  }

td-agentのインストールとfluentdの設定が終わったところで、/var/log/messagesの権限設定と、各種fluent-pluginのインストールをしておきましょう。

            "commands" : {
              "0-chmod" : {
                "command" : "chgrp td-agent /var/log/messages && chmod g+r /var/log/messages"
              },
              "1-fluent-update" : {
                "command" : "/usr/lib64/fluent/ruby/bin/fluent-gem update"
              },
              "2-fluent-plugin-s3" : {
                "command" : "/usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-s3"
              },
              "3-fluent-plugin-forest" : {
                "command" : "/usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-forest"
              },
              "4-fluent-plugin-config-expander" : {
                "command" : "/usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-config-expander"
              }

最後に、td-agentを起動します。要するにchkconfig td-agent onservice td-agent startを行っているのが以下の記述です。

            "services" : {
              "sysvinit" : {
                "td-agent"  : { "enabled" : "true", "ensureRunning" : "true" }
              }
            }

動作確認

というわけで、このテンプレートでスタックを立ち上げると、自分自身の/var/log/messagesをS3バケットに上げ始めますので、しばらくすると下記のようにログがアップロードされて来ます。

2013-10-09_1818

ついでですので、/var/log/messagesだけではなく、fluent-catを使ってTCPのソースからもログも上げてみましょう。

$ echo '{"foo":"bar"}' | /usr/lib64/fluent/ruby/bin/fluent-cat debug.test

こちらも、しばらくすると下記のようにファイルがアップロードされて来ます。

2013-10-09_1838

2013-10-09T09:35:22+00:00	debug.test	{"foo":"bar"}

ちゃんと来てますね!

脚注

  1. http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-init.html
  2. 出典: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-init.html (commandsがどこに入るのか、記述が見つかりませんでしたが…)