It's a peacehell world.

こんな名前ですが情報技術について主に扱うブログです。

[Node.js] sinon の stub を使う

Node.js のユニットテストが初めてで一苦労したのでまとめておきます。

目的

Node.js のユニットテストPython でいうところの

docs.python.org

をやりたい。

例えば AWS Lambda 関数で SSM および DynamoDB から値を取得する箇所があった場合、外部サービスとの結合なので実際に AWS にアクセスするのではなくモック化してテストを行いたいのです。

前提

  • AWS Lambda 関数が Node.js で実装されている
  • AWS Lambda 関数では aws-sdk を使って SSM および DynamoDB にアクセスしている

環境

  • Lambda ランライム Node.js 14.x
  • aws-sdk 2.1215.0
  • mocha 10.0.0
  • power-assert 1.6.1
  • proxyquire 2.1.3
  • sinon 14.0.0

実装

Lambda

const aws = require('aws-sdk')
const docClient = new aws.DynamoDB.DocumentClient()

exports.handler = async (event) => {
  // SSM
  const { Parameters } = await (new aws.SSM())
    .getParameters({
      Names: ["SECRET_KEY_1", "SECRET_KEY_2"],
      WithDecryption: true,
    })
    .promise()
  // DynamoDB
  const params = {
    TableName: process.env.TABLE_NAME,
    Key: {
      id: event.arguments.id
    }
  }
  const result = await docClient.get(params).promise()

  // 処理

  return {}
}

テストコード

'use strict'
const assert = require('power-assert')
const sinon = require('sinon')
const proxyquire = require('proxyquire')

describe('Test', () => {
  let lambda
  let proxyDynamoDB
  let proxySSM
  describe('#handler()', () => {
    beforeEach(() => {
      // hide lambda log
      sinon.stub(console, 'log').returns()
      sinon.stub(console, 'error').returns()
      // stub aws
      proxyDynamoDB = class { batchGet() { } get() { } query() { } put() { } delete() { } }
      proxySSM = class { getParameters() { } }
      // inject
      lambda = proxyquire('../index.js', {
        'aws-sdk': {
          DynamoDB: { DocumentClient: proxyDynamoDB },
          SSM: proxySSM,
        }
      })
      // mock SSM getParameters
      sinon.stub(proxySSM.prototype, 'getParameters').returns({
        promise: () => Promise.resolve({
          Parameters: [
            {
              Name: 'KEY_1',
              Value: 'DUMMY_VALUE_1',
            },
            {
              Name: 'KEY_2',
              Value: 'DUMMY_VALUE_2',
            }
          ]
        })
      })
      process.env.TABLE_NAME = ''
      sinon.stub(process.env, 'TABLE_NAME').value('DUMMY_TABLE_NAME')
      const dynamoDbStub = sinon.stub(proxyDynamoDB.prototype, 'get')
      // mock DynamoDB get
      dynamoDbStub.withArgs({
        TableName: process.env.TABLE_NAME,
        Key: {
          id: 'DUMMY_ID'
        }
      }).returns({
        promise: () => Promise.resolve({
          Item: {
            name: 'DUMMY_NAME',
            email: 'DUMMY_EMAIL',
          }
        })
      })
    })
    it('テスト1', async () => {
      const event = {
        "requestContext": {
          "id": "DUMMY",
        }
      }
      await lambda.handler(event, context, () => {
      }).then(
        (data) => {
          assert.equal(data.isAuthorized, true)
        },
        (err) => {
          # FIXME: Lambda で error 発生時にテスト失敗としたい。
          # この書き方間違ってる気がするが、とりあえずテスト失敗には気がつける。
          assert.equal(0, 1, 'test failed.')
        })
    })
    afterEach(() => {
      sinon.restore()
    })
  })
})

テスト実施

$ npm i
$ npm test

補足

下書きのまま長期間熟成した記事なので色々古いと思います。

[AWS SES] AWS SES でサンドボックス外に移動してから送信元メールアドレスを設定したい

結論

できません。

docs.aws.amazon.com

では先に検証済み ID を作れとの記載はないですが、通常は送信元にするメールアドレスを準備の上 SES サンドボックス解除を行うと思いますので、ほとんどの方には無意味な記事になっております。

前提

  • 日毎の送信メール数の上限緩和をしたい。 → つまりサンドボックス解除したい。
  • 送信元メールアドレスはまだ決まっていない。→ つまりまだ検証済み ID を登録できない。

要は「サンドボックスから脱出だけしておいて、メールアドレスを取得してから送信元メールアドレスを指定したい」という状況です。

やったこと

  1. AWS 管理コンソールから Amazon SES > Account dashboard を開くとサンドボックス環境下である旨の警告が表示されているので、「Request production access」を選択します。

  2. ユースケースに沿った内容を「Use case description」に記載して「Submit request」を選択します。

  3. AWS Support > Your support cases にユースケースの記述が不足している旨の返信が即座に届きます。(最初からしっかり書いていれば届かないかもしれません。)

    Amazon から届いた返信 平素は Amazon Web Servicesをご利用いただき、誠にありがとうございます。

    このたびは、送信制限の申請をいただきありがとうございます。残念ながら現時点ではお客様のユースケースについて十分な情報がないため、この申請を承認することはできません。

    Amazon SES の使用計画について詳しい情報をご提供いただければ、申請が承認される可能性があります。返信の際に、お客様のメール送信プロセスや手順について、可能な限り詳しい情報を記載してください。

    たとえば、メールを送信する頻度、受信者リストのメンテナンス方法、バウンス、申し立て、解除申請の管理方法についてご説明ください。送信する予定のメールのサンプルをご提供いただければ、お客様が高品質のコンテンツを送信していることを確認するために役立ちます.

    この情報を受け取り次第、お客様の申請を審査させていただきます。24時間以内にご連絡をいたします。必要な情報をすべてご提示いただいた場合は、24時間以内に申請を承認いたします。追加で情報が必要な場合は、申請の実装に長くかかる可能性がございますことをご了承ください。

    Amazon Web Services にお問い合わせいただき、誠にありがとうございます。

  4. AWS Support > Your support cases > Details の画面の「Reply」を選択し、ユースケースについて詳細を記述して「Submit」を選択します。

  5. 返信は即座に返ってこないため翌営業日まで待ちます。(午前中に返信していれば当日中に返信来るかもしれません。)
  6. 検証済み ID を登録する必要がある旨の返信が届きます。
    Amazon から届いた返信 平素は Amazon Web Servicesをご利用いただき、誠にありがとうございます。

    このたびは送信送信制限の引き上げの申請をご送信いただき、ありがとうございます。 残念ながら、お客様のアジアパシフィック (東京)リージョンに検証済みIDが登録されていないため、現時点ではお客様の申請を承認することができません。

    IDとはメールを送信するドメインまたは E メールアドレスのことを指します。詳細については、こちらをご覧ください(https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/verify-domains.html)。

    Amazon SESのメールが送信されるメールアドレスやドメインについての情報をご提供いただければ、申請が承認される可能性があります。この情報をご提供いただける場合は、このメールにご返信ください。

    返信の際、検証済みIDの登録の確認をお願いいたします。

    この情報を受け取り次第、お客様の申請を審査させていただきます。24時間以内にご連絡をいたします。また、必要な情報をすべてご提示いただいた場合は、24時間以内に申請を承認いたしますが、追加で情報が必要な場合は、申請の実装に長くかかる可能性がございますことをご了承ください。

    Amazon Web Services にお問い合わせいただき、誠にありがとうございます。
  7. Amazon ご指摘の通り、検証済み ID は未登録です。

おわりに

先に Amazon SES > Configuration: Verified identities > Create identity にて検証済み ID を登録してからサンドボックス解除申請(Request production access)を行いましょう。

ちなみにまだ送信元メールアドレスは手に入ってないので、このあと検証済み ID を登録すればそれでサンドボックス解除できるかは確証無いです。(他にもハマリポイントあるかも。)

2022.10.7 追記

このあと検証済み ID を登録すればそれでサンドボックス解除できるかは確証無いです。(他にもハマリポイントあるかも。)

検証済み ID を作成し、作成した旨をサポートの「Reply」から返答したところ翌営業日にはサンドボックス解除と上限緩和ができました。

[Amplify] ARM マシンでの開発で Amplify の Lambda Layer に x64 の sharp をインストールしたい

8 ヶ月ぶりの更新がこんなしょぼい内容であれですが、メモ代わりということで。

前提

  1. Amplify および AppSync リゾルバとして Lambda を利用している。
  2. AppSync リゾルバの Lambda で画像処理ライブラリ sharp を使いたい。

課題

  1. AWS Lambda で sharp を利用するには arch=x64 で npm install 必要。 sharp.pixelplumbing.com

  2. 従って npm install --arch=x64 sharp でデプロイしたいが amplify push の際には npm install でインストールされるようなのでオプション指定をどうする?

※ x64 のマシンで運用する分には問題ありませんが、ARM のマシン(例:M1 Mac)の際に端末のアーキテクチャでインストールされてしまいます。

対策

※ 最善策である保証はありません。

docs.npmjs.com

script の postinstall を利用して ARM でインストール後に x64 でのインストールで上書きするようにします。

{
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "sharp": "^0.31.0"
  },
  "scripts": {
    "postinstall": "SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp"
  }
}

これによって npm install した際に x64 で sharp がインストールされます。

めでたし。めでたし。

2022-09-13 追記

初回は問題ないのですが、複数回 push していると sharp/build ディレクトリが生成されない現象がありました。

npm install でそもそも端末アーキテクチャ依存の sharp をインストールしないように preinstall で消すことで対応できました。

{
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "sharp": "^0.31.0"
  },
  "scripts": {
    "preinstall": "npm uninstall sharp",
    "postinstall": "SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp"
  }
}

[DNS] BIND でローカル DNS サーバー構築

ちゃちゃっと DNS サーバーを構築をします。

AWS Route53 はゾーン設定が Web コンソールでできたので使いやすかったんだなぁと今更実感しました。

⌨️ 環境

はじめに

DNS サーバーには BIND 以外にも PowerDNSNSD などあるようですが、

en.wikipedia.org

にて

As of 2015, it is the most widely used domain name server software, and is the de facto standard on Unix-like operating systems.

と、BIND こそが Unix ライク OS の DNS サーバーでデファクトスタンダート(自称)だと仰るので、初めてですし BIND を使います。

余談ですがバージョンとしては BIND10 もあったらしいですが、プロジェクト消滅して BIND9 のリファクタリングを行う方針になったようです。(そもそも ISC の ホームページ の Products に BIND9 しかない。)

BIND9 には脆弱性があるという声が多いのが気になりますが、ローカル DNS ということで今回は見なかったことにします。

参考:https://ubuntu.com/search?q=Bind+vulnerability

(解消してるものも多いと思いますが、結構あります。)

1. BIND9 インストール

apt-get で BIND9 をインストールします。

$ sudo apt-get install -y bind9

2. /etc/bind/named.conf.options 設定

/etc/bind/named.conf.options を編集します。

options {
    directory "/var/cache/bind";
    dnssec-validation auto;
    //listen-on-v6 { any; };
    listen-on port 53 { localhost; };
    allow-query { any; };
    forwarders { 8.8.8.8; };
    recursion no;
}

補足

  • IPv6 使わない場合は listen-on-v6 をコメントアウトする
  • listen-on には適宜プライベートネットワークアドレスを追記する
  • allow-query でアクセス元を制限できる

3. ゾーンの設定

/etc/bind/named.conf を見てみると

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

とあるので、

  1. /etc/bind/named.conf.local に zone を追記
  2. /etc/bind/named.conf.default-zones に zone を追記
  3. 自分で追加したファイルに zone を記述して include させる

などの方法でゾーンの設定を追加できそうです。

.conf が全部そうなのかわかってませんが、やり口は Nginx の conf と同じですね。

今回は named.conf.local に追記します。

zone "mydomain.com" {
        type master;
        file "/etc/bind/db.mydomain.com";
};

ローカル DNS なので、ここでつける名前(手順の例では mydomain.com )は取得してないドメインでも名前つけ放題です。

file "/etc/bind/db.mydomain.com" のファイルは次の手順で作成します。

4. ゾーンファイルの作成

/etc/bind/db.local をもとに編集します。

$ sudo cp /etc/bind/db.local  /etc/bind/db.mydomain.com
;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     mydomain.com. root.mydomain.com. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@       IN      NS      www
www     IN      A       192.168.0.x

この場合は www.mydomain.com192.168.0.x に名前解決されます。

test.mydomain.com も追加したい場合は NS レコードと A レコードを追加します。

@       IN      NS      www
www     IN      A       192.168.0.x
@       IN      NS      test
test    IN      A       192.168.0.x

余談ですが、

  • NS レコード左辺の @ は何なのか
  • SOA レコードってそもそも何なのか

あたりがさっぱりなので、今後書くであろうゾーンファイルを掘り下げる記事でお会いしましょう。

5. BIND9 再起動

$ sudo systemctl restart bind9

6. 確認

dig コマンドで確認。

$ dig www.mydomain.com
・・・
;; ANSWER SECTION:
www.mydomain.com. 604800 IN A   192.168.0.x
・・・

名前つけ放題と先ほど言いましたが、この世に存在するドメインを名付けてパブリック DNS サーバーが先に参照されて名前解決された場合は当然ローカル DNS サーバーでの名前解決はされません。

そのため、/etc/resolv.conf でパブリック DNS サーバーの前にローカル DNS サーバーを記述してください。

resolv.conf の nameserver の順序については

linuxjm.osdn.jp

を参考にしてください。

名前解決先に Web アプリがあればそれぞれ curl で確認して同じ内容が得られます。

$ curl http://192.168.0.x
(何かしらの HTML)

$ curl http://www.mydomain.com
(何かしらの HTML)

おわりに

ゾーンファイルはテンプレートの /etc/bind/db.local をもとにしただけで使いこなせていません。

なので、今後はゾーンファイルを掘り下げていきたいですね。

[kubernetes][Secret] kubernetes の Secret を使って credentials を Pod にマウントする

credentials を秘匿しながら Pod にマウントしよう。

🤔 kubernetes の Secret とは?

まず kubernetes には ConfigMapSecret と、Pod に値を渡すための役割として似たようなものが2種類あります。

公式の ConfigMap の注意書きにも

注意: ConfigMapは機密性や暗号化を提供しません。保存したいデータが機密情報である場合は、ConfigMapの代わりにSecretを使用するか、追加の(サードパーティー)ツールを使用してデータが非公開になるようにしてください。

と記載されているので、

と使い分けたいところです。

🔍 ConfigMap と Secret の違い

kubernetes.io

に書いてあるとおりですが、

  • Secret は dataフィールドのすべてのキーの値は base64エンコードされた文字列である必要がある
  • Secret は tmpfs に保存されるのでディスクストレージに書き込まれない
    • 復元のリスクが少ない(と思われる)

機密情報を Secret にする根拠としては「tmpfs に保存されるのでディスクストレージに書き込まれない」からですかね〜。(base64 は根拠にならないよね)

📜 Secret を使って credentials を Pod にマウントする

ようやく本題です。

まずは手元の credentials.json をもとに Secret を作成します。

$ kubectl create secret generic {Secret名} --from-file={podに配置するファイル名}={credentials.json格納先}

# 例
$ kubectl create secret generic my-credentials --from-file=credentials.json=/home/test-user/credentials.json

次に Pod から Secret を参照します。

マニフェストファイルは以下のような形になります。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
      - name: my-key
        secret:
          secretName: my-credentials
      containers:
      - image: nginx
        name: sample-pod
        ports:
        - containerPort: 80
        volumeMounts:
        - name: my-key
          mountPath: /var/secret/

Secret 作成時の kubectl create secret generic {Secret名} で設定した Secret名 を secretName: の値として設定します。

mountPath が credentials の格納先ですね。

kubectl apply -f {マニフェストファイル} で起動して Pod の中に入って mountPath で指定したパスを確認すると credentials が格納されていることを確認できます。

おわりに

ConfigMap と Secret についてはなんとなく使える状態まで仕上がりました。

kubernetes 大変ですが、やってて損はないと信じて今後も頑張りましょう。💪

[soccer-cli] ターミナルで海外サッカーの試合結果を確認しよう

記事数稼ぐためにツール紹介の小ネタでお茶を濁します。

はじめに

クリスティアーノ・ロナウドマンチェスター・ユナイテッド復帰、セリエ A 昨シーズン MVP ロメル・ルカクチェルシー復帰、冨安健洋選手のアーセナル移籍など、今シーズンもプレミアリーグから目が離せませんね。

さて、皆さんターミナルでサッカーの試合結果や順位を確認したくなることありますよね?

そんなときにおすすめしたいのが soccer-cli です。

早速ターミナルで我らがチェルシーの試合結果と順位を確認してみましょう。

🎸 余談

OASIS ノエル・ギャラガー氏によるとチェルシーファンは人間以下のクズどもとのことです。😇

f:id:ghostzapper:20211115182637j:plain

⌨️ 環境

  • MacOS Big Sur 11.4
  • git version 2.32.0
  • Python 3.9.0
  • 動作確認日:2021-11-19

API トークン取得

football-data.orgAPI token を取得する必要があります。

こちらから名前とメールアドレスを入力して登録すると「Your free API-key for football-data.org」の件名のメールが届き、本文に

Your API token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

と記載されています。

soccer-cli インストール

soccer-cli の Build from source に従い soccer-cli をインストールします。

$ git clone https://github.com/architv/soccer-cli.git
$ cd soccer-cli
$ python setup.py install

⚽️ 使う

我らがチェルシーの試合結果と現在の順位を確認してみます。

まず、対象チームの試合結果を確認するには --team オプションでチームを指定します。

しかし、--team で指定するチームコードがわからないので、soccer --list でチームコードを確認してみます。

$ soccer --list
...(プレミアリーグ以外も出力されますが割愛します)
Premier League
AFC: Arsenal FC
AFCB: AFC Bournemouth
AVFC: Aston Villa FC
CFC: Chelsea FC
CRY: Crystal Palace FC
EFC: Everton FC
LCFC: Leicester City FC
LFC: Liverpool FC
MCFC: Manchester City FC
MUFC: Manchester United FC
NCFC: Norwich City FC
NUFC: Newcastle United FC
SCFC: Stoke City FC
SFC: Southampton FC
SUN: Sunderland AFC
SWA: Swansea City FC
THFC: Tottenham Hotspur FC
WAT: Watford FC
WBA: West Bromwich Albion FC
WHU: West Ham United FC
...(プレミアリーグ以外も出力されますが割愛します)

我らがチェルシーのチームコードは CFC であることがわかりました。

soccer --team CFC で試合結果を確認してみます。

初回実行時は API token の入力を求められるので、先程取得した API token を設定します。

(なお、ここで設定した API token は ~/.soccer-cli.ini で保持されるようです。)

$ soccer --team CFC
No API key found!
Please visit http://api.football-data.org/v2/ and get an API token.
Enter API key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2021-08-14  Chelsea FC                  3  vs  0          Crystal Palace FC
2021-08-22  Arsenal FC                  0  vs  2                 Chelsea FC
2021-08-28  Liverpool FC                1  vs  1                 Chelsea FC
2021-09-11  Chelsea FC                  3  vs  0             Aston Villa FC
2021-09-14  Chelsea FC                  1  vs  0  FK Zenit Sankt-Petersburg
2021-09-19  Tottenham Hotspur FC        0  vs  3                 Chelsea FC
2021-09-25  Chelsea FC                  0  vs  1         Manchester City FC
2021-09-29  Juventus FC                 1  vs  0                 Chelsea FC
2021-10-02  Chelsea FC                  3  vs  1             Southampton FC
2021-10-16  Brentford FC                0  vs  1                 Chelsea FC
2021-10-20  Chelsea FC                  4  vs  0                   Malmö FF
2021-10-23  Chelsea FC                  7  vs  0            Norwich City FC
2021-10-30  Newcastle United FC         0  vs  3                 Chelsea FC
2021-11-02  Malmö FF                    0  vs  1                 Chelsea FC
2021-11-06  Chelsea FC                  1  vs  1                 Burnley FC

今シーズンのチェルシーの試合結果の一覧が取得できました。

マンチェスター・シティに悔しい敗戦を喫しましたが、リーグ戦はその後は無敗です。

さて、気になる順位は…

$ soccer --standings --league=PL
POS     CLUB                              PLAYED        GOAL DIFF     POINTS    
1       Chelsea FC                        11            23            26
2       Manchester City FC                11            16            23
3       West Ham United FC                11            10            23
4       Liverpool FC                      11            20            22
5       Arsenal FC                        11            0             20
6       Manchester United FC              11            2             17
7       Brighton & Hove Albion FC         11            0             17
8       Wolverhampton Wanderers FC        11           -1             16
9       Tottenham Hotspur FC              11           -7             16
10      Crystal Palace FC                 11            1             15
11      Everton FC                        11            0             15
12      Leicester City FC                 11           -2             15
13      Southampton FC                    11           -2             14
14      Brentford FC                      11           -1             12
15      Leeds United FC                   11           -7             11
16      Aston Villa FC                    11           -6             10
17      Watford FC                        11           -7             10
18      Burnley FC                        11           -6             8
19      Newcastle United FC               11           -12            5
20      Norwich City FC                   11           -21            5

11月19日現在チェルシープレミアリーグ1位です!!!🎉🎉🎉

おわりに

これでターミナルから試合結果を確認できるようになりました。

基本的にリアルタイム観戦、FotMob等のアプリ、Twitter で試合結果を確認することが多いと思いますが、ターミナルで贔屓チームの試合結果や順位をみてモチベーションをあげる際には soccer-cli を活用してみてください。

[kubernetes] Error registering network: failed to acquire lease: node "ノード名" pod cidr not assigned

flannel のエラー解消についてメモ残しておきます。

事象

kube-flannel が CrashLoopBackOff になるので、kubectl logs でログを確認すると表題のエラーが出力されていた。

対策

github.com

の通り、/etc/kubernetes/manifests/kube-controller-manager.yaml のコマンドに

  • --allocate-node-cidrs=true
  • --cluster-cidr=10.244.0.0/16

を追加して systemctl restart kubelet 実行することで kube-flannel のステータスが無事に Running になりました。

そもそも、kubeadm init の際に --pod-network-cidr で CIDR を設定していれば問題なかった?