知識のリンク集

技術系アウトプット

SAMでマイクロサービスインフラ環境構築

SAMとCloud Formationの違いは?

これはよく会話でごっちゃになっていたので整理。
SAMはCloud Formationの拡張機能

Cloud FormationはAWS リソースを正確に記述しつつそれらの関係性を表現することができるテンプレート仕様となっておりその分仕様が少しややこしい。
一方SAMはサーバーレス・アプリケーションを定義するためのものでありCloudFormationの機能を使いつつリソース間の関係性をより直感的に記述できるようになっている。

というまとめを下記記事より学んだ。
同じアーキテクチャをSAMとCloudFormationそれぞれで用意したテンプレートコードが掲載されており、SAMがいかに簡易化されているのかがわかる。
https://toris.io/2017/12/aws-sam-as-a-good-development-toolchain-for-serverless-apps/

手順
  1. アーキテクチャ図作成
  2. テンプレートファイル作成
  3. 実行&確認
1. アーキテクチャ図作成

最近は資料を作成する際にFigmaを活用することが多い。
今回の図もFigmaを使った。

f:id:yuri_iOS:20200406000840p:plain

実際にはテーブル名やLambdaのfanction名も記載する。
誰が開発を担当しても命名がずれないようにアーキテクチャを決める話し合いで決めておく。

GraphQLを使っているためにEndpointの入り口をわけていたり、CQRSを採用して書き込み用と読み取り用のDBをわけたりしている。

2. テンプレートファイル作成

事前準備として、SAMでサーバーレスアプリケーションを作成するにはテンプレートファイルをパッケージ化してデプロイするという流れ。
この時にAWS SAM CLIを使うのでインストールしておく。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install-mac.html


SAMのテンプレートはJSONYAMLでかけますがYAMLを採用。

テンプレートはCould Formationの最新のものをベースに、ファイルの冒頭にて "Transform: AWS::Serverless-2016-10-31" を宣言することでAWS CloudFormation テンプレートを AWS SAM テンプレートとして識別されるようにする。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-formats.html
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-specification-template-anatomy.html

Transform: AWS::Serverless-2016-10-31

Globals: 全体で共有する設定

Parameters: 変数定義
            例)環境によって処理をわけるためにENVを変数として定義しておく

Mappings: キーと名前付きの一連の値とが対応付けらる。
      例) 環境によって参照するAPIのパスを本番・STG・開発用と変える

Resources: スタックに含める AWSリソースを宣言


採用するリソースの記述方法と各プロパティは公式ドキュメントで確認
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-specification-resources-and-properties.html

3. 実行&確認

テンプレートファイルができたらsamのコマンドでパッケージ化&デプロイを行う。
この時デプロイ先としてS3にバケットが必要になるので作成しておく。

パッケージコマンド

sam package --template-file [template.yaml] --output-template-file [output-template.yaml]  --s3-bucket ${BUCKET_NAME}

デプロイコマンド

sam deploy --template-file [output-template.yaml] --stack-name [stack-name] --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset --parameter-overrides Env="dev"

上記のコマンドをCircle CIなどで実行する際に読みやすいよう、あわせてMakeFileにコマンドを作成しておく。

deploy:
	sam package \
		--template-file template.yaml \
		--output-template-file output-template.yaml \
		--s3-bucket ${BUCKET_NAME}

	sam deploy \
		--template-file output-template.yaml \
		--stack-name [stack name] \
		--capabilities CAPABILITY_IAM \
		--no-fail-on-empty-changeset \
		--parameter-overrides Env="dev"

スタックとは関連リソースを単一のユニットとしてまとめたもの。
Cloud Formationの方にスタックについての記載がある。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-whatis-concepts.html#w2aab5c15b9

deployが成功するとAWS コンソールのCloud Formationにてスタックが確認できる。
SAMは一回成功すれば以降失敗してもロールバックしてくれるし差分だけ見て環境を構築しなおしてくれるらしい。

Circle CIとAWS SAMのデプロイ

今回はCircle CI設定ファイルを作成した際に触ったものについて調査内容をまとめる

なにはともあれ公式ドキュメント
Circle CI
https://circleci.com/docs/ja/2.0/configuration-reference/

Cicrle CIが動く条件

CircleCI と連携したリポジトリのブランチに .circleci/config.yml ファイルがあること
この時動かせるCircle CIのバージョンは2.1

orbs

(公式ドキュメント: https://circleci.com/docs/ja/2.0/orb-intro/)
ジョブ、コマンド、Executor のような設定要素をまとめた共有可能なパッケージ。

使用するためにはCicrcle CI version2.1であり、config.yml にて下記のように宣言する。

version: 2.1

orbs:
  aws-serverless: circleci/aws-serverless@1.0.2

今回はCircle CIパイプラインでSAMによるインフラ環境のデプロイを実行するために下記のorbを使用した。
https://circleci.com/ja/integrations/aws/

AWSのCircle CI上でのCI/CDについてはここにまとまっている
https://circleci.com/orbs/registry/orb/circleci/aws-serverless#quick-start

executors

ジョブステップの実行環境を定義し、jobごとに再使用することができる

GoのプロジェクトのCIを作成するとして、testやlintといったjobで毎回goの環境を指定するのはではなく、executorとして実行環境を定義して活用。
今回はLambda上でgoが実行されるというシステムのためdockerにはgolangのimageなどを指定した。
またSAMをつかう所以でpythonも必要。
enviromentには環境変数(goではos.Getenv()で取得できるやつ)を定義する。

version: 2.1
executors:
  go-executor:
    docker:
      - image: circleci/golang:1.13-node
      - image: circleci/python:3.6.8
      - image: XXX   
    enviroment:
      - ENV: ""
      - XXX: ""

jobs:
  my-job:
    executor: go-executor
jobs

job直下の階層は[job-name]で任意のjob名を開発者がつける。
大体はこの4つですみそう。

1. dependencies...依存関係解決(モジュールなどの取得)

  executor: go-executor
    steps:
      - run: go mod download

2. lint

  lint:
    executor: go-executor
    steps:
      - checkout
      - run: go get -u golang.org/x/lint/golint
      - run: golint ./...
      - run: go vet ./...

3. test

  test: 
    executor: go-executor
      steps:
        - checkout
        - run: go test ./...

4. build

  build:
    executor: go-executor
    parameters:
      環境変数
    steps:
      - checkout
      - run: Lambaで実行されるgoプログラムを全てビルド
      - aws-serverless/install
      - run: samのデプロイコマンドを実行
steps

step_typeと各項目
checkout: working_directoryでデフォルトとして設定したパスに戻る
restore_cache: key に設定されている内容を元に、キャッシュを復元する
- key: 復元するキャッシュキー
- keys: 復元するキャッシュキー(複数)
save_cache: CircleCIのオブジェクトストレージにある依存関係やソースコードのようなファイル、ディレクトリのキャッシュを生成し、保存
- key: キャッシュ識別用のユニークID
- paths:
run:
- name: ステップ名(CicrcleCI上で表示)
- command: シェルコマンド
- working_directory: 実行ディレクト
persist_to_workspace: Workflows の実行時に、他のジョブが使っていた一時ファイルを保持しておくための特殊なステップ
...正直必要性がわかっていない

親コンポーネントから子コンポーネントの関数を実行したい

コンポーネント

コンポーネント自体を forwardRef() に渡し、親コンポーネントのref.current を通じて子コンポーネントの ref にアクセスすることができるようにする。

import { forwardRef } from 'react';

const VideoPlayer: React.FC<Props> = (props, ref) => {

・・・

}

export default forwardRef(VideoPlayer);

コンポーネントの第二引数でrefを受けとっておく。

forwardRef
ref を配下のツリーの別のコンポーネントに受け渡す React コンポーネントを作成する。


useImperativeHandleを使って親コンポーネントに渡せる関数を定義する。

import { useImperativeHandle } from 'react';

const VideoPlayer: React.FC<Props> = (props, red) => {

・・・

  useImperativeHandle(ref, () => {
    return {
      pause: () => onPressPause();
    }
  });

・・・

}

useImperativeHandleの第一引数でrefを受け取り、第二引数に参照できる関数を返却する即時関数を渡す。

useImperativeHandle
ref が使われた時に親コンポーネントに渡されるインスタンス値をカスタマイズする。
forwardRef と組み合わせて使う。

コンポーネント

import React, { useRef } from 'react';

const videoPlayer = useRef<any>(null);

const Detail: React.FC<Props> = (props) => {
    
  <VideoPlayer
    ref={videoPlayer}
  />
}

コンポーネントのプロパティにrefを追加し、ref オブジェクトを格納する変数を渡す。

refオブジェクトのcurrent配下にuseImperativeHandleで定義した関数があるので下記のように子コンポーネントのメソッドを呼び出せる。

videoPlayer.current.pause();

AWS Cloud FormationでDaynemoDBのテーブル作った備忘録

AWS CloudFormationでテーブルを作ってみた。

CloudFormationはコードでインフラ環境構築できちゃうその手軽さからインフラ畑の人たちからこよなく愛されている。

AWSCloudFormation

今回はDynamoDBのテーブルを1つ新規追加した。
ymlファイルに30行ほど追加するだけで作業は完了した。


テンプレートの基礎をおさえればあとはリソースごとのサンプルがあるのでテンプレートをベースに微調整していく。

テンプレートの基礎
DynamoDBのテンプレートサンプル

  [リソースオブジェクト名]:
    Type: AWS::DynamoDB::Table // リソース
    Properties:
      AttributeDefinitions: // キーで使用するアトリビュートの型定義
        - AttributeName: "UserID"
          AttributeType: "N"
        - AttributeName: "Timestamp"
          AttributeType: "N"
      KeySchema: // キーの指定
        - AttributeName: "UserID"
          KeyType: "HASH"
        - AttributeName: "Timestamp"
          KeyType: "RANGE"
      TableName: !FindInMap [TableMap, !Ref Env, UserActivities]

DynamoDBの設定項目はキーとするアトリビュート(ハッシュ&レンジ)とその型くらいなものでカラムを全部論う必要はない。

TableNameはFindInMap関数で開発環境(Env)に応じて参照先を変えるようにしている

TableMap:
    pro:
      UserActivities: [テーブル名]
    stg:
      UserActivities: STG[テーブル名]
    dev:
      UserActivities: Dev[テーブル名]

最後に開発環境に応じて定まるテーブルネームをLambdaのビルド環境変数としてセットする

Resources:
  VideoMutationEndpoint:
    Type: AWS::Serverless::Function
    Properties:
      // ・・・
      Environment:
        Variables:
          [Lambdaの環境変数名]: !Ref [リソースオブジェクト名]
||>

GraphQL & Apolloを導入してよかったこと

現在開発しているモバイルアプリは既に5年ものとなりました。
APIに関してはRESTを採用しています。

5年も経つと機能も増え、それに伴いAPIやデータ構造も複雑になってきてチーム全体として仕様の把握に課題を感じるようになってきました。

そんな折、新しい機能を作る際にGraphQLを採用し、 クライアント側にはApolloを導入しました。

これにより個人的に嬉しかったことが下記の4つです。

  • スキーマ駆動開発で作業がブロックしない
  • 画面単位で情報設計を考えることができる
  • コードレビューしやすい
  • 脱Reduxによりテスト箇所が減った
スキーマ駆動開発で作業がブロックしない

先にスキーマを定義することで、サーバー側の実装を待たずしてフロントに着手できました。

スキーマ定義はスプリントプランニングの時点で行い、画面単位で「この画面にはどんなデータが必要か」という情報設計をチームで話し合いました。

現段階ではクエリは画面単位で用意しています。

画面単位で情報設計を考えることができる

クエリを画面単位で用意することで情報設計をシンプルにできました。

またクエリの命名(=オペレーションネーム)はAPIのレスポンスに揃えてフロントで自由に変えられるのでクエリのリストを作成する際もフロントの把握しやすいように管理できます。

余談: クエリーとミューテーションの型の命名規約

クエリ: 名詞(どのデータを取得するか)
ミューテーション:動詞+名詞(どのデータをどう変更するか)

これに対し、フロント側でさらに同じクエリでも使用シーンを説明できるようなオペレーションネームをつけることでデータオブジェクトとその扱い方を分離し、後者に関してフロントで管理しやすくなりました。

コードレビューしやすい

オペレーションネームと実際に取得できるデータがコードで確認できるためコードレビュー時にデータに関しても把握・レビューすることができます。

脱Reduxによりテスト箇所が減った

ApolloのおかげでReduxにデータを保持する必要がないため、Reduxの処理をまるっと書かなくてよくなりました。

Reduxに関するコードがない分テスト箇所も減りますし、データの取得も欲しい形でリクエストできるためフロントでは受け取ったデータをそのまま使うだけですみました。

今までは複雑化したREST APIの都合上、フロントでデータ整形が必要なケースもあり、その分受け取ったデータをそのまま使うだけとはいかずロジックが分散することがあったのでこれは本当に解消できて嬉しい部分でした。