読者です 読者をやめる 読者になる 読者になる

Prometheusのec2 service discoveryを試す

Prometheusには ec2 service discovery機能があり、EC2インスタンスに監視用のagent(exporter) をインストールして、特定のタグを設定するだけで監視・モニタリングの対象とすることができます。

通常のノードの追加

設定方法は以下のように、対象となるノードの情報をPrometheusの設定ファイルに書き、設定を反映されるためPrometheusを再起動します。
これの作業はノードを追加するたびに発生します。

  - job_name: 'node'
    scrape_interval: 30s
    scrape_timeout:  15s
    static_configs:
      - targets: ['172.0.0.3:9100', '172.0.0.3:9256']
        labels:
          name: 'server1'
          stage: 'prod'

ec2 service discoveryを利用したノードの追加方法

ec2 service discoveryを利用するには設定ファイルを以下のように書きます。
ec2 service discoveryを利用することで、この例ではインスタンスのタグ(Stage)に、prodかstgを値と設定すると自動的に監視・モニタリング対象として自動的に検出します。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

job_name はPrometheusでよく設定される監視対象をグルーピングするラベルです。
ec2_sd_configsにある設定項目は Configuration | Prometheus で確認できます。
ここでは最低限のAWSの設定と、Node exporter がリクエストを受け付ける port番号の設定のみをしています。

relabel_configsは以下のようになっています。

1. 最初の source_labels

監視・モニタリング対象とするかどうかの判断をします。この例ではEC2インスタンスのTag(Stage) の値が、stgかprodの場合は監視・モニタリング対象になります。

2. 2番目と3番目の source_labels

EC2インスタンスのTagの値をPrometheusのラベルに設定しています。
ここでは、EC2のタグのNameの値をPrometheusのラベルのnameに設定し、同様にStageの値をstageに設定しています。
このようにPrometheusのラベルに値を設定しておかないと、Prometheus内(Alertmanager含む)で値を利用できないためこのように設定します。

relabel_configsの設定は EC2 Discovery Relabelling - Robust Perception が参考になりました。

複数の exporter を利用する場合

監視に利用する exporter が複数の場合は、port を複数設定する必要が出てきますが、ec2_sd_configs では port は int値のみ指定可能で複数のportをまとめて書くことはできません。
そのため Node exporter と Process exporter を利用するような場合には、以下のように ec2_sd_configs を書く2つ書くことになります。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

  - job_name: 'process'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9256
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

Swift3でファイルを読み込む

swiftファイルと同じ場所にある、sample.txtを読み込むコードは以下のようになる。

        let path = Bundle.main.path(forResource: "sample", ofType: "text")!
        if let data = NSData(contentsOfFile: path){
            print(String(NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)!))
        }else{
            print("データなし")
        }

Capistrano3のコードを読んでみる1 ~設定ファイルのパス設定~

Capistranoを実行時に利用するconfig/deploy.rbとかproduction.rb、staging.rbのパスを変更できるのか気になったのでソースコードを読んでみました。
結論としては、変更可能ですがcapistrano/setupをCapfileでrequireする前にsetする必要があります。
また、Capfileファイルのパスも変えられるかが気になって見てみましたが、capfile、capfile.rb、Capfile.rbにファイル名の変更は可能という結論になりました。

/lib/capistrano/application.rb

色々省略してますが、初期化処理としてcapfileの読み込みを行います。

module Capistrano
  class Application < Rake::Application
    def initialize
      super
      @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile
    end

    private

    # allows the `cap install` task to load without a capfile
    def capfile
      File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile'))
    end
  end
end

/lib/capistrano/setup.rb

load 'capistrano/defaults.rb'では、/lib/capistrano/defaults.rbで、setしなかった場合のデフォルト値が定義されています。
load:defaultsをImmutableTask(/lib/capistrano/immutable_task.rb)として定義しているので、CapistranoのタスクでもOverride出来ません。
stage_config_pathやdeploy_config_pathは/lib/capistrano/dsl/paths.rbで定義されています。
configure_backend は/lib/capistrano/configuration.rbで定義されていて、sshkitの設定を行います。

namespace :load do
  task :defaults do
    load 'capistrano/defaults.rb'
  end
end

stages.each do |stage|
  Rake::Task.define_task(stage) do
    set(:stage, stage.to_sym)

    invoke 'load:defaults'
    Rake.application["load:defaults"].extend(Capistrano::ImmutableTask)
    load deploy_config_path
    load stage_config_path.join("#{stage}.rb")
    load "capistrano/#{fetch(:scm)}.rb"
    I18n.locale = fetch(:locale, :en)
    configure_backend
  end
end

require 'capistrano/dotfile'

setup.rbの最後でrequireしているdotfile.rbの内容は以下のとおりです。

dotfile = Pathname.new(File.join(Dir.home, '.capfile'))
load dotfile if dotfile.file?

/lib/capistrano/install.rb

ファイルの中身は、以下のようになっています。

load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake'))

/lib/capistrano/task/install.rakeを実行しています。
install.rakeはcap install の際に実行される処理が定義されています。

/lib/capistrano/deploy.rb

require 'capistrano/framework'

load File.expand_path("../tasks/deploy.rake", __FILE__)

/lib/capistrano/framewok.rb

以下のように、framework.rakeを呼び出しています。

load File.expand_path("../tasks/framework.rake", __FILE__)
require 'capistrano/install'

task/deploy.rake

task/framework.rakeでは、Capistranoが提供するコマンド(?)のインターフェースが定義されています。

namespace :deploy do

  desc 'Start a deployment, make sure server(s) ready.'
  task :starting do
  end

  desc 'Started'
  task :started do
  end
 
  #以下、省略

また、:rollback、:deployの処理はここで定義されています。

  #以下、省略

  desc 'Rollback to previous release.'
  task :rollback do
    %w{ starting started
        reverting reverted
        publishing published
        finishing_rollback finished }.each do |task|
      invoke "deploy:#{task}"
    end
  end
end

desc 'Deploy a new release.'
task :deploy do
  set(:deploying, true)
  %w{ starting started
      updating updated
      publishing published
      finishing finished }.each do |task|
    invoke "deploy:#{task}"
  end
end
task default: :deploy

task/framework.rake

task/deploy.rake はtask/framework.rakeで定義してあったインターフェースの実装を担当しています。(全てではないですが)
また、実際の処理はinvokeで処理を委譲しています。

namespace :deploy do

  task :starting do
    invoke 'deploy:check'
    invoke 'deploy:set_previous_revision'
  end

  #以下、省略

Nginxのモジュールの作り方 C言語でHandlers編

概要

モジュールの種類

Nginxで開発できるmoduleの種類は、ざっくりとわけると以下の3種類があります。

  • Handlers・・・locationを指定して処理が可能
  • Load balancers・・・upstream moduleやload balancer moduleと連携して使う。load balancer moduleにはRound RobinとIP Hashがbuild-inされているけど、それ以外の分散方法として使うことが出来る
  • Filters・・・Handlerのレスポンスに対して処理を行う。例えばgzip filterはresponseをgzip圧縮する。

今回対象とするのはHandlersをC言語で実装することにします。

必要となるファイル

特定パスにアクセスしたら『hello world』と出力するようなモジュールを作ってみます。
stub_status moduleのイケてない版のようのモジュールです。
nginxのモジュールを実装するには、最低限2つのファイルが必要になります。

  • config
  • ngx_http_hello_module.c

configファイルにはモジュールの概要を書き、モジュール実態はngx_http_hello_module.cになります。

動作イメージ

configファイルのserverディテクティブの中に以下のように記載すると、http://localhost/hello/ にアクセスするとHello WorldをHTTP Responseとして返す。

location /hello/ {
   hello 'Hello World';
} 

実装

config

configファイルにはモジュールの概要や説明などを書きます。

ngx_addon_name=ngx_http_hello_module
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_module.c"

ngx_addon_nameにはモジュールの名前を書きます。
HTTP_MODULESには依存するモジュールの名前を書きます。
NGIX_ADDON_SRCSにはモジュールのパスを書きます。

HTTP_MODULESの他にどんなModuleがあるかは、auto/modulesのスクリプトを見るとわかります。
1.9.2では以下のように書かれています。

459     modules="$modules $HTTP_MODULES $HTTP_FILTER_MODULES \
460              $HTTP_HEADERS_FILTER_MODULE \
461              $HTTP_AUX_FILTER_MODULES \
462              $HTTP_COPY_FILTER_MODULE \
463              $HTTP_RANGE_BODY_FILTER_MODULE \
464              $HTTP_NOT_MODIFIED_FILTER_MODULE"

module.c

コンフィグファイルの定義

NginxのConfigファイルのどのディテクティブ(main, server, location)で、このモジュールの設定が定義出来るか決めます。
ngx_http<モジュール名>(main|srv|loc)_conf_t という構造体で、ディレクティブの設定値を保存する構造体を定義します。

その他のngx_http_core_loc_conf_tなどの定義は、src/core/ngx_config.h にある。
ngx_str_t のような型は、src/core/nginx_config.h にある。

typedef struct {
  ngx_str_t   name;
} ngx_http_hello_loc_conf_t;

ディテクティブに具体的にどんな内容を書くかは以下のように定義します。

static ngx_conf_post_handler_pt ngx_http_hello_p = ngx_http_hello;

static ngx_command_t ngx_http_hello_commands[] = {
  { 
    ngx_string("hello"),
    NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
    ngx_conf_set_str_slot,
    NGX_HTTP_LOC_CONF_OFFSET,
    offsetof(ngx_http_hello_loc_conf_t, name),
    &ngx_http_hello_p 
  },
  ngx_null_command
};

このモジュールを利用するには、コンフィグファイル内にhelloというKeyを定義するのが一番最初の『ngx_string("hello")』です。
次の『NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1』は、HTTPのLocationディテクティブで利用し、"hello"が引数を1つ取るということです。
引数の個数についてはsrc/core/ngx_conf_file.h に、コンフィグファイルのどのディテクティブに定義を表するかは src/http/ngx_http_config.h に定義されています。
3番目の『ngx_conf_set_str_slot』引数を取る場合の型を定義するお作法として、関数ポインタを記載します。ここで記載出来る関数名はsrc/core/ngx_conf_file.h の328行目から340行目までに定義されています。
4番目の 『NGX_HTTP_LOC_CONF_OFFSET』はLocationディテクティブの時にはこれを設定します。src/http/ngx_http_config.hの50〜52にsrv、mainの時に設定する関数が定義されています。
5番目では、ngx_http_hello_loc_conf_t->nameに設定
ngx_null_commandは、配列の最後を表します。モジュールとしてhello以外Keyが存在する場合は、ngx_null_commandの前に同様の定義をします。

module contextの設定

ngx_httpmodule_ctxという名前で、下記のコメントのようなコンフィグに対する処理を定義します。
ngx_http_hello_create_loc_conf関数で、コンフィグの初期化をしていますが、複数のlocationに対応するような場合にはngx_httpmerge_loc_confというような名前にする必要があります。

static ngx_http_module_t ngx_http_hello_module_ctx = {
  NULL,                          /* pre configuration */
  NULL,                          /* post configuration */
  NULL,                          /* create main configuration */
  NULL,                          /* init main configuration */
  NULL,                          /* create server configuration */
  NULL,                          /* merge server configuration */
  ngx_http_hello_create_loc_conf, /* create location configuration */
  NULL                           /* merge location configuration */
};

/* The function which initializes memory for the module configuration structure
*/
static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf)
{
  ngx_http_hello_loc_conf_t  *conf;
  conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_hello_loc_conf_t));
  if (conf == NULL) {
   return NULL;
  }
  return conf;
}

moduleの定義

NGX_MODULE_V1とNGX_MODULE_V1_PADDINGで挟んだ内容がモジュールの定義(ngx_module_t)になります。
それぞれの処理に合わせてコールバック処理登録を登録します。

/*
 * The module which binds the context and commands
 *
 */
ngx_module_t ngx_http_hello_module = {
  NGX_MODULE_V1,
     &ngx_http_hello_module_ctx,    /* module context */
     ngx_http_hello_commands,       /* module directives */
     NGX_HTTP_MODULE,               /* module type(CORE, MAIL, EVENT, ...) */
     NULL,                          /* init master */
     NULL,                          /* init module */
     NULL,                          /* init process */
     NULL,                          /* init thread */
     NULL,                          /* exit thread */
     NULL,                          /* exit process */
     NULL,                          /* exit master */
     NGX_MODULE_V1_PADDING
};

Handler

Handlerでは、リクエストを受けてレスポンスか返すという実質的な処理を実装します。

/*
 * Main handler function of the module.
 */
static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r)
{
  ngx_int_t    rc;
  ngx_buf_t   *b;
  ngx_chain_t  out;
  /* we response to 'GET' and 'HEAD' requests only */
  if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
    return NGX_HTTP_NOT_ALLOWED;
  }

  /* discard request body, since we don't need it here */
  rc = ngx_http_discard_request_body(r);
  if (rc != NGX_OK) {
    return rc;
  }

  /* set the 'Content-type' header */
  r->headers_out.content_type_len = sizeof("text/html") - 1;
  r->headers_out.content_type.data = (u_char *) "text/html";
  /* send the header only, if the request type is http 'HEAD' */
  if (r->method == NGX_HTTP_HEAD) {
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = hello_string.len;

    return ngx_http_send_header(r);
  }
  /* allocate a buffer for your response body */
  b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
  if (b == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }
  /* attach this buffer to the buffer chain */
  out.buf = b;
  out.next = NULL;

  /* adjust the pointers of the buffer */
  b->pos = hello_string.data;
  b->last = hello_string.data + hello_string.len;
  b->memory = 1;    /* this buffer is in memory */
  b->last_buf = 1;  /* this is the last buffer in the buffer chain */
  /* set the status line */
  r->headers_out.status = NGX_HTTP_OK;
  r->headers_out.content_length_n = hello_string.len;
  /* send the headers of your response */
  rc = ngx_http_send_header(r);
  if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
    return rc;
  }

  /* send the buffer chain of your response */
  return ngx_http_output_filter(r, &out);
}

ビルドしてみる

モジュールのソースコードを配備します。

~# mkdir /usr/local/src/ngx_http_hello_module
~# ls /usr/local/src/ngx_http_hello_module
config  ngx_http_hello_module.c

nginx-buildの0.4.0以降を使ってビルドします。

~# useradd --shell /sbin/nologin nginx
~# apt-get update
~# apt-get install gcc make
~# wget https://github.com/cubicdaiya/nginx-build/releases/download/v0.4.1/nginx-build-linux-amd64-0.4.1.tar.gz
~# tar xvfz nginx-build-linux-amd64-0.4.1.tar.gz
~# chmod +x nginx-build 
~# ./nginx-build -d work -pcre -zlib -openssl -verbose --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx.lock --http-log-path=/var/log/nginx/access.log --with-http_stub_status_module --with-debug --add-module=/usr/local/src/ngx_http_hello_module
~# cd work/1.9.2/nginx-1.9.2/
~/work/1.9.2/nginx-1.9.2# make install
~#  /usr/sbin/nginx -c /etc/nginx/nginx.conf

ソースコードhideji/ngx_http_hello_module · GitHubにあります。

起動スクリプトを利用る場合はJasonGiedymin/nginx-init-ubuntu · GitHubを参照してください。

参考資料

簡単なHandlerの作り方は以下が参考になります。

TerraformをDigital Oceanで試してみる3 - provisionerにchefを使う

Terraformのprovisionerとしてchefを使うにはChefサーバが必要になります。
Chefサーバの準備が面倒な場合は、Hosted Chefで代用できます。Hosted Chefの設定については前回の記事が参考にしてください。

Chef Clientのインストール

provisionerにChefを指定したときに、skip_installをtrueにしなければ(デフォルトではfalse)Chef Clientをインストールできますが、remote-execを利用してChef Clientをインストールします。
inlineに実行したいコマンドを指定するので、ここではOmnibus Installerを実行します。
remote-execではConnectionを指定してSSHで接続出来るようにします。

    provisioner "remote-exec" {
        inline = [
        "curl -L https://www.chef.io/chef/install.sh | sudo bash"
        ]

        connection {
            user = "root"
        type = "ssh"
            key_file = "${var.ssh_key_file}"
    }
    }

key_fileで指定する鍵ファイルはdecryptされている必要があります。decryptしていない場合は以下の様なエラーがでます。

digitalocean_droplet.node-1: Provisioning with 'remote-exec'...
digitalocean_droplet.node-1: Error: 1 error(s) occurred:

* Failed to read key '/Users/username/.ssh/id_rsa': password protected keys are
not supported. Please decrypt the key prior to use.
Error applying plan:

1 error(s) occurred:

* 1 error(s) occurred:

* 1 error(s) occurred:

* Failed to read key '/Users/username/.ssh/id_rsa': password protected keys are
not supported. Please decrypt the key prior to use.

Terraform does not automatically rollback in the face of errors.
Instead, your Terraform state file has been partially updated with
any resources that successfully completed. Please address the error
above and apply again to incrementally change your infrastructure.

その場合は以下のコマンドでdecryptします。

openssl rsa -in <Encrypted key filename>  -out < desired output file name>

Chefの実行

provisionerとしてchefを利用します。
Chef Serverを使う必要が有るため事前にChef Serverを利用できるようにする必要があります。
ここでは、CookBookとしてgetting-startedを指定しています。
getting-startedを使う場合はattributesは利用しないので設定不要ですが、サンプルとして記述しています。 また、Terraform v0.5.2ではchef provisionerはterraform.tfvarsに定義した値を使えない※ようなので直接値を書きます。 ※変数名が定義した文字列にならず、変数名のままchef-clientに渡っているようなエラーがでます。

    provisioner "chef"  {
        attributes {
            "key" = "value"
            "app" {
                "cluster1" {
                    "nodes" = ["webserver1", "webserver2"]
                }
            }
        }
        environment = "_default"
        run_list = ["getting-started"]
        node_name = "node-1"
        server_url = "https://api.opscode.com/organizations/"
        validation_client_name = "Organazation名-validator"
        validation_key_path = "./Organazation名-validator.pem"
        skip_install = true
 
        connection {
            user = "root"
            type = "ssh"
            key_file = "${var.ssh_key_file}"
        }
    }

注意すべき点としては、destroyしてもChef Server側に登録されたnode(node_name)は削除されないので別途削除する必要があります。
作成されたnodeを削除しないでterraform applyを再度実行した場合は、以下の様なエラーが表示されます。

ocean_droplet.node-1 (chef): Creating a new client identity for node-1 using the validator key.
digitalocean_droplet.node-1 (chef): 
digitalocean_droplet.node-1 (chef): ================================================================================
digitalocean_droplet.node-1 (chef): Chef encountered an error attempting to create the client "node-1"
digitalocean_droplet.node-1 (chef): ================================================================================

digitalocean_droplet.node-1 (chef): Authorization Error:
digitalocean_droplet.node-1 (chef): --------------------
digitalocean_droplet.node-1 (chef): Your validation client is not authorized to create the client for this node (HTTP 403).
digitalocean_droplet.node-1 (chef): 
digitalocean_droplet.node-1 (chef): Possible Causes:
digitalocean_droplet.node-1 (chef): ----------------
digitalocean_droplet.node-1 (chef): * There may already be a client named "node-1"
digitalocean_droplet.node-1 (chef): * Your validation client (Organization名-validator) may have misconfigured authorization permissions.
digitalocean_droplet.node-1 (chef): 
digitalocean_droplet.node-1 (chef): [2015-06-13T07:26:44-04:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
digitalocean_droplet.node-1 (chef): Chef Client failed. 0 resources updated in 3.789611695 seconds
digitalocean_droplet.node-1 (chef): [2015-06-13T07:26:44-04:00] ERROR: 403 "Forbidden"
digitalocean_droplet.node-1 (chef): [2015-06-13T07:26:44-04:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
digitalocean_droplet.node-1: Error: 1 error(s) occurred:

* Command "chef-client -j \"/etc/chef/first-boot.json\" -E \"_default\"" exited with non-zero exit status: 1
Error applying plan:

1 error(s) occurred:

* 1 error(s) occurred:

* 1 error(s) occurred:

* Command "chef-client -j \"/etc/chef/first-boot.json\" -E \"_default\"" exited with non-zero exit status: 1

Terraform does not automatically rollback in the face of errors.
Instead, your Terraform state file has been partially updated with
any resources that successfully completed. Please address the error
above and apply again to incrementally change your infrastructure.

do.tfファイル、variables.tf

最終的な do.tfは以下のようになります

provider "digitalocean" {
      token = "${var.digitalocean_token}"
}

resource "digitalocean_droplet" "node-1" {
     image = "ubuntu-14-04-x64"
     name = "node-1"
     region = "sgp1"
     size = "512mb"
     ssh_keys = [${var.ssh_key_id}]

    provisioner "remote-exec" {
        inline = [
        "curl -L https://www.chef.io/chef/install.sh | sudo bash"
        ]

        connection {
            user = "root"
            type = "ssh"
            key_file = "${var.ssh_key_file}"
        }
    }

    provisioner "chef"  {
        attributes {
            "key" = "value"
            "app" {
                "cluster1" {
                    "nodes" = ["webserver1", "webserver2"]
                }
            }
        }

        environment = "_default"
        run_list = ["getting-started"]
        node_name = "node-1"
        server_url = "https://api.opscode.com/organizations/"
        validation_client_name = "Organazation名-validator"
        validation_key_path = "./Organazation名-validator.pem"
        skip_install = true
 
        connection {
            user = "root"
            type = "ssh"
            key_file = "${var.ssh_key_file}"
        }
    }
}

variables.tfとterraform.tfvarsは前回から変更は無いです。