動かざることバグの如し

近づきたいよ 君の理想に

Let's EncryptのワイルドカードSSL証明書の更新に苦労した話

経緯

一応ドメインを持っていて、turai.work ドメインを持っているのだが、Let's EncryptワイルドカードSSL証明書で運用している。

期限も近づいてきたし、certbot-autoコマンドで更新するか〜と思って更新を実行したのだが

/root/certbot/certbot-auto renew --force-renewal

以下のようなエラーが

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/turai.work.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Could not choose appropriate plugin: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.',)
Attempting to renew cert (turai.work) from /etc/letsencrypt/renewal/turai.work.conf produced an unexpected error: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.',). Skipping.
All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/turai.work/fullchain.pem (failure)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/turai.work/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 renew failure(s), 0 parse failure(s)

なんで?????ちなみに他にワイルドカードではないLet's EncryptのSSL証明書もあるのだが、そっちは上記のコマンドで正常に更新できた。

エラー原因

どうやらワイルドカードSSL証明書の場合はイレギュラーで、更新のたびにDNSの認証が必要らしい。メンドクセーーーー

が、更新しないとエラーになって悲しいことになるのですることに。てかDNS更新必要ならcronで定期実行できないじゃんと嘆いてたらcertbot-dns-route53なるプラグインを発見

certbot用のプラグインで、AWSのキーを予めセットしておけば更新時に自動で処理してくれる。素晴らしい

ってことで導入

certbot-dns-route53のインストール

通常 /opt/eff.org/certbot/venv/bin/pip にcertbotが使っているPythonの環境が入っているのでその中のpipを使ってインストール

/opt/eff.org/certbot/venv/bin/pip install certbot-dns-route53

インストールが完了したらプラグイン一覧で確認

./certbot/certbot-auto plugins

以下のような項目があればインストールに成功している

* dns-route53
Description: Obtain certificates using a DNS TXT record (if you are using AWS
Route53 for DNS).
Interfaces: IAuthenticator, IPlugin
Entry point: dns-route53 = certbot_dns_route53.dns_route53:Authenticator

AWSのキー取得

AWSのIAM管理画面へログインして新しいIAMを作成、Route53の権限を設定してアクセスキーとシークレットキーを取得

rootコマンドで実行するので/root/.aws/credentialsに以下を作成

[default]
aws_secret_access_key = *************
aws_access_key_id = ******

あとはdns-route53のプラグインを使用することを明示的に書いて更新実行

/root/certbot/certbot-auto renew --force-renewal --no-self-upgrade --server https://acme-v02.api.letsencrypt.org/directory --agree-tos --dns-route53
Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/turai.work/fullchain.pem (success)

って言われたら成功 やったね

あとの詳しいことは公式ドキュメント参照

参考リンク

GCPのGoogle Cloud Storage無料枠を使うには注意が必要

GoogleAWSと違って半永久的に使える「枠」が用意されている。例えばUSリージョンの一番スペックの低いマシンならずっと起動していても課金されない(1台のみだが)

AWSでいうEC2にあたるGoogle Cloud Storageにも無料枠が用意されている。さすがGoogle先生太っ腹

で、パット見5GBまで無料だが、チュートリアル等で紹介されている以下のようなバケット作成コマンドを叩くと無料枠の対象外になる。

# NG
gsutil mb -p PROJECT_ID gs://BUCKET_NAME

なぜ無料枠の対象外なのか

GCPの公式サイトには以下のような記述がある。

Google Cloud Storageの項目より、

  • 5 GB の Regional Storage(バージニア州北部を除く米国リージョンのみ)
  • 5,000 回のクラス A オペレーション(1 か月あたり)
  • 50,000 回のクラス B オペレーション(1 か月あたり)
  • 1 GB の北米から全リージョン宛ての下りネットワーク(1 か月あたり、中国とオーストラリアを除く)

つまりリージョンが米国リージョンじゃないからか、と理解し、以下のコマンドでバケットを作成する

gsutil mb -p PROJECT_ID -l us-central1 gs://BUCKET_NAME

が、残念ながらこれでも無料枠の対象ではない

ストレージに種類がある。

GCSにはStorage Classなるものがあって、Multi-Regional Storage、Regional Storage、Nearline Storage、Coldline Storageの4種類があり、左から順に1GBあたりの料金が高い。

詳しい説明は公式ドキュメントに譲るとして、要は無料枠はRegional Storageだけだが、クラス無指定だとデフォルトのMulti-Regional Storageになってしまう。よって課金されてしまうのである。。。

無料枠に入るバケット作成コマンド

明示的にストレージクラスを指定してあげればいよいので以下になる

gsutil mb -p PROJECT_ID -l us-central1 -c regional gs://BUCKET_NAME

ちなみに無料枠のリージョンは他にもあって

がある。北米だが北バージニアにあたる「us-east4」は無料枠の対象外なので注意。

NodejsでGoogle Cloud Storageへファイルをアップロードしてみる

環境

  • nodejs v8.8.1

ここではAWSでいうS3にあたるGoogle Cloud StorageへNodejsでローカルのファイルをアップロードしたりしてしてみる。

サービスアカウントの作成

まずは権限設定 アクセスに必要なキーを含むJSONファイルをゲットする必要がある。

  • GCPコンソールへログイン
  • GCP のプロジェクトを選択
  • 上の検索バーで「API」と入力 「認証情報 APIとサービス」と項目をクリック
  • 「認証情報を作成」をクリック「サービスアカウントキー」を選択
  • 新しいサービスアカウントを選択、サービスアカウント名は任意、役割でストレージに権限を振る
  • 作成をクリック
  • するとJSONファイルがダウンロードされるので保管

詳しくは以下 Google Cloud Platform のサービスアカウントキーを作成する | MAGELLAN BLOCKS

ライブラリのインストール

公式ライブラリの@google-cloud/storageを使う yarnでインストール

yarn add @google-cloud/storage

基本

async/awaitによる同期処理が可能なので使う。以下のように

const {Storage} = require('@google-cloud/storage');

const storage = new Storage({
  projectId: 'プロジェクトID',
  keyFilename: 'さっき保存したJSONのパス'
});

const bucketName = 'バケット名';

const main = async() => {
  var filename ='index.js';
  await storage
    .bucket(bucketName)
    .upload(filename, {gzip: true})
    .then(res => {
      // 公開状態にする場合
      // res[0].makePublic();
      console.log(res[0].metadata);
      console.log(`${filename} uploaded to ${bucketName}.`);
    })
    .catch(err => {
      console.error('ERROR:', err);
    });
}

main();

これでnode index.jsをするとファイルがGCSにアップロードされる。

一覧表示

ファイル一覧
await storage
  .bucket(bucketName)
  .getFiles()
  .then(results => {
    const files = results[0];
    files.forEach(file => {
      console.log(file.name);
    });
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

プレフィックスで絞ることもできる(suffixはない模様

await storage
  .bucket(bucketName)
  .getFiles({
    prefix: 'us-central1-projects/'
  })
  .then(results => {
    const files = results[0];
    files.forEach(file => {
      console.log(file.name);
    });
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

その他のAPI

以下のサンプルコード集が参考になる。

https://github.com/googleapis/nodejs-storage/blob/master/samples/files.js

GASで3行でS3にアップロードする

まず、ライブラリを登録します。 [リソース]→[ライブラリ]から、MB4837UymyETXyn8cv3fNXZc9ncYTrHL9を登録します。

function uploadS3() {
  var s3 = S3.getInstance('ACCESS_KEY', 'ACCESS_SECRET');
  var data = UrlFetchApp.fetch("http://www.google.com").getBlob();
  var response = s3.putObject('media.turai.work', 'google', data, {logRequests:true});
  Logger.log(response);
}