AWS Lambda / Amazon API Gateway の連携・エンドポイント作成(REST API)

この記事は個人ブログと同じ内容です

www.ritolab.com


サーバレスアーキテクチャ構築の第一弾です。

今回は AWS の Lambda と API Gateway を連携させて、エンドポイントを作成していきます。

サーバーレス・コンピューティング

自身でサーバーを運用せずに IaaS 等のサービスを利用して処理のロジックだけをデプロイ。必要な時にだけホストした処理を動作させる一連の仕組み。

例えば、いくつかの処理を行う API サーバを持っているとして、そのサーバは 24 時間 365 日稼働させる必要がある。

サーバレスにすれば処理のロジックをリモートにホストしておき、必要な時にだけ動作させる事ができて、自身でサーバーを運用する手間が省ける。(サーバーに関するセキュリティや可用性をその分気にしなくて良くなる)

さらには動作した時間だけの課金になるので、24 時間 365 日サーバーを稼働させるよりも(大抵は)費用が抑えられる。

サーバーレス・コンピューティングを提供している主な IaaS

余談ですが AWS では、Well-Architected Framework という指針(AWS を用いた設計のベストプラクティスなど)を出していて、その中の「パフォーマンス効率」の章でサーバーレスアーキテクチャを推奨していたりもします。

docs.aws.amazon.com

AWS Well-Architected Framework

wa.aws.amazon.com

AWS Lambda

AWS Lambda はサーバーレスコンピューティングサービスで、サーバーのプロビジョニングや管理、ワークロード対応のクラスタースケーリングロジックの作成、イベント統合の維持、ランタイムの管理を行わずにコードを実行できます。

引用元:AWS Lambda(イベント発生時にコードを実行)| AWS

AWS でサーバレスやるぞってなったらこいつですね。

aws.amazon.com

Amazon API Gateway

フルマネージド型サービスの Amazon API Gateway を利用すれば、デベロッパーは規模にかかわらず簡単に API の作成、公開、保守、モニタリング、保護を行えます。API は、アプリケーションがバックエンドサービスからのデータ、ビジネスロジック、機能にアクセスするための「フロントドア」として機能します。

引用元:Amazon API Gateway(規模に応じた API の作成、維持、保護)| AWS

こちらはサーバレス云々というより、API を作成できるサービスです。今回は API Gateway でステートレス API を作成して、アプリケーションからそのエンドポイントを叩こうと思います。

API Gateway で作成したエンドポイントにリクエストしたら、Lambda の処理が動く。という流れです。

aws.amazon.com

開発環境

今回の開発環境は以下になります。

  • Terraform v1.0.2

ちなみに今回、AWS Lambda は Node でやります。

ホストするコードは JavaScript になるわけですが、今回はこれも terraform で管理します。

実運用だと別々での管理が望ましいと考えますが、それよりもとにかく動かしてみたい欲が先行したので今回はその辺は割愛します。

それと、tf ファイル作成していく中で functions へのパスを書いているのでディレクトリ構成を事前共有しておきます。

.
├── functions
├── src
└── terraform
  • functions(Lambda にホストするコード)
  • src(アプリケーション)

Lambda にホストするコード

まずは、AWS Lambda にホストするコードを作成しておきます。

functions/tf-test-node-hello-world/index.js

exports.handler = async (event) => {
    return {
        isBase64Encoded: false,
        statusCode: 200,
        headers: {},
        body: JSON.stringify('Hello from Lambda!'),
    };
};

「Hello from Lambda!」を返すだけの関数です。

また、API Gateway が Lambda からレスポンスを受け取る場合は形式が決まっているため、そちらに倣った形式にしています。

docs.aws.amazon.com

Lambda の構築

Lambda を構築していきます。

CloudWatch

Lambda の実行ログを CloudWatch に流すのでロググループとロールを作成します。

AWS Lambda の Amazon CloudWatch ログへのアクセス

docs.aws.amazon.com

main.tf

variable "function_name" {
  type    = string
  default = "tf-test-node-hello-world"
}

# CloudWatch Logs for lambda
resource "aws_cloudwatch_log_group" "node_lambda_hello_world" {
  name = "/aws/lambda/${var.function_name}"
}

# Lambda Role for logging CloudWatchLogs
resource "aws_iam_role" "lambda_node_logging" {
  name = "${var.app_name}-lambda-role"

  assume_role_policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_node_logging" {
  name        = "${var.app_name}-lambda-policy"
  description = "IAM policy for logging from a lambda"

  policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Action   = "logs:CreateLogGroup",
        Effect   = "Allow",
        Resource = "arn:aws:logs:${var.aws_region}:${var.aws_id}:*"
      },
      {
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        Effect = "Allow",
        Resource = [
          "${aws_cloudwatch_log_group.node_lambda_hello_world.arn}:*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_node_logging" {
  policy_arn = aws_iam_policy.lambda_node_logging.arn
  role       = aws_iam_role.lambda_node_logging.name
}

Lambda へのデプロイ

今回は terraform で Lambda にデプロイするコードも管理するので、ローカルで ZIP を作成するようにしておきます。

main.tf

data "archive_file" "lambda_function" {
  type        = "zip"
  source_dir  = "../functions/tf-test-node-hello-world"
  output_path = "../functions/upload/tf-test-node-hello-world.zip"
}

これで terraform plan ないし terraform apply 時に ZIP が作成されます。

Lambda function の作成

最後に、メインである関数を定義します。

main.tf

resource "aws_lambda_function" "hello_world" {
  filename      = data.archive_file.lambda_function.output_path
  function_name = var.function_name
  role          = aws_iam_role.lambda_node_logging.arn

  handler = "index.handler"

  source_code_hash = filebase64sha256(data.archive_file.lambda_function.output_path)

  runtime = "nodejs14.x"

  depends_on = [
    aws_iam_role_policy_attachment.lambda_node_logging,
    aws_cloudwatch_log_group.node_lambda_hello_world
  ]
}

docs.aws.amazon.com

docs.aws.amazon.com

Lambda 構築実行

ここまで書いて terraform apply を実行すると、Lambda に関数が出来上がります。

f:id:ro9rito:20210720113408p:plain

API Gateway の構築

次に、作成した Lambda 関数を実行するためのインターフェースを Amazon API Gateway を使って作成していきます。

API 作成

API を作成します。インターフェースの大本を作成するイメージ。

API タイプは用途によって最適なものを選択する必要があります。今回は REST API で作成します。

docs.aws.amazon.com

main.tf

# API Gateway
resource "aws_api_gateway_rest_api" "to_lambda_node" {
  name        = "${var.app_name}-to-lambda-node-api"
  description = "REST API for lambda node test"
}

リソース作成

/hello_world のリソースを作成します。

main.tf

## for Function: hello world
### リソース作成
resource "aws_api_gateway_resource" "hello_world" {
  rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id
  parent_id   = aws_api_gateway_rest_api.to_lambda_node.root_resource_id
  path_part   = "hello_world"
}

メソッド作成

メソッドはリソースに対して GET や POST などの HTTP リクエストメソッドを作成します。ここでは GET で作成します。

main.tf

### メソッド設定
resource "aws_api_gateway_method" "hello_world" {
  rest_api_id   = aws_api_gateway_rest_api.to_lambda_node.id
  resource_id   = aws_api_gateway_resource.hello_world.id
  http_method   = "GET"
  authorization = "NONE"
}

lambda 統合を設定

今回は API Gateway で作成したエンドポイントがリクエストを受け取ったら Lambda function を実行するので、API Gateway と Lambda を紐付けます。

API Gateway では Lambda との連携がスムーズにできるようにこういった設定も用意されているので紐付けが簡単に行えます。

main.tf

### lambda 統合
resource "aws_api_gateway_integration" "hello_world" {
  rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id
  resource_id = aws_api_gateway_resource.hello_world.id
  http_method = aws_api_gateway_method.hello_world.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.hello_world.invoke_arn
}

ポイントとしては、integration_http_method を POST に指定している点です。

lambda への統合 method は POST で固定となっています。

aws.amazon.com

デプロイメント作成

デプロイメントを作成します。デプロイメントは、REST API 構成のスナップショットのイメージ。作成したデプロイメントを次の、ステージと紐付ける事で API が公開されます。

main.tf

### デプロイメント
resource "aws_api_gateway_deployment" "hello_world" {
  rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id

  depends_on = [
    aws_api_gateway_integration.hello_world
  ]
}

再度デプロイメントを行う場合の注意点

新しいリソースやメソッド作成、もしくは既存のリソースやメソッドの変更を terraform から反映する場合は、再度デプロイメントを作成する必要があります。

単純にリソースやメソッドをコード上で追加して反映しても、リソース上で追加されるだけでそれはデプロイされないからです。

そしてデプロイメントは、リソースやメソッドを追加・変更しても apply 時に変更対象とはなりません。(既に作成済みのため)

そのため AWS コンソールから手動でデプロイしてあげる必要があります。

リソースやメソッドの新規作成や変更を検知する、もしくは毎回の apply 時にデプロイメントの作成を強制する仕組みも作れますが、terraform 上での反映でデプロイメントを作成する場合は現在のものを削除した上で作り直す必要があるため、カスタムドメイン名を設定していない場合はエンドポイントのサブドメイン名が変更になってしまうので注意が必要です。

resource "aws_api_gateway_deployment" "hello_world" {
  .
  .
  .
  lifecycle {
    # 変更に対応するため一度削除して作り直す
    # REST API ID の変更によりエンドポイントの URI が変わるので注意
    create_before_destroy = true
  }
}

また、上記の設定を行わなずにデプロイメントの変更を apply した場合は以下のエラーが出力されます。

Error: error deleting API Gateway Deployment (xxxxx): BadRequestException: Active stages pointing to this deployment must be moved or deleted

どうしても仕組みが必要な場合以外はここは記述しないか false にして手動で API デプロイを行うのが良いと思います。(今回は特段必要としていないので lifecycle については記述しない(=false)で進めます。)

ステージ作成

ステージを作成します。ここでデプロイメントを参照して作成する事で API が公開されます。

main.tf

### ステージ
resource "aws_api_gateway_stage" "hello_world" {
  deployment_id = aws_api_gateway_deployment.hello_world.id
  rest_api_id   = aws_api_gateway_rest_api.to_lambda_node.id
  stage_name    = var.app_name
}

API Gateway 側の設定はこれで終わりです。

Lambda へのアクセスを制限する

最後に Lambda 関数(hello_world)へのアクセス許可を API Gateway に付与します。

main.tf

resource "aws_lambda_permission" "execution_hello_world_for_api_gateway" {
  statement_id  = "test-AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.hello_world.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_rest_api.to_lambda_node.execution_arn}/*/*"
}

source_arn を本 API のみにしたので、今回作成したエンドポイントからでしかこの Lambda 関数は実行できないようになっています。

API Gateway 構築実行

ここまで書いて terraform apply を実行すると、API Gateway が作成され、Lambda 関数にも関連付いた事が確認できます。

f:id:ro9rito:20210720120201p:plain

動作確認

一通りミニマムで作成したので、アプリケーションからエンドポイントを叩いてみます。

f:id:ro9rito:20210720120235p:plain

正常にレスポンスが返ってきました。これで API Gateway 経由で Lambda 関数を実行できました。

今回はここまでになります。作成したエンドポイントはデフォルトの URL であったり制限がかかっていなかったりするので、次回はエンドポイントの設定や制御を行っていこうと思います。

【読書メモ】「成長する企業はなぜ SSO を導入するのか」

この記事は個人ブログと同じ内容です

【読書メモ】「成長する企業はなぜ SSO を導入するのか」

---

最近チームのメンバーが誕生日で、よくバラエティ番組で流れる曲「Happy Birthday - Stevie Wonder」が頭から離れない sekitats です。

今月は個人的に認証について学ぶ月間で、本を3冊読みました。認証周りわからんことばかりだったので。

OAuth については、別記事を書いてみたのでそちらも興味があれば読んでみてください。

ratatatat30.hatenablog.jp

2冊目の Auth0 + Nuxt + Rails のデモも試した。

本記事は3冊目「成長する企業はなぜ SSO を導入するのか」の要約となります。
引用元:日本ヒューレット・パッカード株式会社 (著) . 『成長する企業はなぜSSOを導入するのか 』. 日経BP, 2017

なぜ、この本を読んだのかというと、SSO に関する本を amazon で検索したら、本書しかなかったからです。(あとはミリタリーバックパック

認証はすでに経営課題

以下のような企業における認証の課題があげられている。

  • 社員のパスワードの使い回しによってパスワード盗難リスクが高まる。企業側の危機意識が低い
  • 厳しすぎるパスワードポリシーが返って生産性を低下させる
  • 個々の業務システムの担当者は、自分が担当している業務システムだけを考えて対策を立ててしまう傾向にある → サイロ化
  • サイロ化した中で認証強化してもそれは「部分最適化」でしかない。「全体最適化」とは程遠い。

    ※ サイロ化:企業内にある部門が他部門との連携をすることなく、自らの業務の部分最適化のみを優先するようになった状態をいう。元々サイロとは穀物などを貯蔵するタンクのことだが、英語圏では「窓がなく周囲が見えない」という意味も含んでいる。

出ました。サイロ化。大規模になればなるほどサイロ化の弊害は大きくなっていく。

企業経営にまで影響を与える問題となっているということ。

パスワード認証の課題

ユーザーの本人確認を行う認証にも問題は多い。

  • 他人に知られた場合になりすましが可能
  • 安易なパスワードの場合類推や解読が容易
  • 忘れたり間違えたりしやすい
  • 複雑なパスワードは記憶が困難
  • パスワードの使い回しにより、安全性が低下

パスワード認証は、「知られない」、「推測されない」、「同じものを使い続けない」、「同じものを使わない」といった「努力」をユーザーに強いていて、その「努力」には限界がある。

パスワード認証を強化する他要素認証(ワンタイムパスワード、生体認証)も導入が難しい。

そんな、企業の課題、ユーザーの課題を SSO(シングルサインオン)が解決してくれると言うてます。

SSO 導入メリット

  • SSO とは、業務システムで個別に行っている認証を一元化し、ユーザーがいずれかのシステムを利用する際に認証を一度行えば、連携している全てのシステムが利用できるようになる。ユーザー ID,パスワード、ユーザー属性などのユーザー情報の管理や、問い合わせ対応の負荷が軽減される。
  • SSO が導入されるということは、入口が1カ所で管理されるということ。唯一の入口に防犯対策をを一本化できる。情報漏洩の防止策として SSO は役に立つ。
  • 新規に導入する業務システムだけでなく、既存の業務システムにも SSO は効果を発揮する。既存の業務システムで管理されているユーザー情報に大きな変更を加えないため、導入も用意で短期間かつ少ない労力で導入できる
  • 導入によってユーザーの利便性が向上するだけでなく、認証に関する管理の負荷も軽減される。
  • 今後登場する未知のクラウドサービスの導入と行った変化にも柔軟に対応できる
  • 各業務システム間のサイロ化の課題も、個々の業務システム担当者に大きな負担を強いることなく解決でき、IT 投資を効率化できる。

なんのことはない。一つのユーザーID・パスワードだけ管理できるようになれば問題は解決する。

SSO の 4 つの方式と特徴

方式には「リバースプロキシ方式」、「エージェント方式」、「クライアントエージェント方式」「フェデレーション」の 4 つ紹介されているが、最初の 3 つはオンプレミスでSSOを用意する方式。クラウドサービスを利用するものが「フェデレーション」。SSO = フェデレーションと捉えてしまってよい。

フェデレーションの仕組み

フェデレーションの仕組みの一つとして「SAML」や「OpenID Connect」といった。「標準規約」が用意されている。
SAML とは(Security Assertion Markup Language)の頭文字で、OASIS(Organization for the Advancement of Structured Information Standards)が定めた、クラウドサービスと企業の間で認証の連携を行う標準規格のこと。ほとんどのクラウドサービスがこの SAML に対応しており、クラウドサービスと認証の連携を行う場合は、SAML を利用することが多い。

OpenID Connect つまり JWT を使うこともできるのだろう。が、SSO では SAML を使う。

SSO(フェデレーション) はシンプル

本書では、フェデレーションの仕組みをパスポートの仕組みに例えて説明している。

パスポートは所持する本人の国籍の国で発行され、その発行には比較的厳格な本人確認、つまりユーザーの認証が求められる。 ただし、一旦パスポートが発行されれば、渡航先の国ではパスポート自体が本人確認となる。 入国の際にパスポート以外の本人確認の手続きは必要とされない。

f:id:sekilberg:20210713002050p:plain

  1. IdP(IdentityProvider)で厳格な本人確認をする
  2. IdP がアサーションSAML = パスポート)を発行
  3. アサーションがブラウザを介して SP(Service Provider = アプリケーション)に提示される
  4. アサーションの正当性と信頼した IdP による発行かどうかを SP が確認する
  5. SP がユーザーに利用を許可する

SAMLアサーションがパスポートと異なるのは、一度発行されたアサーションは特定の SP へのログインにしか使えないこと。SP 毎にアサーションを発行することが必要。(これは SAMLの仕様)


IdP による厳格な審査を通過していることとSPとの信頼関係によって成り立っているわけですね。

おまけ

いきなり、IdP(IdentityProvider)、SP(Service Provider)といった用語が出てきました。 OAuth(= 認可)の世界での登場人物は、リソースオーナー、クライアント、認可サーバー、リソースサーバーですが、OpenID Connect や SAML(= 認証)の世界になるとユーザー、SP、IdPといった用語になるところも混乱しがちなところです。(トークン→アサーションとか)

まとめ

そもそも技術書ではないし、SSO導入事例、オンプレ導入方法といった企業の経営者や情報システム担当者向けの内容だったので、OneLogin, okta などの IDaaS に関するお目当ての情報はなかった。
ただ、SSOの概要は理解できました。特にパスポートに例えた説明がわかりやすく、OAuth に比べずっとシンプルに感じました。(どう実装するかは別として。。)SSO=パスポートの仕組みということがわかっただけでも収穫アリですね。

SAML に関してはほとんど説明されていませんが、id token のような個人情報が XML 形式になっただけと捉えれば良いかと思います。

近い将来 SSO による生体認証が当たり前になって、フォームに E メール, パスワードを入力していたことが時代遅れになる未来もそう遠くないのかもしれません。

プロジェクトの振り返りに LeanCoffee を試してみた

back check 事業部開発チームの匠平@show60です。

プロジェクトの振り返りに LeanCoffee を用いてみたので、今回はこちらを振り返ってみたいと思います。

振り返りを行った経緯

back check チームではスプリントの終わりに KPT を用いた振り返りを行っていますが、今回これとは別にプロジェクト単位での振り返りを行いました。

チームではこの1ヶ月ほど新機能の開発を進めていたのですが、進行の障害となる事象が起こり始め、うまくいっていないと感じるようになりました。 何が原因で "うまくいってない" のかを洗い出すため、各自が課題と感じていることを収集し議論する場を設けることにしました。

LeanCoffee の実施方法

LeanCoffee とは

何が課題であるかを探るところから始めるため、振り返り手法には LeanCoffee を用いることにしました。

アジェンダのないミーティング方法です

参加者が集まり、アジェンダを作り、議論を始めます

Lean coffee

メリットとしては以下のことが挙げられています。

  • 広い範囲のトピックを扱うことができる
  • 事前・事後の準備が (ほぼ) 不要
  • ネクストアクションを出すこともできる

LeanCoffee の進め方

準備と進行方法は、こちらのスライドを参考に実施しました。

www.slideshare.net

振り返り会場は miro で作成し、事前に以下のような枠組みだけ用意しました。

f:id:show-hei:20210706205703j:plain

話したいトピックを黄緑色の付箋に書いて左の Ready の枠に入れていきます。出揃った付箋のなかから話す順番を決めるため、緑の●を各自2票ずつ投票します。

話すトピックをピックアップして Doing に移します。トピックについて話す時間を7分間とし議論します。

7分経ったら、このトピックの議論を続けるかを投票します。手のジェスチャーで意思表示しますが、今回 miro で行ったためカーソルアイコンを手の絵文字に乗せることで投票としました。

「議論を継続」に4票以上投票されていたら議論時間を4分追加します。これを繰り返し、4票未満になるまで議論を行っていきます。

やってみた結果どうだったか

良かったこと

ミーティング時間が延々と長引くことがない

議論が白熱したり、煮詰まるとついつい長引いてしまうことがあります。LeanCoffee では投票時間を設けてあるため、ミーティング時間を区切る機会が多く、コントロールがしやすいと思います。

広いトピックを扱うことができる

投票によって議論するトピックが決まります。選出理由に制限はなく、多くのメンバーの興味を引いたトピックが選出されます。個人で抱えている課題も場に出すことができるため、相互理解にも効果があると感じました。

良くなかったこと

トピックが大きい場合、別途切り出すなど工夫したほうがいい

LeanCoffee に議論の制限時間を設けているのは、トピックを速く処理するための仕掛けだと思っています。その特性上、取り扱う課題が大きいトピックや、前後関係の深いトピックの場合は扱いが難しいと感じました。

これらのネクストアクションまで話し合うには、ファシリテーションを含め慣れや訓練が必要そうです。

「◯回の延長が行われた場合、別のミーティングを設ける」など独自ルールを決めてもいいかもしれません。

他の手法を上回るメリットが出てこなかった

課題の収集という点ではタイムライン振り返りのほうが抜け落ちが少ないため、LeanCoffee を課題収集・理解の目的と割り切るには難しいように思います。

また課題を選出して議論するという手法は KPT も同じですが、ネクストアクションを必ず出すという点においては KPT のほうが強力です。

KPT での振り返りに慣れているチームとしては、上記で挙げている「良かったこと」を KPT で享受できており、今回試してみた限りでは大きく勝るメリットを感じられませんでした。

準備の手軽さという点は大きなメリットなので、これから振り返りを導入しようというチームにとっては選択肢としてありなのではと思います。

同じように、普段とは違う部署やメンバーとミーティングするときなんかも上手く機能するのではないかと感じました。

おまけ: 振り返り方法の選定

振り返り方法として LeanCoffee 以外にも KPT とタイムライン振り返りも検討しましたので、不採用理由もそれぞれ記述します。

結果として、今回の目的であればタイムライン振り返りを採用してもよかったなと思っていますので、ぜひ次回チャレンジしてみます。

KPT

スプリントの終わりの振り返りには KPT を用いています。 KPT はチームの課題 (Problem) に対して改善のためのネクストアクション (Try) を決めるという強いメリットを持つフレームワークです。

議論したいトピックを投票で選出することで短時間でネクストアクションを考えられる反面、選出されなかったトピックは議論されないまま、また日の目を見ることを期待してそっと過去に消えていきます。

今回の目的は各自が課題に思っていることを洗い出し、できる限り多くを議論の場に持っていくことが目的であったため KPT の採用は見送りました。

タイムライン振り返り

プロジェクトの始まりから終わりまでを時系列に沿って書き出す、データの収集を目的としたフレームワークです。 今回 LeanCoffee を試してみたかったこともあり採用を見送ったのですが、振り返り材料の収集という点では効果のある手法です。

タイムラインで収集した振り返り材料を KPT に持っていく、という組み合わせでぜひチャレンジしてみたいです。

エンジニアのインプットと読書術

この記事は 個人Qiita と同じ内容です

qiita.com/sekiyaeiji

まえおき

フルリモート時代においてエンジニアの情報インプットのスタイルも様変わりしているように感じます。

2019年までのリアル社会では、競争率の高い勉強会とか海外カンファレンスの招待枠が取れる/取れないのような価値観が、たしかに存在していた気がしますが、物理の制約を解かれたセミナーイベントにはもはや人数制限はなく国境も超え無償になり、エンジニアの情報インプットにおいては劇的に恵まれた時代に突入したと言えます。

また、オンラインが主流になることでイベント内の登壇発表部分には収録動画の配信を活用するケースも増えてきています。

私が新しい技術情報をインプットするメディアの割合は、

  • オンライン文字 > オンライン動画 > 書籍

ではありますが、まさにいまこうして文章を書くための情報インプットなどでは、よくまとまってて精査されている書籍の文章も非常に頼りになります。

オンラインイベントが自由を手に入れ、生まれ変わりつつある一方で、日本の活字媒体の事情も変化しつつあるようです。

活字メディアの動向

新型コロナ影響下の書籍の動向

2020年、日本のコミックの売上が、過去最高記録を打ち出したのをご存知でしょうか。

25年ぶりのコミック売上記録更新という奇跡的な回復には、新型コロナの巣ごもり需要が大きく影響しているそうです。

同様に、新書、ビジネス書、専門書(技術書はこれに含まれる)の売上も前年比でプラスに転じていることから、巣ごもりでできた時間を自己研鑽に投じる意識の現れかもしれません。

自費メディアの浸透

活字メディアの動向で気になっているもう1つのトピックは、技術書典技書博のような自費出版カルチャーの浸透です。

技術書典は、3年前にはすでに盛り上がっていたように感じますが、新型コロナ影響によりリアルイベント中止の代替手段としてオンライン販売を全面展開してから、結果的により身近になり、より多くの読者が参加・購入できるようになりました。

盛り上がりに比例して出品ラインナップの玉石混交感も増しており w 書籍の選択には注意が必要ですが、良書を見つけると、ネット記事と本格的な技術書の中間ぐらいの、知りたいことをピンポイントで知るのにちょうどいいボリュームの良書に出会うことができます。

活字メディアによるインプット

さて、そんな私の活字メディアによるインプット方法ですが、まず、できれば電子書籍よりもリアル本を利用します。

学びを結果に変えるアウトプット大全/樺沢紫苑』や『大人のための読書の全技術/齋藤 孝』で共通する内容で、書籍に線を書き込むという手の運動が脳を刺激し、読書時の記憶を促進するという趣旨の記載があります。

書き込んだり折り目をつけたりメモしたりしながら、本を自分色に染めながら読む習慣のために、紙の本がある場合はなるべく入手します。

また、とくに印象に残したい書籍についてはなるべく読書記録を残すようにしています。

これも『学びを結果に変えるアウトプット大全/樺沢紫苑』にある、自己成長の量はインプットの量ではなくアウトプットの量で決まるという提言に従い励行しています。

インプット速度を上げる

さて、そんな私が昨今課題に感じていたのが、インプット速度の向上です。

限られた時間により多くの情報を扱いたくなったときに、ヒントになる内容が『大人のための読書の全技術/齋藤 孝』の中で触れられていたので、具体的にいくつかの方法を引用してご紹介します。

ちなみに、本書における"速読"とは、◯◯協会が提唱する速読トレーニングみたいな内容ではなく、社会人が書籍の内容を、限られた時間内で素早く把握するための具体的な方法について説明されています。

読書速度を上げるテクニック

大人のための読書の全技術/齋藤 孝』の「第2章 : 読書の量を増やす − 速読の全技術」の中で紹介されている、読書の速度と量を上げるテクニックが実施しやすく役に立ったので、以下の通り7つ、引用してみます。

1.目的と締切を同時に設定する

  • 目次を読む
    • 概要を知り、大事な箇所をはっきりさせる
    • どこをしっかり読めばいいかをあらかじめ把握する
  • 本をさばく
    • 1冊につき20分ぐらいかけて本の内容を人に話せるぐらいまで把握する作業
      • 次に開いたときにすぐ読める
    • 買ったその日に、その本に一目惚れしたテンションのまま一気に中身を把握しておく
    • タイトル、帯、カバー袖、目次、小見出しで全体の趣旨をつかむ
    • 練習すれば新書1冊5分も可能

2.逆算読書法

  • 結論から読み始める
  • 内容を要約できればいい
    • 大事なところから読めばいい

3.二割読書法

  • 全体の二割を読んで全体をつかむ
  • 覚える対象になりそうな箇所だけ精読する
  • 目次から全体把握が大切

4.書店で鍛える

  • 購入に値するか、真剣に見極めるトレーニン

5.サーチライト方式

  • 読む前に大事なキーワード5〜6個を決めておく
  • そのキーワードを見つけたら次々とボールペンで丸をつける
  • 大事だと思う箇所のページの上端を折る
  • まあまあ大事な箇所はページの下端を折る

6.同時並行読書術

  • 複数冊を並行して読めると読書量が増やせる
  • 1冊ごとの読破に重きを置かない
  • そのときの気分によって手にとった本の続きから読む

7.TPOに応じて読む

  • TPOに応じて読み分ける
  • 読みかけの本を自宅のあちこちに散らばらせておく
  • 暇を見ては本を読む習慣が身につく

実践する際の注意点

以上は、各テクニックのポイントを簡潔に箇条書きで引用しましたので、より詳細を知りたい方はぜひ書籍を購入してください。

これらの読み方のテクニックはもちろん、ビジネス書や専門書の読み方として紹介しておりまして、小説の読み方として適さないことは言うまでもありません。

私は上記をビジネス書や技術書の内容を把握する場合に利用していますが、たしかに始まりから終わりまで精読するよりは遥かに時短になり、一度トライしたら辞められない読書法になってしまいました。

最初は筆者に対する罪悪感のようなものを感じなくもなかったのですが、実用書は活用することが目的の書物と割り切って一度試してみると、筆者の言いたいことをよりよく理解しようと努力している、むしろ良いことをしてるかも、という感覚にもなれるかもしれません。

まとめ

紙の本を使い倒すことによる効率的な情報インプットについて、情報を集めてご紹介してみました。

技術書もそうですが、変化に対応し、さらなる変化を求める市場において、ビジネスやテクノロジーに関するさまざまなアイデアが雨後の筍のようにアウトプットされる現在、情報を効率よく取り込むテクニックは、備えていて損はなさそうです。

積ん読(つんどく)本とのつき合い方が変わるかもしれません。

輪読会の読み順をランダムで決める chrome 拡張機能を作る for Google Meet [React + TypeScript]

この記事は個人ブログと同じ内容です

輪読会の読み順をランダムで決める chrome 拡張機能を作る for Google Meet [React + TypeScript]

最近弊社の開発メンバーでブログを書こうという運動があります。 ネタ探しをしていたらよさげな記事を見つけたのでアイデアをお借りします。 shohei さんありがとう🙏 ※決してパ○リではありません https://techblog.roxx.co.jp/entry/2021/04/23/064837

毎週チーム内での読書会や、部署をまたいだエンジニアでの輪読会を行っているのですが、都度読み順を決めるのが面倒なので、参加メンバーをシャッフルしてリストで返す chrome 拡張機能を作ってみました。 なお、弊社の読書会は Google Meet で行うことが多いため必然的に Google Meet 用の拡張機能となっておりますのでご理解ください。

作ったもの

現在 Meet に参加しているメンバーをランダムに並び替えて一覧表示する拡張機能を作りました ※store で公開はしていない

image

こちらで公開しているのでぜひ覗いてみてください https://github.com/SotaYamaguchi/member-sort-extension-for-google-meet

技術選定

UI の構築を含むので書き慣れたフレームワークを使いたかったので React + TypeScript を採用しました。

chrome extension をつくるにあたって、以下のテンプレートを使えば React + TypeScript が入った状態でサクッと始められそうだったので使わせていただきました。 https://github.com/chibat/chrome-extension-typescript-starter

パッケージ類

Chrome 拡張機能の型定義を読み込むために @types/chrome を使いました。

image

popup.tsx と content_script.tsx

  • popup.tsx
  • content_script.tsx
    • 開いているタブで実行できる script

上記の 2 箇所でデータをやりとりするためには以下の chromeAPI を活用します

chrome.tabs.sendMessage() chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {} )

https://developer.chrome.com/docs/extensions/reference/tabs/ https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage

popup 側の実装

ユーザー一覧 drawer の表示

ユーザー一覧 drawer が表示されないと参加メンバー名を持った DOM が生成されないため、 初回レンダリング時に sendMessage でcontent_script 側でイベントを発火させます

参加メンバー一覧の取得処理

一覧取得ボタンをoukajini getMemberList() が走り、 sendMessage を送信することで content_script 側で script を実行し、参加メンバー一覧を文字列で取得します それをよしなに加工して view 側で表示させてあげることで参加メンバー一覧を表示しています

  const currentChromeTab = (callback: (tabId: number) => void) => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      // 現在表示しているタブを取得
      const tab = tabs[0];
      if (tab.id) {
        callback(tab.id);
      }
    });
  };

  const getMemberList = () => {
    currentChromeTab((tabId) => {
      chrome.tabs.sendMessage(
        tabId,
        undefined, // message は不要なため undefined とする
        (msg) => {
          if (typeof msg === "string") {
            const shuffledMembers = shuffle(msg.split(","));
            setCurrentTime(new Date());
            setMembers(shuffledMembers);
          }
        }
      );
    });
  };

  // init
  useEffect(() => {
    currentChromeTab((tabId) => {
      // ユーザー一覧 drawer を表示させる
      chrome.tabs.sendMessage(tabId, undefined);
    });
  }, []);

content_script 側の実装

ユーザー一覧 drawer の表示判定

初回表示時にユーザー一覧 drawer の表示判定を行い、未表示だった場合は自動で開くようにしました

参加メンバーの名前一覧を取得

cylMye というクラス名を持った DOM の子孫に参加メンバー名が格納されていたので取得します

const openAllUserDrawer = () => {
  /*
   * 各ボタンの aria-label 属性にラベルに表示するボタン名が格納されている
   * 全てのボタンを DOM から取得して全員を表示ボタンを探す
   */
  const ariaLabelElems = document.querySelectorAll("[aria-label]");
  for (let i = 0; i < ariaLabelElems.length; i++) {
    if (
      ariaLabelElems[i].getAttribute("aria-label") === ALL_USER_BUTTON_LABEL
    ) {
      const chatOpenButton = ariaLabelElems[i] as HTMLButtonElement;
      chatOpenButton.click();
    }
  }
};

const getUserNameList = (sendResponse: (response?: any) => void) => {
  let names: string[] = [];
  const elems = document.querySelectorAll(".cylMye");
  if (!elems.length) {
    // .cylMye が存在しない = ユーザー一覧 drawer が表示されていない
    openAllUserDrawer();
    return;
  }
  for (let i = 0; i < elems.length; i++) {
    names = [...names, findUserNameFromElm(elems[i])];
  }
  // 重複した名前を省く
  names = Array.from(new Set(names));
  sendResponse(names.join(","));
};

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  getUserNameList(sendResponse);
});

メンバーをシャッフルする関数

今回はランダムの偏りをなくすためフィッシャーイェーツのシャッフル という方法をアルゴリズムに採用しました

const shuffle = (value: string[]) => {
  for (let i = value.length - 1; i >= 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [value[i], value[j]] = [value[j], value[i]];
  }
  return value;
};

export default shuffle;

.zip ファイルを生成する

Github Actions で master ブランチへ push 時に build を実行 + 実行結果を .zip に圧縮しリリースに含めるように build.yml に記載

      - name: yarn install & build
        run: |
          yarn
          yarn build --if-present
      - name: zip output
        run: |
          cd dist
          zip release *.*
      ・
      ・
      ・
      - name: upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./dist/release.zip
          asset_name: chrome-extention.zip
          asset_content_type: application/zip

まとめ

参加してるメンバーを集計する目的でも利用できるのでわりと便利かと思うのでぜひ使ってみてください。 今後、公開まで試してみたい。

GASでテスト書くときのちょうどいい塩梅を探る

この記事は個人ブログの転載です

kotamat.com

GASは気軽にコードが書けるというのもあり、テストを書かないケースが多いのかなとは思っているのですが、とはいえある程度の規模になったらテストも書きたくなってくると思うので、どのへんをライン引きとしてテストを書くか、またそのテストの環境はどう設計するかを考えてみたいと思います。

Level 0: Webコンソールでいじる場合

Webコンソール上で直接触るケースはおそらく非エンジニアも触る環境ないしは、すぐに捨てるコードであることが多いかなと思います。 そういったケースの場合、テスト環境どころか開発環境に対してのカスタマイズする余地が殆どないか、その環境を作る過程で実装が終わってしまうレベルの案件となってしまい、費用対効果が出せない状態になるかと思います。

こういったときは潔くテストは書かず、ただつらつらとコードを書き、サクッとデプロイ・実行するというのが良いかなと思います。

Level 1: 保守が見込める場合

今後ある程度の期間使われ、かつ保守メンテが必要になるであろうスクリプトの場合、コード自体はバージョン管理ツールに載せた上で機能開発をしていく必要があるかと思います。

その場合は clasp を用い、ローカルでの開発をしていくことになるのですが、後述のLevel2に満たないレベルだとしても TypeScript 化して最低限の型担保はしておかないと逆に開発生産性が下がってしまうため、TS化をこの範囲での保守性担保のラインとします。

導入方法

導入方法は簡単で、

yarn add -D clasp @types/google-apps-script

を実行した上で、tsconfig.jsonに下記を記載します。

{
  "compilerOptions": {
    "lib": ["esnext"],
    "experimentalDecorators": true
  }
}

その後、下記コマンドを実行し、スクリプトを作成します

npx clasp create --type standalone --rootDir src

あとはsrcディレクトリにて .ts ファイルを作成し、コードを書いていくだけです。

リリースはclasp pushコマンドで反映でき、clasp openで当該GASをWebコンソールで確認することができます。

メリット

今回依存に追加している@types/google-apps-script はGASでよく使うAPIのインターフェースが予め実装されているため、

  • あれ、Spreadsheetから取得したsheetってどういうAPI持ってるんだっけ?
  • getRangeの引数ってrowが先だったっけ?columnが先だったっけ?

という細かな仕様確認を、型情報をみるだけでわかるようになるため、不必要なtypoや、間違ったコードを書きづらくなります。

当然この状態ではテストコードレベルの品質は担保できないため、API上は問題ないけど意図しない挙動が発生するリスクはあります。 そのため、そのあたりの担保もしたい場合はLevel2を検討する必要が出てきます

Level 2: コードの実行結果が実行してみないとわからない場合

スプレッドシートのデータを高度なロジックで置換する場合や、複数のリソースに対して参照し計算結果を算出する場合、時間発火のロジックを書く場合は、コードの実行結果が実行してみないとわからない状態になるかと思います。

ここまで複雑になってくると、テストコードなしでの開発そのものが生産性悪くなるため、外部APIとの通信をしているところと処理ロジック部分を分離した上で、処理ロジック部分のユニットテストを書く必要が出てきます。

考慮点

GASの場合下記を考慮する必要が出てきます。

  1. GAS本体にテスト実行基盤があるわけではないため、別途テストツールを導入する必要がある
  2. テストコードはGAS上では不要なファイルになるため、rootDir外に必要がある
  3. ファイルを切り出してテスタビリティを向上する場合、ファイルの命名規則が大事になる。

1.に関しては今回はjestを使ってみます。こちらもTSベースでコードを書くため、下記の依存をインストールします。

yarn add -D jest "@types/jest" ts-jest 

2.に関しては、Level1でも紹介したようにデプロイ対象のファイルを src/ディレクトリに入れ、テストコードを同階層の__tests__ディレクトリに設置することで解消します。

3.が躓いたポイントなのですが、GASそのものはexport/importの機能を有していないため、GASでTSを書くとexport/importの記述が消された上で反映されます。 GASはファイル名が若い順にコードを読み込むため、エントリーポイントとなるファイルよりもファイル名が後ろのファイルは読み込まれず、実行時エラーが発生します。 現在はエントリーポイントをindex.ts、依存系ファイルを_utils.tsのように_をつけて先頭で読み込まれるようにしています。ここイケてない感じがすごいので、いい方法あれば教えてほしいです。

実際の書き方

テストを書くとはいえ、このレベル感であればソースコードとしてはそれほど肥大化しないことが想定されます。 雑にUtilsというクラスを用意し、そこに諸々のロジックを詰め込んでいくことを考えてみます。

UtilsはGASがない環境でユニットテストを実行する必要があるため、GASのAPIには依存しないインターフェースにする必要があることだけ考慮し設計していきます。

export class Utils {
  constructor(private now: Date = new Date) {}

  public isDateInMinute(date: Date, minute: number = 5): boolean {
      const minutesAfter: Date = new Date(this.now.getTime())
      minutesAfter.setTime(minutesAfter.getTime() + (1000 * 60 * minute))
      return this.now.getTime() < date.getTime() && date.getTime() <= minutesAfter.getTime()
  }
}

コンストラクタに何を入れるかと言うのは議論の余地はありそうですが、GASで頻出する表現は時間発火の概念なので、第一引数に現在を示す変数を入れていきます。

ここで雑にisDateInMinuteという関数を考えてみます。これは引数に指定した日時が現在時刻と比較して所定の分数の間にあるかどうかを判定する関数です。GASは毎分起動するということを設定できるため、スプレッドシートに記載した時間に発火するとかができるようになります。

これをベースにテストを書いてみます。

import { Utils } from "../src/_utils";

const data_isDateInMinute = [
    {
        now: "2021-06-07 10:00:00",
        target: "2021-06-07 10:01:00",
        inMinutes: 5,
        result: true
    },
    {
        now: "2021-06-07 10:00:00",
        target: "2021-06-07 10:06:00",
        inMinutes: 5,
        result: false
    },
    {
        now: "2021-06-07 10:00:00",
        target: "2021-06-07 10:05:00",
        inMinutes: 5,
        result: true
    },
    {
        now: "2021-06-07 10:00:00",
        target: "2021-06-07 10:00:00",
        inMinutes: 5,
        result: false
    },
]
describe.each(data_isDateInMinute)('Utils.isDateInMinute', (data) => {
    it(`${data.now} to ${data.target} in ${data.inMinutes} minutes?`, () => {
        const util = new Utils(new Date(data.now))
        const targetDate = new Date(data.target)
        expect(
            util.isDateInMinute(targetDate, data.inMinutes)
        ).toBe(data.result)
    })
})

このテストコードでは境界値を確認するテストを入れてみました。jestのdescribe.eachを使うことで、複数のシナリオを同時に実行できます。 大事なのはit()のところで new Utils(new Date(data.now))としているところかなと思います。時間に依存している処理を、コンストラクタでDIすることにより、実行時間に依存しないテストにすることができています。

僕はめんどくさがりなので(?)テーブルドリブンなテストケースの作成にjsonを書くとフラストレーションがたまります。 その場合は下記のようにcsvにテストケースを書き、csvを読み込んでシナリオを構築するのでもいいでしょう。いくらここで依存ファイルをimportしたところでGASのアウトプットには一切入ってこないので。

import { Utils } from "../src/_utils";
import csv from "csv-parse/lib/sync";
import fs from "fs";
import path from "path";

// CSVから同期的に読み込む
const data_isDateInMinute = csv(fs.readFileSync( path.join(__dirname, './data/isDateInMinute.csv')))
describe.each(data_isDateInMinute)('Utils.isDateInMinute', (data) => {
    it(`${data.now} to ${data.target} in ${data.inMinutes} minutes?`, () => {
        const util = new Utils(new Date(data.now))
        const targetDate = new Date(data.target)
        expect(
            util.isDateInMinute(targetDate, data.inMinutes)
        ).toBe(data.result)
    })
})

メリット

当然ここまで書けば、テストしたい粒度のものをUtilにぶちこんであげるだけで簡単にテストをかけるようになってきます。殆どのケースであればここまでやっておけばいいでしょう。

Level 3: Webアプリケーションを作る

もはやここまで来ると、GASが動く環境をサーバーとした、Webアプリケーションを作るレベルになるかと思います。 この領域になるとWebアプリケーションとしての設計やテストを求められるようになるため、webpackでまとめたり、テストの分割の仕方も一筋縄ではいかなくなるかと思います。 infrastructure層をGASの外部APIとしたような設計を行い、ユニットテストだけではなくフィーチャーテストも書くようになるかもしれません。

正直ここの領域までGASでやろうと思ったことはないので未知数ですが、やる機会があったらチャレンジしてみようと思います。

まとめ

GASの保守性担保の方針を考えてみました。もしもっとこういうのやってみるといいかもとかあればTwitterまでいただけるとうれしいです

AWS App Runner でアプリケーションをデプロイする

この記事は個人ブログと同じ内容です

www.ritolab.com


2021 年 5 月下旬 に AWS から App Runner というサービスがローンチされました。

今回は App Runner を使ってアプリケーションをデプロイしてみます。

AWS App Runner

AWS App Runner は、インフラストラクチャを管理せずに AWS にアプリケーションをデプロイするサービスです。

ソースコードまたはコンテナイメージを指定するだけで、App Runner がアプリケーションを自動的にビルドおよびデプロイし、ネットワークトラフィックの負荷を分散し、自動的にスケールアップまたはスケールダウンし、アプリケーションの状態を監視し、暗号化を提供します。

aws.amazon.com

  • ソース(Github ソースコードまたはコンテナイメージ)が update されると自動でデプロイしてくれる(自動・手動は選択可)
  • 自動でスケーリングが行われ、設定したしきい値に従って自動でスケールアップ・ダウンされる
  • 公開されたアプリケーションはロードバランシングされており自動でトラフィックを分散してくれる
  • 証明書(TLS)も管理されデフォルトで付与される URL は HTTPS でアクセス可能。更新も自動で行われる

aws.amazon.com

docs.aws.amazon.com

App Runner では 1 つのデプロイを「サービス」と呼び、サービスを作成するだけでアプリケーションを公開できます。

アプリケーションについて

App Runner では、アプリケーションソースを「Githubソースコード」または「コンテナイメージ」かのどちらかを選択する事ができます。

今回は ECR にコンテナイメージを設置して、それをデプロイしていこうと思います。

アプリケーションについては、以下 App Runner のワークショップで使われていた node のソースとコンテナを使用します。

www.apprunnerworkshop.com

App Runner を構築する

ここから実際に App Runner を使ってデプロイができるまで進めていきます。

いつもであれば構成は terraform で管理するのですが、App Runner は「サービス作成=デプロイ」となるため、App Runner の構成を terraform で管理する必要性がありませんでした。(terraform で予めサービスを作成しておいてそれを何かで起動したり... といった概念ではない)

なので、最低限だけ terraform で作成して、ECR への image の push と、App Runner のサービス作成は Github Actions で行っていきます。

先述のアプリケーションと併せて、最終的には以下のファイル構成になります。

root/
├── .github
│   └── workflows
│       └── deploy.yml
├── Dockerfile
├── index.js
├── package.json
├── source-configuration.json.template
└── terraform
    ├── main.tf
    └── terraform.tfvars

ECR Repository 作成

コンテナイメージを設置する ECR Repository を作成します。

main.tf

# variables
variable "aws_id" {}
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "aws_region" {}

provider "aws" {
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
  region     = var.aws_region
}

# ECR Repository
resource "aws_ecr_repository" "app" {
  name                 = "sample_node_for_app_runner"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

ECR リポジトリを作成しているだけです。当然空っぽなので、コンテナイメージについては後で Github Actions で push していきます。

ポイントとしては自動デプロイを有効にしたい場合、イメージのタグは固定する必要があるため、image_tag_mutability は MUTABLE である必要がありました。(サービス作成時にイメージのタグも指定するため)

App Runner の IAM Role 作成

App Runner に付与する IAM Role を作成します。

App Runner が ECR にアクセスできるようにするロールです。

main.tf

# IAM Role for AppRunner
## AWS管理ポリシー
data "aws_iam_policy" "AWSAppRunnerServicePolicyForECRAccess" {
  arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

## IAM Role - for AppRunner
resource "aws_iam_role" "for_app_runner" {
  name        = "tf-AppRunnerECRAccessRole"
  description = "This role gives App Runner permission to access ECR"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = [
            "build.apprunner.amazonaws.com"
          ]
        }
      },
    ]
  })
}

## Attach Policy to Role
resource "aws_iam_role_policy_attachment" "app_runner" {
  role       = aws_iam_role.for_app_runner.name
  policy_arn = data.aws_iam_policy.AWSAppRunnerServicePolicyForECRAccess.arn
}

ポリシー自体は AWS で管理しているものを使用しています。

デプロイ用 IAM User 作成

Github Actions から ECR への image の push と App Runner のサービス作成を行なうために、デプロイ用の IAM User を作成します。

main.tf

# deploy user
## IAM User
resource "aws_iam_user" "deploy_app_runner" {
  name = "deploy_app_runner"
}

## IAM Policy
resource "aws_iam_policy" "for_deploy_app_runner" {
  name        = "deploy-app-runner-policy"
  description = "ECR push and App Runner operations Policy."
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload",
          "ecr:PutImage",
          "apprunner:ListServices",
          "apprunner:CreateService",
          "iam:PassRole",
          "iam:CreateServiceLinkedRole",
        ]
        Resource = "*"
      }
    ]
  })
}

## attach Policy
resource "aws_iam_user_policy_attachment" "deploy_app_runner" {
  user       = aws_iam_user.deploy_app_runner.name
  policy_arn = aws_iam_policy.for_deploy_app_runner.arn
}

ポリシーは「ECR への push」と「App Runner のサービス作成」のためのミニマムの権限を付与しています。

terraform で IAM User を作成しましたが、アクセスキーは AWS コンソール画面から手動で生成します。

ECR への image 登録と App Runner サービス作成

ここからは Github Actions で ECR への image 登録と App Runner サービス作成を行っていきます。

deploy.yml

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Amazon ECR "Login" Action for GitHub Actions
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      # App Runner サービス作成
      - name: Create App Runner Service if no exist.
        env:
          AWS_ID: ${{ secrets.AWS_ID }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          SERVICE_NAME: sample_node
        run: |
          SERVICE=`aws apprunner list-services --query "length(ServiceSummaryList[?ServiceName=='$SERVICE_NAME'])"`
          if [ $SERVICE -eq 0 ]; then
              sed -e "s/<AWS_ID>/${AWS_ID}/" -e "s/<AWS_REGION>/${AWS_REGION}/" ./source-configuration.json.template > ./source-configuration.json
              aws apprunner create-service --cli-input-json file://source-configuration.json
          fi

App Runner に関しては、既にサービスが作成されているかチェックし、存在しなければサービスを作成する。という処理にしています。

実際のところ、AWS CLI を使えばコマンド一発でサービスは作成できます。

docs.aws.amazon.com

今回でいうとここです

aws apprunner create-service --cli-input-json file://source-configuration.json

サービスを作成する際に、設定項目を渡す必要があるので、それは Github Actions の中で json ファイルを作成しています。

アプリケーションのソースに json ファイルのテンプレートを作成しておいて、 Github Actions で必要な設定値を入れているだけです。

source-configuration.json.template

{
    "ServiceName": "sample_node",
    "SourceConfiguration": {
        "ImageRepository": {
            "ImageIdentifier": "<AWS_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/sample_node_for_app_runner:latest",
            "ImageConfiguration": {
                "Port": "3000"
            },
            "ImageRepositoryType": "ECR"
        },
        "AutoDeploymentsEnabled": true,
        "AuthenticationConfiguration": {
            "AccessRoleArn": "arn:aws:iam::<AWS_ID>:role/tf-AppRunnerECRAccessRole"
        }
    },
    "HealthCheckConfiguration": {
        "Protocol": "TCP",
        "Interval": 10,
        "Timeout": 5,
        "HealthyThreshold": 1,
        "UnhealthyThreshold": 5
    },
    "AutoScalingConfigurationArn": "arn:aws:apprunner:<AWS_REGION>:<AWS_ID>:autoscalingconfiguration/minimum_setting/1/xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

この辺の項目は AWS コンソール画面でのサービス作成時に出てくるものとほぼ一緒なので、最初はコンソール画面からサービス登録してみるとイメージが掴みやすいと思います。

ちなみに今回は、スケール設定に関してはミニマムの設定にしておきたかったので、デフォルトのものではなくて予め作成した設定を指定しています(minimum_setting)

動作確認

一通りの設定は完了したので、terraform で AWS のリソースを作成したら Github Actions からデプロイすると、App Runner のサービスが作成されます。

f:id:ro9rito:20210614191344p:plain

デフォルトドメインに表示されている URL にブラウザからアクセスすると、アプリケーションが公開されていることが確認できます。

f:id:ro9rito:20210614191411p:plain

また、コンテナイメージが更新される(アプリケーションのソースコードを更新してイメージを再 pushする)と、自動的にデプロイが走ります。

f:id:ro9rito:20210614191423p:plain

ソースが更新されるとサービスも更新されることが確認できました。

f:id:ro9rito:20210614191440p:plain

まとめ

簡単にデプロイできて、ネットワーク周りやスケーリングを気にしなくて良いのは便利だなと思いました。

ただし、コンテナイメージでも github リポジトリでも、1つのソース(イメージ・リポジトリ)しか選択できないので、フロントエンドとバックエンドがソースとして別れている場合はすべてを 1 つのサービスで一撃構築!みたいな用途では使えない。(nginx と php-fpm 2コンテナ兄弟みたいなやつも同じくダメ)

ネットワークからスケーリングまでフルマネージドである特性上、全部のせアプリケーションでないと成立しないっていうのには納得。(片側だけ動かすとかなら良いかも)

Github リポジトリを使う場合も、AWS コンソール画面から App Runner のサービス作成をやってみましたが、サービス作成画面で AWSGithubリポジトリを連携させるだけなので操作は簡単でした。

ちなみにサービス作成してから構築完了までは約 5 分ほど、削除に関してはおよそ 1 分以内程度かかりました。

AWS からは便利なサービスがどんどん出てきますが、特性を知って必要な時に選択肢の一つとして出せるようになっておきたいですね。