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_http
ngx_http_hello_create_loc_conf関数で、コンフィグの初期化をしていますが、複数のlocationに対応するような場合にはngx_http
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は前回から変更は無いです。
Hosted Chefを使ってみる
サーバがインターネットに接続可能でChefサーバを建てるまではないけど、少しChefサーバを使ってみたいというようなときにHosted Chefが便利なので使ってみました。
Chef 12のオープンソース版と25ノード制限について を見ると、5ノードまでは問題なく使えるようなのでちょっとした確認には使うことが出来ます。
アカウントが無い場合はStart your free trial of hosted Chef から作ることが出来ます。
以降はログインが完了している前提で記載しています。
HOSTED CHEFでの設定
Administration -> Organazationを選択します。
knife.rbの作成
Generate Knife Configを選択すると、kinife.rbが ダウンロード出来ます。
validation_keyのダウンロード
Reset Validation Keyを選択すると、kinife.rbのvalidation_keyにOrganazation名書かれているpemファイル(Organazation名-validator.pem)がダウンロード出来ます。
client_keyのダウンロード
Account Management画面でPassword and Keyのタブを選択して、 Get a New Keyを選択するとpemファイル(ユーザ名.pem)がダウンロード出来ます。
WORKSTATIONでの作業
Starter Kitのダウンロード
CookBookのアプロードやRoleの作成などをWorkStationとして実施するための環境を整えるためにStarter Kitをダウンロードします。 左側のメニューに有るStarter Kitを選択するとダウンロード出来ます。 settings ディレクトリを作成して、kinife.rbとvalidation_key、client_keyを移動します。
これらを実行すると以下の様な構成になります。
. ├── README.md ├── settings │ ├── Organazation名-validator.pem │ ├── ユーザ名.pem │ └── knife.rb ├── cookbooks │ ├── chefignore │ └── starter │ ├── attributes │ │ └── default.rb │ ├── files │ │ └── default │ │ └── sample.txt │ ├── metadata.rb │ ├── recipes │ │ └── default.rb │ └── templates │ └── default │ └── sample.erb └── roles └── starter.rb
コミュニティCookBookの追加
この状態でsettingsディレクトリでkinife listコマンドを実行する以下のように表示されます。
$ knife client list Organazation名-validator
chef-repoをgitの管理下にします。
chef-repo$ git init . chef-repo/.git/ chef-repo$ git add . chef-repo$ git commit -m "Initial commit" 11 files changed, 230 insertions(+) create mode 100644 README.md create mode 100644 settings/Organazation名-validator.pem create mode 100644 settings/ユーザ名.pem create mode 100644 settings/knife.rb create mode 100644 cookbooks/chefignore create mode 100644 cookbooks/starter/attributes/default.rb create mode 100644 cookbooks/starter/files/default/sample.txt create mode 100644 cookbooks/starter/metadata.rb create mode 100644 cookbooks/starter/recipes/default.rb create mode 100644 cookbooks/starter/templates/default/sample.erb create mode 100644 roles/starter.rb
コミュニティCookbookのgetting-startedを追加します。
chef-repo$cd settings chef-repo/settings$ knife cookbook site install getting-started ...省略... Cookbook getting-started version 0.4.0 successfully installed
Cookbookのアップロード
Cookbookをアップロードします。
chef-repo/settings$ knife cookbook upload --all ERROR: Chef::Exceptions::MetadataNotValid: Cookbook loaded at path(s) [/home/clavier/Dev/chef/chef-repo/cookbooks/getting-started] has invalid metadata: The `name' attribute is required in cookbook metadata
meatadata.rbやmetadata.jsonにnameの項目がないので、以下の内容を追加します。
chef-repo/settings$ vim ../cookbooks/getting-started/metadata.rb name 'getting-started'
再度、Cookbookをアップロードします。
chef-repo/settings$ knife cookbook upload --all Uploading getting-started [0.4.0] Uploading starter [1.0.0] Uploaded all cookbooks.
アップロードが完了したCookbookは Policy -> Cookbooks を選択することで確認出来ます。
TerraformをDigital Oceanで試してみる2 環境情報の外出し
前回では、do.tfファイルの中に全ての情報を書き込みました。
それではtoken情報やssh keyの値などはgitなどで管理しづらいので、ssh_keysとtokenを別のファイルで管理します。
ファイルを外出しするようになると、以下の様な構成になります。
. ├── do.tf ├── terraform.tfvars └── variables.tf
この構成になるとterraform.tfvarsにtoken情報やssh keyの値を書くことで、このファイルのみをコミット対象外するとこでgitなどで問題無く扱えるようになります。
variables.tf
terraform.tfvarsで宣言する変数を宣言するためのファイルです。
do.tfの中で宣言しても、問題無いですが別ファイルで管理します。ファイルの内容はInput Variablesにあるように、書いていきます。
variable digitalocean_token {} variable ssh_key_id {}
terraform.tfvars
このファイルでは、変数と値を定義します。
terraform.tfvarsファイルという名前にしておくと、上記のような構成にしておくと-var-fileオプションを付けなくても自動的にTerraformが読み込んでくれます。
dgitalocean_token = "前回作成したtoken" ssh_key_id = "512189"
do.tf
前回作成したdo.tfのtokenとssh_keysの値を、variables.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}] }
これで、terraform plan や terraform applyの引数は変更なく実行出来ます。
TerraformをDigital Oceanで試してみる1
Digital Oceanの情報取得
Terraform を Digital Ocean で触ってみた (初級編) を参考にDigital Ocean のtokenを作成します。
Degital OceanのAPI V2を利用して、curlからSSH鍵の情報を取得します。
参考:https://developers.digitalocean.com/documentation/v2/#ssh-keys
# curl -X GET -H 'Content-Type: application/json' -H 'Authorization: Bearer 先ほど作成したtokenを記載' "https://api.digitalocean.com/v2/account/keys" { "ssh_keys": [ { "id": 512189, "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "My SSH Public Key" } ], "links": { }, "meta": { "total": 1 } }
ここで返ってきたidとDegital Oceanのtokenを次で作成する.tfファイルで利用します。
.tfファイルの作成
terraformでは、.tfファイルを作成しそこから環境を作成出来ます。
ここで、今まで行った作業を元にDegital Ocean用のdo.tfファイルを作成してみます。
provider "digitalocean" { token = "作成したtokenを記載" } resource "digitalocean_droplet" "node-1" { image = "ubuntu-14-04-x64" name = "node-1" region = "sgp1" size = "512mb" ssh_keys = [512189] }
dropletの作成
do.tfを利用して、dropletを作成します。 planのみでは実際にdropletは作成されず、applyまで実行することでdropletが作成されます。
$ terraform plan Refreshing Terraform state prior to plan... The Terraform execution plan has been generated and is shown below. Resources are shown in alphabetical order for quick scanning. Green resources will be created (or destroyed and then created if an existing resource exists), yellow resources are being changed in-place, and red resources will be destroyed. Note: You didn't specify an "-out" parameter to save this plan, so when "apply" is called, Terraform can't guarantee this is what will execute. + digitalocean_droplet.node-1 image: "" => "ubuntu-14-04-x64" ipv4_address: "" => "<computed>" ipv4_address_private: "" => "<computed>" ipv6_address: "" => "<computed>" ipv6_address_private: "" => "<computed>" locked: "" => "<computed>" name: "" => "node-1" region: "" => "sgp1" size: "" => "512mb" ssh_keys.#: "" => "1" ssh_keys.0: "" => "512189" status: "" => "<computed>" $ terraform apply digitalocean_droplet.node-1: Creating... image: "" => "ubuntu-14-04-x64" ipv4_address: "" => "<computed>" ipv4_address_private: "" => "<computed>" ipv6_address: "" => "<computed>" ipv6_address_private: "" => "<computed>" locked: "" => "<computed>" name: "" => "node-1" region: "" => "sgp1" size: "" => "512mb" ssh_keys.#: "" => "1" ssh_keys.0: "" => "512189" status: "" => "<computed>" digitalocean_droplet.node-1: Creation complete
status: "" => "
dropletにログイン
作成したdropletの情報を確認してログインします。
$ terraform show digitalocean_droplet.node-1: id = 5556133 image = ubuntu-14-04-x64 ipv4_address = 128.199.64.45 locked = false name = node-1 region = sgp1 size = 512mb ssh_keys.# = 1 ssh_keys.0 = 512189 status = active
IPアドレスがわかったので、実際にログインしてみます。
秘密鍵は~/.ssh/id_rsaにあるものとしています。
ssh -i ~/.ssh/id_rsa root@128.199.64.45
dropletの破棄
作成したdropletを破棄します。
$ terraform destroy Do you really want to destroy? Terraform will delete all your managed infrastructure. There is no undo. Only 'yes' will be accepted to confirm. Enter a value: yes digitalocean_droplet.node-1: Refreshing state... (ID: 5556133) digitalocean_droplet.node-1: Destroying... digitalocean_droplet.node-1: Destruction complete Apply complete! Resources: 0 added, 0 changed, 1 destroyed.