Try T.M Engineer Blog

多摩市で生息するエンジニアが「アウトプットする事は大事だ」と思って始めたブログ

個人的に気になった話題、ガジェット2021/08

はじめに

最近、仕事の方が忙しくて定期的なアウトプットができていなかったので、個人的に気になった話題やガジェットなどを箇条書きで書いていこうと思います。

個人的に気になった話題

  • Elastic社がAWSのElasticsearchのクライアントライブラリ使用を拒否

aws.amazon.com

8月に入って、大きな話題の1つでした。
ちょうど、ElasticsearchのクライアントライブラリをAWSで使用していたので、自動的にバージョンを上げないよう設定しました。

www.infoq.com

これも話題になりました。
気になるな・・・と思ってみたら、順番待ちだそうなので、しばらく待ってみようと思います。

  • GitHub Discussionsのリリース

github.blog

あまり使いこなせていませんが、GitHub Discussionsがベータ版ではなくなったみたいですね。
ちゃんと使いこなせると、結構便利そう。。。

aws.amazon.com

最近、PythonよりもTypeScriptを使うことが多いので、認識だけはしておく。

  • TypeScript バージョン4.4のリリース

devblogs.microsoft.com

最近になってTypeScriptを追い始めたので、何が新しくできるようになったか理解できていませんが、今後はTypeScriptも追っていこうと思います。

個人的に気になったガジェット

store.google.com

「完全ワイヤレスイヤホン」をまだ試した事がなく、Googleから良さげなイヤホンが出たので、ちょっと気になっている。(Air Podsは高いしなぁ。。。)

『レベルアップNode.js』を読んだ話

最近、Node.jsを使うことが増えきて、だいぶコードを書くことに慣れてきたので、そろそろレベルアップしたいと考えていたところ、ドンピシャな本がアマゾンプライムデーで、セール販売されていたので、購入して読んでみた。

はじめに

先に全体的な感想を言うと、良本です。

著者自身がSocket.IOを使ってNode.jsでウェブシステムを構築した経験と知識を本に纏めてくださっており、Node.jsについて理解を深めて、レベルアップしていくには、どのような知識が必要なのかが書いてあった。

なので、この本を読むだけではレベルアップはできませんが、この本から「こういった知識を知っておくとレベルアップできるよ」といったことが学べます。

以下、私が特に刺さった部分をピックアップして、感想を書いていく。

第1章 Node.jsの全体像

まず、最初の第1章が面白かった。Node.jsがどのように構築されているかをさらっと説明している。Node.jsがV8エンジンを採用しているのは周知されていると思うが、他にもlibuvやnode-coreがあり、それらがどのような特徴を持ち、どう機能しているのかが書かれていて面白かった。

残念ながら、詳しい話は「Node.jsデザインパターン」を読むようにとのことだが、こういったNode.jsについて深い知識を得るにはどういった本や記事を読むべきかを纏めてくれているだけでも、有り難いし良本と言える。

第3章 サーバーとしてのNode.js

次に面白いと感じたのは第3章。Node.jsはシングルプロセス・シングルスレッドで動く故、その利点や問題点が書かれている。私自身も気になっていた部分ではあったものの、スルーしていたため、非常に勉強になった。ここでレベルアップに向けてクラスタリングやプロセスマネージャーが重要になってくるということが知れたのは大きい。

第4章 イベントループ

第4章は、はじめて知ることばかりだった。そもそも私はイベントループを知らなかった。この本では「イベントループについて知りたいなら、本を閉じてこの記事を読みなさい!」とあり、他の記事へ誘導するだけだったが、私もそれで十分だと思えた。

それくらい丁寧にまとめられている記事が既に存在し、むしろその記事の情報を貰えただけで有り難いと思えた。

第7章 Stream

やはりStreamは外せない。「Streamを制するものは、Node.jsを制す」という言葉があるくらいStreamへの理解は重要とのこと。とはいえ、Streamは難易度が高い部類に入るため、Streamをきちんと理解して利用しているエンジニアは少ないとのこと。ここでもStreamへの理解を深めるには「Node.jsデザインパターン」をオススメされており、これはもう「Node.jsデザインパターン」を読め。と言っているに等しいのでは・・・(= = ;;

付録A Node.jsの習得に役立つ情報を得るには

「Node.jsデザインパターン」をはじめ、他にもイベントや良記事を紹介している。

このあたりはNode.js界隈では有名な記事だそうだ。

PLAIDのgamiさんの記事もありました。。。知らんかった。。。

さいごに

上でも書いたとおり、この本は良本でした。

Node.jsへの理解を深めていくための方法や何を知っておくべきなのかが纏まっており、本のタイトル通り「レベルアップするための情報」がこの本に詰められていました。

まだ紹介された記事をすべて読んではいませんが、記事を読みつつ写経しつつで、理解を深めたいと思います。そして「Node.jsデザインパターン」もいつか読んでみよう。。。と思います。(= =;;

試しにCopilotを使ってAWS App Runnerを動かしてみた話

はじめに

最近発表されたAWSの新サービス「AWS App Runner」

コンテナ化したウェブアプリケーションを直接デプロイできるフルマネージドなサービス。

近いところで言うとGCPの「Cloud Run」のAWS版といった感じです。

こいつは便利そうだぜ。。。というわけで、さっそく試してみることにしました。

Copilot

コンソール画面からポチポチしても良かったのですが、これに合わせてかCopilotのバージョンも上がり、「AWS App Runner」にも対応しているとのことなので、こちらも試してみることにしました。(ちなみに、私は今回初めてCopilotを使います)

Copilotは、コンテナアプリケーションのビルド、 リリース、運用を更に加速させたいという思いから産まれたCLIツールだそうです。詳細は以下を参照ください。

aws.github.io

コンテナ

今回は、簡単にNginxのコンテナをアップします。

// Dockerfile
FROM alpine:latest

# nginxのインストール
RUN apk update && \
    apk add --no-cache nginx

# ドキュメントルート
ADD app /app
ADD default.conf /etc/nginx/conf.d/default.conf

# ポート設定
EXPOSE 80

RUN mkdir -p /run/nginx

# フォアグラウンドでnginx実行
CMD nginx -g "daemon off;"
// default.conf
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /app;

        location / {
        }
}
// app/index.html
<h1>Copilot - App Runner</h1>

デプロイ

copilot init を入力すると、名前(Application Name, Service Name)とDockerfileを指定すればデプロイが始まります。(はやぃ・・・)

$ copilot init
Note: It's best to run this command in the root of your Git repository.
Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with a containerized application on AWS. An application is a collection of
containerized services that operate together.

Application name: sample-app
Workload type: Request-Driven Web Service
Service name: nginx
Dockerfile: ./Dockerfile
Ok great, we'll set up a Request-Driven Web Service named nginx in application sample-app listening on port 80.

✔ Created the infrastructure to manage services and jobs under application sample-app.

✔ Wrote the manifest for service nginx at copilot/nginx/manifest.yml
Your manifest contains configurations like your container size and port (:80).

✔ Created ECR repositories for service nginx.

All right, you're all set for local development.
Deploy: Yes

✔ Linked account XXXXXXXXXXX and region ap-northeast-1 to application sample-app.

✔ Proposing infrastructure changes for the sample-app-test environment.
- Creating the infrastructure for the sample-app-test environment.       [create complete]  [83.3s]
  - An IAM Role for AWS CloudFormation to manage resources               [create complete]  [22.0s]
  - An ECS cluster to group your services                                [create complete]  [11.0s]
  - Enable long ARN formats for the authenticated AWS principal          [create complete]  [2.4s]
  - An IAM Role to describe resources in your environment                [create complete]  [22.2s]
  - A security group to allow your containers to talk to each other      [create complete]  [4.4s]
  - An Internet Gateway to connect to the public internet                [create complete]  [16.5s]
  - Private subnet 1 for resources with no internet access               [create complete]  [18.7s]
  - Private subnet 2 for resources with no internet access               [create complete]  [18.7s]
  - Public subnet 1 for resources that can access the internet           [create complete]  [18.7s]
  - Public subnet 2 for resources that can access the internet           [create complete]  [18.7s]
  - A Virtual Private Cloud to control networking of your AWS resources  [create complete]  [16.5s]
✔ Created environment test in region ap-northeast-1 under application sample-app.
Environment test is already on the latest version v1.4.0, skip upgrade.
[+] Building 2.8s (11/11) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 356B                                                                                                                                            0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                                                                                2.0s
 => [auth] library/alpine:pull token for registry-1.docker.io                                                                                                                   0.0s
 => [1/5] FROM docker.io/library/alpine:latest@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f                                                          0.0s
 => [internal] load build context                                                                                                                                               0.0s
 => => transferring context: 277B                                                                                                                                               0.0s
 => CACHED [2/5] RUN apk update &&     apk add --no-cache nginx                                                                                                                 0.0s
 => [3/5] ADD app /app                                                                                                                                                          0.0s
 => [4/5] ADD default.conf /etc/nginx/conf.d/default.conf                                                                                                                       0.0s
 => [5/5] RUN mkdir -p /run/nginx                                                                                                                                               0.5s
 => exporting to image                                                                                                                                                          0.1s
 => => exporting layers                                                                                                                                                         0.1s
 => => writing image sha256:caf72e9509ffe1e28b2070021afe000b6304b8a1fda457160f373aa2d38f4f2c                                                                                    0.0s
 => => naming to XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample-app/nginx                                                                                             0.0s
Login Succeeded
Using default tag: latest
The push refers to repository [XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample-app/nginx]
4056d7cdbba7: Pushed
27836a6a132d: Pushed
1d8fedb1407f: Pushed
4f56318aa220: Pushed
b2d5eeeaba3a: Pushed
latest: digest: sha256:81d1c8846ae62ca6c4e86bd5c277b65b9e5ef8ec088df9be4e4e5422c8099b89 size: 1360
✔ Proposing infrastructure changes for stack sample-app-test-nginx
- Creating the infrastructure for stack sample-app-test-nginx                     [create complete]  [290.1s]
  - An IAM Role for App Runner to use on your behalf to pull your image from ECR  [create complete]  [20.1s]
  - An IAM role to control permissions for the containers in your service         [create complete]  [20.1s]
  - An App Runner service to run and manage your containers                       [create complete]  [258.6s]
✔ Deployed nginx, you can access it at https://yrqh7trqcy.ap-northeast-1.awsapprunner.com.
$

作成されたURLに接続すると・・・見れました。

デプロイ成功です。

f:id:special-moucom:20210601094556p:plain

AWS App Runner」の設定もコンソール画面から見てみると、構成(CPU/メモリ)は最小のようですね。

f:id:special-moucom:20210601094616p:plain

Copilotは、コンテナに特化しているCLIツールだけあって、CDKを使って書くよりも全然早い。。。裏側ではCloudFormationのテンプレートを作成して、それを使ってデプロイを行っている模様。

デプロイしたリソースの削除もcopilot app deleteコマンドで一発削除。

$ copilot app ls
sample-app
$ copilot app delete --name sample-app
Are you sure you want to delete application sample-app? Yes
✔ Deleted service nginx from environment test.
✔ Deleted resources of service nginx from application sample-app.
✔ Deleted service nginx from application sample-app.
✔ Deleted environment test from application sample-app.
✔ Cleaned up deployment resources.
✔ Deleted application resources.
✔ Deleted application configuration.
✔ Deleted local .workspace file.
$

感想

AWS App Runner」は、まさにGCPの「Cloud Run」のAWS版。Copilotを使用することで、CLIによるデプロイも可能。

気になる料金ですが、「AWS App Runner」はコンテナインスタンスを0にすることはできない模様。なので、最低1つのコンテナは動き続ける分、課金が発生します。

0.009 USD/GB/時なので、0.009 * 2(最小構成が2GB) * 24時間 * 30日 = 約 13 USD/月といったところでしょう。うーん、これならGCPの「Cloud Run」の方が低コストですね。。。

でも、AWSには他の様々なサービスと連携できるメリットもあるので、「AWS App Runner」という新しい選択肢ができた。というだけでも、素晴らしい事だと思います。

今後は、ちょっと試したい!ちょっと動かして確認したい!ってな時に「AWS App Runner」を使う機会はやってくでしょう!!いやぁ、便利なサービスがまた1つできましたね(= =)ノ

Objectのデータを取得する時、ブラケット記法で書くと型がanyになって辛かった話

TypeScriptでObjectからデータを取得する時、ブラケット記法で書くと強制的に型がanyになって、大変辛い思いをしたので、以下に残しておく。

Objectのサンプルは以下の通り。ポイントは、exampleAB.a以降とexampleAB.b以降とで型が違うこと。

interface ExampleAB {
  'a': {
    a_example: {
      example: string
    }
  },
  'b': {
    b_example: {
      example: number
    }
  }
}

const exampleAB: ExampleAB = {
  'a': {
    a_example: {
      example: 'dummy'
    }
  },
  'b': {
    b_example: {
      example: 1
    }
  }
}

たとえば、以下のようにするとObjectからデータを取得することができる。

当たり前ですね。

console.log(exampleAB.a.a_example.example) // 'dummy'

しかし、これをブラケット記法で書くとTypeScript側で型を判断できずにエラーになる。

( T 0 T ) ォォォォ...

const k = 'a'
console.log(exampleAB[k as strng].a_example.example) // 型 'any' の式を使用して型 'ExampleAB' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。

keyofを使えばワンチャンあるかも・・・と思い、試すもダメだった。

const k = 'a'
console.log(exampleAB[k as keyof ExampleAB].a_example.example) // プロパティ 'a_example' は型 '{ a_example: { example: string; }; } | { b_example: { example: number; }; }' に存在しません。
  プロパティ 'a_example' は型 '{ b_example: { example: number; }; }' に存在しません。

どうやら、Objectの構造が深いとダメな模様。

Objectのデータ構造が、'exampleAB.a'と'exampleAB.b'までであれば、TypeScriptが判断してくれることを確認した。

interface ExampleAB2 {
  'a': string
  'b': number
}

const exampleAB2: ExampleAB2 = {
  'a': 'dummy1',
  'b': 1
}
const i = 'a'
console.log(exampleAB2[i as keyof ExampleAB2]) // 'dummy'

もうすこし、根気よくやってみよう。

「そうだ!interfaceを分けてみよう!」と思い、試してみた結果がこちら。。。

interface ExampleA {
  'a': {
    a_example: {
      example: string
    }
  }
}

interface ExampleB {
  'b': {
    b_example: {
      example: number
    }
  }
}

const exampleAB: (ExampleA | ExampleB) = {
  'a': {
    a_example: {
      example: 'dummy'
    }
  },
  'b': {
    b_example: {
      example: 1
    }
  }
}

const k = 'a'
const i = 'b'
console.log(exampleAB[k as keyof ExampleA].a_example.example) // 型 '"a"' の式を使用して型 'ExampleA | ExampleB' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
  プロパティ 'a' は型 'ExampleA | ExampleB' に存在しません。
console.log(exampleAB[i as keyof ExampleB].b_example.example) // 型 '"b"' の式を使用して型 'ExampleA | ExampleB' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
  プロパティ 'b' は型 'ExampleA | ExampleB' に存在しません。

orz... ダメだ...

ならば・・・と、exampleABに対して型アサーションを定義してあげればうまくいきました。

const k = 'a'
const i = 'b'
console.log((exampleAB as ExampleA)[k as keyof ExampleA].a_example.example) // 'dummy'
console.log((exampleAB as ExampleB)[i as keyof ExampleB].b_example.example) // 1

まさかブラケット記法で書くと型が強制的にanyになってしまうとは。。。。

TypeScriptで型を定義するのは、なかなか慣れないものですね。

最近は、QiitaでTypeScriptの型定義の演習問題を作ってくれている方がいて、こちらを使って修行しています。

qiita.com

とはいえ、なかなか問題が難しくて軽々解けない。

これが軽々解けるようになると、大幅にレベルアップできるかもしれない。。。そんな事を思いながら日々修行を続けていますm( = = )m

Jestについて勉強してみた

最近、Jestを使ってテストコードを頻繁に書き始めたので、自身の使い方整理のため、以下雑にメモを残しておく。

test('null', () => {
  const n = null;
  expect(n).toBeNull(); // null一致
  expect(n).toBeDefined(); // undefined以外一致
  expect(n).not.toBeUndefined(); // undefinedではない
  expect(n).not.toBeTruthy(); // Truthyではない
  expect(n).toBeFalsy(); // Falsyである
});

test('0', () => {
  const z = 0;
  expect(z).not.toBeNull(); // nullではない
  expect(z).toBeDefined(); // undefined以外一致
  expect(z).not.toBeUndefined(); // undefinedではない
  expect(z).not.toBeTruthy(); // Truthyではない
  expect(z).toBeFalsy(); // Falsyである
});

test('数値', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3); // value > 3
  expect(value).toBeGreaterThanOrEqual(3.5); // value >= 3.5
  expect(value).toBeLessThan(5); // value < 5
  expect(value).toBeLessThanOrEqual(4.5); // value <= 4.5
  expect(value).toBe(4); // value = 4
  expect(value).toEqual(4); // value = 4
});

test('浮動小数点', () => {
  const value = 0.1 - 0.2;
  //expect(value).toBe(-0.1);         このように書くと、丸め込み誤差が原因で期待通りに動作しない
  expect(value).toBeCloseTo(-0.1); // これならば正しく動く
});

test('正規表現', () => {
  expect('team').not.toMatch(/I/); // 正規表現に一致しない
  expect('Christoph').toMatch(/stop/); // 正規表現に一致
});

test('配列', () => {
  const shoppingList = [
    'diapers',
    'kleenex',
    'trash bags',
    'paper towels',
    'milk',
  ];
  
  expect(shoppingList).toContain('milk'); // 配列に含まれているか
  expect(new Set(shoppingList)).toContain('milk'); // Setに含まれているか
});

test('例外', () => {
  function compileAndroidCode() {
    throw new Error('you are using the wrong JDK');
  }

  expect(() => compileAndroidCode()).toThrow(); // throwされているか
  expect(() => compileAndroidCode()).toThrow(Error); // throwのErrorが呼ばれているか
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK'); // messageの一致
  expect(() => compileAndroidCode()).toThrow(/JDK/); // 正規表現も可
});

test('予定', () => {
  test.todo('テストの予定を立てれる')
})

test('モック', () => {
  // モック関数を作る
  const mockFn = jest.fn()
  mockFn
    .mockImplementationOnce( () => {
      return '返り値1'
    })
    .mockImplementationOnce( () => {
      return '返り値2'
    })
    .mockImplementationOnce( () => {
      return '返り値3'
    })

  expect(mockFn()).toBe('返り値1') // 1回目の呼び出し
  expect(mockFn()).toBe('返り値2') // 2回目の呼び出し
  expect(mockFn()).toBe('返り値3') // 3回目の呼び出し

  // モックインスタンスを作る
  const mockInsFn = jest.fn()
  mockInsFn
    .mockImplementation( () => {
      return {
        hoge: jest.fn()
          .mockImplementationOnce( () => {
            return 'hoge関数の返り値1'
          })
          .mockImplementationOnce( () => {
            return 'hoge関数の返り値2'
          }),
        fuga: () => {
          return 'fuga関数の返り値'
        }
      }
    })

  const mockIns = new mockInsFn()
  expect(mockIns.hoge()).toBe('hoge関数の返り値1') // hoge関数の1回目の呼び出し
  expect(mockIns.hoge()).toBe('hoge関数の返り値2') // hoge関数の2回目の呼び出し
  expect(mockIns.fuga()).toBe('fuga関数の返り値') // fuga関数の呼び出し
})

// CDKを呼び出す上で必要なものを定義
import { SynthUtils } from '@aws-cdk/assert'
import * as cdk from '@aws-cdk/core'
import { SampleStack } from '../src/deploy/sampleStack'

test('スナップショット', () => {
  const app = new cdk.App()
  const stack = new SampleStack(app, `SampleStack`, {
    stage: 'dev',
    roleName: 'sampleRole'
  })
  expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot()
})

/*
// スナップショットの結果
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`スナップショット 1`] = `
Object {
  "Resources": Object {
    "sampleRole5138FD4B": Object {
      "Properties": Object {
        "AssumeRolePolicyDocument": Object {
          "Statement": Array [
            Object {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": Object {
                "Service": "lambda.amazonaws.com",
              },
            },
          ],
          "Version": "2012-10-17",
        },
        "ManagedPolicyArns": Array [
          Object {
            "Fn::Join": Array [
              "",
              Array [
                "arn:",
                Object {
                  "Ref": "AWS::Partition",
                },
                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
              ],
            ],
          },
        ],
        "RoleName": "sampleRole",
      },
      "Type": "AWS::IAM::Role",
    },
  },
}
`;
*/

感想

Jest は使ってみた感じ、むちゃくちゃ便利だ。

とにかくモックが簡単で、関数やインスタンスが作りやすい印象を受けた。

スナップショットも取得することができ、イマドキ感も感じる。

試しに、CDKのCloudFormationテンプレートのスナップショットを取得してみたが、これは使えそうだ。個人的には、CloudFromationのテンプレートテストはそこまで必要性を感じていないのだが、スナップショット残しておくだけでも、差分確認ができるという点で便利だと思えた。

まだまだ使い慣れていないので、ドキュメントを読みつつ、さらなる使い方を学んでいきたい。