小さなエンドウ豆

まだまだいろいろ勉強中

AWS SQS + Lambda + SES でメール送信システムを作る

AWS SQS + Lambda + SES でメール送信システムを作る

Web サービスを運営しているとメールを送信したいタイミングが山程あります。
例えば、ユーザーの新規登録時、定期的に送信するメルマガ用途は多岐に渡ります。

メール送信の仕組みをモノリシックにサービスの一部として組み込むとパフォーマンスが低下してしまう恐れがあります。 そこでどのような設計にすれば悩んでいたところ、SQS をすすめられてため調査してみました。

また SES や SNS としった AWS による通知系のサービスと連携されることで完全マネージドな通知サービスとして構築を目指します。

Amazon Simple Queue Service(SQS)

名前の通りキューイングのサービスです。

キューには 2 種類あり、標準キューと FIFO キューが選べます。
2 つの違いは配信の順序です。
標準キューの配信順序はベストエフォート型で、配信は少なくとも 1 回行われます。
FIFO キューは、その名の通りメッセージが送信される順序のとおりに 1 回のみ確実に処理されるように設計されています。

また SQS にキューイングすることを Lambda のイベントとすることができます。
今回は SQS にイベントを登録し、それをフックにメールを送ります。

SQS を使う理由

SQS を使う理由として以下が挙げられます。

  • キューがあることによって非同期通信が有効になりパフォーマンス向上が見込める
    • 例えば会員登録時にメールを送ることを SQS にまかせて会員登録処理自体は別にすすめることが可能になる
  • キューに登録することによりデータの一貫性が保たれるため信頼性の向上につながる
  • 完全マネージドなサービスのため運用が楽

aws.amazon.com

Amazon Simple Email Service(SES)

クラウドベースの E メールサービスです。

使い方は簡単で、SES から送信されるメールの送信元メールアドレスを有効化したあとに SDK を使ってリクエストするとメールが送信できるようになります。

今回は Lambda からのリクエストを受けてメールを送信する役割を担います。

※ 注意 SES は東京リージョンではサポートされていないため他のリージョンで利用しなければならない。 また Lambda や Lambda のイベントとなるリソースも同じリージョンに存在しなければならない。 https://docs.aws.amazon.com/ses/latest/DeveloperGuide/regions.html#region-receive-email

Amazon Simple Notification Service(SNS

クラウドベースの通知サービスです。
SNS を使うとメールを送ることが可能です。

ただ送信元のアドレスが固定になってしまうため今回使用は見送りました。
(ちなみに no-reply@sns.amazonaws.com からのメールになります。)

SES と SNS では以下のような違いがあります。

違い SES SNS
東京リージョンでの使用 不可
送信元メールアドレス 可変 固定
HTML形式 不可
料金 1000通につき0.10USD 1000通まで無料,10万件あたり2USD

先程も書きましたが、今回の要件を SNS では満たせなかったことや今後の拡張性を考慮して SES を使用します。

実際にやってみる

改めて今回の構成です。

f:id:h-piiice16:20200215205412p:plain
構成

SQS は標準キューを選んで新しく登録します。
SES は先程言ったように送信元メールアドレスを有効化します。

次に Lmabda だが今回 Serverless Framework を使って実装してみます。

Serverless Framework

サーバーレスアプリケーションの構成管理ツールです。
yml 形式で連携するサービスを記述することが出来ます。
またデプロイをコマンドで行うことができるため非常に楽です。

Serverlass Framework に関しては詳しく記しませんが、プロジェクト内にある serverless.yml を以下のように書くと SQS をフックに lambda を呼び出すことができます。

functions:
  hello:
    handler: handler.mail
    events:
      - sqs:
          arn: arn:aws:sqs:ap-south-1:xxxxxx:node_mail

arn には作成したキューのものを記します。 events には他にも API Gateway や DynamoDB など他のリソースを書くこともできます。

Lambda 関数

実装は以下のような感じです。

import { SQSHandler, SQSRecord } from "aws-lambda";
import "source-map-support/register";
import * as AWS from "aws-sdk";
import nodemailer from "nodemailer";

const ses = new AWS.SES({
    accessKeyId: process.env.AWS_KEY_ID,
    secretAccessKey: process.env.AWS_KEY,
    region: "ap-south-1",
    apiVersion: '2010-12-01'
});

const transporter = nodemailer.createTransport({ SES: ses });

export const mail: SQSHandler = async (event: SQSEvent) => {
  const addresses: string[] = event.Records.map((r: SQSRecord) => {
    return r.body;
  });
  const params = {
    from: process.env.SEND_ADDRESS,
    to: addresses,
    subject: "Email Testing",
    html: "<h1>Title</h1>"
  };

  console.log(params);

  try {
    await transporter.sendMail(params)
  } catch (e) {
    console.log(e);
  }
};

SES のインスタンスを作成する際にはシークレットキーを設定する必要がありました。
TS で記述しているため mail という関数や SQS からのイベントの型を指定しています。

なぜ nodemailer を使うのか?

参考にした実装例では nodemailer をいうモジュールで SES のインスタンスをラップしてから使っていました。
その理由が以下のサイトに載っていたため挙げます。
簡単に言うとメール送信時にパラメータの指定の仕方がこちらの API のほうが簡単だそうです。
特に今回は要りませんでしが、ファイル添付する際など SES の標準 API を使うと煩わしいみたいです。

nodemailer.com

実行

SQS でイベントを発行することができます。
本来は EC2 から SQS にイベントを発行するというシチュエーションを想定していますが、GUI ベースでテストも可能です。
メッセージに送信先のメールアドレスを入力し登録するとメールが送られてきました!

f:id:h-piiice16:20200215224212p:plain
メール本文

html も解釈出来ていますね。

まとめ

まだ不十分ではあるがミニマムでメール送信サービスを作ることができた。
残タスクとして、SQS へのイベントの登録や各サービスで起きたエラーのハンドリング、バウンスメールへの対応などを考えなくてはいけません。

SQS や SES は初めて使うサービスだったがフルマネージドもあってか簡単に使うことができました。
実際に Web アプリケーションでメール機能を実装するよりこちらのほうがコストをかけずできると思います。
また運用、保守のコストも同じくかからないと思うのでおすすめです!

ソースコード

github.com

スタートアップのための AWS Fargate 入門

スタートアップのための AWS Fargate 入門

本記事は以下の内容を読んでより具体的な方法について書いたものです。

aws.amazon.com

そもそもコンテナが解決しているもの

アプリケーションを動かすにはコードだけでなく関連するコンポーネントが揃っている必要があります。
例えば Ruby on Rails の場合は以下が挙げられます。

このような依存関係を解決するための手段としてコンテナが利用されます。
アプリケーションの実行に必要な依存ブルをすべてコンテナの中にパッケージングし、開発環境から本番環境まで同一の環境で動作させることが出来ます。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/08/07/docker2.png

パッケージングされたコンテナを実行するツールとしてデファクトスタンダードとなっているのが Docker です。
Docker Engine が動いている環境であればコンテナを実行することが出来ます。
Docker Engine の管理するスコープとしては単一ホストマシン上でのコンテナの動作になります。
それ以外の部分、例えばコンテナのオートスケーリングや複数ホストマシンにまたがるような配置、ローリングアップデートなどは別の仕組みとして実装する必要があります。

ローリングアップデートとは

同じ機能を持った複数のコンピュータで構成している場合のシステムをアップデートする手法の一つです。 システムの稼動状態を維持しながら、1台ずつ順番にアップデートを行っていきます。

コンテナオーケストレーション

AWS ECS や EKS といったサービスがこれに当たります。
複数のホストマシンにまたがるコンテナの配置やコンテナのアップデート、ロードバランサーへの紐づけなどを管理してくれます。
以下のイメージのようにこちらから API 経由で変更後の状態を支持することでツールがその状態になるように維持・動作します。
これを宣言的デプロイと呼ぶそうです。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/08/07/docker3.png

コンテナオーケストレーションをを使うとオートスケールや複数のマシンでのコンテナ管理が簡単になり、開発者はコンテナの運用に集中できると思われるが、
実際はホストマシンそのものの管理や運用は依然として残っています。
コンテナが動くホストマシンは OS が動いていますし、Docker Engine が動いている必要があるからです。

コンテナ解決することは上記にも書きましたが、その一方でコンテナとホストマシンの両方を管理・運用しなければならないということになります。

AWS Fargate とは

上記のような二重管理を解決するのが AWS Fargate です。
ECS でコンテナを実行する際の起動タイプの一つで、EC2 と Fargate があります。 EC2 で実行したい際のイメージが以下です。
「どのホストマシン」で「どのコンテナ」を「いくつ起動する」かなどのハンドリングは、オーケストレーションツールである Amazon ECS から行うことが可能ですが、各ホストマシンの管理・運用業務は依然として残ります。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/09/10/ecs1-2.png

一方 Fargate がこちらです。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/09/10/fargate2-1.png

この画像ではホストマシンが見えなくなり、OS や Docker Engine, ecs-agent が抽象化され Fargate プラットフォームに隠蔽されていることを表しているようです。

他にも以下のようなメリットがあります。

クラスタの管理が不要(キャパシティ)

通常、EC2 等の仮想マシンクラスタを構築し運用する際には様々なことを考慮する必要があります。
AWS Fargate を利用すると、こういったことを意識することなくコンテナを実行することが出来ます。
コンテナ実行時に必要な CPU, メモリの組み合わせを選択するだけでよく、リソースの調達等は Fargate プラットフォーム側で行われます。

ホストマシンの管理が不要(セキュリティ等)

コンテナワークロードに限らず、通常 EC2 等の仮想マシンを運用する際は、 OS やミドルウェアのバージョンアップやセキュリティパッチの適用などをやらなければなりません。
Fargate プラットフォームの管理運用は AWS にて行うため、前述の OS や Docker Engine, ecs-agent 等のバージョンアップやセキュリティパッチの適用もこの中に含まれるため、行う必要はありません。

実践

AWS が用意しているチュートリアルをもとに気になった用語などをまとめていきます。

https://ap-northeast-1.console.aws.amazon.com/ecs/home?region=ap-northeast-1#/firstRun

ほとんどデフォルトのままで起動が可能でした。

Container definition

どんなコンテナを使うか宣言する部分。
チュートリアルでは Apache, Nginx, Tomcat とカスタムが用意されています。 今回は Nginx を選択。

カスタムにするとマシンスペックも選ぶことが出来ます。(CPU, メモリなど)

Task Definition

どのように起動させるかここで決めることが出来ますが、チュートリアルではすでに設定があるため必要ありませんでした。

Service

先程設定したタスク定義を何個実行して維持するか設定できます。
必要なタスクの数やロードバランサ(なし or ALB)を選ぶことができます。

Cluster

VPC, サブネットを設定します。 新規作成してくれるようなのでいじらずにそのまま作成へ。

これだけで Nginx に接続ができました。
後にタスクの数を 2 つにして起動し直し、2つにアクセスが振り分けれることも確認できました。

まとめ

  • コンテナを使うとアプリケーション実行に必要な依存関係をパッケージングしてくれる
  • コンテナを複数マシンで実行するためにンテナオーケストレーションツール(ECSやEKS)は必須
  • Fargate は ECS の起動モードの一つでホストマシンの管理が不要になる
  • Fargate のチュートリアルは雰囲気を味わうにはよい

今回はチュートリアルの内容をただやった実施しただけなので、次回は自作のコンテナを Fargate 上で実行するってことに挑戦してみます。

Slack(Bolt)から Github Actions を実行

Github Actions を Slack から実行

背景

先日 Github Actions を使って Nuxt アプリケーションを S3 にアップロードし CloudFront で配信するワークフローを作りました。

h-piiice16.hatenablog.com

ただ、このワークフローではブランチが固定になっており、固定の内容しかデプロイすることが出来ません。
これだと検証用のブランチをステージング環境にデプロイしたいというシチュエーションで困ります。

ブランチの分だけワークフローを書くのも一つの手ですが、できればワークフロー実行時にブランチ名を指定したい…
と探してたところ、ワークフロー実行のトリガーを deployment にすることでブランチを可変にできるという記事を見つけました。

qiita.com

この記事によると Github API の Create a deployment を叩くとイベントが発火し、ワークフローを実行することができます。
また、Create a deployment のリクエスト時にブランチ名をパラメータに渡すこととで、そのブランチの内容でワークフローが実行されるそう。

これで万事解決するのだが、Slack アプリと組み合わせてより簡単にワークフローを実行できないかと思いつきやってみることにしました。

Slack Bolt

Bolt とは Slack アプリを作るためのフレームワークです。
開発言語は Node.js で Bot や自動返信、Slack コマンドなどを作ることが出来ます。

Bolt の入門ガイドが秀逸で、簡単なアプリを 1 時間もかからずに作れるようになります。

slack.dev

今回作るアプリは Nuxt アプリをデプロイするためのアプリです。
流れとしては以下のような感じです。

  1. /deploy という Slack のコマンドをアプリに向けて打つ
  2. 完成イメージにあったようなセレクトボックスが出くる
  3. ブランチを選ぶと Github API にリクエストする
  4. ワークフローが実行されデプロイ

f:id:h-piiice16:20200125222904p:plain
完成イメージ

Bolt 側のソースコードが以下です。

const { App } = require('@slack/bolt')
const https = require('https')

const githubToken = process.env.GITHUB_TOKEN
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  githubToken: githubToken
})

const url = 'https://api.github.com/repos/hiracky16/nuxt-app-test/branches'
const options = { headers: { 'User-Agent': 'Mozilla/5.0' } }

const deployUrl = "https://api.github.com/repos/hiracky16/nuxt-app-test/deployments"
const deployOptions = {
  method: 'POST',
  headers: { 'User-Agent': 'Mozilla/5.0', 'Authorization': `token ${githubToken}` }
}

app.command('/deploy', async ({ command, ack, say }) => {
  ack()
  https.get(url, options, (response) => {
    let body = '';
    console.log('STATUS: ' + response.statusCode);
    response.setEncoding('utf8');
    response.on('data', (chunk) => { body += chunk })

    response.on('end', (res) => {
      res = JSON.parse(body);
      const branches = res.map(r => { text: r.name, value: r.name })
      say({
        text: 'どのブランチをデプロイしますか?',
        response_type: 'in_channel',
        attachments: [{
          text: 'ブランチを選んでください',
          attachment_type: 'default',
          callback_id: 'select_branch',
          actions: [{
            name: 'branch_list',
            text: 'branches',
            type: 'select',
            options: branches
          }]
        }]
      })
    })
  }).on('error', (e) => {
    say({text: 'ブランチが取得できませんでした。'})
  })
})

app.action({ callback_id: 'select_branch' }, ({ body, ack, say }) => {
  const value = body.actions[0].selected_options[0].value
  ack()
  say(`${value} でデプロイします!`)
  const postData = JSON.stringify({"ref": value})
  const request = https.request(deployUrl, deployOptions, (response) => {
    response.on('data', (chunk) => { console.log(`BODY: ${chunk}`) });
    response.on('end', () => say({text: `${value} でデプロイはじめました。`}));
  }).on('error', (e) => say({text: 'デプロイに失敗しました。'}))
  request.write(postData)
  request.end()
});

(async () => {
  // Start your app
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

メッセージの形式は以下のサイトにテンプレートがたくさんあるので参考にしました。

https://api.slack.com/tools/block-kit-builder

まずはブランチ一覧をリモートリポジトリから取得します。
それをセレクトボックスにセットし、選ばせます。
選ぶと app.action() が実行され、deployment にリクエストが行き、Github Actions が実行されます。

GITHUB_TOKEN は API を使うために必要だったため「Settings > Developer settings > Personal access tokens」から作りました。
API を叩く際に User Agent を設定しないとエラーになっていしまったためブランチ取得時、Deploy 時どちらも設定しています。

Slack のコマンドを作る際は Slack App の設定画面へ行き「Slack Command」で作ることが出来ます。

f:id:h-piiice16:20200125231319p:plain
コマンド作成画面

余談ですが、入門ガイドでもあった ngrok で localhost:3000 を外部公開するやり方だと立ち上げ直すたびに URL が変わってしまい、
コマンドの Webhooks に加えて Event Subscriptions や Interactive Components のリクエスト URL も変えなければならないため大変でした。。

これで完成イメージのようなアプリが出来上がります。

Github Actions 側の修正

これは Qiita を参考に on 句をpush から deployment に変更しました。

on:  on:
+ deployment:
-  push:      deployment:
-    branches:  
-      - master

これにより Slack 側で /deploy コマンドを実行するだけで好きなブランチの内容でデプロイが可能になりました。

まとめ

今回は Slack コマンドから Github Actions を呼び出して Nuxt アプリをデプロイすることに成功しました。
Slack コマンドから Github Actions が呼び出せるということはデプロイだけでなくテストや他のことを Slack を通じて実行できることがわかります。
また Bolt は他のことにも大いに応用できるので今後もネタが付きないと思われます。

Vuex の型定義全部書いてみた

Vuex の型定義全部書いてみた

Vuex + TypeScript での問題点

  • getters の型定義が any
  • mutations / actions の payload が any 型

payload とは以下のような mutation があったとすると、第一引数が state オブジェクトでそれ以外の引数のことを言います。

mutations: {
  increment (state, n) {
    state.count += n
  }
}

これから具体的な例をもとに深ぼっていきます。

getters の定義が any

例えば getter 関数の第2引数 getters を利用することで他の getter 関数への参照を持つことが出来ます。
ただ getters 自身が any 型となってしまうためコンパイル時に型が違う点に気づくことが出来ません。

state: {
  name: null as string | null
},

getters: {
  getName(state) {
    return state.name
  },
  greet(state, getters) {
    return `My name is ${getters.getName.toUpperCase()}.`
    // ここでインラインアサーションを使うとアンチパータン
    // 実装と型が違うケースがあるため
    // 例: return `My name is ${(getters.getName as string).toUpperCase()}.`
  }
}

payload のスキーマ間違え

mutation, action 関数の payload は any 型です。
そのため型アノテーションを付けたとしても実行側はこのアノテーションに関与しないためダウンキャストとなってしまいます。

mutations: {
  setName(state, payload: string) { // ダウンキャスト
    state.name = payload
  }
},
actions: {
  asyncSetName(ctx, payload) {
    ctx.commit('setName', {name: payload}) // スキーマ違い
  }
}

存在しない action 関数の dispatch

これはあるあるかもですね。
commit と dispatch を間違える場合も考えられます。

これらも TypeScript で検知したいところ。

解決へのアプローチ①: 公式型定義を使う

Vuex の types の中に型を見ると以下のようになっている。

export type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;
export type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>
export type Mutation<S> = (state: S, payload: any) => any;

export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>;
}

export MutationTree<S> {
  [key: string]: Mutaion<S>;
}

export ActionTree<S, R> {
  [key: string]: Action<S, R>;
}

state の型が総称型になっているため実装側で指定が可能です。
[key] { process } みたいな形で積み上げることによって store インスタンスの方を表現することが出来ます。
またインデックスシグネチャを使うとすべての関数の第一引数に State 型を付与出来ます。

ただ、依然として getters が any だったり、 mutation, action の payload が any であるため解決したとは言えません。
完全に解決するためには独自に型を作って付与していくしかなさそうです。

getters の型を解決する

予め getters の要件を interface で明示しておきます。
また Getter 型を再定義することで getters の interface を利用します。

interface IGetters {
  double: number
  expo2: number
  expo: (amount: number) => number
}

type Getters<S, G> = {
  [K in keyof G]: (state: S, getters: G) => G[K]
}

ここで in

mutations の型を解決する

Getters と同じように interface を用意します。
ここで用意するのは payload に対する interface です。

interfece IMutations {
  setCount: { amout: number },
  increment: void
}

type Mutations<S, M> = {
  [K in keyof M]: (state: S, payload: M[K]) => void
}

const mutations: Mutations<State, IMutation> = {
  setCount(state, payload) => {...},
  increment(state) => {...}
}

actions の型を解決する

action 関数はこれまで定義した getters と mutations への参照があります。
まず interfece を定義するが、これは mutations と同じように payload に対して定義します。
次に Actions の型を定義するのですが、action 関数では第一引数に ctx が渡されるため Context 型を作成します。
これには store 内のすべてのオブジェクトを型として定義する必要があります。
また commit や dispatch も以下のように表すことができます。

interface IActions {
  asyncSetCount: { amount: number },
  asyncIncrement: void
}

// Promise オブジェクトを返す関数もあるので any とする
type Actions<S, A, G={}, M={}> = {
  [K in keyof A]: (ctx: unknown, payload: A[K]) => any
}

type Context<S, A, G, M, RS, RG> = {
  commit: Commit<M>,
  dispatch: Dispatch<A>,
  state: S,
  getters: G
}

type Commit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void

type Dispatch<A> = <T extends keyof A>(type: T, payload?" A[T]) => any

const actions: Actions<State, IActions, IGetters, IMutations> = {
  asyncSetCount(ctx, payload) {
    ctx.commit('setCount', { amount: payload.amount })
  },
  asyncIncrement(ctx) {
    ctx.commit('increment')
  }

定義してきた型を別ファイルで管理してもいいかもです。

まとめ

Vuex に型をつけたい一心で調べた公式が提供している型だけだと不十分だということがわかりました。
ここまでして型をつけるかは費用対効果を考えなくてはいけませんね。。
Decorator を使って module ライクに Vuex を書き換える方法もあるので次はそれを記して行きたいと思います。

参考: 実践TypeScript ~ BFFとNext.js&Nuxt.jsの型定義~ https://www.amazon.co.jp/%E5%AE%9F%E8%B7%B5TypeScript-BFF%E3%81%A8Next-js-Nuxt-js%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9-%E5%90%89%E4%BA%95-%E5%81%A5%E6%96%87/dp/483996937X/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=typescript&qid=1579435873&sr=8-1

Rails アプリケーションから gRPC サーバー に接続するまで

Rails アプリケーションから gRPC サーバーに接続するまで

そもそも RPC とは

あるコンピュータで動作するソフトウェアから、通信回線やコンピュータネットワークを通じて別のコンピュータ上で動作するソフトウェアへ処理を依頼したり、結果を返したりするための規約。

要するに別アプリにある処理(関数)を呼び出すこと。
API はこれに当たるのかなと思っています。

gRPC とは

gRPCはRPCを実現するためにGoogleが開発したプロトコルの1つで、インターフェイス定義言語のもとになるメッセージ交換形式として Protocol Buffers を利用できます。gRPC上のアプリケーションでは、別マシン上にあるアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができ、分散アプリケーションおよびサービスの作成を簡単にできます。

Protocol Buffers が現時点ではわかりませんが、RPC を実現するためのプロトコルGoogle が定めたものという認識でいます。

Protocol Buffers

インターフェース定義言語(IDL)でデータ構造を定義する通信や永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されています。

Protoc インストール

protoc は protocol buffers で定義されるスキーマから構造体やクラスを生成するコンパイラ
Mac では homebrew でインストールが可能みたいだけどなぜか出来なかったのでソースコード落としてきてビルドしました。

github.com

解凍後、make コマンド実行!

Go で gRPC サーバーを実装

まずは実装例が多い Go 言語で書いてみたいと思います。
Go をあまり書いたことがないため少々時間がかかりましたが、下記の記事を参考に作りました。 記事中ではリクエストで受けた文字列をそのまま返す GetEcho という関数を実装しています。

qiita.com

$ go run client.go
result:&echoService.EchoResponse{Input:"hiracky16", XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
error::<nil>

成功したっぽい。

Rails から gRPC サーバーに問い合わせる

ここからが本題です。 今度は Rails サーバーから GetEcho を呼び出します。

まず Gemfile に 2 つの gem を追加します。 grpc は Ruby で grpc を実現されるための gem、
grpc-tool は参考記事で作成した Protocol Buffers を Ruby のクラスに変換するツールです。

gem 'grpc'
gem 'grpc-tools'

次に以下のコマンドを実行し、

$ bundle exec grpc_tools_ruby_protoc -I ../service --ruby_out=lib --grpc_out=lib ../service/*.proto

これによって Rails プロジェクトの lib 配下に echoService_pb.rbechoService_services_pb.rb が生成されます。
(proto ファイルがキャメルケースだったためファイル名までそうなってしまった。。)

簡単に説明すると echoService_pb.rb では文字列やシンボルベースで proto ファイルの内容からリクエストとレスポンスのクラス(GetEchoMessage, EchoResponse)を作っていた。
要するに Ruby で proto ファイルの内容を表現したものです。
それらのクラスを使って echoService_service_pb.rb では Echo という module の形で get_echo というメソッドを提供しています。

echoService_service_pb.rb の内容は以下です。

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# Source: echoService.proto for package ''

require 'grpc'
require 'echoService_pb'

module Echo
  class Service

    include GRPC::GenericService

    self.marshal_class_method = :encode
    self.unmarshal_class_method = :decode
    self.service_name = 'Echo'

    rpc :GetEcho, GetEchoMessage, EchoResponse
  end

  Stub = Service.rpc_stub_class
end

これをコントローラーから呼び出します。

require 'echoService_services_pb.rb'
require 'echoService_pb.rb'

class ApplicationController < ActionController::API
  def echo
    echo_stub = Echo::Stub.new('localhost:2525', :this_channel_is_insecure)
    res = echo_stub.get_echo(Echo::Service::GetEchoMessage.new(target_echo: 'test'))
    render json: { message: res.input }
  end
end
  1. echoService_service_pb.rb に記述のあった stub のインスタンスを生成
  2. 引数には GetEchoMessage のインスタンスを new して上げて、get_echo(スネークケースなので注意)を呼ぶ
  3. レスポンスに EchoResponse のインスタンスが返ってくるので proto ファイルで指定のあった input というキーの値を取り出して返す

まとめ

やっと gRPC について勉強することができました。 Protocol Buffers を使ってスキーマを定義しておくといろいろなサービスや言語で使い回すことができるので、マイクロサービス設計には欠かせないのかもと思いました。

また、たとえ proto ファイルの内容が変更された場合でも生成し直せば動くことが期待されるため、スキーマの変更に強いのも使われている要因なのかなと思います。

プライベートで gRPC を使うことはそうそうないかもですが、チーム開発では便利なので使える場面があったら試してみたいです。

Github Actions と S3 + CloudFront を使って Nuxt アプリケーションを配信

Github Actions と S3 + CloudFront を使って Nuxt アプリケーションを配信

Nuxt.js を CloudFront で配信する方法が手軽で便利そうなのでやってみました。 Github Actions を組み合わせることによって master への push を検知して S3 へのアップロードを自動化してみます。

Nuxt

Nuxt 側での準備はほとんど必要ありません。 とりあえずプロジェクトを作成し、静的ファイルを生成します。

# プロジェクト作成
$ yarn create nuxt-app sample-app

# 静的ファイル生成
$ cd sample-app
$ yarn generate

これらを Github に push しておきます。

S3

S3 側ではバケットを作成します。 ポリシーはジェネレータを使って作成しました。

awspolicygen.s3.amazonaws.com

{
  "Version": "2012-10-17",
  "Id": "xxxxxxxxxxxxx",
  "Statement": [
    {
      "Sid": "xxxxxxxxxxxxxxxxxx",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": ["arn:aws:s3:::{バケット名}/*"]
    }
  ]
}

Github Actions

Github Actions はワークフローを構築できる Github 上のサービスのことです。 CI / CD 環境を用意する必要がなく Github 上で起きるアクションをフックにビルドやテスト、デプロイを走らせることが出来ます。

ワークフローは Yaml 形式で記述ができ、Github 上にテンプレートがあるため容易に作成が可能です。 このファイルを .github/workflows 配下に置くと Github 側で認識されます。

構文に関しては以下のページに記載されています。

help.github.com

まずはテンプレートを選びます。 ※ AWS に関係ありそうな以下のテンプレートを選んでみましたが ECR にイメージを push するもので今回の題材とはあまり関係ありませんでした。

f:id:h-piiice16:20200104142240p:plain
テンプレートを選択

生成された yml ファイルを Github 上で少し編集します。

on:
  push:
    branches:
      - master

name: Deploy to Amazon S3

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Setup Python 3.7 for awscli
      uses: actions/setup-python@v1
      with:
        version: '3.7'
        architecture: 'x64'

    - name: Setup Python 3.7 for awscli
      uses: actions/setup-python@v1
      with:
        version: '3.7'
        architecture: 'x64'

    - name: Build
      run: |
        yarn install
        yarn generate

    - name: Copy to s3
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: aws s3 cp ./dist ${{ secrets.S3_PATH }} --recursive --acl public-read

on 句の部分で対象とするアクションを指定します。 見たとおり master を push した際に実行されます。

jobs 配下にある run-on では実行するインスタンスを指定することができます。 Windows なんかもあるみたいです。今回はテンプレートと同じ ubuntu で行きます。

env は名前の通り環境変数を定義する場所で、AWS のアクセスキーなどを設定しました。 リポジトリの設定にある screts に AWS_ACCESS_KEY などのキーともに値を設定すると反映されます。

f:id:h-piiice16:20200105134033p:plain
github screts

次に画面右上の緑のボタンから編集した deploy.yml を作成するコミットをします。

f:id:h-piiice16:20200104142235p:plain
作成した yml ファイルを commit

コミットすると早速 Github Actions が実行されます。

f:id:h-piiice16:20200105083524p:plain
github actions の実行風景

S3 を見るとアップロードが完了していました。

CloudFront

CloudFront は AWS 製のコンテンツ配信サービス(CDN)です。 世界中にエッジサーバーがあり、それぞれでキャッシュが効いており高速にコンテンツを配信することが出来ます。

CloudFront は配信データ量による課金制になっており 10TB までなら $0.114 ととても安いです。

それでは「Create Distribution」で作って行きます。

設定したのは Origin の欄だけで S3 のバケットを選択します。(あとはデフォルトのままでいけたはず。。) location がデフォルトだと all になっているため料金を考慮して US だけにしました。

作成後、Error Page の設定が必要です。

なぜかと言うと Nuxt の動的ルーティングするページの場合(_id.vue のようなファイルの場合)html ファイルが実体として存在せず、CloudFront からすると 404 になってしまいます。
これだとまずいので CloudFront 側で 404 のとき /index.html を返すようにすると Nuxt 側で処理して正しいページを返してくれるようになります。

f:id:h-piiice16:20200105135114p:plain
Error page 設定

これで準備完了。

xxxx.cloudfront.net みたいな URL が発行されるのでアクセスすると繋がりました!

参考

tech.actindi.net

nuxt-property-decorator を使って Vue コンポネントをクラス構文に対応させる

nuxt-property-decorator を使って Vue コンポネントをクラス構文に対応させる

Nuxt + TypeScript のプロジェクトを考える際にあると便利な nuxt-property-decorator について調べたのでまとめる。
とりあえず使ってみた感想はクラス構文にすることによって簡潔に書くことができるのが良い点だと思った。

用語の説明や使い方などを記していく。

デコレーターとは

TypeScript で注釈(またはアノテーション)を class や method に付与することのできる宣言のことである。
Vue(Nuxt)の場合、クラスやメソッドに付与することにより、 コンポネントであることや、メソッドが Getter や Action を示すなどの視覚的な情報や そのアノテーション独自の機能を対象に付与することができる。

以下が公式の定義である(英語)

www.typescriptlang.org

日本語もあるよ。

js.studio-kingdom.com

nuxt-class-decorator とは

Nuxt プロジェクトにデコレータを持ち込むためのライブラリである。 もともとの vue-class-decorator という Vue + TypeScript でクラス構文を可能にするライブラリの派生らしい。

そのためシンタックスは Vue のクラス構文になる。

create-nuxt-app をしたあとの pages/index.vue の内容をクラス構文に変えてみる。

before

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
       {{ description }}
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'
export default {
  components: {
    Logo
  },
  data(): {
    return { title: 'test app' }
  },
  computed: {
    description: { `${title} is a My app` }
  }
}
</script>

after

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
        {{ subtitle }}
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import Logo from '~/components/Logo.vue'

@Component({
  components: {
    Logo
  }
})
export default class Index extends Vue {
  public title: string = 'test app'
  public subtitle: string = `${this.title} is My app`
}
</script>

クラス構文に書き直すとこのように変わる。
ここで登場するのが @Component というアノテーションです。 これが「このクラスが Vue コンポーネントですよー」と宣言している。 Component にはいろいろなオプションを与えることができる。(今回だと components がそれに当たる。) アノテーションを付与することによって内部的にはメタプロしているみたいだけどどのようなことしているのかまではわかっていない。

また Vue コンポーネント特有のメソッド data や computed がクラスのインスタンス変数みたいに定義することができる。

次に子コンポネント(components/Child.vue)を新たに追加してみる。

# components/Child.vue
<template>
  <div class="child">
    <p>{{ child.name }}</p>
  </div>
</template>

<script lang="ts">
import { Prop, Component, Vue } from "nuxt-property-decorator";
import ChildType from "../models/child"

@Component
export default class Child extends Vue {
  @Prop()
  child!: ChildType
}
</script>

# pages/index.vue
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
        {{ subtitle }}
      </h2>
      <div v-for="child in childs" :key="child.id">
        <Child :child="child"/>
      </div>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import Logo from '~/components/Logo.vue'
import Child from '~/components/Child.vue'
import { store } from '~/store'

@Component({
  components: {
    Logo,
    Child
  }
})
export default class Index extends Vue {
  public title: string = 'iVoteFront'
  public subtitle: string = `${this.title} is My app`

  get childs() {
    return store.childs
  }
  async asyncData() {
    await store.loadDatas()
  }
}
</script>

コンポーネントの属性には @Prop というアノテーションを付与する。 また Props に Type を指定することでどのような値が親コンポーネントから渡ってくるかがわかりやすい。

ここからは余談ですが、 親コンポーネントでは store から子コンポーネントにわたすデータを取得する例で書いている。

メソッドの前に get というキーワードを書くことによってそのメソッドが算出プロパティ( Vue で言うところの computed)であることを表す。

asyncData は Nuxt 特有の機能で SSR 途中などに外部の API などからデータを得てレンダリングしたい場合などに使われる。 今回の例だと loadData() で childs という state に値をセットしている処理が書かれている。

ここらへんの話は vuex-module-decorator の回でまとめる。

まとめ

デコレータとはなにか nuxt-property-decorator の使い方がわかった。

注意が必要なのはデコレータの機能は実験的らしくこれが今後スタンダードになっていくかがまだわからないということ。

まだ使ったことない注釈もたくさんあり場合によっては便利なシチュエーションがあると思うので今後を追っていきたい。