- 投稿日:2020-01-24T22:03:28+09:00
CLI のテンプレートプロジェクト by node and TypeScript
node で CLI(Command Line Interface) を開発する機会が数回あって、せっかくなのでテンプレートプロジェクトとしてまとめてみた。
テンプレートプロジェクト
必要なモノ
- nodejs: v11.13.0+
- typescript: v3.7.3+
試し方
- 上記のリポジトリを Clone する
- リポジトリのディレクトリに cd して
npm ci
するnpm run build
するnpm link
する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-great
はpackage.json
のbin:
で指定している。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 では 引数の必須チェックを自力で行わなければならない ようなので、定義体の
paramDef
にrequire: boolean
を追加し、パースした実際の引数であるXxxxConfig
にrequire = 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
を採用している。コマンドを追加するには
index.ts
のCommandType
にxxxx
を増やすcommand-xxx.ts
(CommandXxxx
クラス) を作るindex.ts
のcommandMap
に追加するmainUsage
になんか書く参考
- 投稿日:2020-01-24T19:25:06+09:00
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.ymlservice: 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.jsdescribe('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.jsdescribe('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' }
)を出力することができました。
- 投稿日:2020-01-24T19:25:06+09:00
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.ymlservice: 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.jsdescribe('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.jsdescribe('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' }
)を出力することができました。
- 投稿日:2020-01-24T19:05:11+09:00
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 は標準ライブラリが充実しているため、さほど苦労することなく扱うことができます。
以下のコードは 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: trueNode.js
Node.js も標準ライブラリを利用できますが、秘密鍵・公開鍵や署名はエンコードされた情報はとれるものの、整数値を直接取得することはできないようです。
(DER, PEM デコードするライブラリは数多く存在していたので、必要があれば簡単に取得はできそうです)以下のコードは 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: trueKotlin (Android)
Android 開発においては Keystore システムでサポートされる API を利用することができます。
鍵データの管理を委譲できるのは大きなメリットである反面、秘密鍵の情報にアクセスする API が用意されていないようです。
例えば、独自にバックアップを取るなど、特殊なことをする場合は工夫が必要そうです。署名については Node.js 同様に ASN.1 エンコード後のバイナリデータが返されます。
以下のコードは 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: trueSwift
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まとめ
標準化をされている技術ではあるものの、各言語ごとに書き方の癖があってハマりがちな処理を並べました。デバッグのしづらさはデジタル署名のセキュリティの高さの裏返しではあるものの、ハマってしまった方にこの記事が少しでも役にたてば嬉しいです。
記載・認識ミス、もっと良い書き方などありましたら、ご指摘お願いします。
参考
- 投稿日:2020-01-24T17:59:23+09:00
[初学者向け]AzureFunctions(TimerTrigger)+nodejsをはじめてみよう
はじめに
社内勉強用のコンテンツをそのまま公開しました
Azure Functionsで処理を書きたいんだけど何から勉強すればいいんだろう・・・という方向けに、AzureFunctionsのTimerTriggerを使ってURL監視するサービスを一緒に作っていきたいと思います。
こんなことを勉強していきます
- 必要なソフトウェアのインストール
- nodejsの超基本中のキホン
- AzureFunctionsへのデプロイ
- AzureFunctionsをローカルで動かしてみよう
- モジュールの活用
- AzureFunctionsにおける環境変数の活用
- 学んだことを使ってURL監視サービス(証明書チェック機能付き)を作ってみる
じゃあ早速やっていきましょう
必要なもの
- Azureアカウント
必要なもののインストール
nodejs(LTS)のインストール
yarn(パッケージマネージャ)のインストール
Azure cliのインストール
Azure Functions Core Toolsのインストール
- v2とv3がありますが、今回はv2を使います。
- https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local#v2
必要に応じてインストール
お好みのエディタ
私のお好みはIntelliJですが、VisualStudioCodeは割ととっつきやすいので使ってみてください。まずはnodejsの基本中のキホンから
azure-functions-samplesというディレクトリを作ってその中にindex.jsを作って実行してみましょう
index.jsfunction hello() { console.log("test"); } hello();実行してみる
node index.jsyarnで実行してみよう
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-timertimerディレクトリの下で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を起動します。
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_NAMEFunctionsができたらAzureの画面で確認しておきます。
今度はローカルに戻ってデプロイしていきます。
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のポータルに戻り、関数を選択して「統合」というところを見ると変更したスケジュールが反映されているのがわかると思います。(少し薄いです)
「監視」を見ると10秒おきに起動していることと、さらに進むと出力したログが出ていることを確認できます。
ローカルで実行する
毎回デプロイしているのは時間の無駄なのでローカルで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 start10秒間隔で実行にしていた場合には下記のようにいつ動くのかが書かれたメッセージの後に、時間が来たら実行結果が表示されているはずです。
[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.jsconst 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.jsconst 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.jsconst 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.jsconst 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.jsconst 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; }); };環境変数に切り出す
接続先のURLなどは環境変数に切り出すことで1つの関数で色々な役割をこなすことができますよね。
たとえばHTTPアクセスしてエラーにならないことを確認する関数を作った場合、URLの部分だけ差し替えることができれば再利用性が増しますよね。index.jsconst 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ポータル上で設定し、ソース上から環境変数を利用します。関数アプリの設定をクリックします。
アプリケーション設定の管理をクリックします。
環境変数を設定します。ここではURLに
https://www.googleapis.com/books/v1/volume?q=Azure
を入力します。最後に保存ボタンを押すのを忘れないようにしましょう
ソースを修正していきます。
- 環境変数から値を取得するにはprocess.env.[環境変数]を使います。
- 環境変数URLが設定されていない場合はエラーログを出力します。
index.jsconst 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を使うことで指定された期間に不要なサービスを自動停止・開始することなども実装できるはずなので是非ともチャレンジしてみてください。
- 投稿日:2020-01-24T15:50:29+09:00
Lambda Node.js8.10から10.xへの バージョンアップに伴うImageMagickの対応
はじめに
今回、LambdaランタイムNode.js8.10のサポート終了に伴い、Node.js10.xへアップデートを行いました。
結構詰まった部分などが合ったので、その備忘録として残します。LambdaランタイムNode.js8.10のサポート終了について
以下、AWS公式より
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtime-support-policy.html2020/2/3を持ってLambdaのランタイムNode.js8.10の更新が終了します。
これに伴い、Node.js8.10で実装しているLambdaのランタイムを10.xにバージョンアップをしました。Lambdaのランタイム変更による変更内容
大きな変更は、コンテナ内で起動するEC2のOSがAmazon LinuxからAmazon Linux2へ変わることです。
これにより、もともとデフォルトでOSにインストールされていた、graphicsmagick と imagemagickがAmazon Linux2ではインストールされていないという状態になります。こちらが参考記事です
https://note.com/mohya/n/n48692d8e4a57#jgl1s
つまり、Node.js10以上で、imagemagickなどを使いたい場合は、インストールしなければけません。本記事では、この点についての対応方法を紹介します。
手順
- 単純にランタイムを変更する
- Lambdaを実行させ、エラー内容を確認する
- graphicsmagickとimagemagickのレイヤーを追加する
- Lambdaを実行させ、成功を確認する
1. 単純にランタイムを変更する
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/191Imagemagickレイヤーの設置
サーバーレスレポジトリにて提供されているので、こちらから作成します。
https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~image-magick-lambda-layerGaphicsmagickレイヤーの設置
こちらは、誰か知らない人が作ってくれているので、使っちゃいます。
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へあげたい方はやってみてください。
- 投稿日:2020-01-24T09:47:34+09:00
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
に書かれています。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を用意しましょう!こんな感じ。
次に、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
という認証情報が登録されたことが確認できます。
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 created3.登録されたサービスアカウントに、先ほど作成した認証情報を紐づけていきます。
$ 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-dock4.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 created5.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 createdgit-source
6.いよいよ、動かしてみましょう!
下記のコマンドを実行して、Tekton Pipelineを起動します。
$ kubectl apply -f appsody-pipeline-run.yaml pipelinerun.tekton.dev/appsody-manual-pipeline-run createdダッシュボードのPipelineRunsで処理が実行されていることを確認できます。
約5分くらいで処理が完了します。このように各ステップのログを確認することができます。
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/
にアクセスしてみます。
良い感じですね。
これで、Appsodyを使ったCI/CD環境を構築することができました!
- 投稿日:2020-01-24T08:58:51+09:00
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のリポジトリを参照してください。
ここからが本題
さて、実際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 8sTekton 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-24T01:18:58+09:00
Azure DevOps を社内 npm registry として使う時の Tips
社内開発フレームワークの npm package を管理するために、Azure DevOps の Artifacts がメンバー限定 npm registry として使える事が判った。
その設定を行った際に判ったことをメモ。Azure DevOps の設定
に書いてある通りなのだが、サラッと言うと。
- Azure DevOps にプロジェクトを作成し、Artifacts を選択
- Create Feed で Feed を作成。名前は適当に、Visibility は "Member of " で。"Upstream Source" もチェックを入れる。
- 右上のギアアイコンをクリック → Upstream sources タブを選択し、npmjs 以外 の Source を削除する(npm registry としてのみ使うので)
- Artifacts の Top に戻り、"Connect to Feed" を押す。次の画面で npm グループ内の npm を選択。
- Linux(WSL) なので、Project setup 内の Other タブを選択。
registry=https://xxx
で始まる2行をコピーしておく。 ※A- 次に、
; begin auth token
から; end auth token
までをコピーしておく。 ※B- Step 2 にある personal access token をクリックする。
- パッケージデプロイする用の Token を作成する。Token Name は
npm-uploader
とし、期限は任意(最長で1年のようだ)、Scopes は Packaging の Read & Write を選択して保存する。表示される token をコピーしておく。 ※C- パッケージを利用する人用の Token を作成する。Token Name は
npm-reader
とし、期限は任意(最長で1年のようだ)、Scopes は Packaging の Read のみ を選択して保存する。表示される 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==クライアント(パッケージデプロイ者)の設定
- 端末のホームディレクトリ(
$home
) に.npmrc
を作成し、※A の2行を貼り付けて保存する。- デプロイする npm プロジェクトの
package.json
と同じディレクトリに.npmrc
を作成し、※B の; begin auth token
から; end auth token
を貼り付ける。2つある_password=
に、※C の token を BASE64 エンコードした文字列 を貼り付けて保存する。- 2箇所ある
email=
には、パッケージ開発者のメールアドレスを設定しておく(任意)。これで
npm publish
を実行すると、Artifacts に npm パッケージがデプロイされる。
DevOps の Artifacts の Top ページを更新すると、デプロイされた npm パッケージが表示される。クライアント(パッケージ利用者)の設定
- 端末のホームディレクトリ(
$home
) に.npmrc
を作成し、※A の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 が使えないだろうか?要検証。