コマンドラインで Route53

EC2 のサーバで Web サイトを運用する場合、DNS には Route53 を利用される方もいらっしゃるかと思います。

Route53 は「可用性が高くスケーラブルなクラウドドメインネームシステム (DNS) ウェブサービス」(本家サイトより) です。


いくつかのドメインの Web サイトを EC2 インスタンスで運用していたのですが、そのインスタンスには Elastic IP アドレスを設定していませんでした。
つまり、停止 → 起動 を行うと、IP アドレスが変わってしまう状態でした。

あるとき、このインスタンスの能力を増強するためにインスタンスタイプの変更を行うことになったのですが、インスタンスタイプの変更を行うためには一度インスタンスを停止する必要があります。

インスタンスタイプの変更自体は問題なく行えたのですが、IP アドレスが変わってしまいました。

そのインスタンスにひもづいているドメインDNS レコードは Route53 で管理しており、インスタンスの IP アドレスが変わったので、各ドメインの A レコードを Route53 で変更しなければなりません。

1つ1つ修正する方法もありますが、数が多かったため、コマンドで変更することにしました。以下がそのコマンドです。(1ドメイン分)


aws route53 change-resource-record-sets --hosted-zone-id <ゾーンID> --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"<ドメイン名>","Type":"A","TTL": 300,"ResourceRecords":[{"Value":"<IPアドレス>"}]}}]}'


便利ですね。普通のドメイン管理サービスではなかなかこうはいきません。

ドメインのゾーンIDもコマンドで取得することができます。

aws route53 list-hosted-zones | jq -r '.HostedZones[]|[.Name,.Id]|@csv'



aws コマンドを使用するためにはそれなりに(もしかしたら面倒な)準備が必要ですが、それを補ってあまりある便利さです。




C# でチャットワークの API を使ってメッセージを投稿する

タイトルの通りです。

チャットワークには Web ベースの API が用意されており、それを使用することでプログラムからメッセージを投稿することができます。

今のところ認証は単一の API トークンによるもののみで、とてもシンプルです。 APIトークンはこちら https://www.chatwork.com/service/packages/chatwork/subpackages/api/token.php から確認できます。

投稿先(グループ)は、ルームID で指定します。URL で #!ridXXXXXXXXX となっている XXX... の数字部分がルームIDになります。

各エンドポイントについては公式ドキュメントに記載があります。メッセージを投稿する場合は /rooms/<ルームID>/messages です。
リクエストを作るところはこんな感じになります。

var req = (HttpWebRequest)WebRequest.Create("https://api.chatwork.com/v2/rooms/<ルームID>/messages");

req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
req.Headers.Set("X-ChatWorkToken", <APIトークン>);


ここで、コンテントタイプを application/x-www-form-urlencoded にするのを忘れないようにします。
チャットワークの API は、レスポンスは JSON 形式ですが、POSTする内容はURLエンコード形式になります。

POSTする部分はこんな感じになります。

var message = "こんにちは\n世界";
var content = "body=" + Uri.EscapeDataString(message);
var bytes = Encoding.ASCII.GetBytes(content);
req.ContentLength = bytes.Length;

new MemoryStream(bytes).CopyTo(req.GetRequestStream());

using (var resp = req.GetResponse() as HttpWebResponse)
using (var reader = new StreamReader(resp.GetResponseStream()))
{
    // レスポンスでは投稿したメッセージのIDが取れる
    reader.ReadToEnd();
}


メッセージに改行を入れれば、チャットワーク上でもちゃんと改行されます。
メッセージは body というパラメタで設定することになっているので、それを POST します。




C# で Worpdress の API を使って記事を投稿する

WordpressAPI が備わっていることはこれまでも記事で紹介していますが、今回は具体的に使う場合のコードのメモになります。

API では JSON でデータをやり取りしますので、DynamicJson を使用することにします。NuGet でインストールするかソースコードを取り込んで、 DynamicJson を使用できるようにします。

コードです。まず、送信するデータのもととなる匿名オブジェクトを作成します。

var postContent = new
{
    title = <記事タイトル>,
    content = <記事本文>,
    status = "publish", // 公開は"publish" 下書きは"draft"
    categories = "1,2,3", // カテゴリのIDをカンマ区切り
    tags = "4,5" // タグのIDをカンマ区切り
}

content には、普通に HTML タグを含む投稿の本文を文字列で指定します。

categories や tags などは不要であれば指定しなくてかまいません。
status も指定しなければデフォルトで公開状態になります。これらの他にもパラメタを指定でき、それは こちら で確認できます。

この匿名オブジェクトを JSON 形式の文字列に変換するのに、DynamicJson を使用します。

var serialized = DynamicJson.Serialize(postContent);

あとは送信するだけです。

var postData = Encoding.UTF8.GetBytes(serialized);

var req = HttpWebRequest.Create("<サイトのURL>/wp-json/wp/v2/posts") as HttpWebRequest;
req.Method = "POST";
req.ContentLength = postData.Length;
req.ContentType = "application/json"; // 送信するコンテントタイプを application/json にするのを忘れないこと

using (var reqs = req.GetRequestStream())
{
    reqs.Write(postData, 0, postData.Length);
}

using (var resp = req.GetResponse() as HttpWebResponse)
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
{
    var s = reader.ReadToEnd();
}

最後の変数 s には、API のレスポンスが JSON 形式の文字列で格納されています。

これをまた DynamicJson で扱うなら、

var post = DynamicJson.Parse(s);

こうすればOKです。こうすると、例えば投稿IDは post.id で取得できます。何が取得できるかはこちらで確認できます。




Redirect ディレクティブによるリダイレクト

ある URL にアクセスされた場合、それを別の URL に飛ばしたい場合があります。

それは内部的なものと外部的なものに分けられます。

内部的なものは、ある URL にアクセスされたときに、その URL を処理する機能を別の機能にまかせたいケースです。
Wordpress が自動的に .htaccess に追加するリライトの設定などがそれにあたります。
Wordpress の場合は、/foo/bar などのアクセスがあった場合、基本的には index.php に処理が転送されます。
ブラウザのアドレスバーに変更がないのが特徴です。

外部的なものは、単に別の URL に転送したいケースです。これには HTTP のリダイレクトが使用されます。リダイレクトでは、ブラウザのアドレスバーに表示される URL は転送先のものになります。
リダイレクトでは、同じサイト内で別の URL に飛ばしたい場合と、他のサイトへ飛ばしたい場合があります。


行いたいのは大抵は外部的な URL の転送だと思いますが、リダイレクトする方法を検索すると、リライトによる方法を紹介しているページが多く目に付きます。
しかし、複雑なことをしないのであれば、Apache であればリライトではなく Redirect ディレクティブによるリダイレクトで十分なケースもあるでしょう。

URL階層を引き継いで、別のサイトに飛ばしたい


例えば、http://foo.test/aaa/bbb.htmlhttp://bar.test/aaa/bbb.html に飛ばしたいというケースでは、以下の設定が使えます。

Redirect / http://bar.test/

301リダイレクトにする場合は、以下のようにします。

Redirect permanent / http://bar.test/
#または
RedirectPermanent / http://bar.test/

/aaa の下だけを飛ばしたい場合は、以下のようにすればOKです。

Redirect /aaa http://bar.test/aaa/


URL階層を引き継がないで、別のサイトに飛ばしたい


http://foo.test/aaa/bbb.htmlhttp://bar.test/ に飛ばしたいなど、/aaa/bbb.html であろうが ccc.html であろうが、とにかく http://bar.test/ という1つの URL に飛ばしたいというケースです。

この場合は、RedirectMatch を使用することで実現できます。

RedirectMatch .* http://bar.test/

RedirectMatch は本来、正規表現を使用して Redirect ディレクティブでは実現できない少し複雑な転送を行うものですが、RedirectMatch は Redirect ディレクティブのように URL 階層を引き継ぎませんので、この目的で使用することができます。



certbot-auto で ImportError: No module named cryptography.hazmat.bindings.openssl.binding エラー

AWS の EC2 で運用しているWebサイトがあり、そのサーバでは certbot-auto を使用してサイトのSSL化を行っていました。

先日、certbot-auto を使用してSSL証明書の作成を行おうとしたところ、以下のエラーが発生しました。

Upgrading certbot-auto 0.27.1 to 0.28.0...
Replacing certbot-auto...
Creating virtual environment...
Installing Python packages...
Installation succeeded.
Traceback (most recent call last):
  File "/opt/eff.org/certbot/venv/bin/letsencrypt", line 7, in <module>
    from certbot.main import main
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/certbot/main.py", line 10, in <module>
    import josepy as jose
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/josepy/__init__.py", line 44, in <module>
    from josepy.interfaces import JSONDeSerializable
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/josepy/interfaces.py", line 8, in <module>
    from josepy import errors, util
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/josepy/util.py", line 4, in <module>
    import OpenSSL
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/OpenSSL/__init__.py", line 8, in <module>
    from OpenSSL import rand, crypto, SSL
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/OpenSSL/rand.py", line 12, in <module>
    from OpenSSL._util import (
  File "/opt/eff.org/certbot/venv/local/lib/python2.7/dist-packages/OpenSSL/_util.py", line 6, in <module>
    from cryptography.hazmat.bindings.openssl.binding import Binding
ImportError: No module named cryptography.hazmat.bindings.openssl.binding

Pythoncertbot も詳しくないのですが、タイトルのエラーについてはネット上にいくつも情報が転がっていました。

ただ、どれも微妙に異なる内容で、検索結果の上から試していきましたが一発では解決しませんでした。

結論、この環境では以下で復旧させることができました。

unset PYTHON_INSTALL_LAYOUT
/opt/eff.org/certbot/venv/bin/pip install --upgrade certbot

しかしネット上の情報と上記のエラーとを総合すると、原因は certbot-auto がアップグレードをかけるときに上手くいっていないためだろうと考えられました。
であるならば、certbot-auto を実行する前に unset PYTHON_INSTALL_LAYOUT すればよいのではないかと考え、別のサーバで unset PYTHON_INSTALL_LAYOUT してから certbot-auto を実行してみたところ、無事にアップグレードされました。

一度 certbot-auto してこのエラーが出てしまった後では、/opt/eff.org/certbot/venv/bin/pip install --upgrade certbot で明示的にアップグレードしてあげる必要があります。




XPath 基礎 (4)

XPath の基礎 (4) です。


XPath では、CSS ではできないような強力な選択を行うことができます。

これはテキストノードを使用できることがその主な要因であると感じます。

個人的によく使用するのが、「ある文字列が要素に含まれているか」という判断です。
例えば「"マツタケ"という言葉を含む div タグを探す」などです。これは CSS で行うことはできません。

このケースでは、以下のような XPath で選択することができます。

//div[contains(text(),'マツタケ')]


text() は、そのノードの子テキストノードという意味でした。ですから上記で、「すべての div タグのうち、子テキストノードに"マツタケ"を含むもの」を選択することになります。


ところで、上記の XPath では、以下のどちらの div も選択されるでしょうか?

<div>
マツタケの土瓶蒸し
</div>
<div>
  <span>マツタケ</span>の土瓶蒸し
</div>


実は上記の XPath では、2つ目の div は選択されません。text() はあくまで子テキストノードを指すからです。
2つ目の div の子テキストノードは空であり、条件に一致しません。


こういった場合はどうするかというと、以下のようにします。


//div[contains(.,'マツタケ')]


text() を . に変えています。. は、self::node() の省略形です。つまり上記でいえばそれぞれの div を指しています。

これでなぜ2つ目の div も選択されるかというと、

  • 関数呼び出しにおいては、引数が文字列型の場合は文字列値に変換化される
  • 要素の文字列値は、その子孫ノードのすべての子テキストノードを連結したものである

からです。

つまり、contains(.,'マツタケ') に渡される . は div 要素であり、contains の引数は文字列型なので引数は文字列化され、div 要素を文字列化したものはその div が囲っているすべてのテキストノードを連結したものであり、それはどちらの div も "マツタケの土瓶蒸し" なので、"マツタケ" を含むため条件に一致するということになります。

まとめると、タグのすぐ直下にある文字列だけを狙い撃ちしたいときは text() を使い、タグの下全部を対象にしたい場合は . を使うようにすればよいでしょう。



XPath 基礎 (3)

XPath の基礎 (3) です。

//div[@name='hoge']


上記のように書けば、name 属性が "hoge" であるすべての div タグを選択できます。

実は XPath には様々な省略記法があり、実は上記もその省略記法で書かれていて、省略しないで書くと以下のようになります。

/descendant-or-self::node()/child::div[attribute::name='hoge']


省略記法で書かれたものに比べると、省略記法を使用しないものは長ったらしくなっていますが、XPath の構成要素をちゃんと理解することは、XPath を書くときの一助になってくれると思います。

XPath の主要な構成要素には、ノードテスト述部 があります。

軸は、選択するもののベースを決めるもので、child、self、parent、attribute などがあります。

child であれば子ノード、self であれば自ノード、parent であれば親ノード、attribute であれば属性を意味します。

なお軸の後ろには :: をつける決まりです。

ノードテスト

ノードテストは、指定した軸の何を選択するかを決めるもので、要素名や属性名 や text()、 * などがあります。

例えば child::div であれば、子ノードのうちの div 要素を意味し、child::* であれば、子ノードのうちのすべての要素を意味します。

child::text() であれば、子テキストノードを意味します。CSS では意識することはありませんが、タグとタグの間にあるテキスト部分(<div>ここ</div>)がテキストノードです。

述部

述部は、軸とノードテストで指定したものをフィルタリングするものです。なくてもかまいません。

述部は [ と ] で表し、その中に を指定します。ここでは簡単に、真か偽かを判定する式だと考えましょう。

例えば attribute::name='hoge' なら「属性 name が "hoge" である」という条件でフィルタリングすることになります。



ここで最初に戻って、以下の XPath について考えてみます。

/descendant-or-self::node()/child::div[attribute::name='hoge']

まず、最初の / は「文書のルート」です。これはそのように決められています。

descendant-or-self が軸で、その意味は「自分およびその子孫」になります。

node() はノードテストで、すべての子ノードを意味します。

したがって /descendant-or-self::node() は、「全部のノード」を意味することになります。

次に child::div[attribute::name='hoge'] ですが、この child は軸で、/descendant-or-self::node() で選択されたそれぞれの子、ということになります。

child::div の div はノードテストで、要素 div をあらわしています。つまり、/descendant-or-self::node() で選択されたそれぞれの子の div 要素、平たく言えば 全部の div 要素 ということになります。

[attribute::name='hoge'] は述部で、上記にある通り「name 属性が "hoge" である」という条件になります。ですから、すべての div 要素のうち、name 属性が "hoge" であるもの を選択するということになります。