20200124のNode.jsに関する記事は9件です。

CLI のテンプレートプロジェクト by node and TypeScript

node で CLI(Command Line Interface) を開発する機会が数回あって、せっかくなのでテンプレートプロジェクトとしてまとめてみた。

テンプレートプロジェクト

必要なモノ

  • nodejs: v11.13.0+
  • typescript: v3.7.3+

試し方

  1. 上記のリポジトリを Clone する
  2. リポジトリのディレクトリに cd して npm ci する
  3. npm run build する
  4. npm link する
  5. source ~/.bash_profile を行うかまたはターミナルを再起動する

これでどのディレクトリでも my-great コマンドが使用できるようになる。

$ my-great hello -f Echizen -s Ooka -a 42

Hello Echizen Ooka.
You're 42 years old.
$ my-great something wrong param

Command Line Interface for My great service

  Sample for CLI.

Commands

  my-great hello -f <first_name> -s <second_name>   Say Hello.
  my-great version                                  Show version.

アンインストール

npm uninstall -g @amay/my-great-cli

要点

コマンドライン引数の解析と使用方法の表示

yargs とかいろいろあったけど、自分的に使いやすかったのでこれを選択。

プログラム構成

my-great hello -f <first_name> -s <second_name>   Say Hello.
my-great version                                  Show version.

のように、第一引数を「コマンド」とし、第2引数以降をそのコマンド専用の引数群としたかったので、index.ts で第一引数のみを parse して取得し、コマンド毎に command-xxxx.ts へ委譲している。

command-line-args では commandLineArgs(this.paramDef, { partial: true })partial:true を設定すると、引数定義(paramDef) に存在しない引数があっても無視する。

cli コマンド名

cliコマンド名 my-greatpackage.jsonbin: で指定している。

package.json

{
  "name": "@amay/my-great-cli",
<省略>
  "bin": {
    "my-great": "build/index.js"
  },
<省略>

ビルドされた ./build/index.js を指すように設定している。
ちなみに npm run 経由で node を実行する場合は、引数の前に -- を付ける(例: node ./build/index.js -- version)。

必須引数のチェック

command-line-args では 引数の必須チェックを自力で行わなければならない ようなので、定義体の paramDefrequire: boolean を追加し、パースした実際の引数である XxxxConfigrequire = true な項目が含まれているかをチェックするようにした。

// Valid require params
const requiresNotSetted = this.paramDef
  .filter(x => x.require)
  .filter(x => cfg[x.name] == null)
  .map(x => `--${x.name}`);

if (requiresNotSetted.length > 0) {
  console.log(`Param: ${requiresNotSetted.join(' ')} is required.`);
  console.log(`------------------------------------`);
  this.usage[1].optionList = this.paramDef;
  const usg = commandLineUsage(this.usage)
  console.log(usg);  
  return -1;
}

kebab-case VS camelCase VS snake_case

コマンドの引数は kebab-case がデファクトスタンダードの模様。
command-line-args では commandLineArgs(this.paramDef, { camelCase: true }) とすると、--first-name に渡された引数を、firstName 変数に格納してくれる。

が、前述の必須引数のチェックが(定義体と実体の変数名が異なるため)正しく機能しなくなるので妥協案として snake_case の --first_name を採用している。

コマンドを追加するには

  1. index.tsCommandTypexxxx を増やす
  2. command-xxx.ts(CommandXxxx クラス) を作る
  3. index.tscommandMap に追加する
  4. mainUsage になんか書く

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jest実行時にserverless.ymlのfunctionsのenvironmentを読み込む

jest-environment-serverless を利用してserverless.ymlから環境変数を読み込みます。
.envを読み込む以外の方法を試してみたかった)

手順

パッケージのインストール

必要なパッケージをインストールします。

$ npm install serverless jest jest-environment-serverless

プロジェクトの作成

$ npx serverless create --template aws-nodejs

設定

serverless.ymlに読み込む環境変数の名称と値を記述します。

serverless.yml
service: sample

provider:
  name: aws
  runtime: nodejs12.x

functions:
  hello:
    handler: handler.hello
    environment:

      # これを読み込みます
      SAMPLE_VALUE: Sample

package.jsonにjestの設定を追加します。

package.json
{
  :
  "jest": {
    "testEnvironment": "jest-environment-serverless",
  }
}

テストの作成

環境変数を console.log で出力する処理を記述します。

__test__/handler.spec.js
describe('Sample', () => {
  it('check env', () => {

    // 表示
    console.log(process.env.SAMPLE_VALUE);

    expect('');
  });
});

テストの実行

$ npx jest
 PASS  __test__/handler.spec.js (10.71s)
  Sample
    ✓ check env (67ms)

  console.log __test__/handler.spec.js:5
    Sample

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        16.159s
Ran all test suites.

環境変数の値(Sample)を読み込むことができました。

おまけ

環境変数の名称と値をAPIから取得することもできます。

__test__/handler.spec.js
describe('Sample', () => {
  it('check env', () => {

    // serverless.yml の functions.hello.environment の読み込み
    const envVars = ServerlessWrapper.getEnv('hello');

    // 出力
    console.log(envVars);

    expect('');
  });
});
$ npx jest
 PASS  __test__/handler.spec.js (8.714s)
  Sample
    ✓ check env (68ms)

  console.log __test__/handler.spec.js:8
    { SAMPLE_VALUE: 'Sample' }

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        13.556s
Ran all test suites.

環境変数の名称と値({ SAMPLE_VALUE: 'Sample' })を出力することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jest実行時にserverless.ymlの環境変数を読み込む

jest-environment-serverless を利用してserverless.ymlから環境変数を読み込みます。
.envを読み込む以外の方法を試してみたかった)

手順

パッケージのインストール

必要なパッケージをインストールします。

$ npm install serverless jest jest-environment-serverless

プロジェクトの作成

$ npx serverless create --template aws-nodejs

設定

serverless.ymlに読み込む環境変数の名称と値を記述します。

serverless.yml
service: sample

provider:
  name: aws
  runtime: nodejs12.x

functions:
  hello:
    handler: handler.hello
    environment:

      # これを読み込みます
      SAMPLE_VALUE: Sample

package.jsonにjestの設定を追加します。

package.json
{
  :
  "jest": {
    "testEnvironment": "jest-environment-serverless",
  }
}

テストの作成

環境変数を console.log で出力する処理を記述します。

__test__/handler.spec.js
describe('Sample', () => {
  it('check env', () => {

    // 表示
    console.log(process.env.SAMPLE_VALUE);

    expect('');
  });
});

テストの実行

$ npx jest
 PASS  __test__/handler.spec.js (10.71s)
  Sample
    ✓ check env (67ms)

  console.log __test__/handler.spec.js:5
    Sample

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        16.159s
Ran all test suites.

環境変数の値(Sample)を読み込むことができました。

おまけ

環境変数の名称と値をAPIから取得することもできます。

__test__/handler.spec.js
describe('Sample', () => {
  it('check env', () => {

    // serverless.yml の functions.hello.environment の読み込み
    const envVars = ServerlessWrapper.getEnv('hello');

    // 出力
    console.log(envVars);

    expect('');
  });
});
$ npx jest
 PASS  __test__/handler.spec.js (8.714s)
  Sample
    ✓ check env (68ms)

  console.log __test__/handler.spec.js:8
    { SAMPLE_VALUE: 'Sample' }

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        13.556s
Ran all test suites.

環境変数の名称と値({ SAMPLE_VALUE: 'Sample' })を出力することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang, Node.js, Kotlin, Swift で ECDSA

はじめに

楕円曲線DSA (ECDSA) は楕円曲線暗号を利用した電子署名方式で、通信のセキュリティ確保のために広く使われています。様々なプログラミング言語の標準的なライブラリでサポートされているため、言語をまたいで利用することができます。

ただし、実際に言語をまたいで署名生成 & 検証をしようとしてみると API の違いやフォーマット方式によりハマること多々あったため、書き方をまとめておくことにしました。

この記事に書かれていること

Golang, Node.js, Kotlin, Swift での(できる限り)標準ライブラリを利用した キーペア生成、署名生成・検証方法を記載します。

以降の記述は基本的に ECDSA を前提に書かれています。

全体的に、エラーハンドリングは省略しているので注意してください。

この記事に登場するキーワード

キーワード 概要
EC 楕円曲線、もしくは楕円曲線暗号のこと。
P-256 利用する楕円曲線の種類。キーペア生成、署名生成・検証時にパラメータとして指定します。NIST で規定されているものとしては他には P-384 などがあります。
キーペア 秘密鍵とそれに対応する公開鍵のペアのこと。 ECDSA では秘密鍵は 1 つの整数, 公開鍵は楕円曲線上の点を表す 2 つの整数で構成されます。
SHA256 256 ビット(32 バイト)のハッシュ値を生成するハッシュ関数。
r, s ECDSA の署名値。 r, s のどちらも整数値です。
ASN.1 データ構造を定義するための標準インターフェイス記述言語(wikipedia のグーグル翻訳まま)
DER エンコード ASN.1 の標準符号化規則の一つ。秘密鍵・公開鍵や署名データのシリアライズに利用する。
PEM 形式 DER エンコードの結果のバイナリデータを base64 エンコードして -----BEGIN [TYPE]-----, -----END [TYPE]----- で囲ったもの。

Golang, Node.js, Kotlin, Swift での ECDSA

それぞれの言語でキーペア生成、署名生成、署名検証をおこないます。

公開鍵は PEM 形式, 署名データは ASN.1 エンコードした結果のバイナリデータを Base64 形式で出力します。

公開鍵を PEM 形式としたのはただ単によく見かけるから、という理由からです。(検証をおこなうという意味では DER エンコード されたバイナリデータを Base64 もしくは Hex 形式で出力するだけで十分だと、後から気づきました...)

Golang

Golang は標準ライブラリが充実しているため、さほど苦労することなく扱うことができます。

参考: golang ecdsa パッケージ

以下のコードは version: 1.13.3 で動作を確認しています。

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/sha256"
    "crypto/x509"
    "encoding/asn1"
    "encoding/base64"
    "encoding/pem"
    "fmt"
    "math/big"
    "os"
)

const (
    msg = "Hello, ECDSA!"

    targetPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----`
    targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="
)

type rawSignature struct {
    R, S *big.Int
}

func main() {
    sign()
    verify()
}

func sign() {
    fmt.Println("================================ start signing ================================\n")

    // P-256 をパラメータに指定してキーペアを生成
    privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    publicKey := privateKey.PublicKey

    // 秘密鍵の整数値を出力
    fmt.Printf("private key is %d\n", privateKey.D)
    fmt.Println()

    // 秘密鍵を SEC 1, ASN.1 DER エンコード
    sec1FormPrivateKey, _ := x509.MarshalECPrivateKey(privateKey)
    // PEM 形式で出力
    _ = pem.Encode(os.Stdout, &pem.Block{
        Type:    "EC PRIVATE KEY",
        Headers: nil,
        Bytes:   sec1FormPrivateKey,
    })
    fmt.Println()

    // 公開鍵の整数値のペアを出力
    fmt.Printf("public key is (x: %d, y: %d)\n", publicKey.X, publicKey.Y)
    fmt.Println()

    // 公開鍵を PKIX, ASN.1 DER エンコード
    pkiFormPublicKey, _ := x509.MarshalPKIXPublicKey(&publicKey)
    // PEM 形式で出力
    _ = pem.Encode(os.Stdout, &pem.Block{
        Type:    "PUBLIC KEY",
        Bytes:   pkiFormPublicKey,
    })
    fmt.Println()

    // メッセージのハッシュ値を取得
    hash := sha256.Sum256([]byte(msg))

    // 署名生成
    r, s, _ := ecdsa.Sign(rand.Reader, privateKey, hash[:])
    fmt.Printf("signature: (r: %d, s: %d)\n", r, s)

    // 署名を ASN.1 エンコード
    asn1Signature, _ := asn1.Marshal(rawSignature{r, s})
    // Base64 形式で出力
    fmt.Printf("asn1 base64 encoded signature: %s\n\n", base64.StdEncoding.EncodeToString(asn1Signature))
}

func verify() {
    fmt.Println("================================ start verification ================================\n")

    // PEM ブロックを取得
    block, _ := pem.Decode([]byte(targetPublicKeyPEM))
    if block == nil || block.Type != "PUBLIC KEY" {
        panic("invalid public key pem data")
    }

    publicKey, _ := x509.ParsePKIXPublicKey(block.Bytes)

    asn1Signature, _ := base64.StdEncoding.DecodeString(targetSignature)

    var sig rawSignature
    asn1.Unmarshal(asn1Signature, &sig)

    // メッセージのハッシュ値を取得
    hash := sha256.Sum256([]byte(msg))

    // 署名検証
    valid := ecdsa.Verify(publicKey.(*ecdsa.PublicKey), hash[:], sig.R, sig.S)
    fmt.Printf("signature was verified: %t\n", valid)
}

出力結果:

================================ start signing ================================

private key is 86406366532313532520773863615456167011096149492537621067924417740068666801996

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIL8IRTYAiQtNKvAMDxMtucbcrF40K9lPEJr1eFG3JP9MoAoGCCqGSM49
AwEHoUQDQgAECLCYIbdaHGU4phHj28OXTy04YcKD2wsL0fqbSCP4pMQIghdIGvCd
jwZ9nntlLfpdY/d6Wnp/GcwEosAYSCQFjg==
-----END EC PRIVATE KEY-----

public key is (x: 3930517846499297788187286115327721111010190045004457380847771725537278993604, y: 3848353591206560331525005698968473002174715783271413427180613072969827353998)

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECLCYIbdaHGU4phHj28OXTy04YcKD
2wsL0fqbSCP4pMQIghdIGvCdjwZ9nntlLfpdY/d6Wnp/GcwEosAYSCQFjg==
-----END PUBLIC KEY-----

signature: (r: 47681670912106433244589806297495994583210073185120436994285114290076291204903, s: 93735733074916934648422947032629918680486834787857816571963967793396929295074)
asn1 base64 encoded signature: MEUCIGlq3o447llhyWn8G/p9GN3e1NMDC7zZm21OUIj+RIcnAiEAzzyLeJtUyecBmFvxA/bV0uXEuZ5B1fN4xyEcilv8cuI=

================================ start verification ================================

signature was verified: true

Node.js

Node.js も標準ライブラリを利用できますが、秘密鍵・公開鍵や署名はエンコードされた情報はとれるものの、整数値を直接取得することはできないようです。
(DER, PEM デコードするライブラリは数多く存在していたので、必要があれば簡単に取得はできそうです)

参考: Node.js crypto モジュール

以下のコードは version: 12.14.1 で動作を確認しています。

const crypto = require("crypto");

const msg = "Hello, ECDSA!";

const targetPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----`
const targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="

main();

function main() {
  sign();
  verify();
}

function sign() {
  console.info("================================ start signing ================================\n")
  // P-256 をパラメータに指定してキーペアの生成
  const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
    namedCurve: "P-256",
  });

  // 秘密鍵を SEC 1, ASN.1 DER エンコード & PEM 形式で出力
  console.info(privateKey.export({
    type: "sec1",
    format: "pem",
  }));

  // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力
  console.info(publicKey.export({
    type: "spki",
    format: "pem",
  }));

  // 署名生成
  const signer = crypto.createSign("SHA256"); // ハッシュ関数を指定
  signer.update(msg);
  signer.end();
  const signature = signer.sign(privateKey, "base64");

  // 署名は ASN.1 エンコード され、 Base64 形式で出力されている
  console.info(`asn1 base64 encoded signature: ${signature}\n`);
}

function verify() {
  console.info("================================ start verification ================================\n")
  const publicKey = crypto.createPublicKey(targetPublicKeyPEM)

  // 署名検証
  const verifier = crypto.createVerify("SHA256"); // ハッシュ関数を指定
  verifier.update(msg);
  verifier.end();
  const valid = verifier.verify(publicKey, targetSignature, "base64");
  console.info(`signature was verified: ${valid}`);
}

出力結果:

================================ start signing ================================

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILtZOGwW/gh1geY6yu4bfEuzSrwa4BJnuE37gwAsZb/IoAoGCCqGSM49
AwEHoUQDQgAEPu/QDDiV4ry2T4Ki9r9VIXgvLH09x/4J32HVdOXUlnVQegD52191
DQJ3Q2H41MTnD+uZdlGnQAUkgYSRt1A7jw==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPu/QDDiV4ry2T4Ki9r9VIXgvLH09
x/4J32HVdOXUlnVQegD52191DQJ3Q2H41MTnD+uZdlGnQAUkgYSRt1A7jw==
-----END PUBLIC KEY-----

asn1 base64 encoded signature: MEQCIBdoySVlQAjUSVb61H+7FzPI3+b4m4Agy62MO6/vVFkEAiAWPRjje4g/6/LpY/dUg+4dteQRK/qMI/kn3s0zIJbrTQ==

================================ start verification ================================

signature was verified: true

Kotlin (Android)

Android 開発においては Keystore システムでサポートされる API を利用することができます。
鍵データの管理を委譲できるのは大きなメリットである反面、秘密鍵の情報にアクセスする API が用意されていないようです。
例えば、独自にバックアップを取るなど、特殊なことをする場合は工夫が必要そうです。

署名については Node.js 同様に ASN.1 エンコード後のバイナリデータが返されます。

参考: Android Keystore システム

以下のコードは android SDK version: 29, kotlin version: 1.3.61 で動作を確認しています。

(PEM 形式を扱う箇所はかなり強引な書き方をしています。適切なライブラリを使ったほうが良いです)

package com.example.ecdsa

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.interfaces.ECPublicKey
import java.security.spec.X509EncodedKeySpec

class MainActivity : AppCompatActivity() {

    companion object {
        const val msg = "Hello, ECDSA!"

        const val targetPublicKeyPEM = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExW7riGWvlxmRofxQNuRhsF9anb+8\
F/1NRGzZziCC/utzFMXSg9YwzaRb0Yw+K2n0+1IkWH7lQT9j4DZhF6Npfg==
-----END PUBLIC KEY-----"""
        const val targetSignature = "MEUCICzZzFaPemBrWBLNlbbEG+CyXEdAbum9YnOe7lK0rNonAiEA8p1QN/1VcuWRvrPSDnELXedMfiP1FPtk/dmP3Sf/7gA="
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sign()
        verify()
    }

    private fun sign() {
        println("================================ start signing ================================")

        val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
            "ECPrivateKey",
            KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
        ).run {
            setDigests(KeyProperties.DIGEST_SHA256) // ハッシュ関数を指定
            build()
        }

        // キーペア生成
        val keyPair = KeyPairGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_EC,
            "AndroidKeyStore"
        ).let {
            it.initialize(parameterSpec)
            it.generateKeyPair()
        }

        val publicKey = keyPair.public as ECPublicKey

        // 秘密鍵は KeyStore 内で管理される前提であるためか、内部のデータにアクセスする API が見当たらなかった

        // 公開鍵の整数値のペアを出力
        println("public key is (x: ${publicKey.w.affineX}, y: ${publicKey.w.affineY})")

        // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力 (かなり強引...)
        println("-----BEGIN PUBLIC KEY-----")
        Base64.encodeToString(publicKey.encoded, Base64.DEFAULT).trim().chunked(64).forEach {
            println(it.replace("\n", "\\n"))
        }
        println("-----END PUBLIC KEY-----")

        // 署名生成
        val signature = Signature.getInstance("SHA256withECDSA").run {
            initSign(keyPair.private)
            update(msg.toByteArray())
            sign()
        }

        // 署名は ASN.1 エンコード されているため、Base64 形式で出力
        println(String.format(
            "asn1 base64 encoded signature: %s",
            Base64.encodeToString(signature, Base64.DEFAULT).trim().replace("\n", "\\n")
        ))

    }

    private fun verify() {
        println("================================ start verification ================================\n")
        val signature = Base64.decode(targetSignature, Base64.DEFAULT)

        val spec = X509EncodedKeySpec(
            Base64.decode(
                targetPublicKeyPEM.trim()
                    .replace("-----BEGIN PUBLIC KEY-----\n", "")
                    .replace("-----END PUBLIC KEY-----", ""),
                Base64.DEFAULT
            )
        )

        val pubKey = KeyFactory.getInstance("EC").generatePublic(spec)
        val valid: Boolean = Signature.getInstance("SHA256withECDSA").run {
            initVerify(pubKey)
            update(msg.toByteArray())
            verify(signature)
        }
        println("signature was verified: $valid")
    }
}

出力結果:

I/System.out: ================================ start signing ================================
I/System.out: public key is (x: 82167081552335602286200448410416710140443532428724715799809599812531686098238, y: 51175083263095894836653222459656189252260595373682081626878418494739276551753)
I/System.out: -----BEGIN PUBLIC KEY-----
I/System.out: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtajriseSquTJ0f2EQZQli7czMp6v
I/System.out: pHAHTW2Tq25e\nRT5xJBIYA6AgPrEdKuPtVcgamRFSKE82w1YEdxMBQCrWSQ==
I/System.out: -----END PUBLIC KEY-----
I/System.out: asn1 base64 encoded signature: MEQCIBeZSNHoN3VD7laNSDl0CGGgjrqGp50RCG6azqXmjrR/AiBKUHXJyXNLmIUCPwv33zvRfwfr\n83mfi5cJOV5Zf2QVgQ==
I/System.out: ================================ start verification ================================
I/System.out: signature was verified: true

Swift

Swift ではネイティブ API の扱いが煩雑だったため、外部ライブラリを利用しました...。

利用ライブラリ: BlueECC

ネイティブ API の扱いについてはこちらの記事が参考になります。

以下のコードは iOS: 13.1, Swift: 5.1.3 で動作を確認しています。

let msg = "Hello, ECDSA!"
let targetPublicKeyPEM = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV4ZTwqTk5Sd5no5ibjjTXSTZCHQV
vpe4qdp2rodC\nMdgCmdl/ZyuCpg/6PH6arDviA2HYVR13/ssin6/Etp93RQ==
-----END PUBLIC KEY-----
"""
let targetSignature = "MEUCIAU0/hEz2+RRIwzXkau64jfmUSbFoFMltXEGtl3LHlZHAiEAqak5H/QdRlheYpSpfTGTInQs\nWOUq0mDavgif8+X5uAM="

func ecdsa() {
    sign()
    verify()
}

func sign() {
    print("================================ start signing ================================\n")

    // P-256 をパラメータに指定して秘密鍵を生成
    let privateKey = try! ECPrivateKey.make(for: .prime256v1)

    // 秘密鍵を SEC 1, ASN.1 DER エンコード & PEM 形式で出力
    print(privateKey.pemString)
    print()

    // 公開鍵を PKIX, ASN.1 DER エンコード & PEM 形式で出力
    let publicKey = try! privateKey.extractPublicKey()
    print(publicKey.pemString)
    print()

    // 署名生成
    let signature = try! msg.sign(with: privateKey)

    // 署名を ASN.1 エンコードしたものを Base64 形式で出力
    print("asn1 base64 encoded signature: \(signature.asn1.base64EncodedString())\n")
}

func verify() {
    print("================================ start verification ================================\n")

    let publicKey = try! ECPublicKey(key: targetPublicKeyPEM)
    let signature = try! ECSignature.init(
        asn1: Data(base64Encoded: targetSignature,
                   options: Data.Base64DecodingOptions.ignoreUnknownCharacters)!)
    let valid = signature.verify(plaintext: msg, using: publicKey)
    print("signature was verified: \(valid)");
}

出力結果:

================================ start signing ================================

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILu54xIBwH3Vd45Fgx9yCCgOTynjxvIMh+PnL86qOx7roAoGCCqGSM49
AwEHoUQDQgAENa6T19s23zEVLBvUYyVbZjRGPqhUkYJcv7SA8J05F8Vql7Aw9GR+
G/uxgYFqe6j1MYQ2tPF9MN32cc+xG2OCUw==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENa6T19s23zEVLBvUYyVbZjRGPqhU
kYJcv7SA8J05F8Vql7Aw9GR+G/uxgYFqe6j1MYQ2tPF9MN32cc+xG2OCUw==
-----END PUBLIC KEY-----

asn1 base64 encoded signature: MEYCIQD+fGwKEVX8aTzdbRgpEy9/nWHAsAw0JQXAKH4IJo4uEgIhAJKfFkN1Akl18rrnyfwwsqMa2dWwWXLbX1yRaHLZwdRG

================================ start verification ================================

signature was verified: true

まとめ

標準化をされている技術ではあるものの、各言語ごとに書き方の癖があってハマりがちな処理を並べました。デバッグのしづらさはデジタル署名のセキュリティの高さの裏返しではあるものの、ハマってしまった方にこの記事が少しでも役にたてば嬉しいです。

記載・認識ミス、もっと良い書き方などありましたら、ご指摘お願いします。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初学者向け]AzureFunctions(TimerTrigger)+nodejsをはじめてみよう

はじめに

社内勉強用のコンテンツをそのまま公開しました

Azure Functionsで処理を書きたいんだけど何から勉強すればいいんだろう・・・という方向けに、AzureFunctionsのTimerTriggerを使ってURL監視するサービスを一緒に作っていきたいと思います。

こんなことを勉強していきます

  • 必要なソフトウェアのインストール
  • nodejsの超基本中のキホン
  • AzureFunctionsへのデプロイ
  • AzureFunctionsをローカルで動かしてみよう
  • モジュールの活用
  • AzureFunctionsにおける環境変数の活用
  • 学んだことを使ってURL監視サービス(証明書チェック機能付き)を作ってみる

じゃあ早速やっていきましょう

必要なもの

  • Azureアカウント

必要なもののインストール

nodejs(LTS)のインストール

yarn(パッケージマネージャ)のインストール

Azure cliのインストール

Azure Functions Core Toolsのインストール

必要に応じてインストール

お好みのエディタ
私のお好みはIntelliJですが、VisualStudioCodeは割ととっつきやすいので使ってみてください。

まずはnodejsの基本中のキホンから

azure-functions-samplesというディレクトリを作ってその中にindex.jsを作って実行してみましょう

index.js
function hello() {
    console.log("test");
}
hello();

実行してみる

node index.js

yarnで実行してみよう

yarn initコマンドを実行することでpackage.jsonのひな型を作ります。とりあえずは何も入力しなくても大丈夫です。

azure-functions-samples> yarn init
yarn init v1.21.1
question name (fetch-timer):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
Done in 13.50s.
  • package.jsonではライブラリの依存関係を記述したりするのですが、それは後で説明するとして、package.jsonのscriptsの部分に書けば好きなコマンドを実行できます。
package.json
{
  "name": "simple-timer-function",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "node index.js"
  },
  "author": "",
  "main": "index.js",
  "license": "MIT"
}

package.jsonのscriptsの部分を書いておくことでyarn startすると実行することができる。

まぁここまでは大丈夫ですよね。

AzureFunctionsについて

AzureFunctionsにはいろいろな機能があります。

  • HTTPアクセスを受け取って処理を実行する
  • CosmosDBにデータが入ったらTriggerとして起動する
  • BlobStorageにデータが格納されたら処理を実行する
  • 定期的に実行する

また、AWSLambdaのように必要に応じて随時動かしておくこともできますし、常時動かしておくこともできます。
AWS Lambdaでは随時動かした場合、初回起動に時間がかかったりしますが、これはAzureFunctionsでも同様です。許容できない場合は定期的にポーリングしておくことや常時起動にしておくことで回避します。

今回は、定期的にURLを監視したいのでTimerTrigger(定期的に実行する)を使っていきます。

シンプルなTimerTrigger

simple-timerディレクトリを作ります。

$ mkdir simple-timer

timerディレクトリの下でfunctionのひな型を作ります。

  • 言語はJavaScriptを選択してください
azure-functions-samples\simple-timer> func new
Select a language: Starting from 2.0.1-beta.26 it's required to set a language for your project in your settings
'node' has been set in your local.settings.json
Select a template: Timer trigger
Function name: [TimerTrigger] simple-timer
Writing C:\Users\uzres\git\azure-functions-samples\simple-timer\simple-timer\index.js
Writing C:\Users\uzres\git\azure-functions-samples\simple-timer\simple-timer\readme.md
Writing C:\Users\uzres\git\azure-functions-samples\simple-timer\simple-timer\function.json
The function "simple-timer" was created successfully from the "Timer trigger" template.

せっかちなので10秒に1回起動するようにしましょう

function.json
{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "*/10 * * * * *"
    }
  ]
}

デプロイ

CloudShellを起動します。

image.png

CloudShellが起動出来たらFunctionsを作っていきます。
<適当な文字列>にはuzreskなど自身のものとわかるような名前にしておきましょう。

export RESOURCE_SUFFIX=<適当な文字列>

export RG_NAME=rg-$RESOURCE_SUFFIX
export STORAGE_NAME=azfunsamples$RESOURCE_SUFFIX
export FUNCTION_NAME=azfun-samples-$RESOURCE_SUFFIX
export LOCATION=japaneast

az group create --name $RG_NAME --location $LOCATION

az storage account create \
  --name $STORAGE_NAME \
  --location $LOCATION \
  --resource-group $RG_NAME \
  --sku Standard_LRS

az functionapp create \
  --name $FUNCTION_NAME \
  --storage-account $STORAGE_NAME \
  --consumption-plan-location $LOCATION \
  --resource-group $RG_NAME

FunctionsができたらAzureの画面で確認しておきます。

image.png

今度はローカルに戻ってデプロイしていきます。

azure-functions-samples\simple-timer> func azure functionapp publish azfun-samples-xxxxxxx
Getting site publishing info...
Creating archive for current directory...
Uploading 1.49 KB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Syncing triggers...
Functions in azfun-samples-xxxxxxx:
    simple-timer - [timerTrigger]

Azureのポータルに戻り、関数を選択して「統合」というところを見ると変更したスケジュールが反映されているのがわかると思います。(少し薄いです)

image.png

「監視」を見ると10秒おきに起動していることと、さらに進むと出力したログが出ていることを確認できます。

image.png

ローカルで実行する

毎回デプロイしているのは時間の無駄なのでローカルでAzureFunctionsを実行してみましょう。
ローカルで動かすための設定はlocal.settings.jsonに記載します

local.settings.json
{
  "IsEncrypted": true,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "ConnectionStrings": {}
}

local.settings.jsonに何を書けばいいのかは実は考える必要はありません。
コマンドを一発実行するだけでサーバから設定を取得することができるのです。

func azure functionapp fetch-app-settings <functionAppName>

実行するとlocal.settings.jsonが以下のように変更されるはずです。

local.settings.json
{
  "IsEncrypted": true,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "FUNCTIONS_EXTENSION_VERSION": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "AzureWebJobsStorage": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "AzureWebJobsDashboard": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_NODE_DEFAULT_VERSION": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_CONTENTSHARE": "",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_RUN_FROM_PACKAGE": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "ConnectionStrings": {}
}

それではローカル環境で実行してみましょう。
VSCodeのターミナルを開いて以下のコマンドを実行します。

func start

10秒間隔で実行にしていた場合には下記のようにいつ動くのかが書かれたメッセージの後に、時間が来たら実行結果が表示されているはずです。

[2020/01/24 4:27:44] The next 5 occurrences of the 'simple-timer' schedule (Cron: '0,10,20,30,40,50 * * * * *') will be:
[2020/01/24 4:27:44] 01/24/2020 13:27:50+09:00 (01/24/2020 04:27:50Z)
[2020/01/24 4:27:44] 01/24/2020 13:28:00+09:00 (01/24/2020 04:28:00Z)
[2020/01/24 4:27:44] 01/24/2020 13:28:10+09:00 (01/24/2020 04:28:10Z)
[2020/01/24 4:27:44] 01/24/2020 13:28:20+09:00 (01/24/2020 04:28:20Z)
[2020/01/24 4:27:44] 01/24/2020 13:28:30+09:00 (01/24/2020 04:28:30Z)

モジュールを追加する

先ほどはひな型をそのまま動かしてみましたが今度は外部のリソースにHTTPSアクセスし、それをログに出力してみます。
外部のリソースにHTTPSアクセスするコードを一から頑張って自分で書く必要はありません。ライブラリをローカルにインストールして、そのライブラリを使ってHTTPSアクセスしてみましょう。

azure-functions-samplesにfetch-timerというディレクトリを作成します。

先ほどと同じようにfunctionのひな型を作りましょう

PS C:\Users\uzres\git\azure-functions-samples\fetch-timer> func new
'node' has been set in your local.settings.json
Select a template: Timer trigger
Function name: [TimerTrigger] fetch-timer
Writing C:\Users\uzres\git\azure-functions-samples\fetch-timer\fetch-timer\index.js
Writing C:\Users\uzres\git\azure-functions-samples\fetch-timer\fetch-timer\readme.md
Writing C:\Users\uzres\git\azure-functions-samples\fetch-timer\fetch-timer\function.json
The function "fetch-timer" was created successfully from the "Timer trigger" template.

yarn initしてpackage.jsonのひな型を作ってみましょう

package.jsonを確認してみましょう。

package.json
{
  "name": "fetch-timer",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
}

http/httpsアクセスするモジュールaxiosをインストールします。

PS C:\Users\uzres\git\azure-functions-samples\fetch-timer> yarn add axios
・・・
Done in 0.58s.

再度package.jsonを確認するとaxiosが追加されていることがわかります。

package.json
{
  "name": "fetch-timer",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "axios": "^0.19.2"
  }
}

また、node_modulesというディレクトリができていて、その中にライブラリが入っていることが確認できると思います。
package.jsonにはライブラリの依存関係が書かれており、package.jsonがある状態でyarn initすると必要なライブラリが自動でインストールされるようになっています。
yarn add xxxxとすることでpackage.jsonに自動で書き込まれますが、パッケージのバージョンを固めたりするために手で編集することもあります。

とりあえずpackage.jsonのパッケージで気をつけておきたいバージョン記法のキャレット(^)とチルダ(~)的なお話を読んでふーん。くらいで覚えておきましょう。

HTTPSアクセスしてみよう

Google Books APIにアクセスして「Azure」がタイトルにつくデータを取得してみます。

こちらにアクセスしてみましょう。

https://www.googleapis.com/books/v1/volumes?q=Azure

qの後ろに検索する文字列を入れることで書籍のデータをjson形式で取得することができます。

データにアクセスして、ステータスコードとjsonデータを取得するコードを書いてみます。
(コピペしないで確認しながら写経しましょう)

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const result = await axios(`https://www.googleapis.com/books/v1/volumes?q=Azure`)
        .then(response => {
            context.log('status code: ', response.status);
            context.log('data', response.data);
        })
};

ローカルで実行して問題なければAzureにデプロイして実行してみましょう。

問題①

レスポンスのデータの中にはitemsという書籍のデータが入っている配列があります。
その数を出力してみてください。
答えは次の章にありますが、まずは見ないでやってみましょう。

タイトルだけ抽出してみる

JSONを解析してタイトルだけ表示してみましょう。for文を使ってくるくる回して表示してみます

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const searchValue = "Azure";
    const result = await axios(`https://www.googleapis.com/books/v1/volumes?q=${searchValue}`)
        .then(response => {
            const items = response.data.items;
            for (i=0; i<items.length; i++) {
                context.log(items[i].volumeInfo.title);
            }
        }).catch(error => {
            context.log('error: ' + error);
        });
};

for文でもよいのですが、forEachを使う書き方もあります。

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const searchValue = "Azure";
    const result = await axios(`https://www.googleapis.com/books/v1/volumes?q=${searchValue}`)
        .then(response => {
            response.data.items.forEach( item => {
                context.log(item.volumeInfo.title)
            })
        }).catch(error => {
            context.log('error: ' + error);
        });
};

こんな感じで実行結果が出るはずです。

[2020/01/24 4:27:49] Host lock lease acquired by instance ID '0000000000000000000000009E8328FA'.
[2020/01/24 4:27:50] Executing 'Functions.fetch-timer' (Reason='Timer fired at 2020-01-24T13:27:50.0705362+09:00', Id=a5682498-bb5a-46b3-b3fd-78bea46c3de8)
[2020/01/24 4:27:51] status code:  200
[2020/01/24 4:27:51] Azureテクノロジ入門 2019
[2020/01/24 4:27:51] PowerShell for Azure
[2020/01/24 4:27:51] Azureテクノロジ入門 2018
[2020/01/24 4:27:51] ひと目でわかるAzure Active Directory 第2版
[2020/01/24 4:27:51] Microsoft Azure導入ガイドブック
[2020/01/24 4:27:51] Windows Azure実践クラウド・プログラミングfor C#/Visual Basic/PHP
[2020/01/24 4:27:51] Microsoft Azureへの招待
[2020/01/24 4:27:51] Azure定番システム設計・実装・運用ガイド
[2020/01/24 4:27:51] ひと目でわかるAzure 基本から学ぶサーバー&ネットワーク構築
[2020/01/24 4:27:51] Windows Azure APIリファレンス

Azure上にもデプロイして出力されることを確認しておきましょう。
ちなみにAzure上だとロケールの関係で日本語のタイトルの本が出てこないかもしれません。

エラーの時にログを出力する

接続先が間違っていた時には正常にデータがとれずエラーになるはずです。
エラーの場合にログが出力されるように記述してみましょう。
(接続先をわざとlocalhostに変更しています)

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const result = await axios(`https://localhost/books/v1/volumes?q=Azure`)
        .then(response => {
            context.log('status code: ', response.status);
            context.log('data', response.data);
        }).catch(error => {
            context.log('error: ' + error);
        });
};

Azureポータル上でログを見るとエラーが出たときにcatchできていることが確認できていると思います。

2020-01-24T02:00:39.107 [Information] error: Error: connect EACCES 127.0.0.1:443

監視タブの一覧上ではグリーンのランプがついていたと思いますが、これはcatchだけして正常終了してしまっているからです。
異常終了させるにはcatchしたあとにthrowします。

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const searchValue = "Azure";
    // const result = await axios(`https://www.googleapis.com/books/v1/volumes?q=${searchValue}`)
    const result = await axios(`https://localhost/books/v1/volumes?q=${searchValue}`)
        .then(response => {
            response.data.items.forEach( item => {
                context.log(item.volumeInfo.title)
            })
        }).catch(error => {
            context.log('error: ' + error);
            throw error;
        });
};

image.png

環境変数に切り出す

接続先のURLなどは環境変数に切り出すことで1つの関数で色々な役割をこなすことができますよね。
たとえばHTTPアクセスしてエラーにならないことを確認する関数を作った場合、URLの部分だけ差し替えることができれば再利用性が増しますよね。

index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const result = await axios(`https://www.googleapis.com/books/v1/volume?q=Azure`)
        .then(response => {
            response.data.items.forEach(item => {
                context.log(item.volumeInfo.title)
            })
        }).catch(error => {
            context.log('[ERROR]: ' + error);
            throw error;
        });
};

https://www.googleapis.com/books/v1/volume?q=Azureの部分を環境変数に切り出してみましょう。
環境変数はAzureポータル上で設定し、ソース上から環境変数を利用します。

関数アプリの設定をクリックします。

image.png

アプリケーション設定の管理をクリックします。

image.png

環境変数を設定します。ここではURLにhttps://www.googleapis.com/books/v1/volume?q=Azureを入力します。

image.png

最後に保存ボタンを押すのを忘れないようにしましょう

ソースを修正していきます。

  • 環境変数から値を取得するにはprocess.env.[環境変数]を使います。
  • 環境変数URLが設定されていない場合はエラーログを出力します。
index.js
const axios = require('axios');
module.exports = async function (context, myTimer) {
    const URL = process.env.URL;
    if (typeof process.env.URL === 'undefined') {
        context.log("[ERROR]URL is undefined.");
    }
    const result = await axios.get(URL)
        .then(response => {
            response.data.items.forEach(item => {
                context.log(item.volumeInfo.title)
            })
        }).catch(error => {
            context.log('[ERROR]: ' + error);
            throw error;
        });
};

ソースを修正したらそのまま実行しましょう(func start)

このようなエラーが出るはずです。

[2020/01/24 5:42:50] [ERROR]URL is undefined.
[2020/01/24 5:42:50] URL: undefined

環境変数がされていないので出ているエラーですね。
ローカル環境で動かす手順で紹介しましたが、fetch-app-settingsは環境変数の情報を取得するコマンドなので、このコマンドを再度実行してみましょう

func azure functionapp fetch-app-settings azfun-samples-xxxxxxx

実行した結果、local.settings.jsonは以下のようにURLが追加されているはずです。

local.settings.json
{
  "IsEncrypted": true,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "FUNCTIONS_EXTENSION_VERSION": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "AzureWebJobsStorage": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "AzureWebJobsDashboard": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_NODE_DEFAULT_VERSION": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_CONTENTSHARE": "",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "WEBSITE_RUN_FROM_PACKAGE": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "URL": "xxxxxxxxxxxxxxxxxxxxxxx"
  },
  "ConnectionStrings": {}
}

再度実行してローカル環境でうまく動くことを確認しましょう。
問題なければAzure上にデプロイして実行してみましょう。

問題②

証明書の有効期限が残り何日かを取得してみましょう
ヒント:ssl-checkerを使ってみよう

ログには以下が出力されるようにしてください

[INFO]SSL daysRemaining  <証明書有効期限の残り日数>

問題③

ここまで学んだことを使ってURL監視を実装してみよう

  • 対象URLにとある間隔でアクセスし以下を確認してください。
    • HTTP Responseのステータスコードが200であること
    • ResponseBodyに特定の文字列があること
      • BooksAPIを使うのであればtotalItemsという文字列があるかを確認してみてください。
    • 証明書の有効期限内であること
  • 対象URL、証明書の有効期限残り日数は環境変数で指定できるようにしましょう
  • エラーの場合は文字列の先頭に[ERROR]という文字を付与しましょう。

後片付け

リソースグループごとガツンと消しておきましょう。取り返しがつかないので間違えないように!!

az group delete --name <リソースグループ名>

おわりに

TimerFunctionsを使うことで指定された期間に不要なサービスを自動停止・開始することなども実装できるはずなので是非ともチャレンジしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Lambda Node.js8.10から10.xへの バージョンアップに伴うImageMagickの対応

はじめに

今回、LambdaランタイムNode.js8.10のサポート終了に伴い、Node.js10.xへアップデートを行いました。
結構詰まった部分などが合ったので、その備忘録として残します。

LambdaランタイムNode.js8.10のサポート終了について

以下、AWS公式より
スクリーンショット 2020-01-24 14.17.37.png
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtime-support-policy.html

2020/2/3を持ってLambdaのランタイムNode.js8.10の更新が終了します。
これに伴い、Node.js8.10で実装しているLambdaのランタイムを10.xにバージョンアップをしました。

Lambdaのランタイム変更による変更内容

大きな変更は、コンテナ内で起動するEC2のOSがAmazon LinuxからAmazon Linux2へ変わることです。
これにより、もともとデフォルトでOSにインストールされていた、graphicsmagickimagemagickがAmazon Linux2ではインストールされていないという状態になります。

こちらが参考記事です
https://note.com/mohya/n/n48692d8e4a57#jgl1s
つまり、Node.js10以上で、imagemagickなどを使いたい場合は、インストールしなければけません。

本記事では、この点についての対応方法を紹介します。

手順

  1. 単純にランタイムを変更する
  2. Lambdaを実行させ、エラー内容を確認する
  3. graphicsmagickとimagemagickのレイヤーを追加する
  4. Lambdaを実行させ、成功を確認する

1. 単純にランタイムを変更する

まず、状況確認をするために単純にランタイムを変更します。
スクリーンショット 2020-01-24 15.36.06.png

2. Lambdaを実行させ、エラー内容を確認する

以下のようなエラーが出ました。

2020-01-24T02:40:09.509Z    c28c7e8b-b775-48ae-ac36-aec733c9b025    ERROR   Invoke Error    
{
    "errorType": "Error",
    "errorMessage": "Could not execute GraphicsMagick/ImageMagick: identify \"-ping\" \"-format\" \"%wx%h\" \"-\" this most likely means the gm/convert binaries can't be found",
    "stack": [
        "Error: Could not execute GraphicsMagick/ImageMagick: identify \"-ping\" \"-format\" \"%wx%h\" \"-\" this most likely means the gm/convert binaries can't be found",
        "    at ChildProcess.<anonymous> (/opt/nodejs/node_modules/gm/lib/command.js:232:12)",
        "    at ChildProcess.emit (events.js:198:13)",
        "    at ChildProcess.EventEmitter.emit (domain.js:448:20)",
        "    at Process.ChildProcess._handle.onexit (internal/child_process.js:246:12)",
        "    at onErrorNT (internal/child_process.js:415:16)",
        "    at process._tickCallback (internal/process/next_tick.js:63:19)"
    ]
}

ImageMagickが見つからず、実行できないと言われてしまっています。

3. graphicsmagickとimagemagickのレイヤーを追加する

ここがメインです。
Amazon Linux2ではImageMagickが存在しないので、インストールする必要があります。
こちらのissueが大変参考になりました。
https://github.com/ysugimoto/aws-lambda-image/issues/191

Imagemagickレイヤーの設置

サーバーレスレポジトリにて提供されているので、こちらから作成します。
https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~image-magick-lambda-layer

Gaphicsmagickレイヤーの設置

こちらは、誰か知らない人が作ってくれているので、使っちゃいます。
https://github.com/rpidanny/gm-lambda-layer
こちらは、Dockerになっていて、あれ???と思いましたが
LayerもそれぞれDockerを起動してくれ、起動時にDockerfileに記載してあるこの辺りでインストールしてくれます。

RUN curl https://versaweb.dl.sourceforge.net/project/graphicsmagick/graphicsmagick/${GM_VERSION}/GraphicsMagick-${GM_VERSION}.tar.xz | tar -xJ && \
cd GraphicsMagick-${GM_VERSION} && \
./configure --prefix=/opt --enable-shared=no --enable-static=yes --with-gs-font-dir=/opt/share/fonts/default/Type1 && \
make && \
make install

このレイヤーをLambdaに登録しておけば、いいらしいですね。便利です。

レイヤーの適応

今回はServerless Frameworkを使用しているため、ymlで記載します。

functions:
  storePhotoData:
    handler: storePhotoData.handler
    layers:
      - arn:aws:lambda:ap-northeast-1:579663348364:layer:image-magick:1
      - arn:aws:lambda:ap-northeast-1:175033217214:layer:graphicsmagick:2
      - { Ref: MyLayerLambdaLayer }
    tracing: Active
    description: create storePhotoData
    memorySize: 2048
    timeout: 10
    role: storePhotoDataLambdaFunctionRole

レイヤーには実行順序という概念もあるため、レイヤーの順番も気をつけなければなりません。
MyLayerLambdaLayer はカスタマイズして作ったレイヤーで、graphicsmagickとimagemagickがないと実行できないので、念の為後ろに持ってきました。
(あんまり意味ないかも...)

4. Lambdaを実行させ、成功を確認する

ログが出せないのが申し訳ないですが、これで成功しました!!

Node.js 12.xの対応

この時どうせならと思ってランタイムをNode.js12.xにしてみたのですが、一応動きました。w
少しだけ気になる点もありましたが、恐らく大丈夫かなと思います。
一気に12へあげたい方はやってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tektonを使ってAppsodyで作ったNodejsアプリケーションをK8sにデプロイする。

さて、
Tektonを使ってAppsodyで作ったNodejsアプリケーションをK8sにデプロイする。(Tekton準備編)で、Tekton Dashboadの用意ができたら、いよいよAppsodyで作ったNode.jsのモジュールをデプロイしていきます。

今回は私のリポジトリをクローンします。

$ git clone https://github.com/motuo1201/appsody-test-build.git
$ cd appsody-test-build/

一度、ローカルで動かしてみましょう。

こちらの記事にも書きましたが、下記のコマンドでローカルの稼働が確認できます。

$ appsody run
......
[Container] App started on PORT 3000

http://localhost:3000でアクセスが可能になります。文言自体は、app.jsに書かれています。

スクリーンショット
スクリーンショット 2020-01-24 9.05.35.png

app.jsの中身

const app = require('express')()

app.get('/', (req, res) => {
  res.send("Hey! Hello Appsody!");
});

module.exports.app = app;

では、これをいよいよTekton経由でデプロイしてみましょう。

Docker Registoryとアクセスする情報を予め用意する。

Tektonはデプロイをする時に、Docker Registoryを使います。そのため、事前にDocker Hubなどに自分が操作できるDocker Registoryを用意しましょう!

こんな感じ。

スクリーンショット 2020-01-24 9.15.10.png

次に、Kubernetesにdocker hubの認証情報を登録します。
secret.yamlという名前(何でも良いのですが、、、)のファイルを作って、認証情報を作成します。

apiVersion: v1
kind: Secret
metadata:
  name: my-nodejs-dock
  annotations:
    tekton.dev/docker-0: https://index.docker.io/v1/
type: kubernetes.io/basic-auth
stringData:
  username: ********
  password: ********

このファイルを作成したら、Kubernetesに登録しましょう。

$ kubectl apply -f secret.yaml 
secret/my-nodejs-dock created

これによって、Tektonからもmy-nodejs-dockという認証情報が登録されたことが確認できます。
スクリーンショット 2020-01-24 9.21.19.png

Tektonを動かすための各種定義を行う。

1.appsodyで作成したモジュールをTektonでデプロイするためのサンプルが既に用意されています。これをCloneしましょう。

$ git clone https://github.com/appsody/tekton-example
$ cd appsody-tekton/tekton-example/

2.次にサービスアカウント作成と適切な権限を付けていきます。(特にファイルの修正はありません)

$ kubectl apply -f appsody-service-account.yaml
serviceaccount/appsody-sa created
$ kubectl apply -f appsody-cluster-role-binding.yaml
clusterrolebinding.rbac.authorization.k8s.io/appsody-admin created

3.登録されたサービスアカウントに、先ほど作成した認証情報を紐づけていきます。

$ kubectl edit serviceaccount appsody-sa

これをするとファイルの修正画面が出るので、secretに先ほど作成した認証のnameを設定
(ここでは最終行に- name: my-nodejs-dockを追加しています。)

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"appsody-sa","namespace":"default"}}
  creationTimestamp: "2020-01-24T00:24:36Z"
  name: appsody-sa
  namespace: default
  resourceVersion: "3056"
  selfLink: /api/v1/namespaces/default/serviceaccounts/appsody-sa
  uid: 257efc19-e7d4-400f-a32c-b758956172e6
secrets:
- name: appsody-sa-token-6tw82
- name: my-nodejs-dock

4.Tektonのパイプラインおよび、タスクの登録をします。(特にファイルの修正はありません)

$ kubectl apply -f appsody-build-task.yaml
task.tekton.dev/appsody-build-task created
$ kubectl apply -f appsody-build-pipeline.yaml
pipeline.tekton.dev/appsody-build-pipeline created

5.Tektonで使用するDockerとgitのリソースを登録します。

appsody-pipeline-resources.yamlにあるDockerのimageとgitのリポジトリを修正します。

apiVersion: v1
items:
- apiVersion: tekton.dev/v1alpha1
  kind: PipelineResource
  metadata:
    name: docker-image
  spec:
    params:
    - name: url
      value: index.docker.io/yenomoto1201/appsody-deploy-nodejs #ここを修正
    type: image
- apiVersion: tekton.dev/v1alpha1
  kind: PipelineResource
  metadata:
    name: git-source
  spec:
    params:
    - name: revision
      value: master
    - name: url
      value: https://github.com/motuo1201/appsody-test-build #ここを修正
    type: git
kind: List

修正したら、この情報をKubernetesに登録

$ kubectl apply -f appsody-pipeline-resources.yaml
pipelineresource.tekton.dev/docker-image created
pipelineresource.tekton.dev/git-source created

この情報もTekton Dashboad上で確認可能です。
スクリーンショット 2020-01-24 9.33.30.png

docker-image
スクリーンショット 2020-01-24 9.34.07.png

git-source

スクリーンショット 2020-01-24 9.35.13.png

6.いよいよ、動かしてみましょう!

下記のコマンドを実行して、Tekton Pipelineを起動します。

$ kubectl apply -f appsody-pipeline-run.yaml 
pipelinerun.tekton.dev/appsody-manual-pipeline-run created

ダッシュボードのPipelineRunsで処理が実行されていることを確認できます。

スクリーンショット 2020-01-24 9.37.13.png

約5分くらいで処理が完了します。このように各ステップのログを確認することができます。

スクリーンショット 2020-01-24 9.43.57.png

Serviceを確認すると、きちんとデプロイできていることがわかります。

$ kubectl get svc
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
appsody-test-build   NodePort    10.109.30.147   <none>        3000:32272/TCP   108s
kubernetes           ClusterIP   10.96.0.1       <none>        443/TCP          54m

ここで確認した、PORT:32272を元に、http://localhost:32272/にアクセスしてみます。
スクリーンショット 2020-01-24 9.46.48.png

良い感じですね。
これで、Appsodyを使ったCI/CD環境を構築することができました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tektonを使ってAppsodyで作ったNodejsアプリケーションをK8sにデプロイする。(Tekton準備編)

先日、Appsodyを使ってNode.jsモジュールをK8sに簡単にデプロイする方法を記事にしました。

この記事ではさらに踏み込んで、作成したNode.jsモジュールをTektonを使ってK8sにデプロイするCI/CDパイプラインを作ってみましょう!
まず、この記事では以下の環境にTektonを準備するところから始めます。
※前回と同様に環境はmac。docker for macでKubernetesが有効であることが前提です。

What's Tekton?

ざっくりいうと、Kubernetes上で稼働するクラウドネイティブなCI/CDパイプラインとなります。
代表的な使い方は、Githubからソースコードを取得して、それを自動的にKubernetesへデプロイする、というところでしょうか。
詳細は本筋とは逸れるのであまり触れませんが、Githubのリポジトリを参照してください。
Tekton

ここからが本題

さて、実際Kubernetesで運用することを考えたときに、毎回、ソースコードをどこからかコピーしてきて

appsody deploy

を叩くのは、あまり現実的ではありませんよね?
そこで、上記で書いたTektonの出番です。

まずはTektonを導入する

はじめに
以下の記事は
https://www.skyarch.net/blog/?cat=518
→こちらを参考にさせて頂きました。ありがとうございます。

Tektonのインストール

$ kubectl apply --filename https://storage.googleapis.com/tekton-releases/latest/release.yaml

下記のコマンドを打つことで、Tektonがインストールされたことを確認できます。

$ kubectl get pods --namespace tekton-pipelines
NAME                                           READY   STATUS              RESTARTS   AGE
tekton-pipelines-controller-5888756f5c-8jdhz   0/1     ContainerCreating   0          8s
tekton-pipelines-webhook-7494f6f84b-644nn      1/1     Running             0          8s

Tekton Dashboadを導入する

ただ、Tektonをインストールしただけでは、実際にKubernetesでのデプロイ結果を確認することが難しいです。ログなどを確認できるようにTekton Dashboadを導入しましょう。

はじめにTekton DashboardのソースをGit Hubからクローンします。

$ git clone https://github.com/tektoncd/dashboard.git

下記のコマンドでTekton Dashboardのインストールは完了です。

$ kubectl apply -f dashboard/config/release/gcr-tekton-dashboard.yaml
serviceaccount/tekton-dashboard created
customresourcedefinition.apiextensions.k8s.io/extensions.dashboard.tekton.dev created
clusterrole.rbac.authorization.k8s.io/tekton-dashboard-minimal created
clusterrolebinding.rbac.authorization.k8s.io/tekton-dashboard-minimal created
deployment.apps/tekton-dashboard created
service/tekton-dashboard created
pipeline.tekton.dev/pipeline0 created
task.tekton.dev/pipeline0-task created

下記のport-forwardコマンドを実行後、http://localhost:9097でダッシュボードにアクセスしてみましょう。

$ kubectl --namespace tekton-pipelines port-forward svc/tekton-dashboard 9097:9097

下記の画面が表示されたら準備は完了です。
スクリーンショット 2020-01-24 8.56.53.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure DevOps を社内 npm registry として使う時の Tips

社内開発フレームワークの npm package を管理するために、Azure DevOps の Artifacts がメンバー限定 npm registry として使える事が判った。
その設定を行った際に判ったことをメモ。

Azure DevOps の設定

に書いてある通りなのだが、サラッと言うと。

  1. Azure DevOps にプロジェクトを作成し、Artifacts を選択
  2. Create Feed で Feed を作成。名前は適当に、Visibility は "Member of " で。"Upstream Source" もチェックを入れる。
  3. 右上のギアアイコンをクリック → Upstream sources タブを選択し、npmjs 以外 の Source を削除する(npm registry としてのみ使うので)
  4. Artifacts の Top に戻り、"Connect to Feed" を押す。次の画面で npm グループ内の npm を選択。
  5. Linux(WSL) なので、Project setup 内の Other タブを選択。
  6. registry=https://xxx で始まる2行をコピーしておく。 ※A
  7. 次に、 ; begin auth token から ; end auth token までをコピーしておく。 ※B
  8. Step 2 にある personal access token をクリックする。
  9. パッケージデプロイする用の Token を作成する。Token Name は npm-uploader とし、期限は任意(最長で1年のようだ)、Scopes は PackagingRead & Write を選択して保存する。表示される token をコピーしておく。 ※C
  10. パッケージを利用する人用の Token を作成する。Token Name は npm-reader とし、期限は任意(最長で1年のようだ)、Scopes は PackagingRead のみ を選択して保存する。表示される token をコピーしておく。 ※D

※A ~ D の例

※A

registry=https://pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/registry/

always-auth=true

※B

; begin auth token
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/registry/:username=myorg
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/registry/:_password=[BASE64_ENCODED_PERSONAL_ACCESS_TOKEN]
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/registry/:email=youraddress@mycompany.com
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/:username=myorg
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/:_password=[BASE64_ENCODED_PERSONAL_ACCESS_TOKEN]
//pkgs.dev.azure.com/myorg/myproj/_packaging/myfeed/npm/:email=youraddress@mycompany.com
; end auth token

↑なんだか後半の3行は特に必要ないみたい。※A の registry URL に対応してるのだからそうだよね。

※C、※D 値は適当です

# エンコード前の token
wbeogamr5bhqnyxpiodntpdb57tffqbqholocat3e6iwvyx2rd3q

# Base64 エンコード
echo -n "wbeogamr5bhqnyxpiodntpdb57tffqbqholocat3e6iwvyx2rd3q" | base64

# Base64 エンコードされた token
d2Jlb2dhbXI1Ymhxbnl4cGlvZG50cGRiNTd0ZmZxYnFob2xvY2F0M2U2aXd2eXgycmQzcQ==

クライアント(パッケージデプロイ者)の設定

  1. 端末のホームディレクトリ($home) に .npmrc を作成し、※A の2行を貼り付けて保存する。
  2. デプロイする npm プロジェクトの package.json と同じディレクトリに .npmrc を作成し、※B; begin auth token から ; end auth token を貼り付ける。2つある _password= に、※C の token を BASE64 エンコードした文字列 を貼り付けて保存する。
  3. 2箇所ある email= には、パッケージ開発者のメールアドレスを設定しておく(任意)。

これで npm publish を実行すると、Artifacts に npm パッケージがデプロイされる。
DevOps の Artifacts の Top ページを更新すると、デプロイされた npm パッケージが表示される。

クライアント(パッケージ利用者)の設定

  1. 端末のホームディレクトリ($home) に .npmrc を作成し、※A の2行を貼り付けて保存する。
  2. 1. の .npmrc に、※B; begin auth token から ; end auth token を貼り付ける。2つある _password= に、※D の token を BASE64 エンコードした文字列 を貼り付けて保存する。

これで、npm install <デプロイしたnpmパッケージ名> とすると、そのパッケージがダウンロード、インストールされる。

ポイント

デプロイ用と読み取り専用

パッケージをデプロイできる token と、利用しかできない token を用意して、利用しかできない token をユーザーホームの(既定の) 認証情報として設定する。

パッケージのデプロイが必要な場合は、プロジェクトごとに .npmrc を作り設定することで、間違えてデプロイしてしまう危険性を排除している。

Upstrem source

セカンダリの npm registry として、既定の https://registry.npmjs.org/ が使用されるため、https://www.npmjs.com/ で公開されているすべての npm パッケージも利用できる。

ただし、

  • token が expire した場合は、https://www.npmjs.com/ からのインストールも失敗する
  • DevOps を経由しているせいか、通常より遅い?(気のせい)

という留意点がある。

認証方式

Personal Access Token の代わりに SSH public keys が使えないだろうか?要検証。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む