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

TenForward

技術ブログ。はてなダイアリーから移転しました

Linux 4.11 での cgroup 関連の話題

Linux 4.11 で cgroup に動きがありました。と言ってもしばらく新機能追えてないので、これまでも色々変更されているかも?

追加された事自体を忘れてしまいそうなのでメモしておくだけで、詳しく調べるわけではありません。

rdma controller

rdma コントローラというコントローラが新たに追加されています。RDMA は “Remote Direct Memory Access” ですか。知りませんでした。

Added rdma cgroup controller that does accounting, limit enforcement on rdma/IB resources.

とのことですから、Infiniband 関係でリソース制限を行うために追加されたのでしょうか。

Drop the matching uid requirement on migration for cgroup v2

cgroup v2 での、root 以外のユーザによる cgroup 操作の制限を一部外しましょう、というもののようですね。cgroup v2 では別に制限かかっているから、これがなくても OK みたいな。

slub: make sysfs directories for memcg sub-caches optional

cgroup 初期化時の v1 の処理だけ抜き出して追ってみる

cgroup 初期化のメモ。間違っている可能性大なので信用しないでください。何度も同じところを繰り返し見てるので忘れないようにメモです。

サブシステム (cgroup_subsys) と各グループのサブシステムの状態 (cgroup_subsys_state) とそれらのセット (css_set) の関係とかわかってないと以下を見てもわけわからんかも。自分用のメモで、間違いあるかもしれません。

start_kernel からまず cgroup_init_early が呼び出された後、cgroup_init が呼び出される。

   492 asmlinkage __visible void __init start_kernel(void)
    :(snip)
   511         cgroup_init_early();
    :(snip)
   661         cgroup_init();

cgroup_init_early

  4960         RCU_INIT_POINTER(init_task.cgroups, &init_css_set);

init_taskcss_set 型のメンバ cgroupsinit_css_set で初期化します。init_css_set は cgroup.c で定義されています。

  4962         for_each_subsys(ss, i) {

これは静的に定義されているサブシステム分要素を持つ cgroup_subsys 型の cgroup_subsys[] 変数 (配列) 分ループをまわすものでした (cgroup の SUBSYS マクロ 参照)。ss には i 番目のサブシステムが入ります。

  4963                 WARN(!ss->css_alloc || !ss->css_free || ss->name || ss->id,
  4964                      "invalid cgroup_subsys %d:%s css_alloc=%p css_free=%p name:id=%d:%s\n",
  4965                      i, cgroup_subsys_name[i], ss->css_alloc, ss->css_free,
  4966                      ss->id, ss->name);

まずは

  • サブシステムに必須のメソッドである css_alloc/free が存在しているかのチェック
  • サブシステム名とIDがまだ構造体のメンバに設定されていないチェック

を行います。

  4967                 WARN(strlen(cgroup_subsys_name[i]) > MAX_CGROUP_TYPE_NAMELEN,
  4968                      "cgroup_subsys_name %s too long\n", cgroup_subsys_name[i]);

次にサブシステム名 (cgroup_subsys_name[i] に入ってます) の名前が長すぎないかチェックしています。

  4969 
  4970                 ss->id = i;
  4971                 ss->name = cgroup_subsys_name[i];

サブシステム (変数の各メンバ) に、ID と名前を設定します。(各サブシステムで cgroup_subsys を定義する際には定義されていないメンバ)

  4973                 if (ss->early_init)
  4974                         cgroup_init_subsys(ss, true);

そしてサブシステムの early_init を行うように 1 が設定されている時は cgroup_init_subsys を呼び出します。

cgroup_init_subsys を見てみると、

  4895 static void __init cgroup_init_subsys(struct cgroup_subsys *ss, bool early)
  4896 {
   :(snip)
  4906         /* Create the root cgroup state for this subsystem */
  4907         ss->root = &cgrp_dfl_root;
  4908         css = ss->css_alloc(cgroup_css(&cgrp_dfl_root.cgrp, ss));

ss->root には v2 の root が放り込まれます。つまり cgroup_init_subsys は v2 の処理を行うだけです。

cgroup_init

cgroup_init はちょっと長いのですが、これも v2 の処理を無視するとやっていることはシンプルです。

  4992         BUG_ON(cgroup_init_cftypes(NULL, cgroup_legacy_base_files));

まずはこれ。cgroup を操作するために各グループに現れるファイルがあります。このファイルを cftype という構造体で定義します。

このファイルのうち、サブシステムの処理とは直接関係ない、cgroup 自体の操作に関連する cgroup コアで扱うファイル群があり、その定義が cftype の配列として静的に定義されています。それが cgroup_legacy_base_files 変数です。これを cgroup_init_cftypes 関数に渡して、ファイル操作の定義を行います (多分)。

  4996         /* Add init_css_set to the hash table */
  4997         key = css_set_hash(init_css_set.subsys);
  4998         hash_add(css_set_table, &init_css_set.hlist, key);

システム上の css_set はすべて css_set_table というハッシュテーブルで管理されますので、それに init_css_set を登録します。css_set 内のサブシステムから計算される値を使ってハッシュのキー (key) を生成し、使用します。とは言っても init_css_set って v2 用じゃ…

そしてまたサブシステム分ループ。ループの最初に以下のような処理がありますが、

  5005                 if (ss->early_init) {
  5006                         struct cgroup_subsys_state *css =
  5007                                 init_css_set.subsys[ss->id];
  5008 
  5009                         css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2,
  5010                                                    GFP_KERNEL);
  5011                         BUG_ON(css->id < 0);
  5012                 } else {
  5013                         cgroup_init_subsys(ss, false);
  5014                 }

ここですが、cgroup_init_early で初期化したサブシステムは ID を確保、されなかったサブシステムは cgroup_init_subsys で初期化します。とは言っても、いずれも v2 処理。

その後の条件分岐、

  5035                 if (ss->dfl_cftypes == ss->legacy_cftypes) {
  5036                         WARN_ON(cgroup_add_cftypes(ss, ss->dfl_cftypes));
  5037                 } else {
  5038                         WARN_ON(cgroup_add_dfl_cftypes(ss, ss->dfl_cftypes));
  5039                         WARN_ON(cgroup_add_legacy_cftypes(ss, ss->legacy_cftypes));
  5040                 }

if 文 true の条件は特別な起動オプションを付けないと満たさない開発目的の条件なので無視。通常は false になるので、v2 の設定を行った (cgroup_add_dfl_cftypes呼び出し) 後の 5039 行目だけが v1 の処理でしょう、と思えますが、実は通常はここは何もしません。この理由は後で。

  5042                 if (ss->bind)
  5043                         ss->bind(init_css_set.subsys[ssid]);
  5044         }

その後、サブシステムに bind 関数が設定されている場合はそれを実行。

  5046         err = sysfs_create_mount_point(fs_kobj, "cgroup");

sysfs にマウントポイントとなるディレクトリを作成 (/sys/fs/cgroup)。

  5050         err = register_filesystem(&cgroup_fs_type);

cgroup というファイルシステムを登録。

  5056         proc_create("cgroups", 0, NULL, &proc_cgroupstats_operations);

proc に “cgroups” というファイルを作成。で終わりです。

cgroup_add_legacy_cftypes で何もしない理由

cgroup_add_legacy_cftypes で v1 っぽい処理を呼び出しているにも関わらず「通常は何もしません」と書いた理由を追ってみます。

cgroup_add_legacy_cftypes

cgroup_add_legacy_cftypes にサブシステムと cgroup_subsys 構造体型の各サブシステムの変数で定義されている legacy_cftypes を渡します。legacy_cftypes は各サブシステムで v1 のグループに出現させるファイルが各サブシステムごとに定義されています。

cgroup_add_legacy_cftypes 関数を見てみます。

  3313 int cgroup_add_legacy_cftypes(struct cgroup_subsys *ss, struct cftype *cfts)
  3314 {
    :(snip)
  3322         if (!cgroup_legacy_files_on_dfl ||
  3323             ss->dfl_cftypes != ss->legacy_cftypes) {
  3324                 for (cft = cfts; cft && cft->name[0] != '\0'; cft++)
  3325                         cft->flags |= __CFTYPE_NOT_ON_DFL;
  3326         }
  3327 
  3328         return cgroup_add_cftypes(ss, cfts);
  3329 }

通常は (開発目的のフラグがオン、またはデフォルトの cftypes が v1 の場合 (これも通常はない) でなければ)、各 cftype 型の変数のメンバ flags に “v2 階層には表示させない” というフラグを設定したのち、cgroup_add_cftypes を呼び出します。

cgroup_add_cftypes

cgroup_add_cftypes 関数

  3263 static int cgroup_add_cftypes(struct cgroup_subsys *ss, struct cftype *cfts)
  3264 {
   :(snip)
  3273         ret = cgroup_init_cftypes(ss, cfts);
   :(snip)
  3280         ret = cgroup_apply_cftypes(cfts, true);

cgroup_init_cftypescft->ss = ss という cftype 構造体の ss にサブシステムを入れている部分があります。そして cgroup_apply_cftypes 関数を呼びます。

cgroup_apply_cftypes

  3138 static int cgroup_apply_cftypes(struct cftype *cfts, bool is_add)
  3139 {
  3141         struct cgroup_subsys *ss = cfts[0].ss;
  3142         struct cgroup *root = &ss->root->cgrp;

ss には cfts[0].ss を入れてますが、これは cgroup_init_cftypes でサブシステムそのものを入れました。root 変数にはサブシステムの root が入りますが、これは cgroup_init_subsys で v2 の root が入っていたはず。

  3155                 ret = cgroup_addrm_files(cgrp, cfts, is_add);

cgroup_addrm_files

そして cgroup_addrm_files 呼び出し。

  3105 static int cgroup_addrm_files(struct cgroup *cgrp, struct cftype cfts[],
  3106                               bool is_add)
  3107 {
   :(snip)
  3113         for (cft = cfts; cft->name[0] != '\0'; cft++) {
  3114                 /* does cft->flags tell us to skip this file on @cgrp? */
  3115                 if ((cft->flags & __CFTYPE_ONLY_ON_DFL) && !cgroup_on_dfl(cgrp))
  3116                         continue;
  3117                 if ((cft->flags & __CFTYPE_NOT_ON_DFL) && cgroup_on_dfl(cgrp))
  3118                         continue;
   :(snip)

というループがあり、

  • 現在の cgrp は v2 の root の cgroup が入っているはず (cgroup_apply_cftypes の処理参照)
  • cft->flags には __CFTYPE_NOT_ON_DFL が放り込まれていたはず (cgroup_add_legacy_cftypes の処理参照)

というわけで、ここはずっと continue で何もしないままループを抜けます。ループを抜けなければ cgroup_add_file で cgroup 用のファイルを追加するのですが、何もしないということです。

関連

cgroup の SUBSYS マクロ

Linux カーネルの cgroup 関連のコードのお話。

cgroup_subsys.h というヘッダがあって、cpuset の部分だけ抜き出すと

#if IS_ENABLED(CONFIG_CPUSETS)
SUBSYS(cpuset)
#endif

という風に SUBSYS マクロの中に cpuset のようなサブシステム名を与えているだけのヘッダファイルがあります。

この SUBSYS マクロは cgroup.h と cgroup.c で計 4 回ほど定義されています。

まず最初に、cgroup.h で

#define SUBSYS(_x) _x ## _cgrp_id,

と定義されています。サブシステム名と _cgrp_id という文字列を連結します。

直後に enum の定義があり、

enum cgroup_subsys_id {
#include <linux/cgroup_subsys.h>
        CGROUP_SUBSYS_COUNT,
};

上記のように、そこで cgroup_subsys.h が include されているので、さきほどの SUBSYS(cpuset)cpuset_cgrp_id という風に展開されます。

つまり、

enum cgroup_subsys_id {
        cpuset_cgrp_id,

となります。cgroup_subsys.h には、他に存在するサブシステムが (kernel の config で有効になっている場合に) SUBSYS(サブシステム名) という風に定義されているので、(サブシステム名)_cgrp_id という値をサブシステム分持つ enum が定義されます。そして、最後が CGROUP_SUBSYS_COUNT となるわけですね。つまり、

enum cgroup_subsys_id {
        cpuset_cgrp_id,
        cpu_cgrp_id,
        cpuacct_cgrp_id,
        blkio_cgrp_id,
        memory_cgrp_id,
        devices_cgrp_id,
        freezer_cgrp_id,
        net_cls_cgrp_id,
        perf_event_cgrp_id,
        net_prio_cgrp_id,
        hugetlb_cgrp_id,
        debug_cgrp_id,
        CGROUP_SUBSYS_COUNT,
};

という enum が完成します。直後に

#undef SUBSYS

と SUBSYS は undef されているので、ここで上記の enum を作って役割は終わりです。この CGROUP_SUBSYS_COUNT はサブシステム分ループする場合に使えますね。例えばこんなふうに

#define for_each_subsys(ss, ssid)                   \
   for ((ssid) = 0; (ssid) < CGROUP_SUBSYS_COUNT &&      \
        (((ss) = cgroup_subsys[ssid]) || true); (ssid)++)

そして、サブシステムの配列などで、それぞれのサブシステムの ID を表す (サブシステム名)_cgrp_id という値が利用できます。あとは配列の領域確保のときに使われています。


もう 1 か所、cgroup.h に SUBSYS の定義があります。

#define SUBSYS(_x) extern struct cgroup_subsys _x ## _cgrp_subsys;
#include <linux/cgroup_subsys.h>
#undef SUBSYS

サブシステム名を _cgrp_subsys と連結して、extern struct cgroup_subsys の後に書くので、例えば cpuset なら

extern struct cgroup_subsys cpuset_cgrp_subsys;

となります。cpuset_cgrp_subsys 変数は kernel/cpuset.c に定義がありますので、それの extern 宣言になりますね。同様に、有効になっている各サブシステムの cgroup_subsys 構造体ごとに宣言されます。


kernel/cgroup.c にも 2 ヶ所ほどあります。

/* generate an array of cgroup subsystem pointers */
#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,
static struct cgroup_subsys *cgroup_subsys[] = {
#include <linux/cgroup_subsys.h>
};
#undef SUBSYS

最初に紹介した、サブシステム名と _cgrp_id を連結した enum の id が使われていますね。これも同様に

[cpuset_cgrp_id] = &cpuset_cgrp_subsys,

という行が生成されますね。つまり cgroup_subsys という配列の (サブシステム名)_cgrp_id 番目は (サブシステム名)_cgrp_subsys へのポインタということです。

これは先に紹介した for_each_subsys マクロで使われていましたね。


さいごに kernel/cgroup.c にあるのが、サブシステム名を命名するマクロです。

/* array of cgroup subsystem names */
#define SUBSYS(_x) [_x ## _cgrp_id] = #_x,
static const char *cgroup_subsys_name[] = {
#include <linux/cgroup_subsys.h>
};
#undef SUBSYS

これもさきほどと同様の処理でサブシステム名の配列を作っています。配列の (サブシステム名)_cgrp_id 番目は (サブシステム名)、つまり SUBSYS マクロに指定した文字列となります。

[cpuset_cgrp_id] = cpuset,

つまり、カーネルが使うサブシステムに関わる構造体の配列とか ID とか名前の配列なんかは、カーネルの config 時 (make config とか make menuconfig とか) の時点で定義され、コンパイル時点で作成されるってことですね。

いやー、さすがカーネル開発するような人は賢いなあ (←言ってる人アホっぽい)

cgroup のデフォルトルート cgrp_dfl_root

このエントリはほぼ個人的なメモで、色々唐突です。

cgroupのコアは kernel/cgroup.c に色々処理があります。

その中に「デフォルトヒエラルキ(のルート)」という変数があります。

4.1 kernel のコードです。

/*
 * The default hierarchy, reserved for the subsystems that are otherwise
 * unattached - it never has more than a single cgroup, and all tasks are
 * part of that cgroup.
 */
struct cgroup_root cgrp_dfl_root;

cgroup の初期化処理は start_kernel から呼び出される cgroup_init_early と cgroup_init で行われますが、cgrp_dfl_root は cgroup_init で初期化されているようです。

int __init cgroup_init(void)
{
	struct cgroup_subsys *ss;
	unsigned long key;
	int ssid, err;

	BUG_ON(cgroup_init_cftypes(NULL, cgroup_dfl_base_files));
	BUG_ON(cgroup_init_cftypes(NULL, cgroup_legacy_base_files));

	mutex_lock(&cgroup_mutex);

	/* Add init_css_set to the hash table */
	key = css_set_hash(init_css_set.subsys);
	hash_add(css_set_table, &init_css_set.hlist, key);

	BUG_ON(cgroup_setup_root(&cgrp_dfl_root, 0));

ここで cgroup_setup_root に cgrp_dfl_root が渡されます。この中で渡された root (つまり cgrp_dfl_root) の初期化処理がなされていきます。

ここで root = cgrp_dfl_root の場合は cgroup_dfl_base_files

static int cgroup_setup_root(struct cgroup_root *root, unsigned int ss_mask)
{

    : (snip)

	if (root == &cgrp_dfl_root)
		base_files = cgroup_dfl_base_files;
	else
		base_files = cgroup_legacy_base_files;

    : (snip)

	ret = cgroup_addrm_files(root_cgrp, base_files, true);

ここで設定した base_files を cgroup_addrm_files に渡して、cgroup コアが各 cgroup (つまり cgroup を表す各ディレクトリ) に作成するファイルを設定します。

cgroup_dfl_base_files ってのは

/* cgroup core interface files for the default hierarchy */
static struct cftype cgroup_dfl_base_files[] = {
	{
		.name = "cgroup.procs",
		.seq_start = cgroup_pidlist_start,
		.seq_next = cgroup_pidlist_next,
		.seq_stop = cgroup_pidlist_stop,
		.seq_show = cgroup_pidlist_show,
		.private = CGROUP_FILE_PROCS,
		.write = cgroup_procs_write,
		.mode = S_IRUGO | S_IWUSR,
	},
	{
		.name = "cgroup.controllers",
		.flags = CFTYPE_ONLY_ON_ROOT,
		.seq_show = cgroup_root_controllers_show,
	},
	{
		.name = "cgroup.controllers",
		.flags = CFTYPE_NOT_ON_ROOT,
		.seq_show = cgroup_controllers_show,
	},
	{
		.name = "cgroup.subtree_control",
		.seq_show = cgroup_subtree_control_show,
		.write = cgroup_subtree_control_write,
	},
	{
		.name = "cgroup.populated",
		.flags = CFTYPE_NOT_ON_ROOT,
		.seq_show = cgroup_populated_show,
	},
	{ }	/* terminate */
};

という内容ですが、ここで .name で指定されているファイルって cgroup-v2 の各 cgroup (ディレクトリ) に作成されるファイルです。

つまり start_kernel -> cgroup_init で起動時に設定される「デフォルトのヒエラルキ(のルート)」は v2 ?

確かに v2 は単一階層構造なのでここで初期化しておけば、あとはそれを利用すれば良いのに対して、v1 はいくつでもマウントできるので後で設定してもいいのかな? と思えますが、もしかして今後は v2 をデフォルトにしたいという考えの現れ?

3.15 kernel では

ちなみに v2 のコードがまだそれほど入っていないころの 3.15 だと、cgroup_init 内で cgroup_setup_root が呼ばれているのは同じですが、cgroup_setup_root は

static int cgroup_setup_root(struct cgroup_root *root, unsigned long ss_mask)
{

    :(snip)

	ret = cgroup_addrm_files(root_cgrp, cgroup_base_files, true);

と条件文はなくて直接 cgroup_base_files が渡されており、cgroup_base_files は

static struct cftype cgroup_base_files[] = {
	{
		.name = "cgroup.procs",
    : (snip)

	},
	{
		.name = "cgroup.clone_children",
		.flags = CFTYPE_INSANE,
    : (snip0)
	},
	{
		.name = "cgroup.sane_behavior",
		.flags = CFTYPE_ONLY_ON_ROOT,
    : (snip)
	},

	/*
	 * Historical crazy stuff.  These don't have "cgroup."  prefix and
	 * don't exist if sane_behavior.  If you're depending on these, be
	 * prepared to be burned.
	 */
	{
		.name = "tasks",
		.flags = CFTYPE_INSANE,		/* use "procs" instead */
    : (snip)

	},
	{
		.name = "notify_on_release",
		.flags = CFTYPE_INSANE,
    : (snip)

	},
	{
		.name = "release_agent",
		.flags = CFTYPE_INSANE | CFTYPE_ONLY_ON_ROOT,

    : (snip)

	},
	{ }	/* terminate */
};

というふうに v1 のファイルは flags に CFTYPE_INSANE ってのが指定されています。追加の際に v1, v2 を判定して追加するかどうかを決めています。

static int cgroup_addrm_files(struct cgroup *cgrp, struct cftype cfts[],
			      bool is_add)
{

    : (snip)

	for (cft = cfts; cft->name[0] != '\0'; cft++) {
		/* does cft->flags tell us to skip this file on @cgrp? */
		if ((cft->flags & CFTYPE_ONLY_ON_DFL) && !cgroup_on_dfl(cgrp))
			continue;
		if ((cft->flags & CFTYPE_INSANE) && cgroup_sane_behavior(cgrp))
			continue;
		if ((cft->flags & CFTYPE_NOT_ON_ROOT) && !cgrp->parent)
			continue;
		if ((cft->flags & CFTYPE_ONLY_ON_ROOT) && cgrp->parent)
			continue;

		if (is_add) {
			ret = cgroup_add_file(cgrp, cft);
    : (snip)
}

という風に v1, v2 混在したようなコードになってます。

Plamo Linux で Let's Encrypt

はじめに (読まなくても問題ありません)

自前で Plamo Linux で運用しているサーバはずっと StartSSL を使って証明書を取得していました。

Let's Encrypt が出てきた時は、いろいろ調べてみるとコマンド著名ディストリビューション前提で、Plamo には対応していないようなので、更新時期がきた証明書も引き続き StartSSL で取得しました。

その後、Zenlogicで運用している趣味のブログをZenlogicの機能で Let's Encrypt を使った SSL/TLS 化をしたのをきっかけに少し調べてみると、Plamo Linux でも Let's Encrypt の証明書が取得できそうなので、試してみました。

最近は certbot というコマンドを使うようですね。

まず最初に考えたのが、Plamo 用のパッケージを作ってしまうことです。SlackwareでもSlackbuildsにパッケージがありますし、ArchLinuxのスクリプトを見てもそれほど難しくはなさそうです。

しかし、依存パッケージが多い! いくら python setup.py を実行するだけとはいえ、これだけのためにいくつものパッケージを自作するのは面倒だし、それだけのためにたくさんパッケージを入れるのもなんか無駄な気がします。

というわけで、certbot-auto コマンドを実行すると、virtualenv を使った環境を作って、そこで必要なことはやってくれるようなので、お気楽にやってみようということで試しました。

準備とインストール

まずは必要最低限準備するパッケージをインストールしました。(私の作ったオレオレパッケージを公開しているのでリンクしておきます)

pip, virtualenv は Plamo 標準ではパッケージが準備されていませんので、適当にインストールするなり、私の作ったオレオレビルドスクリプト(python2_pip)、(python2_virtualenv)でパッケージを作成するなりしてインストールします (pip パッケージの作成には他にもパッケージインストールが必要かもしれません)。

以下では standalone プラグインしか使ってないので、他のプラグインを使う場合は他にも準備が必要かもしれません (試してません)。

さて、ここまできたらおもむろに certbot を取得します。

$ git clone https://github.com/certbot/certbot.git

準備は OK です。clone した certbot ディレクトリに移動して、virtualenv 環境をします。

$ cd certbot
$ sh tools/venv.sh

これでいろいろと準備が行われて環境が構築されます。

動作テスト

きちんと動作するか確認してみましょう。activate を実行して virtualenv 環境に入り (?)、

# source certbot/venv/bin/activate
(venv) # certbot --help all
usage: 
  certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...

Certbot can obtain and install HTTPS/TLS/SSL certificates.  By default,
it will attempt to use a webserver both for obtaining and installing the
cert. The most common SUBCOMMANDS and flags are:
  :(略)

とりあえず help を表示させてみました。うまくセットアップされていないと、ここでエラーになります。

証明書発行のテスト

これであとは証明書を発行すれば良いのですが、スクリプト化をしたり、テスト実行したりしたいでしょうから、何回も証明書の発行を行うこともあるかもしれません。

Let's Encrypt は一定期間中に発行できる証明書数の上限がありますので、とりあえずテストの証明書を発行してみましょう。そのためにステージング環境が準備されています。これで発行した証明書はブラウザでは警告が出てページがそのままでは表示されませんが、証明書の発行テストにはなります。

ステージング環境で実行するには、本番と同じコマンドに "--staging" とつけるだけなので簡単です。さらにまずは "--dry-run" でオプションに間違いがないかを確認してみましょう。

"--standalone" をつけて standalone プラグインを使う場合は、ローカルで Web サーバが起動しますので、Apache や nginx など、他の Web サーバが起動している場合は停止しましょう。

# apachectl stop
# certbot certonly --dry-run --standalone --domain www.example.com --email myaddress@example.com --non-interactive --staging
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Obtaining a new certificate
Performing the following challenges:
tls-sni-01 challenge for www.example.com
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0001_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0001_csr-certbot.pem

IMPORTANT NOTES:
 - The dry run was successful.

うまくいきそうなら、"--dry-run" を外して実行しましょう。

発行のテストが済んだら証明書更新のテストもやってみましょう。有効な証明書があると言われるので、強制的に新しい証明書を発行するために "--force-renewal" をつけます。

certbot renew --force-renewal --staging

発行

"--staging" で発行できたら、いよいよ本番です。

手元でステージングで発行した証明書がある場合、本番でも有効な証明書があるから発行できないと怒られるかもしれません (いろいろ試してたのでどうだったか忘れたw)。その場合は先と同様に "--force-renewal" をつけます。

# certbot certonly --standalone --domain www.example.com --email myaddress@example.com --non-interactive --agree-tos
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Obtaining a new certificate
Performing the following challenges:
  :(snip)

更新は "renew" コマンドを実行するだけですが、cron なんかで自動実行する場合は、起動中の Web サーバの停止・起動が必要ですので、

certbot renew --force-renewal --pre-hook "apachectl stop" --post-hook "apachectl start"

みたいにしておくと、確認の前後で hook を実行してくれます。

certbot 実行の詳しくは以下が参考になります (以上で書いたのは適当です)。

Apache HTTP server の mod_proxy_html メモ

最近、結構音楽ブログを更新しています。昔は音楽レビューといえばひたすら言葉でレビューするだけだったのが、最近は動画の埋め込みを行うようになって表現力のない私でも気楽にレビューができるようになってきました。

動画の埋め込みというと YouTube がまず思い浮かびますね。私の音楽ブログでもよく使っています。他に音楽ブログの性格上、V LIVEの動画を埋め込むこともあります。どちらも iframe を使った埋め込みコードが動画ページから生成できますので、気楽に貼り付けられます。

さて、私の音楽ブログは最近常時 SSL 化の流れで Let's Encrypt を使って SSL 化を行いました (Zenlogicの機能を使っただけ!!)。http でアクセスしても https に飛ばされます。https でアクセスするページに YouTube のコードを埋め込むのは何の問題もないのですが、V LIVE は埋め込む URL が http なので https でアクセスするページに埋め込みコードを貼り付けてもブロックされてしまいます。

となると、自分で運用している VPS でリバースプロキシの設定をやって、それを貼り付けたらいいんじゃないか? という考えが思い浮かぶのでやってみました。まあ例えば

ProxyPass "/vlive" "http://www.vlive.tv"
ProxyPassReverse "/vlive" "http://www.vlive.tv"

ってな感じですぐに設定できました。早速喜んで埋め込みコードとして、自 VPS の URL を使ってみましたが、これだと iframe の src に書かれた先を読み込むのは https 化されましたが、その中でさらに呼ばれる js や css が相変わらず http なので、動画はブロックされたままでした。残念...

そこで確かそういう場合に使うモジュールあったよなあと思ってググってみると、proxy 通した場合のコンテンツ内の URL を変換する場合は

  • mod_substitute
  • mod_proxy_html

なんかを使うようですね。

そこでmod_proxy_htmlを試すかということで、mod_proxy_htmlの公式マニュアルを見ながら設定してみました (mod_proxy_html は以前使ったことあるような記憶があったので)。

ProxyHTMLEnable On
ProxyHTMLExtended On
ProxyHTMLURLMap http://www.vlive.tv/ https://www.example.com/

# 以下は mod_proxy_html と一緒にインストールされる proxy-html.conf 内で書かれている設定もあります
ProxyHTMLLinks meta content
ProxyHTMLLinks script src
ProxyHTMLLinks link href
  : (snip)

とするけど、ブラウザでエラーになってしまいます(Chrome だと "ERR_CONTENT_DECODING_FAILED")。うーむ、としばらくいろいろ設定を試したのですがうまくいかずに諦めようかなという時にふと目に付いたのが公式マニュアルのコメント欄

RequestHeader unset Accept-Encoding

これを加えると見事動作しました。マニュアル部分に注釈としてでも書いておいてほしいなー。

SetOutputFilter proxy-html

を設定しろみたいに書いてあるページもいくつか見つけましたが、コメント欄にあるように UTF-8 の部分が文字化けしてしまいますので不要です。

ハマったのでメモ代わりに残しておこうと思ってブログ書いたけど…、ふと今日検索してみると…

こんなページがひっかかった!! (^_^;) mod_substitute でも同じ問題が起きるようですね。なるほど、ブラウザの送るヘッダをそのまま送って圧縮した応答なんかが帰ってきた場合にうまく処理できないってことですか。

なぜハマってる最中にググった時に気づかなかったんだ!? というわけで書くほどでもなかったネタのようですが、せっかく書いたのでそのまま公開しておきます。

せっかく問題は解決したのに、V LIVE で貼り付けようとしていた動画を早速貼り付けようとしたら、YouTube でも公開されていたので、まだ音楽ブログでは活用できてません。

ip netns コマンドが意外にきめ細やかにコンテナを作ってくれる

(2016-07-26: 誤記修正しました "setns -> nsenter")

お手軽にシェルスクリプトなんかでコンテナを作る場合の強い味方といえば

  • util-linux の unshare/nsenter コマンド
  • iproute2 の ip netns コマンド

が代表的でしょう。"ip netns" は Network Namespace 作ってコマンド実行するだけの単純なコマンドかと思ったら、中では意外に色々細かくやってくれていることがわかったのでちょっと紹介しておきます。

以下は iproute2 4.2.0 で試しています。

/var/run/netns

まずよく知られているのが /var/run/netns 以下に Namespace 名のファイルを作ってくれることですね。これは、Namespace 内で動いているプロセスがなくなったら Namespace が消滅してくれるのを防ぐためです。だって ip netns add が終了したら、Namespace 内にはプロセスいなくなりますから。

まずはこの辺りで /var/run/netns を MS_SHARED にして、自身を bind mount して、ココをマウントポイントにしています。

   636        while (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, NULL)) {
        :(snip)
   644               /* Upgrade NETNS_RUN_DIR to a mount point */
   645               if (mount(NETNS_RUN_DIR, NETNS_RUN_DIR, "none", MS_BIND, NULL)) {
        :(snip)
   651        }
   (ip/ipnetns.cより)

そして

   667        /* Bind the netns last so I can watch for it */
   668        if (mount("/proc/self/ns/net", netns_path, "none", MS_BIND, NULL) < 0) {
   (ip/ipnetns.cより)

ip netns add コマンドのプロセスディレクトリの ns/net ファイルを /var/run/netns 以下に bind mount していますね。このように Namespace を示す特殊なファイルを残したままにしておけば Namespace が消えません。

Namespace と veth 作成

ここで Network Namespace として "netns01" を作成し、ホストとコンテナ間は veth インターフェースを作成します。

# export NETNS="netns01"
# export VETH="veth0"

それでは Network Namespace を作りましょう。

# ip netns add $NETNS
# ip netns list
netns01

簡単ですね。

それでは veth インターフェースを作りましょう。

# ip link add name $VETH-host type veth peer name $VETH-ns
# ip link set $VETH-ns netns $NETNS
# ip addr add 10.10.10.10/24 dev $VETH-host
# ip link set $VETH-host up
# ip netns exec $NETNS ip addr add 10.10.10.11/24 dev $VETH-ns
# ip netns exec $NETNS ip link set $VETH-ns up

ホスト側は "veth-host"、コンテナ側は "veth-ns" という名前のペアを作りました。それぞれアドレスを与えて、インターフェースを up します。

では、この作成した Namespace で bash を実行してコンテナを作成してみましょう。

# ip netns exec $NETNS /bin/bash
# echo $$
11945
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
11: veth0-ns@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ba:aa:99:70:46:ab brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.10.10.11/24 scope global veth0-ns
       valid_lft forever preferred_lft forever
    inet6 fe80::b8aa:99ff:fe70:46ab/64 scope link 
       valid_lft forever preferred_lft forever

"veth0-ns" インターフェースがあって、ちゃんとアドレスが割りあたってますね。まあ、これはそうなるようにコマンド実行していますので当たり前です。

コンテナ内の sysfs

ここで、Namespace 内で /sys 以下を見てみましょう。

# ls /sys/class/net -l
合計 0
lrwxrwxrwx 1 root root 0  7月 26日  20:01 lo -> ../../devices/virtual/net/lo/
lrwxrwxrwx 1 root root 0  7月 26日  20:01 veth0-ns -> ../../devices/virtual/net/veth0-ns/

ちゃんと Namespace 内のインターフェースのみ見えてますね。これは、この辺りの処理ですね。

    66        if (unshare(CLONE_NEWNS) < 0) {
    67               fprintf(stderr, "unshare failed: %s\n", strerror(errno));
    68               return -1;
    69        }
    70        /* Don't let any mounts propagate back to the parent */
    71        if (mount("", "/", "none", MS_SLAVE | MS_REC, NULL)) {
    72               fprintf(stderr, "\"mount --make-rslave /\" failed: %s\n",
    73                      strerror(errno));
    74               return -1;
    75        }
    76        /* Mount a version of /sys that describes the network namespace */
    77        if (umount2("/sys", MNT_DETACH) < 0) {
    78               fprintf(stderr, "umount of /sys failed: %s\n", strerror(errno));
    79               return -1;
    80        }
    81        if (mount(name, "/sys", "sysfs", 0, NULL) < 0) {
    82               fprintf(stderr, "mount of /sys failed: %s\n",strerror(errno));
    83               return -1;
    84        }
    (lib/namespace.cより)
  • 66行目で Mount Namespace を作ってます
  • 71行目で他の Mount Namespace に自身の Mount が伝播しないようにしています
  • 77行目で現在の /sys を umount して
  • 81行目で sysfs を改めて mount します

まあ、当たり前といえば当たり前ですが、ちゃんとここまで面倒見ててくれたんですね、ip netns。

/etc/netns

そして、以上の辺りのソースをつらつらと眺めていたら、気になる関数を見つけました。

    86        /* Setup bind mounts for config files in /etc */
    87        bind_etc(name);
    (lib/namespace.cより)

これを見てみると

    35               snprintf(netns_name, sizeof(netns_name), "%s/%s", etc_netns_path, entry->d_name);
    36               snprintf(etc_name, sizeof(etc_name), "/etc/%s", entry->d_name);
    37               if (mount(netns_name, etc_name, "none", MS_BIND, NULL) < 0) {
    (lib/namespace.cより)

"etc_netns_path" は /etc/netns/[namespace名] が入っていますので、その下のファイルを作成した Namespace 内に bind mount してくれるようです。

早速、ホスト上で /etc/netns/netns01/{hosts,resolv.conf} を作ってみました。

# cat /etc/netns/$NETNS/hosts 
10.10.10.10	host01
10.10.10.11	ns01
# cat /etc/netns/$NETNS/resolv.conf 
nameserver 8.8.8.8

以上を準備してから、再度コンテナを作成します。

# ip netns exec $NETNS /bin/bash
# cat /etc/resolv.conf 
nameserver 8.8.8.8
# cat /etc/hosts
10.10.10.10	host01
10.10.10.11	ns01

見事に作成した Namespace で実行されるコンテナでは準備したファイルが見えていますね。こんなとこまで面倒見てくれてたとは。(ちゃんと man ip-netns に書いてあるやん)

ip netns コマンドのソース、シンプルでコメントもちゃんと書かれてあって読みやすいのでコンテナ作成の勉強になる気がします。