20200103のAWSに関する記事は17件です。

比較的早く Svelte を Amazon S3 上にホスティングする方法

image.png

The State of JavaScript 2019 にて突然出現した Svelte
比較的早く Svelte をセットアップする方法 に続いて、Amazon S3 に、
静的 website として hosting する方法をまとめてみました?

1_EB-8t3dR5qMQYNXyqgM2wQ.png

Build と deploy には、AWS Amplify を使用します

前提条件

1. リポジトリの作成

Push の準備として、AWS CodeCommit に repository を作成

image.png

接続のステップ を参考に、適宜 setup を実施

image.png

2. Push

$ git init
$ git add .
$ git commit -m "1st commit"
$ git add remote origin https://git-codecommit.${YOUR_REGION}.amazonaws.com/v1/repos/${YOUR_PROJECT_NAME}
$ git push origin master

image.png

3. Build & deploy

Build & deploy の準備として、AWS Amplify に project を作成

image.png

image.png

Build command の baseDirectory を、/ から /public に変更

image.png

Start

image.png

Complete

image.png

4. 確認

https://master.d1dar8nbyr7lfy.amplifyapp.com ( deleted )

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

4年ぶりにエンジニアに戻った人の話

こんにちは、Qiita初投稿です。

色々あって昨年末に4年間勤めていた外資系のゲーム会社でのプロデューサー業を辞めてエンジニアに戻りました。
前職ではエンジニアではなく、翻訳やアカウントマネージメント系の仕事などをやっていました。
エンジニアブログを書く前に、今回は元エンジニアが4年間経ってエンジニアに戻って世界が一変していた、浦島太郎のような話をしたいと思います。

経歴:

2011年: 大手企業に新卒でエンジニア就職

秋入社というのもありまさかの新卒唯一の日本人、まわりは中国人とインド人ばっかり。入社一日目でエンジニア職人気なさすぎオワタと思う。まわりのエンジニアが毎月行われれていた深夜作業に駆り出されていくのを怯える毎日(結局一度もその作業をすることはなかったが)

2013年: ベンチャー企業へ転職

前職ではテスト業務しか触れず、いつの間にかポジションもQAエンジニアになってしまったのでエンジニアらしいことがしたいと一念発起して、当時黎明期だったモバイルアプリ業界へ転職。たまたま入社直後にアプリがヒットし、会社が急成長。カスタマーサポート担当→QAエンジニア→サーバーエンジニアと役割を変え、気づいたらエンジニアリードになっていった。

2015年: 外資系ゲーム会社へ転職

ヘッドハンティングで当時日本サーバーを建てようとしていた世界的人気のゲームを運営する会社へ入社。エンジニアではなく語学力+QA経験でLocalization QAを任される。そのうち、「君これもできるよね?」というノリでユーザーコミュニケーション担当をやっていたら、そちらが本業になりLocalizationチームから外れてMarketingチームへ異動。Abemaでテレビ番組のプロデューサーをやっていたら、半年で今度は当時のesports担当が外れたので急遽、責任者に就任。

2019年: 現在の会社へ

前職を8月末に辞表を出し11月末に退社(理由は色々あったので割愛)。就職先を決めていなかったので、「いい機会だし色んな業界の話を聞いてみよう」と多くの会社に面接に行っていたら現在の社長と意気投合、はれて4年ぶりにエンジニアに復職。

エンジニアに戻ってきて

前置きが長くなってしまいましたが、ここからが本題。まず転職時の感想ですが。

エンジニア職が好待遇に

昔はエンジニア職といえば3K(Kitsui, Kitanai, Kaerenai)のイメージが強かったです。実際「え、エンジニアなんてやってるんですか?」と大学時代の知り合いに言われたぐらいで、エンジニアなんてさっさとやめてマネージメントに入るのが勝ち組なんて影では言われていました。

正直直近の転職活動の時も、最初は「エンジニア戻るのはないなー」と思って、それでも経歴上エージェントから話が来るので渋々話を聞きにいっていたのですが。

…あれ?エンジニア職の待遇変わってね?

聞くと、今IT系エンジニアの求人倍率は10倍なんですね。昔は「私の代わりはいくらでもいるもの」という感じだったのですが、みんなどこへ行ってしまったのか…当然ながら給与も想像より上、同じように応募した翻訳やプロデューサー業よりも上でした。

技術の進歩

エンジニアに4年ぶりに戻るとあって、有給消化中はほぼ毎日10時間ぐらい今の技術を勉強に費やしていました。
完全にキャッチアップとはまだまだ遠いですが、思ったことを簡単に書くと、

サーバーインフラ構築が簡単に

本当にこれ、当時(2013年ぐらい)だとAWSのEC2とS3とCloudFront使ってサーバーエンジニアが汗と涙で毎回デプロイしてたのに、DockerとEBS組み合わせたら何も考えずにサーバーが建てられる、しかもコンテナモデルだからデプロイが速い速い。あとサーバーレスアーキテクチャを使ったマイクロサービスとか、昔だったらEC2の別サーバーでバッチ処理させるしかなかったのに、凄い(求: 語彙力)

Javascriptって便利じゃない?

昔のJavascriptには何度泣かされたことか。しかしいざ、Vue.js触ってみて思ったのが「これ以外の言語で非同期処理って書けるの?」とまで思わされてしまうほど、Javascriptが使える言語になっていたのに驚き。でも言語仕様はやっぱり好きじゃない

Pythonが主流言語に

これが地味に一番嬉しい。大学で時代を先取りしすぎた教授たちに「明日から課題はPythonで提出して」(当時はJavaで提出していた)と言われかれこれ10年以上Pythonを使っているのに、日本でPythonを使うのはスクリプト処理の時ぐらい。サーバーはRuby on Railsだったのも今は昔、若手のエンジニアから「RoR使ってるのは30代後半のおっさん」と言われ軽くショックを受けました。…ええそのせいもあって、弊社はDjangoでサーバー書いてます。

最後に

ここまでつらつらと書きました。エンジニアに戻ってまだ日も浅いのでまだまだわからないことが多いのですが、4年ぶりに戻ってきて思っていることは

  1. エンジニアは雇うから、育てる職種に
    エンジニアの待遇が良くなったという反面、優秀なエンジニアを採用するというのが本当に難しい時代になったと思います。反面、今までのような技術的なハードルは下がってきているので、きっちりと育てることのできるエンジニアをいかに採用し戦力としていくことが重要に思えます。

  2. エンジニアマーケットのグローバル化
    Pythonなどの言語が主流言語として台頭してくる中で、多くのドキュメントなどのリソースは英語で書かれており、海外には多くのコミュニティも存在します。そのため、日本語だけだと最先端の技術を取り込んでいくための情報量に限りがあります。10年ぐらい前からも言われていることですが、エンジニアとして今優先すべきは英語での語学力だと思っています。合わせて、採用も日本人だけでなく海外の技術者を取り込んでいくことが重要なファクターとなると考えています。

2をふまえて、今の会社でのエンジニア採用は「英語のみ可」に入社後に変更し、社内での英語の義務化を導入するか目下検討しています。1は会社のフェーズとして育てられる環境がないのでまだできていませんが、組織が安定してき次第取り掛かりたいと思っています。

ではでは

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS+NodeJSでサーバレスな環境構築②

はじめに

前回の記事ではAPI Gateway+Lambda(NodeJS)を組み合わせてWEBページを表示するというアウトプットでした。今回はDynamoDBのテーブルと項目作成、Lambda関数で使うロールやインラインポリシーの設定を載せていきます。サーバレスに関しては個人的に興味があるとのと、次の案件で用いるからその予習になります。自身も初めてということもあり、表現がわかりにくいところもあるかもしれません。その場合は容赦無く、コメントで指摘していただければ幸いです。
※サーバレスに関してよくわからない方は、前回の記事をご覧いただければと思います。

DynamoDBってなぁに?

簡単に言ってしまうと、AWSがマネージドサービスとして提供しているNoSQL(非リレーショナル)データベースになります。「値」とそれを取得するための「キー」だけを格納するというシンプルな機能を持った「Key-Valueストア」です。

一般的なユースケース
・ミリ秒単位のアクセスレイテンシーが求められる
・データの拡張性が求められる

参考記事
NoSQLとは
DynamoDBをわかりやすく説明

DynamoDBテーブルの作成

DynamoDBダッシュボード>テーブルの作成>テーブル名とプライマリキーだけ入力>作成ボタン

スクリーンショット 2020-01-03 13.32.13.png

項目タブを選択>項目の作成>以下のように項目と値を追加

スクリーンショット 2020-01-03 15.19.31.png
スクリーンショット 2020-01-03 15.20.39.png

あとで使うので、DynamoDBテーブルのリソース名をコピー(黒枠部分)しておきます

スクリーンショット 2020-01-03 15.23.02.png

IAMでロールの作成

IAMダッシュボードのロールを選択>ロール作成ボタン

スクリーンショット 2020-01-03 15.40.49.png

Lambdaを選択>次のステップへ

スクリーンショット 2020-01-03 15.41.56.png

AWSLambdaBasicExecutionRoleにチェック>次のステップへ

スクリーンショット 2020-01-03 15.59.25.png

タグの追加 (オプション)そのまま>次のステップへ

スクリーンショット 2020-01-03 16.02.25.png

ロール名の入力>作成ボタン

スクリーンショット 2020-01-03 16.04.15.png

作成したロールでインラインポリシーの作成

ロール一覧から作成したロールを選択>概要画面でインラインポリシーの作成を選択

スクリーンショット 2020-01-03 16.09.06.png
スクリーンショット 2020-01-03 16.09.59.png

サービスをDynamoDBを選択>2つアクションを追加

⚠️ 下記ではGetItemとPutItemしか追加されませんが、DeleteItemとUpdateItemとScanも追加します。
スクリーンショット 2020-01-03 16.22.29.png

リソースのARNを指定>追加

スクリーンショット 2020-01-03 16.27.35.png

ポリシーの確認>ポリシー名>ポリシーの作成

スクリーンショット 2020-01-03 16.30.59.png
スクリーンショット 2020-01-03 16.35.04.png
スクリーンショット 2020-01-03 16.35.59.png

最後に

次回はAPI Gateway(REST API)+Lambda(NodeJS)+DynamoDBの組み合わせで、DynamoDBのテーブルが更新されるようにしていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon SageMaker リンク集(使い方で悩んだらここを見る!)

Amazon SageMaker の使い方で悩んだら

猫好きのみなさま、そうでないみなさま、こんにちは。
Amazon SageMaker 使ってますよね(使っていない場合もあります)。

サクッと機械学習の開発環境を構築できたり、GPU マシンを使って分散学習できたり(Spot Instance も選択可能!)と、機械学習に関するあれこれに便利な Amazon SageMaker ですが、「SageMaker でこういうことできるの?」「これをやりたいとき、どうすれば良いの?」という状況になることもあると思います。

エンジニアの方は自己解決スキルが高い方が多いので、たいていググって正解にたどり着かれると思いますが、まれにググっても正解が見つからない、見つかるまでに時間がかかることもあると思います。そんなとき、この記事を思い出していただけるとお役に立つかもしれません。

具体的な使い方を知りたいとき:サンプルノートブック

AWS が SageMaker の サンプルノートブックを GitHub で公開 しているのをご存知でしょうか?

SageMaker の 開発者ガイド を読んで使い方を知るのもアリなのですが、こちらのサンプルノートブックから、ご自分のやりたいことに一番近そうなノートブックをベースに、それを改造していくと、やりたいことに最短距離で到達できることがあります。

2019年の AWS re:Invent で Amazon SageMaker Studio, Autopilot, Debugger, Experiments, Model Monitor などの新機能が発表されましたが、これらの機能を使うためのサンプルノートブックも用意されています。フォルダやノートブック名に機能名が入っているので、GitHub リポジトリ で機能名で検索いただくと所望のノートブックが見つかると思います。
スクリーンショット 2020-01-03 16.24.17.png

どんな機能があるのかを俯瞰的に知りたいとき:開発者ガイド

サンプルノートブックを動かしてみて、他に便利な使い方はないかな?と思った場合は、こちらの 開発者ガイド を見ていただくと、サンプルノートブックには書かれていなかった他の方法や便利機能などを知ることができます。最新情報を知りたい場合は、言語設定を英語 にすると、日本語バージョンでは書かれていない情報が出てくることがあります。言語設定は、開発者ガイドの上のメニューから変更することができます。
スクリーンショット 2020-01-03 15.49.21.png

API の使い方の詳細を知りたいとき:API リファレンス

サンプルノートブックで API のだいたいの使い方はわかったけど、他にはどんなオプションがあるの?などの詳細を知りたい場合は、API リファレンスを参照します。
Boto3 (AWS SDK for Python) の API について知りたい場合は こちら を、Amazon SageMaker Python SDK の API について知りたい場合は こちら を参照します。自分がどちらを使っているのかわからない!という方、ざっくり言うと、学習ジョブを立ち上げる際に create_training_job 関数を呼んでいれば AWS SDK for Python で、fit 関数を呼んでいれば Amazon SageMaker Python SDK を使っています。
スクリーンショット 2020-01-03 16.23.36.png

その他の言語で SageMaker を使う場合は、以下を参照します。

謎のエラーがでたとき:Amazon SageMaker Discussion Forums

SageMaker をある程度使っていて、初めて遭遇したエラーが出たとき、Amazon SageMaker Discussion Forums でエラーメッセージで検索したり、機能や API 名で検索すると、ヒントが見つかるかもしれません。また、Forum で質問をすることで、解決策が見つかるかもしれません。
スクリーンショット 2020-01-03 16.22.41.png

新しい機能を知りたいとき:AWS ブログ

新しい機能がリリースされると、AWS ブログ(日本語)AWS ブログ(英語) で紹介されます。日本語の方にしかない記事や、英語の方にしかない記事もありますが、新しい機能の紹介だけでなく、SageMaker の便利な使い方の紹介の記事もあります。スクリーンショット 2020-01-03 19.33.32.png

まとめ

情報が散らばっていて、欲しい情報になかなかたどり着けないことがあるかもしれません。
そういうときに、この記事がお役に立てば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Infrastructure as CodeのTips【Terraform/CDK/Cloudformation】

TL; DR

普段色々なツールを使ってインフラコード化を細々としています。
自分の中で少し溜まってきたので整理と備忘録も兼ねて書いていきます。

Terraform

みんな大好きTerraformです。筆者も大好きです。
なんとなくで書けちゃうのがいいんですよね。とりあえずplan投げてエラー見て直してみたいな。
Terraformのおかげでエラー出ても焦らなくなりました。(initで出るとちょっと自分にムッとはしますがw)
多機能な分、追えていなかったり避けている機能がちょこちょこあったので使ってみるか〜くらいでやったのですがよかったものをピックアップしてます。

for_each

名前から繰り返しの処理をするやつなんだろうな、と思いつつcountで避けてきたfor_eachくん。
慣れてしまえば多用しそうになるやつです。
aws_subnetとかで使ってます。便利です。
resourceブロックでしか利用できないので注意です。

count style
resource "aws_subnet" "public_subnet" {
  count                   = length(split(",",lookup(var.availability_zones,default)))  // 2とかでもOK
  vpc_id                  = aws_vpc.service_vpc.id
  cidr_block              = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, count.index)
  availability_zone       = element(split(",", lookup(var.availability_zones, default)), count.index)
  map_public_ip_on_launch = true
}

variable "availability_zones" {
  default = "ap-northeast-1a,ap-northeast-1c"
}
for_each style
resource "aws_subnet" "public_subnet" {
  for_each                = var.availability_zones
  vpc_id                  = aws_vpc.service_vpc.id
  cidr_block              = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, each.value)
  availability_zone       = each.key
  map_public_ip_on_launch = true
}

variable "availability_zones" {
  default = {
    "ap-northeast-1a" = 0
    "ap-northeast-1c" = 1
  }
}

こんな感じで違った書き方ができます。
なんとなくTerraformのFunctionsが苦手な筆者はこっちのがシンプルやんけ!と書けたときに小躍りしました。
リソースのインデックスもわかりやすいのでプラン結果やデプロイ中も見守りやすいのも理由です。

plan result
  # module.service_vpc.aws_subnet.public_subnet["ap-northeast-1a"] will be created
  + resource "aws_subnet" "public_subnet" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "192.168.0.0/20"
      + id                              = (known after apply)
      + ipv6_cidr_block                 = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = true
      + owner_id                        = (known after apply)
      + tags                            = {
          + "ENV"     = "sandbox"
          + "Name"    = "terraform-sand-public-ap-northeast-1a"
          + "Service" = "terraform"
        }
      + vpc_id                          = (known after apply)
    }

  # module.service_vpc.aws_subnet.public_subnet["ap-northeast-1c"] will be created
  + resource "aws_subnet" "public_subnet" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1c"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "192.168.16.0/20"
      + id                              = (known after apply)
      + ipv6_cidr_block                 = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = true
      + owner_id                        = (known after apply)
      + tags                            = {
          + "ENV"     = "sandbox"
          + "Name"    = "terraform-sand-public-ap-northeast-1c"
          + "Service" = "terraform"
        }
      + vpc_id                          = (known after apply)
    }

これらのリソースをアウトプットするときは少し工夫が必要でfor文で回してあげる必要があります。

output.tf
output "public_subnets" {
  value       = [for subnet in aws_subnet.public_subnet : subnet.id]  // 欲しいattributeは変数から取る
}

// NG例
output "public_subnets" {
  value       = aws_subnet.public_subnet.*.id
}

output "public_subnets" {
  value       = aws_subnet.public_subnet[*].id
}

dynamic block

複数設定可能な項目などは繰り返し書くのが面倒だったり、環境によって必要・不要が分かれていたりすることもあると思います。
そう言った部分を良きに計らってくれる優れものです。個人的には使いすぎはよくないとは思うのでaws_db_parameter_groupなんかはちょうどいいのかな?と思います。

multiple written
resource "aws_db_parameter_group" "mysql_56" {
  name   = "${var.service_name}-${var.env}-${var.engine}-56"
  family = "mysql5.6"

  parameter {
    name         = "character_set_server"
    value        = "utf8"
    apply_method = "pending-reboot"
  }

  parameter {
    name         = "character_set_client"
    value        = "utf8"
    apply_method = "immediate"
  }

  parameter {
    name         = "performance_schema"
    value        = "1"
    apply_method = "pending-reboot"
  }
}
dynamic written
resource "aws_db_parameter_group" "mysql_56" {
  name   = "${var.service_name}-${var.env}-${var.engine}-56"
  family = "mysql5.6"

  dynamic "parameter" {
    for_each = var.db_params

    content {
      name         = each.value.name
      value        = each.value.value
      apply_method = each.value.apply_method
    }
}

variable db_params {
  default = {
    "server_character" = {
        name         = "character_set_server"
        value        = "utf8"
        apply_method = "pending-reboot"
    },
    "client_character" = {
        name         = "character_set_client"
        value        = "utf8"
        apply_method = "immediate"
    },
    "insight" = {
        name         = "performance_schema"
        value        = "1"
        apply_method = "pending-reboot"
    }
  }
}

いや、変数冗長じゃね?と思うかと思いますが、moduleなどを利用して環境ごとに出し分けているとこういうcountとかが使えないものが出てきてつらみに陥ることがあるんです…。それがresource同じでmodule側だけ書き換えるみたいな運用ならまあ許容できるかなと思ってます。(そもそもそんな環境構成にするなよ、みたいなのはまた別のお話しです。)

AWS CDK

最近は細々としたリソースをつくるときに利用したりしています。
ある程度まとまったリソース単位でStackとして利用できるので中々好きです!AWS公式ですし、Typescriptの型の恩恵受けまくりで書きやすいしエラーの発見も簡単です。
新しいリソースへの対応も早いので、ローンチされたばかりのサービスでもガンガン使えちゃいます!(AWS-SDK-JSとかからインポートして使う、が大体ですが。)

cdk.json

cdk initすると自動で作成されるものですが、うまく使うと便利です!
以前IAMを管理している記事を書いたものをちょこちょことアップグレードしていて、今はcdk.jsonを使ってユーザーやロールの出し分けをしています。

旧バージョンのユーザー作成の部分だけ抜き出しています。
ユーザーはベタがきでコードにそのまま書いています。

old style
import { Construct, Stack, StackProps }  from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');

const admins = 'testAdminGroup';
const adminUsers = [
  'demo01',
];

const developers = 'testDevGroup';
const devUsers = [
  'demo02',
  'demo03'
];

export class IamUserStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Admin group
    const adminGroup = new Group(this, admins, { groupName: admins });
    adminGroup.addManagedPolicy(adminPolicy);
    adminGroup.attachInlinePolicy(commonPolicy);

    // Developer group
    const devGroup = new Group(this, developers, { groupName: developers });
    devGroup.addManagedPolicy(powerUserPolicy);
    devGroup.attachInlinePolicy(devPolicy);
    devGroup.attachInlinePolicy(commonPolicy);

    // Create users
    adminUsers.forEach(adminUser => {
      new User(this, adminUser, {
        userName: adminUser,
        groups: [adminGroup],
      });
    });

    devUsers.forEach(devUser => {
      new User(this, devUser, {
        userName: devUser,
        groups: [devGroup]
      });
    });
  }
}

cdk.jsonを利用しているパターンはこちら。
5-14行目でcdk.jsonをcontextとして利用できるように呼び出しています。

using cdk.json
import { Aws, Construct, Stack, StackProps, App, CfnOutput, Fn, Arn }  from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');

const app = new App();
const stage: any = app.node.tryGetContext("stage")

// Admin Users Configure
const admins: string = 'adminGroup';
const adminUsers = app.node.tryGetContext(stage).adminUsers  // contextとしてユーザーをcdk.jsonから呼び出している。

// Develop Users Configure
const developers: string = 'devGroup';
const devUsers = app.node.tryGetContext(stage).devUsers

export class IamUserStack extends Stack {
  /** @returns the ARN of the IAM User */
  public readonly userArn: string;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Admin group
    const adminGroup = new Group(this, admins, { groupName: admins });
    adminGroup.addManagedPolicy(adminPolicy);
    adminGroup.attachInlinePolicy(commonPolicy);

    // Developer group
    const devGroup = new Group(this, developers, { groupName: developers });
    devGroup.addManagedPolicy(powerUserPolicy);
    devGroup.attachInlinePolicy(devPolicy);
    devGroup.attachInlinePolicy(commonPolicy);

    // Create Admin users    
    adminUsers.forEach((adminUser: string) => {
      const adminUserName = new User(this, adminUser, { userName: adminUser, groups: [adminGroup] });
      new CfnOutput(this, `${adminUserName}`, {
        value: adminUserName.userArn,
        description: `${stage} environment admin user arn`,
        exportName: `${stage}AdminUsersArn`
      });
    })

    // Create Develop users
    devUsers.forEach((devUser: string) => {
      const devUserName = new User(this, devUser, { userName: devUser, groups: [devGroup] });
      new CfnOutput(this, `${devUserName}`, {
        value: devUserName.userArn,
        description: `${stage} environment develop user arn`,
        exportName: `${stage}DevUsersArn`
      });
    })
  }
}
cdk.json
{
  "context": {
    "division": "hogehoge",
    "prj": "fugafuga",
    "dev": {
      "adminUsers": [
        "demo01"
      ],
      "devUsers": [
        "demo02",
        "demo03"
      ]
    },
    "stg": {
      "adminUsers": [
        "circleci",
        "github"
      ],
      "devUsers": [
        "jenkins"
      ]
    },
    "prd": {
      "adminUsers": [
        "circleci",
        "github"
      ],
      "devUsers": [
        "jenkins"
      ]
    }
  },
  "app": "npx ts-node bin/iam_user.ts"
}

こんな感じのファイル構成をしています。
divisionprjはタグ付けなどに利用していますので、特に環境ごとの配下にしていません。
これらをデプロイするときはcdk deploy -c stage=devと言った感じで出し分けています。
本当は環境ではなくアカウントナンバーにしてstackの中でアカウントナンバーを取得してからcontextを注入したかったのですが、その前にcdk.jsonが読み込まれてしまうため実現できませんでした。
今は一旦これでいいかな、という感じで落ち着いています。

Cloudformation

AWSの初期からある(たしか…)インフラコード化の走りみたいな存在ですね。
正直あんまり使ってこなかったのですが、AWS CDKの利用に伴い色々と理解が必要だったので使っています。
yaml形式自体なので可読性は高いと思うのですが、リソース名や独自の関数など覚えることいっぱいです。
まあ時間をみつけてゆるゆるとやっている感じです。

ユーザーデーターが使える

完全にシェルこねこねと思っていたのですが、Cloudformationに普通に書けました。

userdata template
Resources:
  BastionLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: bastion-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              # timezone
              timedatectl set-timezone "Asia/Tokyo"

              # time sync
              yum -y remove ntp
              yum -y install chrony
              echo '#Add TimeSync' >> /etc/chrony.conf
              echo 'server 169.254.169.123 prefer iburst' >> /etc/chrony.conf
              systemctl start chrony
              systemctl enable chrony

共通

リソース名の命名規則は最初から揃えて、変数なりParameterなりで使いまわせるようにしましょう。
ベタがき、不揃いは後々つらくなってきます…。

terraform
variable "Name" { default = "hoge" }  
variable "Service" { default = "fuga" }  
variable "Env" { default = "hogefuga" }  

resource "hogehoge" "fugafuga" {

/////////////省略/////////////

  tags = {
    Name     = "${var.service_name}-${var.env}-fugahoge"
    Service  = var.service_name
    Env      = var.env
  }
}
cdk.json
{
  "context": {
    "Name": "hoge",
    "Service": "fuga",
    "Env": "hogefuga"
  }
}
Cloudformation
Parameters:
  Name:
    Description: resource name.
    Type: String
    Default: hoge
  Service:
    Description: service name.
    Type: String
    Default: fuga
  Env:
    Description: environment name.
    Type: String
    Default: hogefuga

##############################
Tags: 
  - 
    Key: "Name"
    Value: !Ref Name
  - 
    Key: "Service"
    Value: !Ref Service
  - 
    Key: "Env"
    Value: !Ref Env

おまけ

どうでもいいけどなんでQiitaのコードブロックからHCLを消してしまったのでしょう…。
Terraformのコードか見づらいじゃないの…布教できない…。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS trusted adviser による制限監視

AWS Limit Monitorを使う
https://dev.classmethod.jp/cloud/aws/aws-answers-try-aws-limit-monitor/

実行している流れ
1. CloudWatch Events作成。定期的にMaster Lambda Functionを呼び出す。
2. Master Lambda Function は、指定したAWSアカウント単位で Child Lambda Function を起動します。複数のAWSアカウントに対応
3. Child Lambda Function は、AWSアカウント単位で Trusted Advisor、および Trusted Advisor で管理されていないEC2インスタンス全体の上限数、CloudFormation Stack の上限数、DynamoDB の各種キャパシティユニットをチェック
4. サービス制限値の80%以上利用しているリソースがある場合、SNSをキック
5. ユーザーはアラートメールを受信

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodeBuild のリンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】Elastic BeanstalkでLinux 用 Windows サブシステムを使用せずにLaravelアプリケーションをデプロイする

Elastic BeanstalkでLinux 用 Windows サブシステムを使用せずにLaravelアプリケーションをデプロイ方法です。
本手順では、代わりに、フリーの圧縮・解凍ソフトを使用しています。

環境

  • PHP:バージョン7.3(Elastic Beanstalk)
  • Laravel:バージョン6.9.0
  • OS:Windows10(ローカル)

手順

Elastic Beanstalk への Laravel アプリケーションのデプロイ | aws
に従って実施します。
タイトルの件を以下に、補足事項としてまとめます。

補足事項

手順では、Linux 用 Windows サブシステムを使用するとありますが、以下の通り実行すればインストール不要です。

1. Laravelプロジェクトの作成

ローカル環境でコマンドプロンプトを起動して、任意のパスでコマンドを実行し、Laravelプロジェクト作成します。
こちらは、手順と同じコマンドで大丈夫です。
※今回は、プロジェクト名を「testLaravelApp」としました。

> composer create-project --prefer-dist laravel/laravel testLaravelApp

2. zipファイルの作成

1で作成したプロジェクトを圧縮します。
Windowsにおいては、デフォルトではzipコマンドを実行できないため、GUIベースで圧縮します。
私はフリーの圧縮・解凍ソフト7-Zipを使用しました。

注意点

圧縮実行時には、以下の点に気をつけてください。

1. デプロイしたいプロジェクト直下で圧縮する

公式の手順(コマンド)からもわかる通り、デプロイしたいプロジェクト直下で圧縮作業を実施します。
プロジェクトをそのまま圧縮してデプロイすると以下のようなエラーが出てしまい、ページへのアクセスもできないので要注意!

ダッシュボード

image.png

ヘルス

image.png

調べたところ、対象プロジェクトをそのまま圧縮すると余計なディレクトリが含まれてしまうから、とのこと。
【参考】
Following services are not running: proxy @ AWS — after Laravel re-config | stack overflow

2. vendorは対象外とする

公式の手順にも記載ありますが、念のため。


上記1, 2を考慮しての7-Zipでの圧縮は以下の通りとなります。
image.png

そうすると、同フォルダ内に対象のLaravelプロジェクトと同じ名称のzipファイルができるので、それをアップロード・デプロイすればOK!

image.png

オマケの注意点

こちらも公式手順に記載ありですが、ドキュメントのルートを
/public
とするのを忘れずに☆
image.png

無事、デプロイ→ページ表示できました!

ダッシュボード

image.png

ページ

image.png

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CodePipeline QAリンク

引用元

一部、理解のために自分の表現に変更
https://aws.amazon.com/jp/codepipeline/faqs/

Q: パイプラインとは何ですか?

パイプラインとは、ソフトウェアの変更がリリースプロセスをどのように通過するかを明確にするワークフロー構造です。ワークフローをステージとアクションのシーケンスで定義できます。
[追記]同一のアクションを開発環境、本番環境のように異なる環境に渡すための仕組みと理解

Q: リビジョンとは何ですか?

リビジョンとは、パイプラインのために定義されたソースロケーションに加えられた変更です。ソースコード、ビルドアウトプット、設定、データなどが対象となります。パイプラインには複数のリビジョンを同時に通過させることができます。

Q: ステージとは何ですか?

ステージとは、1 つまたは複数のアクションのグループです。パイプラインには 2 つ以上のステージを設定できます。

Q: アクションとは何ですか?

アクションとは、リビジョンに対して実行されるタスクです。パイプラインのアクションは、ステージ設定の定義に従い、指定された順番で直列的または並列的に実行されます。詳細は、 Edit a Pipeline および Action Structure Requirements in AWS CodePipeline を参照してください。

Q: アーティファクトとは何ですか?

アクションが実行される場合、1 つまたは一連のファイルが対象となります。これらの対象となるファイルがアーティファクトと呼ばれています。アーティファクトはパイプラインのその後のアクションでも対象となる場合があります。例えば、ソースアクションが、ソースアーティファクトとしてコードの最新バージョンを出力し、ビルドアクションがそのアーティファクトを読み込みます。コンパイル後、ビルドアクションはビルドアウトプットを別のアーティファクトとしてアップロードし、その後のデプロイアクションでそのアーティファクトが読み込まれます。

Q: AWS CodePipeline のイベントに関する通知やアラートを受け取るにはどうすればよいですか?

パイプラインに影響を与えるイベントに関する通知を作成することができます。通知は Amazon SNS 通知という形で送信されます。各通知には、ステータスメッセージに加えて、その通知が生成される原因となったイベントが存在するリソースへのリンクも含まれます。通知に追加費用はかかりませんが、Amazon SNS など、通知に利用される他の AWS のサービスの料金が課金される場合があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppSyncをフロントエンドとバックエンドで利用する

AppSyncをフロントエンドとバックエンドで利用する

この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。

以下を見てからこの記事をみるといい感じです。

イントロダクション

アップロードした画像や処理された画像のデータ管理や、クライアント側とのデータ受け渡しのために、AppSyncというAPIを利用します。AppSyncのデータソースにはDynamoDBが使われます。
AppSyncはAmplify CLIでもセットアップ可能なのですが、DynanoDBのパーティションキーやソートキーの指定ができないようだったので、Amplify CLIは使いません。AWSコンソールでDynamoDBとAppSyncを構築します。
フロントエンドのVueからはAmplifyを利用してSubscription、バックエンドのLambda(Python)からはHTTPでリクエストします。

コンテンツ

AppSyncのセットアップ

Amplify CLIでAppSyncのセットアップはできるのですが、DynamoDBのパーティションキーやソートキーの指定ができないようなんですよね。実はできるのかもしれませんが、やり方わかりませんでした。知ってる人がいたら教えて下さい。
ということで、コマンドラインに比べると若干手間ですが、AWSコンソールで作っていきましょう。

DynamoDB テーブルの作成

AppSyncのデータソースとするDynamoDBを先に作成しておきます。
AWSコンソール > DynamoDB > テーブルの作成
以下の設定でテーブルを作成します。
テーブル名 : sample_appsync_table
パーティションキー : group (文字列)
ソートキー : path (文字列)
Screenshot 2020-01-01 at 15.25.19.png

AppSync APIの作成

DynamoDBが作れたら、続いてAppSyncを作成します。
AWSコンソール > AppSync > APIを作成

Step1 : ご利用開始にあたって
「DynamoDBテーブルをインポートする」を選択して「開始」ボタン押下。Screenshot 2020-01-02 at 23.36.05.png
Step2 : モデルを作成
先ほど作成したテーブル(sample_appsync_table)を選択。
「既存のロールを作成または使用する」では「New role」を選択。
「インポート」ボタンを押下。
Screenshot 2020-01-02 at 23.36.23.png
そのまま「Create」ボタンを押下。
Screenshot 2020-01-02 at 23.40.08.png
Step3 : リソースを作成
API名を設定して「作成」ボタンを押下。
Screenshot 2020-01-02 at 23.42.14.png

schema.jsonの入手

AWSコンソール > AppSync > sample_appsync_table > スキーマ メニューから
schema.jsonをダウンロードします。このファイルはWebアプリで利用します。
Screenshot 2020-01-01 at 16.48.25.png

認証情報の入手

AWSコンソール > AppSync > sample_appsync_table > 設定 メニューから
API Detailの API URLAPI KEYの情報を確認します。この情報はアプリで利用します。
ink.png

認証モードは「APIキー」のままとします。APIキーの有効期限はデフォルト7日となっていますが、編集することで最大365日まで伸ばすことができます。
認証モードをStorageのようにCognitoユーザーとして利用する方法については別記事にしようと思っています。

フロントエンド(VueのWebアプリ)でSubscription

AppSyncのセットアップが完了したら、続いてWebアプリを更新してゆきます。

graphql定義ファイルの追加

src/graphqlフォルダを作成し、3つのファイルを追加します。

src/graphql/schema.json
先ほどダウンロードしたファイルをそのままプロジェクトに追加します。

src/graphql/queries.js
export const listSampleAppsyncTables = `query listSampleAppsyncTables($group: String) {
  listSampleAppsyncTables(
    limit: 1000000
    filter: {
      group: {eq:$group}
    }
  )
  {
    items 
    {
      group
      path
    }
  }
}
`;

パーティションキーのgroupを指定して、レコード一覧を取得するためのqueryです。
これは謎なのですが、limitの指定をしてあげないとデータを取ってこれないんですよ。graphqlではなくてAppSyncの仕様だと思うのですが、、どうなんでしょうか。1000000と適当に大きな数字を指定してますが、正直微妙すぎますよね。もっと良い書き方を知っている人がいたら是非教えて下さい。

src/graphql/subscriptions.js
export const onCreateSampleAppsyncTable = `subscription OnCreateSampleAppsyncTable($group: String) {
    onCreateSampleAppsyncTable(group : $group) {
        group
        path
    }
}
`;

パーティションキーのgroupを指定して、レコードが挿入されたらその情報とともに通知してもらうためのsubscriptionです。
「レコードが挿入されたら」と書きましたが、DynamoDBに直接レコードを挿入してもダメで、AppSyncのcreateによって挿入される必要があります。

aws-exports.jsにAppSync情報を追記

src/aws-exports.jsにAppSyncにアクセスする際に必要となる情報を追記します。

src/aws-exports.js
const awsmobile = {
    "aws_project_region": "ap-northeast-1",
    "aws_cognito_identity_pool_id": "ap-northeast-1:********-****-****-****-************",
    "aws_cognito_region": "ap-northeast-1",
    "aws_user_pools_id": "ap-northeast-1_*********",
    "aws_user_pools_web_client_id": "**************************",
    "oauth": {},
    "aws_user_files_s3_bucket": "sample-vue-project-bucket-work",
    "aws_user_files_s3_bucket_region": "ap-northeast-1", 
    "aws_appsync_graphqlEndpoint": "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql",
    "aws_appsync_region": "ap-northeast-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "da2-**************************"
};
export default awsmobile;

このファイルに書かれている情報は大切な情報なので、漏洩しないよう、取り扱いに注意してください。

Webアプリの実装

Homeで画像を選択してアップロードしたら、ページ遷移した先でアップロードされた画像やモノクロ処理された画像の情報をリストするような実装を施します。

Listというページを追加します。そのためのルーター設定。

src/router.js
import Vue from 'vue'; 
import Router from 'vue-router'; 
import Home from './views/Home.vue'; 
import About from './views/About.vue'; 
import List from './views/List.vue'; 

Vue.use(Router); 

export default new Router({ 
  routes: [ 
    { 
      path: '/', 
      name: 'home', 
      component: Home,
    }, 
    { 
      path: '/about', 
      name: 'about', 
      component: About, 
    }, 
    { 
      path: '/list', 
      name: 'list', 
      component: List, 
    }, 
  ] 
});

Listページのビュー

src/views/List.vue
<template> 
  <List /> 
</template> 

<script> 
  import List from '../components/List' 
  export default { 
    components: { 
      List 
    } 
  } 
</script> 

Listページのコンポーネント

src/components/List.vue
<template>
  <v-container>
    <p>リスト</p>
    <router-link to="/" >link to Home</router-link>
    <hr>

    <v-list>
      <v-list-item v-for="data in this.dataList" :key="data.path">
        <v-list-item-content>
          <a :href="data.image" target=”_blank”>
            <v-list-item-title v-text="data.path"></v-list-item-title>
          </a>
        </v-list-item-content>
        <v-list-item-avatar>
          <v-img :src="data.image"></v-img>
        </v-list-item-avatar>
      </v-list-item>
    </v-list>

  </v-container>
</template>

<script>
import { API, graphqlOperation, Storage } from 'aws-amplify';
import { listSampleAppsyncTables } from "../graphql/queries";
import { onCreateSampleAppsyncTable } from "../graphql/subscriptions";

const dataExpireSeconds = (30 * 60);
export default {
  name: 'List',
  data: () => ({
    group: null, 
    dataList: [], 
  }), 
  mounted: async function() {
    this.getList();
  }, 
  methods:{
    async getList() {
      this.group = this.$route.query.group;
      console.log("group : " + this.group);
      if(!this.group){
          return;
      }

      let apiResult = await API.graphql(graphqlOperation(listSampleAppsyncTables, { group : this.group }));
      let listAll = apiResult.data.listSampleAppsyncTables.items;
      for(let data of listAll) {
        let tmp = { path : data.path, image : "" };
        let list = [...this.dataList, tmp];
        this.dataList = list;
        console.log("path : " + data.path);
        Storage.get(data.path.replace('public/', ''), { expires: dataExpireSeconds }).then(result => {
          tmp.image = result;
          console.log("image : " + result);
        }).catch(err => console.log(err));
      }

      API.graphql(
          graphqlOperation(onCreateSampleAppsyncTable, { group : this.group } )
      ).subscribe({
          next: (eventData) => {
            let data = eventData.value.data.onCreateSampleAppsyncTable;
            let tmp = { path : data.path, image : "" };
            let list = [...this.dataList, tmp];
            this.dataList = list;
            console.log("path : " + data.path);
            Storage.get(data.path.replace('public/', ''), { expires: dataExpireSeconds }).then(result => {
              tmp.image = result;
              console.log("image : " + result);
            }).catch(err => console.log(err));
          }
      });
    }, 
  }
}
</script>

クエリパラメータでgroupを取得します。
画面表示前のmountedで、groupを指定してレコードデータを取得したり、挿入イベント受信時にレコードデータを取得したりしています。
取得したレコードデータはdataListというメンバ変数配列で保持し、画面にv-listで並べて表示します。
v-listでは、レコードデータのpathと画像を表示しています。画像はStrageで有効期限(30分)付きアドレスをgetしてそれでアクセスしています。

src/components/Home.vue
<template>
  <v-container>
    <p>ホーム</p>
    <router-link to="about" >link to About</router-link>
    <hr>
    <v-btn @click="selectFile">
      SELECT A FILE !!
    </v-btn>
    <input style="display: none" 
      ref="input" type="file" 
      @change="uploadSelectedFile()">
  </v-container>
</template>

<script>
import Vue from 'vue'
import { Auth, Storage } from 'aws-amplify';

export default {
  name: 'Home',
  data: () => ({
    loginid: "sample-vue-project-user", 
    loginpw: "sample-vue-project-user", 
  }), 
  mounted: async function() {
    this.login();
  }, 
  methods:{
    login() {
      console.log("login.");
      Auth.signIn(this.loginid, this.loginpw)
        .then((data) => {
          if(data.challengeName == "NEW_PASSWORD_REQUIRED"){
            console.log("new password required.");
            data.completeNewPasswordChallenge(this.loginpw, {}, 
              {
                onSuccess(result) {
                    console.log("onSuccess");
                    console.log(result);
                },
                onFailure(err) {
                    console.log("onFailure");
                    console.log(err);
                }
              }
            );
          }
          console.log("login successfully.");
        }).catch((err) => {
          console.log("login failed.");
          console.log(err);
        });
    },

    selectFile() {
      if(this.$refs.input != undefined){
        this.$refs.input.click();
      }
    }, 

    uploadSelectedFile() {
      let file = this.$refs.input.files[0];
      if(file == undefined){
        return;
      }
      console.log(file);

      let dt = new Date();
      let dirName = this.getDirString(dt);
      let filePath = dirName + "/" + file.name;      
      Storage.put(filePath, file).then(result => {
        console.log(result);
      }).catch(err => console.log(err));

      this.$router.push({ path: 'list', query: { group: dirName }});      
    }, 

    getDirString(date){
      let random = date.getTime() + Math.floor(100000 * Math.random());
      random = Math.random() * random;
      random = Math.floor(random).toString(16);
      return "" + 
        ("00" + date.getUTCFullYear()).slice(-2) + 
        ("00" + (date.getMonth() + 1)).slice(-2) + 
        ("00" + date.getUTCDate()).slice(-2) + 
        ("00" + date.getUTCHours()).slice(-2) + 
        ("00" + date.getUTCMinutes()).slice(-2) + 
        ("00" + date.getUTCSeconds()).slice(-2) + 
        "-" + random;
    }, 
  }
}
</script>

uploadSelectedFileで、ファイルをアップロードした後にListページへ遷移させます。その際、groupというクエリパラメータを付けています。

これでフロントエンド(Webアプリ)の改修は完了ですが、動作確認はバックエンド側が済んでからとします。

バックエンド (LambdaのPython)から叩く

Webアプリからアップロードされたファイルや、Lambdaで生成してアップロードしたモノクロ画像のパス(S3のKey)を、AppSync経由でレコード挿入する実装を施します。

gqlをインストールします。

pip3 install gql -t .

lambda_function.pyを以下のように更新します。

lambda_function.py
# coding: UTF-8
import boto3
import os
from urllib.parse import unquote_plus
import numpy as np
import cv2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client("s3")

from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
ENDPOINT = "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql"
API_KEY = "da2-**************************"
_headers = {
    "Content-Type": "application/graphql",
    "x-api-key": API_KEY,
}
_transport = RequestsHTTPTransport(
    headers = _headers,
    url = ENDPOINT,
    use_json = True,
)
_client = Client(
    transport = _transport,
    fetch_schema_from_transport = True,
)

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))

    fileName = os.path.basename(key)
    dirPath = os.path.dirname(key)
    dirName = os.path.basename(dirPath)

    orgFilePath = u'/tmp/' + fileName
    processedFilePath = u'/tmp/processed-' + fileName

    if (key.startswith("public/processed/")):
        logger.info("not start with public")
        return

    apiCreateTable(dirName, key)

    keyOut = key.replace("public", "public/processed", 1)
    logger.info("Output local path = {0}".format(processedFilePath))

    try:
        s3.download_file(Bucket=bucket, Key=key, Filename=orgFilePath)

        orgImage = cv2.imread(orgFilePath)
        grayImage = cv2.cvtColor(orgImage, cv2.COLOR_RGB2GRAY)
        cv2.imwrite(processedFilePath, grayImage)

        s3.upload_file(Filename=processedFilePath, Bucket=bucket, Key=keyOut)
        apiCreateTable(dirName, keyOut)

        logger.info("Function Completed : processed key = {0}".format(keyOut))

    except Exception as e:
        print(e)
        raise e

    finally:
        if os.path.exists(orgFilePath):
            os.remove(orgFilePath)
        if os.path.exists(processedFilePath):
            os.remove(processedFilePath)

def apiCreateTable(group, path):
    logger.info("group={0}, path={1}".format(group, path))
    try:
        query = gql("""
            mutation create {{
                createSampleAppsyncTable(input:{{
                group: \"{0}\"
                path: \"{1}\"
              }}){{
                group path
              }}
            }}
            """.format(group, path))
        _client.execute(query)
    except Exception as e:
        print(e)

ENDPOINTAPI_KEYは、AppSyncに先ほど作成したAPI設定を参照してください。
zip圧縮してS3にアップロードしてLambdaにデプロイしてください。

動作確認

Webアプリを実行して画像をアップロードすると、LambdaからAppSyncが叩かれて、それを検出してWebアプリ側でリストされます。クエリパラメータ付きのURLを直接叩いても、AppSyncからリストを取得してリストするようにしています。
Screenshot 2020-01-03 at 09.19.47.pngScreenshot 2020-01-03 at 09.20.39.png

Webアプリ(Vue)のプロジェクトは以下においておきます。
https://github.com/ww2or3ww/sample_vue_project/tree/work5

Lambdaのプロジェクトは以下においておきます。
https://github.com/ww2or3ww/sample_lambda_py_project/tree/work3

あとがき

JAWS UG 浜松に参加して初めて声に出して質問した話題がこのあたりでした。
AmplifyのAPIってDynamoDBのパーティションキーとかソートキー指定できないんですかねぇ?
DynamoDBってキー設定無しで使うことってあんまり無いですよねぇ?
WebSocketって詳しく知らないですけどロングポーリングみたいなものですかねぇ?
ドキドキしながら発言したのを覚えています。

ところでネットワークって難しいですよね。ネットワークエンジニアを名乗れる人、尊敬します。
MQTT over WebSocketとか言われても正直良くわからないです。誰か分かりやすく教えて下さい、、。

AppSyncのサンプルって、Amplify CLIとセットで語られることが多いからか、フロントエンドからの利用ばかりですよね。チャットアプリしかり、TODOアプリしかり。DynamoDBも全件スキャンですし。
DynamoDBってレコードの数が増えていきがちというか、そういう用途で利用されがちだと思っていて、そういった意味では全件スキャンは良くないですよね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/Next.jsアプリケーションを作成し、AWS EC2を使って本番環境にデプロイするまで

対象

  • Next.js 等、Node.js で動かすアプリをローカル環境で作成することはできるが、それを本番環境で動かす方法と仕組みがわからない人
  • Heroku や Zeit Now を使うと簡単にリリースできるが、その仕組がさっぱりわかっていない人

この記事の存在意義

初心者が掲題のことをやろうと思ったときに、全体感を把握できる記事が見当たらなかったので、こういう記事があっても良いかなと思った。

流れ

以下のような手順で進める。
1. ローカル環境で動く Next.js を用いたサンプルアプリの作成
2. EC2 インスタンスの作成
3. EC2 インスタンス上でのサンプルアプリの起動&接続

1. ローカル環境で動く Next.js を用いたサンプルアプリの作成

  • 事前に npm をインストール
npm install -g npm

2. EC2 インスタンスの作成

公式に従ってやっていく。
自分の環境から ssh できるようにしておき、かつ http 通信はできるようにしておく。

3. EC2 インスタンス上でのサンプルアプリの起動&接続

3.1 外部アクセスをローカルホストにつなげるようにする

外部からアクセスがあったときに、そのアクセスをサンプルアプリが受け取って結果を返さなければならない。
しかしここまでのところ、localhost:3000 でアプリケーションを起動することしかできていない
そこで、「外部からのアクセスを受けたらそれをローカルホストにつなげるような仕組みを用意する」ことにする。

nginx を使ってそれを実現する。すなわち、「nginx をリバースプロキシとして用い、ポート80へのアクセスをlocalhostの3000ポートに振り向ける」ようにする。

  • ssh で EC2 インスタンスに入る。
ssh -i <key.pem>  <public ip address>
  • nginx の install
sudo su - root
yum update -y
yum install nginx -y

↑これを実行すると、nginx の install 時に Amazon Linux 2だとエラーになる。

Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
No package nginx available.
Error: Nothing to do


nginx is available in Amazon Linux Extra topics "nginx1.12" and "nginx1"

To use, run
# sudo amazon-linux-extras install :topic:

Learn more at
https://aws.amazon.com/amazon-linux-2/faqs/#Amazon_Linux_Extras

エラーメッセージの指示に従う。

sudo amazon-linux-extras install nginx1.12
  • nginx の起動
sudo systemctl start nginx.service

この時点で、ブラウザからアクセスすると以下のような状態になっているはず。

スクリーンショット 2020-01-03 4.09.47.png

  • nginx の設定変更(リバースプロキシの設定)
# 〜省略〜
http {
    # 〜省略〜
    server {
        # 〜省略〜
        location / {
            # 以下の行を追加
            proxy_pass http://localhost:3000;
        }
        # 〜省略〜
    }
}

この1行を追加することにより、ローカルホストの port:3000 に向けることができるようになる。ただし、nginx の設定を反映させるために nginx を再起動させることを忘れてはならない。

  • nginx の再起動
sudo systemctl restart nginx

この時点で、ブラウザからアクセスすると以下のような状態になっているはず。
スクリーンショット 2020-01-03 4.18.37.png

これはまだサンプルアプリを起動しておらず、振り向けた先から応答が返ってこないから。

3.2 サンプルアプリの起動

  • ssh で EC2 インスタンスに入る(既に入っていればそのままでOK)。
ssh -i <key.pem>  <public ip address>
  • git の install
sudo yum -y install git
vim ~/.ssh/id_rsa  # 自分のローカルのものをコピーしてくる
chmod 400 ~/.ssh/id_rsa
git clone https://github.com/<your sample app repository>
  • npm/node の install(公式のダウンロード方法はこちら)
sudo su - root
curl -sL https://rpm.nodesource.com/setup_13.x | bash -
yum install -y nodejs
  • 確認
$ npm -v
6.13.4
$ node -v
v13.5.0
  • アプリケーションのビルド
cd <your repository>
npm install
npm run build
  • アプリケーションの実行
npm run start

ブラウザから見てみると、以下のように適切に表示されているはず!

スクリーンショット 2020-01-03 4.19.03.png

まとめ

個人開発の際、なるべく早めに本番環境へのデプロイができることを確認しておくと、(不慣れな方は特に)不確実性を(そして心理的不安を)大きく減らすことができるかと思います。
まだ問題の切り分けを行いやすい最初のうちにこそ、本番環境でのデプロイを一度試してみることをおすすめします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCP・AWS・Heroku の最低価格帯インスタンスの料金比較(2020年1月版)

はじめに

規模の小さい Web アプリケーション(CPU 1 コア・メモリ 1 GB 弱のサーバ 1 台で動作可能)を
動かすための環境として GCP・AWS・Heroku を検討したので、
各サービスごとの最低価格帯の料金をまとめてみました。(料金は 2020年1月3日現在のもの。)
Azure は今まで全く触ったことがないので検討に入れておらず…すみません…

料金比較表

主なサービスを月間料金の昇順でソートしてみました。

Service Instance (v)CPU RAM(GB) 時間料金(USD) 月間料金(USD) 月間料金概算 Region
Heroku free ?1 0.5 $0 2 0円 US
AWS t3a.nano 2 0.5 $0.0061 $4.392 480円 東京
GCP f1-micro 1 0.6 $0.0092 $4.70 520円 東京
AWS t3.nano 2 0.5 $0.0068 $4.896 540円 東京
AWS t2.nano 1 0.5 $0.0076 $5.472 600円 東京
Heroku Hobby ?1 0.5 $7 770円 US
AWS t3a.micro 2 1.0 $0.0122 $8.784 960円 東京
AWS t3.micro 2 1.0 $0.0136 $9.792 1070円 東京
AWS t2.micro 1 1.0 $0.0152 $10.944 1200円 東京
GCP g1-micro 1 1.70 $0.0322 $16.45 1800円 東京
AWS t3a.small 2 2.0 $0.0245 $17.568 1920円 東京
AWS t3.small 2 2.0 $0.0272 $19.584 2140円 東京
AWS t2.small 1 2.0 $0.0304 $21.888 2400円 東京
GCP n1マシンタイプ 1 1.0 $0.0460 3 $23.5249 4 2600円 東京
GCP n1-standard-1 1 3.75 $0.0610 $31.17 3500円 東京
Heroku Standard 1x ?1 0.5 $25 2750円 US
Heroku Standard 2x ?1 1.0 $50 5500円 US
GAE F1,B1 1 0.25 $0.07 $50.4 5600円 東京

注意事項・補足

  • 月間料金概算は、1ドル約110円として概算
  • 各料金は、各サービスの下記公式ページから抜粋
  • AWS の月間料金は、1時間料金 * 24 * 30 として算出
  • GCP の月間料金は、GCP の料金表より抜粋
    • 一定時間以上使用すると継続利用割引(最大 30 %)が適用される
  • 無料枠
    • GAE の Standard Environment では 1 日あたり 28 インスタンス時間の無料枠がある
    • GCE では 1 インスタンス
      • f1-micro、ただしリージョンは、オレゴン(us-west1)、アイオワ(us-central1)、サウスカロライナ(us-east1) に限られる
    • AWS は、無期限での無料利用枠は AWS Lambda のみ
      • 12ヶ月間限定では、750 時間 / 月の t2.micro (Linux、RHEL、または SLES) インスタンスが含まれる
  • AWS の「東京」は アジアパシフィック(東京) 、GCP の東京は 東京(asia-northeast1)
  • Heroku の US は North Virginia にあるらしい。公式ページ で Region を調べる方法の記載がある。
  • AWS の t3 と t3a の違いは、下記引用の通り、CPU が若干異なるだけとのこと。

T3a インスタンスは T3 インスタンスのバリアントであり、全コアターボのクロック速度が最大 2.5 GHz の AMD EPYC プロセッサを搭載しています。T3a インスタンスは、使用中に一時的なスパイクが生じる中程度の CPU 使用率を持つアプリケーション向けの Amazon EC2 コンピューティング環境で、10% のコスト削減を目指すお客様にとっての新たな選択肢となります。

AWS 公式の T3a 提供開始のお知らせ より引用

個人的な所感

Heroku はとても安いので、GCP の GAE も安いのではと思っていたところ
GCE に比較して全く安くないということがわかりました。
確かに、GAE はマシン自体の管理が必要ないという点で GCE と比較して
運用側のコストが抑えられるものの、Docker コンテナ等でサービスを運用するのであれば
GCE が最適かなーと思いました。

AWS は相変わらず安いですが、色々考えてプラットフォームは一つに絞りたいし、
GCP がいいかなーと考えています。

[参考] Heroku の各タイプの特徴

  • Free
    • スリープ状態への移行あり
    • カスタムドメインは使用可
    • プロセスタイプ数 〜2
  • Hobby
    • スリープ状態への移行なし
    • 高度なアプリケーション用に複数の WORKER を使用可
    • 10分毎のメトリクス
    • プロセスタイプ数 〜10
  • Standard
    • 手動スケール
    • 閾値によるアラート

  1. Heroku 上の CPU コア数は非公表。ただし StackOverflow には、調べた結果 4 コアまたは 8 コアだったという情報がある。 

  2. 1アカウントあたり合計 1000時間 / 月まで無料 

  3. (\$0.040618 / vCPU hour) + ($0.005419 / GB hour) として算出 

  4. (\$20.755798 / vCPU month) + ($2.769109 / GB month) として算出 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS認定デベロッパーアソシエイト合格と、勉強方法と使ったサイトの感想

はじめに

image.png

バックグラウンド

  • 業務アプリメインのSIer、AWSは仕事でほぼ関わっていません。
  • アーキテクト取得のキッカケでAWSにハマり、インフラ知識はほんの少しだけ。
  • ブラックベルトの記事は日頃少しずつ読んでいます。

勉強方法

以下の順番で勉強していきました。最短ルートではないと思いますが、オススメ出来るものをピックアップしていこうと思います。

AWS 認定デベロッパー アソシエイト模擬試験問題集(3回分195問:全5回分準備中)

image.png
image.png
image.png

  • 日本語の模擬試験。Udemyのお得期間に購入しました。
  • いきなりですが、模擬試験を解くところから始めました。
  • 時間を止めたり、途中から開始もできますのでゆっくり進み回答を確認していきます。
  • 公式ドキュメントに記載されている「試験範囲が具体的にイメージできる」これをゴールとしました。
  • 3回分の試験をやり切ったころには、理解度が不足しているサービス名が列挙されているはずです。

実際に動かして確認する

  • AWS環境を用意し模擬試験で頻出していた理解度不足の箇所を組んでみました。以下、私が試した一部、一例です。
  • ポイントはサービスをいくつか組み合わせることです。サービス単体の動作確認ではどうしても試験とは乖離してしまいます。
    • Lambda
      • コンソールでコードを書き、Lambdaを単体で動作させる
      • バージョンとエイリアスをいじくり倒す
      • ZipしてCLIからアップロードしてみる
      • CloudWatchで成功ログと失敗ログを確認する
      • X-Rayと連携する
      • 定期イベントとしてCloudWatchからトリガーしてみる
      • DynamoDBと連携してみる(テーブル操作、IAMロールを体験)
      • API Gatewayと連携する
    • ECS
    • Elastic Beanstack
    • Codeシリーズ
      • 一通り動かしてみる
      • 可能ならECRの連携も試してみる

AWS Certified Developer - Associate 2020

image.png
image.png

  • Udemyの教材。試験対策の動画(英語)
  • 5時間ほどありますが、日本語字幕を使って1週間で消化しました。
  • 1.25倍速でも問題なく進めます。ちょっとしたテストもついています。
  • 模擬試験と実機での検証を済ませてからの視聴している為、復習と理解が浅い箇所を探す作業でした。
  • 最短ルートで合格を目指すのであればカットしても良いと思います。
  • 勉強後、英語文章の読むスピードがレベルアップしていたという想定外の副産物がありました(個人的にはとても嬉しくって寄り道だとしてもやって良かったです)

WHIZLABS

image.png

image.png

image.png

  • AWS試験問題が充実しているサービスです(最終テストと合わせて模擬試験6つあります)
  • Google、Facebook、LinkdInどれかのAAUTHでアカウント登録簡単
  • AWSで検索してHITしたものをPayPalでかんたん決済
  • ほぼ常時半額セールやってて10ドル程度でお手頃、すごくオススメです!
  • 英語できなくてもブラウザの翻訳機能ですべて解決!
  • 右クリック禁止はブラウザによってはアドオンで解除可能です。(リンクは貼りません)

さいごに

  • 最終的にUdemy、WHIZLABS合わせて65問ある模擬試験9種類を繰り返し解いたのが効きました。
  • 日本語の対策本がない不安感は、2つのサイトで模擬試験を行うことで確実に補えます。セールを狙って購入しましょう。もし英語が苦手でも翻訳機能で余裕で乗り切れます。
  • 見たことのない問題が出ると実機で動かした事を思い出して回答すると解けました。動かすことは大事です。
  • アーキテクトと重複するサービスもデベロッパー用に復習が必要です。おろそかにしない方がよいです。(私はおろそかにしました。失点の主な要因はこれでした。)

  • この記事が今後受験する皆様にとって、少しでもお役に立つことができれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSでCycleGANを実行してみる

AWSを使ってみたい

しばらくGoogle Colabを使っていたけど、ディープラーニング以外のところでつまずくことが多かった。AWSを一応使ってもいいので、使ってみようと思います。
本当はPyCharmでリモート開発?をやりたいけど、とりあえずターミナルから触れるように頑張る

CycleGANとは

GANは敵対的生成ネットワークのこと。生成モデルと識別モデルを用意して、偽物を作ってそれを判別する。これらを交互に学習を進めることで精度が高くなる。
CycleGANはそれの応用で、Cycleなので循環させるのがポイント。ペアになっていない2種類の学習用データ(ex. 紅葉と緑葉)を使って、元データ変換後のデータから元データを復元したものとの損失を小さくしていく。説明が難しいから無理

CycleGANの使い方は親切に書いてある

論文はこちら
GitHubにコードは上がっている。いろんなフレームワーク(?)で用意してくれていて、なんかずっとPytorchを触っているのでそれを使います。
GitHubのリンク

READMEの前にやること

AWSの準備

アカウント作るとかは覚えてないので省き

  1. AWSマネジメントコンソールにログイン
  2. インスタンスを立てる
    • サービス > コンピューティング > EC2 > 実行中のインスタンス > インスタンスを作成
    • Deep Learning で検索して出てきた良さそうなやつを選択。ubuntuにした
    • インスタンスタイプは、無料じゃないけどGPUのp2.xleargeを選択。確認と作成
    • セキュリティグループはよくわからないけどlaunch-wizard-32にしてる、わかんない
    • ストレージをでかくしとく。100とか150とか適当
    • キーペアの確認。既存のがあればそれを使う、なければ新しく作る。このpemというのがしぬほど大切らしい
  3. そこにターミナルで入る
    • pemのいるディレクトリでssh
sudo ssh -i n_nagata2.pem ubuntu@ec2-なんとか(パブリックDNS名)

ec2-うにゃららは、実行中のインスタンスのとこからコピーしてくる。

READMEを読む

  1. Pytorchや必要なライブラリを入れる

    • READMEをよく読むとcondaユーザ用のスクリプトが用意されている。
    • これです
    • なんかデフォでcondaになっている感じがした
  2. gitからコードとかいろいろクローンする

git clone https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix
cd pytorch-CycleGAN-and-pix2pix

CycleGANを回す

あとは書いてあるままに…

データセットをダウンロードして

bash ./datasets/download_cyclegan_dataset.sh maps

学習させて

#!./scripts/train_cyclegan.sh
python train.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan

テストして

#!./scripts/test_cyclegan.sh
python test.py --dataroot ./datasets/maps --name maps_cyclegan --model cycle_gan

これは例としてmapというデータセットを使っているから、航空写真と地図とを変換できるやつができるんじゃないかな

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

aws DR クラウド構成例

multi-azはdrにならない?
https://aws.koiwaclub.com/forums/topic/multi-az%E3%81%AFdr%E3%81%AB%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84%EF%BC%9F/

ディザスタリカバリを考慮したクラウド構成例
https://aws.amazon.com/jp/cdp/cdp-dr/

AWS資格試験問題に対応するためにも下記は全部読んだ方がいいかも
https://aws.amazon.com/jp/cdp/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Locustの分散負荷テスト環境をAWS Fargateを使って簡単に用意してやる

概要

この記事では、負荷テストツールLocustで分散負荷テスト環境を構築するに当たって私が使っている方法をまとめます。
構築はAWS Fargateを使って、設定をできるだけ少なくしました。
AWSの操作にはTerraformを使って、構築・破棄を繰り返しできるようにしています。

背景

これまでやっていた負荷テスト

負荷テストはどのようにして実行しているでしょうか?
私はこれまで簡単なものはApacheBenchで行い、
ログインを含むシナリオが必要なものはシェルスクリプトでコードを書いて実行していました。

しかし、ApacheBenchは静的なWebサイトなどで使うには良いのですが、Webアプリのテストとなると機能が足りないと感じていました。
シェルスクリプトを使えば何でもできる反面、テストの作成に時間がかかりがちで、メンテナンスもしずらくなっていました。

Locustを使った負荷テスト

そんなときに、Locustというツールを知りました。
まだ使い始めて一ヶ月ほどですが、満足できそうな予感がしています。
(ツールの特徴については公式をはじめ多数情報があるので、割愛します)

今回紹介する方法の良い点

Locustは気に入ったので、これから何度も使うつもりなのですが、毎回Locustサーバーを立てるのは面倒です。
使わないサーバーを停止状態で放置するのもリソースが無駄になって良くないと思います。
そこでFargateとTerraformも使い、使いたいときに立ち上げてすぐに破棄できるようにコード化しました。

  • Locustを使うことで

    • テストスクリプトが見やすくなる(と思う)
    • イベントやタスクの重み付けで、リアルなユーザーを想定したシナリオでテストできる
    • 分散クラスターの構築に対応していて、テストのスケールアウトに対応できる
  • Fargateを使うことで

    • 多数のコンテナをサーバーの台数やスペックを気にせずに立ち上げられる
  • Terraformを使うことで

    • 使いたくなったときにコマンド一発で環境が立ち上がり、コマンド一発でネットワーク設定もろとも破棄できる

構築手順

実際に環境を構築する方法です。

攻撃スクリプトコンテナを用意する

攻撃スクリプトは事前に用意して、コンテナに入れてイメージリポジトリにpushしておきます。
コンテナ内には複数種類のスクリプトを入れておいて、Terraformの変数でどのスクリプトを実行するか切り替えられるようにしています。

実業務では、攻撃スクリプトコンテナはプライベートなECRリポジトリに置いていますが、今回はgithubとDockerHubでサンプルを作っています。

github: https://github.com/neilli-sable/locust_scripts
dokcerhub: https://hub.docker.com/repository/docker/neilli/locust_scripts

Terraformから環境を構築する

このリポジトリに置いたコードから、攻撃スクリプトコンテナを起動して構築します。
https://github.com/neilli-sable/locust_fargate

設定内容について

「ネットワーク設定する -> ECSのクラスタを作る -> クラスタの上でLocustコンテナを動かす」というような内容になっています。
細かい部分はコードをご確認いただきたいですが、いくつか設定のポイントになった箇所を書いておきます。

接続情報と変数

variable.tf.defaultにプロバイダーと変数設定のサンプルがあります。
variable.tfとリネームして、設定値を変更することで構築するLocustクラスタのスペック変更などができます。

接続情報として、providerに名前付きプロファイル名と、regionを指定しておきます。

provider "aws" {
  profile = "my-profile"
  region  = "ap-northeast-1"
}

変数は以下の種類を用意しました。

  • general_name
    • AWSのリソース名やタグ付けに使うための文字列を指定します
  • slave_count
    • LocustのSlaveサーバーの数を指定します
  • fargate_cpu
    • LocustコンテナのCPUスペックを指定します
  • fargate_memory
    • Locustコンテナのメモリ量を指定します
  • locust_container
    • Locustの攻撃スクリプトを入れたコンテナのURLを指定します
  • locust_script_path
    • 使用する攻撃スクリプトのファイルパスを指定します

開放するポート

Masterサーバーは3つのポートを開放します。

  • 8089
    • WebブラウザからUIにアクセスするためのポート
  • 5557/5558
    • Slaveサーバーからの通信を受けるためのポート

5557と5558はSlaveサーバー向けのセキュリティグループからのみアクセス可にしておきます。
8089は今回アクセス制限していませんが、必要に応じてIP制限などを設定すると良いと思います。

一方、Slaveサーバーはポート開放は必要ありません。
セキュリティグループからingressは削除し、egressだけ設定して、外部からの接続はできないようにしておきます。

MasterサーバーのプライベートDNS

SlaveはMasterに通信しないといけないですが、コンテナは起動するまでIPアドレスが割り当たっていないため、単純なIPアドレスの指定は使えません。
なのでECSのサービスディスカバリ機能を使って、プライベートDNSを設定します。

vpc.tfにある、この以下の部分です。

# Service Discovery
resource "aws_service_discovery_private_dns_namespace" "locust_internal" {
  name        = "locust.internal"
  description = var.general_name
  vpc         = aws_vpc.vpc.id
}

resource "aws_service_discovery_service" "master" {
  name = "master"

  dns_config {
    namespace_id = aws_service_discovery_private_dns_namespace.locust_internal.id

    dns_records {
      ttl  = 10
      type = "A"
    }

    routing_policy = "MULTIVALUE"
  }

  health_check_custom_config {
    failure_threshold = 1
  }
}

そうすることで、タスク定義でmaster.locust.internalというプライベートDNSアドレスを解決できます。

  container_definitions = <<DEFINITION
[
  {
    "cpu": ${var.fargate_cpu},
    "essential": true,
    "image": "${var.locust_container}",
    "memoryReservation": ${var.fargate_memory},
    "name": "locust",
    "command": ["locust", "-f", "${var.locust_script}", "--slave", "--master-host=master.locust.internal"]
  }
]
DEFINITION

Terraformの実行

terraform ディレクトリが実行用のディレクトリなので、移動します。

cd ./locust_fargate/terraform

接続情報と変数」の項目でも書きましたが、variable.tfを作成して、接続情報などを設定します。

初回のみ、initコマンドを使います。

terraform init

一度 init すれば、以後はapplyコマンドで環境構築できます。

terraform apply

負荷テストを実行する

UIへのアクセス

terraform apply の結果、成功すればURLを出力するようにしています。
こんな感じに。

Outputs:

endpoint = access to here (wait a minute): http://locust-fargate-application-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com

terraform applyが終わってしばらくしてからURLにアクセスすると、LocustのUIが表示されます。
変数で設定した数のSlave数になっていることが確認できるはずです。

しばらくしてからと書きましたが、terraform applyの完了からサーバーが実際に起動完了するまでにタイムラグがあります。起動する前にURLにアクセスすると503エラーとなるので数分程度待たないといけません。
いつまでも503が続く場合は、エラーなどが原因でタスク自体が停止している可能性があります。

Locustを使う

UIに接続できれば、普通にLocustを使うことができます。
「Number of users to simulate」「Hatch rate」「Host」を入力して、テストを開始してください。
Hostに「http://」から入力しないといけないのをいつも間違えるのは私だけでしょうか。

お片付け

テストを実行してデータの回収などを完了したら、AWS上に作成したリソースを破棄します。

terraform destroy

これでキレイサッパリなはずなので、気軽に破棄してお金が掛からない状態にしておきます。
またテストしたくなったら、terraform applyからやり直すだけです。

感想

これまでの負荷テストだと、始めるまでがちょっとした仕事量になってしまい、実行がおろそかになっていたように思います。
もっと気軽にテストをしていきたいものです。

参考文献など

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFrontでmulti originをするときにハマったところ

概要

年末で暇だったので、個人サービスの画像配信部分をCloudFront経由で早くしてやろうと思い、見事にハマった出来事についての備忘録です。

ちゃんと調べずに自分でトライアンドエラーをしているので、かなりグダグダした内容になっています。ご了承ください :bow:

TL;DR

内容がグダグダなので、何にハマったか知りたい人はこれだけ見て退散することをオススメします

  • BehaviorのPath PatternがOriginに渡るときにIgnoreされると思っていた
    • Path Patternは正規表現なのでIgnoreされずにOriginに届く
    • 例:
      • Path Pattern(image/*)を持つmulti-origin(image-bucket)なCloudFrontに対してリクエストを送る
      • https://www.app.com/image/full/sample1.pngがOriginに届く時には、s3://image-bucket/full/sample1.pngになると思っていた
      • 実際にOriginに届くリクエストは s3://image-bucket/image/full/sample1.png
  • ErrorになったBehaviorはスキップされて次にマッチするBehaviorに行く(っぽい)

やったこと

前提条件

個人サービスはSPAでS3にアップロードしたものを、CloudFrontで配信していましたので、既にDistributionが一つあり、カスタムドメインも設定されていました。

画像はSPAのS3 Bucketとは別のBucketに保管されており、S3のWeb Hosting機能を使って配信をしていました。
この部分をCloudFrontでCacheさせて高速化を図るというのが、今回やりたいことでした。

別のDistributionを作成しても良かったのですが、設定を作り直すのも面倒だし、別ドメインだとCORSやDNS Lookupなど余計なものを気にしないといけないし面倒だなと思ったので、multi-originでやってみることに。

CloudFrontでmulti-origin

CloudFrontにはmulti-origin機能が既に備えられており、それの通りに作成するだけ...、のハズでした。
これの設定方法については色々な記事で紹介されていました。
(何故かほとんどの記事で扱っていたのが、S3とELBのmulti origin)

やることは以下の3つです。

  1. 既存のDistributionにoriginを追加
  2. Behaviorを追加し、path patternで任意のpathのときに追加したoriginに向くようにする
  3. CloudFrontに設定がDeployされるのを待つ

コンソールからDistributionを作成するときは一つのoriginとBehaviorしか設定できませんが、Distribution作成後追加できるようになります。

大体どの記事を見ても画像付きでこの説明がされていたのですが、自分はなぜかうまくいきませんでした。

1. 既存のDistributionにoriginを追加

画像がSPAとは別のBucketで保管していましたので、originを作成します。

image.png

Origin Pathについては、リクエストが来たときにPrefixでoriginに追加されるPathになります。

例えばSingle Originで上のような設定とリクエストをしていた場合、CloudFrontはs3://sample-image-bucket/thumbnail/sample1.pngからObjectをGetします。

Origin: `image-bucket.s3.amazonaws.com`
Origin Path: `/thumbnail`
Request: `https://img.app.com/sample1.png`

今回の画像のBucketは以下のようなディレクトリ構造になっていたので、Origin Pathは設定していません。

image-bucket
L full
  L ~.png
L thumbnail
  L ~.png

2. Behaviorを追加し、path patternで任意のpathのときに追加したoriginに向くようにする

BehaviorはEdgeロケーションのCacheの振る舞いを設定します。
Behaviorは複数設定することが可能で、Path Patternを設定することによって、複数のBefaviorに振り分けることが可能です。
コンソールからCloudFrontを作成し際には、DefaultのBehaviorだけ設定されています。

ここに画像配信用のBehaviorを作成します。

image.png

Path Patternは正規表現で表します。今回は画像配信部分でしたので、image/*に設定しています。
これで、https://cdn.app.com/image/full/~.pngにリクエストが来れば、画像を返してくれるはずでした...。

3. CloudFrontに設定がDeployされるのを待つ

待つだけです。だいたい10〜15分くらいでDeployされます。

起こったこと

何故かDefaultのBehaviorにルーティングされ、SPAのエラー画面が表示されます (404 Not found)。
CloudFrontの設定が良くないのかと思い色々調べてみました。

1. CloudFrontのログを出してみる

CloudFrontはDistributionの設定でログをS3に出力することができます。
これが設定されてなかったので、ログを有効化しデプロイされるのを待ちます。

デプロイされた後に、該当画像にアクセスしてみます。
するとログが出力されたのですが、CloudFrontのログは基本的にアクセスログしか残らないので、Errorが発生したかどうかはわかるのですが、エラーメッセージまでは見ることができませんでした。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#BasicDistributionFileFormat

とにかく、該当画像にアクセスした際にBehaviorのPath Patternの割り振りは動いているようでした。
権限的な問題かと思いましたが、既にWeb HostingでPublicになっているバケットでしたので、ありえませんでした。

2. 別のDistributionで画像BucketをDefaultのBehaviorのOriginにしてみる

最初の決意がゆらいで別のDistoributionでも良いかなと思い始めてきたので、新しくDistributionを作成し、DefaultのBehaviorを画像BucketのOriginに設定してみました。

すると、問題なくアクセスできました :thinking:
つまり、権限的な問題でもなくBehaviorの問題では無いことがわかりました。

ついでに、新しく作成したDistributionにSPAのOriginも設定してみます。
そのときに、Origin Pathのテストがしたくなったので、Origin Pathを static/ にしてみました。

image.png

BehaviorのPath Patternは app/* です。
それでもアクセスできませんでしたが、エラーの内容は変わりました。

image.png

どうやら、DefaultのBehaviorである画像Bucketの方にリクエストがいっているようです。
予定では、static/以下のjsファイルが取得できる予定でした。

Origin Pathの設定が悪かった可能性もあったので、Origin Pathの設定を削除してみました。
それでも接続できません。どうやらOrigin Pathの設定ではないようです。

手詰まり感が出てきたので、一旦Path Patternをstatic/*にしてみました。
すると、アクセスできるようになりました!

原因

つらつらとトライアンドエラーしてきた内容を書いていきましたが、ようやく原因がわかりました。
BehaviorのPath Patternで設定したPathにマッチしたPathはOriginに送られる際に無視されるわけではなく、Path PatternのPathも含めてOriginに送られるのでした。

前提条件を整理すると以下のような感じでした。

// CloudFrontの設定
- Origin: `image-bucket.s3.amazonaws.com`
- Behavior Path Pattern: `image/*`
- Request: `https://www.app.com/image/full/sample1.png`
// S3のBucket構造
image-bucket
L full
  L ~.png
thumbnail
    L ~.png

この場合、画像Bucketに対するCloudFrontのリクエストはs3://image-bucket/image/full/sample1.pngに飛んでいた訳です。
image-bucketにはimage/をPrefixに持つObjectは無いためCloudFrontはエラーを返し、DefaultのBehaviorであるSPAのOriginにリクエストが飛んでいたということです。
そして、SPAでは/imageというルーティングは存在しないため404 Not Foundが発生していたのです。

まとめ

ということで、画像のBucketにimage/というPrefixをつけるようにすることで、無事multi-originで画像配信が実現できました。

てっきりBehaviorのPath PatternはOriginに到達するときにIgnoreされているという思い込みが自分の中のハマりポイントでした。
しかし、ネタが割れてしまえば当たり前のことで、Path Patternは正規表現だし、元々Single-Originで配信したいObjectの種類に応じてCacheのパターン(Behavior)を変えたいときに使われていたと思われるので、そりゃそうだよなとなった。

大体思い通りに行かずにハマるときは自分の勘違いが招いていることが多い気がする...。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む