[初心者向け] 実務開始の前に知っておきたいAWSのこと

この投稿は、チュートリアルを終えて実務に入る前の人に知っておいて欲しいことをまとめています。
チュートリアルは終えている前提なので、AWSのサービスに関する説明は省略しています。
ベンチャー企業の複数あるうちの1サービスくらいの規模感のアプリケーションを構築すること(移行ではない)を前提としています。
社内メンバー宛なので偏りがあるかもしれないですが、みなさまの役にも経てば幸いです。

設計に役立つツール

AWSを実務で利用する際は、構成図を書くかと思います。その際に利用できるのが Cloudcraft です。
構成図とともに料金もざっくりわかります。
cloudcraft例.png

もっと詳細に料金を出す必要があるならば、AWSが提供している料金計算ツールを使うと良いかもしれません。
SIMPLE MONTHLY CALCULATOR

また、サーバーレス構成を検討している方は、re:Invent2017で発表された「AWS Serverless Application Repository」を利用すると構成案が公開されていますので、参考になると思います(2017年12/11時点ではまだpreview版)

リザーブドインスタンス

リザーブドインスタンスはEC2やRDSのようなサービスで、前払いをすると料金を節約)できる制度です。 (参考: AWSリザーブドインスタンスについて)
インスタンスタイプや期間によりますが、「半年以上使うことが決まってるなら、1年分はリザーブドインスタンス買っちゃった方がお得」ということも少なくありません
ただし、リザーブドインスタンスはリージョン(もしくはアバイラビリティーゾーン)ごとで、インスタンスタイプも指定するので、インスタンスを立て直したり、何も知らずにインスタンスタイプを変更してしまうと「リザーブドインスタンスを買っているのに使えてない」状態になりえます。
リザーブドインスタンスは売買もできますし、t2.small2つをt2.medium1つに変更などもできるので、一度買ったらできるだけ今の構成を追随するようにしましょう。

IAM

IAMはAWS内の権限をつかさどるサービスです。各種サービスへの参照・書き込み・実行などを細かく設定できます。
権限を操作できてしまうので、一度作るとなかなか変更するのが怖いところです。
ここでのtipsはポリシーはできるだけ細かく設定することです。めんどくさいですが、サービスAのs3参照ポリシーとサービスBのs3参照ポリシーは分けた方が良いと思います。たとえ1つしかサービスをローンチしてなくても、サービスAに限定するようなポリシーを設定しておきましょう。
最小単位であるポリシーが細かければその組み合わせで柔軟に設定が可能です。

awsを操作するためのツール

  • aws-cli … コマンドラインからawsを操作できる
  • ansibleのec2.py … ansibleでipを指定するのではなく、ec2についているタグ名で絞ってec2をプロビジョニングするためのツール(というかpython script)

最後に

クラウドになるとlinuxではないもの(IAMやセキュリティグループなど)の設定をたくさんしなければならず、(aws-cliもありますが)guiでの設定も増えてしまいます。
面倒な部分はありますが、慣れてしまえば早いですし、コスパもよいのでどんどん利用していきたいですね
では、よいaws lifeを

[初心者向け] 実務開始の前に知っておきたいシリーズ

[初心者向け] 実務開始の前に知っておきたいSpreadSheetのこと
[初心者向け] 実務開始の前に知っておきたいAnsibleのこと
[初心者向け] 実務開始の前に知っておきたいGoogleAnalyticsのこと
[初心者向け] 実務開始の前に知っておきたいWordPressのこと
[初心者向け] 実務開始の前に知っておきたいRailsのこと

続きを読む

CodeBuildの実行結果をslackに通知する

はじめに

Globis Advent Calendar10日目は、弊社のエンジニアチームを支えるインフラ技術をお伝えいたします。
開発スピードをさらに促進するために、Blue Green Deployment, Infrastrucure as Code, ChatOps など新しい運用思想を積極的に取り入れて、手動でのオペレーションを極力なくしています。
今回は、弊社で実運用しているChatOpsの一例として、CodeBuildの実行結果をslackに通知する方法を、可能な範囲で具体的にお伝えいたします。

設定方法

AWS Lambdaの設定

コード

import os
import json
import urllib.request

URL = os.environ['WEBHOOK_URL']


def send_slack(obj):  ## slackに通知
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    js = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(URL, data=js, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")


def check_status(status):
    fail = False
    if status == 'IN_PROGRESS':
        color = '#008000'  ## green
    elif status == 'SUCCEEDED':
        color = '#00BFFF'  ## deepskyblue
    else:
        color = '#FF00FF'  ## magenta
        fail = True
    return color, fail


def parse_event(event):  ## cloudwatch event から渡された event(dict型)をパース、slackに渡すobjectを生成。
    detail = event['detail']
    status = detail['build-status']
    initiator = detail['additional-information']['initiator']
    log = detail.get('additional-information', {}).get('logs', {}).get('deep-link', 'Not Exist')
    color, fail = check_status(status)
    fields = [{'title': 'Initiator', 'value': initiator, 'short': False},
              {'title': 'Status', 'value': status, 'short': False},
              {'title': 'LogLink', 'value': log, 'short': False}]
    obj = {'attachments': [{'color': color, 'fields': fields}]}
#    if fail == True:  ## Fail時にチャンネルメンション飛ばしたい時はコメントを外す。
#        obj['attachments'][0]['text'] = '<!channel>'
    return obj


def lambda_handler(event, context):  ## lambdaから最初にcallされる関数。
    obj = parse_event(event)
    send_slack(obj)
    return

環境変数の設定

CloudWatch Event の設定

slackでの通知結果

  • ワンクリックでビルドログを眺めることができます。

運用事例のご紹介

  • AWS Athenaのテーブルにクエリを実行し、中間テーブルを生成するETL処理のバッチ
  • Ruby on Rails アプリケーションデプロイ時の db:migrate 処理
  • 最近はCodeBuildをVPC内で実行できるようになったので、利用できる幅が広がっています!

おわりに

いかがでしたでしょうか。python初心者(僕)でもChatOpsに貢献できます。
グロービスではSREエンジニアを募集しています!
インフラの運用担当だけど手作業を自動化したいと考えている方、開発者だけどインフラも含めてコードで管理したい方、一緒に理想のインフラを作ってみませんか?

続きを読む

【CS Hack 第1弾!!】アプリケーション内でファイルの受け渡しを実装する

はじめに

皆さん、カスタマーサクセスしてますか?価値あるプロダクトを作っていますか?
日々自分たちのカスタマーがより良い活動をできるよう、プロダクトを開発・運用しているかと思いますが、活動の中でのヒューマンエラーを無くす動きも大切です。

カスタマーサクセスの為に、検証作業も含め、頻繁にカスタマーとデータのやり取りをする場合(例: 提案資料、データの受け渡し…)、うっかりミスが致命的な問題につながり兼ねません。
メールやFBで気軽に連絡できる時代において、ヒューマンエラーによる情報漏えいはかなり大きな割合を占めております。

leak07.jpg

引用:情報漏洩の原因を徹底解析!原因と結果から学ぶ意識改革

カスタマーのより良い活動を支援しようと思ったのに・・・、その活動がサービスの停止へと導いてしまった、、、なんて事があったら悲しすぎますね。
そんなミスを防ぐ為に、今日はアプリケーション内でファイルをDLする機構を作ることで、セキュアな環境下でのやり取りを実現してみたいと思います。

イメージ図

スクリーンショット 2017-12-09 12.05.27.png

実装

サーバ側

まずは、AWS側の設定を記述します。

config/initializers/aws_s3.rb
Aws.config.update({
    credentials: Aws::Credentials.new(
      #ACCESS_KEY,
      #SECRET_ACCESS_KEY
    ),
    region: #region情報を
    endpoint: #endpoint情報を
})

# もし参照パスにルールがあるなら記述しておきます
module AwsS3
  BUCKET = 'test'.freeze
  BASIC_URL = "https://s3.console.aws.amazon.com/s3/object/#{BUCKET}".freeze
end

ベースのモデルを作成。

class S3Client
  def initialize(*_)
    @client = Aws::S3::Client.new
  end
end

まずはS3にあるファイルを引っ張ってくる処理。
例えばログインしているユーザに応じて、S3のパスを分けます。

class S3Reader < S3Client
  def initialize(user)
    super
    @user_id  = user.id
  end

  def prefix
    "#{Rails.env}/#{@user_id}/"
  end

  def objects
    contents = @client.list_objects(bucket: AwsS3::BUCKET, prefix: prefix).contents
    contents.map { |content| S3Reader::Object.new(content, prefix) }
  end
end

# objectの加工用
class S3Reader::Object
  attr_reader :key, :name, :modified_at

  def initialize(content, prefix)
    @key = content.key
    @name = content.key.gsub(prefix, '')
    @modified_at = content.last_modified
  end
end

controller側に下記を実装。するとS3からDL可能なObjectが引っ張られてきて表示されます。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end
end

S3にアップロードしたファイルが・・・

スクリーンショット 2017-12-09 12.23.08.png

アプリケーション側でも閲覧可能に。

スクリーンショット 2017-12-09 12.23.50.png

引っ張るところまで出来れば次はクリック後、DLできるようにします。
DLの方法としては、一定時間だけオブジェクトにアクセスできるURLを作成し、send_dateをすることで、ブラウザ側にDLされるようにします。

参照)
一定時間だけS3のオブジェクトにアクセスできるURLを生成する

class S3Downloader < S3Client
  # 一時的なURLは120秒に設定
  def download_url(key)
    Aws::S3::Presigner.new(client: @client).presigned_url(
      :get_object, bucket: AwsS3::BUCKET, key: key, expires_in: 120
    )
  end

  def content(key)
    S3Downloader::Object.new(open(download_url(key)), key)
  end
end

class S3Downloader::Object
  attr_reader :content, :key

  def initialize(content, key)
    @content = content
    @key = key
  end

  def name
    @key.split('/').present? ? @key.split('/')[-1] : 'download'
  end

  def read
    @content.read
  end

  # ファイルの種類によってはsend_date時に上手くcontent_typeが出せない場合があるので、オーバーライドしておく
  def type
    @content.content_type
  end
end

後は、リストをクリックした際のアクションをController側に実装。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end

  def download
    downloader = S3Downloader.new
    content = downloader.content(params[:key])
    send_data content.read, filename: content.name, type: content.type
  end
end

これらを実施し、クリックすると・・・

ダウンロード.png

無事DLができました。

最後に

今回は第1弾ということで、まずはファイルをDLできる機構を作ってみました。
ログインすることでDLできるので、カスタマーには、『サービス内にアップロードしたので、DLしてくださいね。』と伝えるだけでいけますね。

ヒューマンエラーを出来る限り排除し、価値にフォーカスできる体制をつくるのもプロダクト開発において非常に重要だと感じています!

第二弾はこの機構に自動でファイルをアップロードしていく仕組みを構築します。
これで活用レポートだったり、定期的に送るデータは自動化してしまいましょう。

続きを読む

Ruby Sample for AWS Cloud9

そもそもAWS Cloud9、Ruby on Railsサポートしてないんじゃない?テンプレ消えてない??」 って言われたので方向転換してとりあえずAWS Cloud9上でRubyを動かしてみようと思います。 トライするのはこれ。 docs.aws.amazon.com Step 1: Install Require… 続きを読む

システム障害解析におけるログのあれこれ

この記事は Akatsuki Advent Calendar 2017 の 8 日目の記事です。
7日目: バイナリのビルド作業はそろそろボタンをポチるだけにしようぜ

背景

システムを運用していると、日々アプリケーション・ミドルウェア・インフラのログが蓄積されていきます。これらのログはシステムの障害対応・解析のための貴重な情報源となりますし、そうであることが期待されます。
しかし、これらのログの取り扱いを誤ると誤った障害解析結果を導き出してしまったり、解析にいたずらに時間がかかったり、障害を特定することができなかったりといったことが起こります。
今回はこれらのログを扱う上で注意すべき点とその改善案を紹介をしたいと思います。

前提

私はソーシャルゲームのインフラとサーバサイドアプリケーションを担当しており、下記のサービス・ソフトウェアを利用しています。

  • AWS

    • ELB (Classic)
    • EC2 (AmazonLinux)
    • RDS (MySQL)
    • CloudWatch
  • nginx
  • Ruby on Rails (unicorn)
  • 他 BigQuery, ElasticSearch, Re:dash, Kibana, Mackerel 等

そのタイムスタンプ、いつのもの?

通常、ログデータにはタイムスタンプが付けられていますが、このタイムスタンプは一体「いつ」の時刻を記録したものなのでしょうか。

ほとんどの場合、対象ソフトウェアが処理を開始した時間が記録されるのですが、実は例外もあります。
私の所属しているプロジェクトで使っているソフトウェアの中では nginx がこれに該当します。
nginx では「処理が完了した時刻 (= レスポンスを返した時刻)」が記録されます。

システムが正常に稼働している限りこれらの違いを気にすることは少ないと思いますが、障害解析時はその限りではありません。
各アプリケーション・ミドルウェア・インフラのログを少なくとも秒単位であわせ解析する必要があるため、各タイムスタンプが「いつ」の時刻を記録したものなのか把握していないと、誤った障害解析結果を導きかねません。
特にタイムアウト処理が絡んだ場合、レスポンスを返した時刻はリクエストを受けた時刻と大きな差が発生します。

何気なく記録されているログのタイムスタンプにも罠があります。ご注意ください。

必要な情報出してる?

前項で「nginx のタイムスタンプはレスポンスを返した時刻」と説明しましたが、ではいったいどうやって「処理を開始した時刻(= リクエストを受けた時刻)」を出力するのでしょうか。
実は nginx のデフォルトの設定ではこれができません。

nginx で「リクエストを受けた時刻」を記録する方法はいくつかあるようなのですが、最も簡単なのは「レスポンスを返すまでにかかった時間」を一緒に記録することです。ログの解析時にそれらの値を使って「リクエストを受けた時刻」を求めることができます。ログ解析時に前処理は必要になりますが、それを低コストで行える環境もあわせて用意しておくとよいです(後述)

(※ 最も良いのはもちろん予めログにリクエストを受けた時刻を記録することですが、ログ収集時に計算させる方法も可能です)

死ぬ前の情報は残した?

エラー時の情報は貴重です。この情報の有無で障害解析のスピードと精度は数倍変わってくるでしょう。しかし、中にはエラー時の情報を残さずに死んでしまうソフトウェアもあります。私のプロジェクトで利用しているもの中では unicorn がこれに該当します。

unicorn はリクエストを処理する worker プロセスと、workerプロセスを管理する masater プロセスから構成されます。
unicorn はタイムアウトの設定を持ち、worker プロセスの処理がこのタイムアウト内に完了しない場合、master プロセスは workerプロセスに対して即座に SIGKILL を送りつけます。その結果、「タイムアウト内に完了しなかった処理」がログに記録されないという事態が発生します。

これに対する改善策はいくつかあります。

  1. より上位にあるソフトウェアで記録を残す

    • 具体的には ELB や nginx でログを残す。当該リクエストを処理したホストの情報、エラーコード、エンドポイント等を記録する。
  2. SIGKILL の代わりにトラップ可能な SIGINT 等を利用し、そこで Rails.logger.flush させる
  3. Rails の ActionController の around_action で “ソフトな” タイムアウトを設定する
around_action :global_timeout

def global_timeout
  Timeout.timeout(TIMEOUT_SEC) do
    yield
  end
end

私の所属するプロジェクトで実際に適用されているのはまだ1のみですが、2,3の手法も評価していく予定です。

ログデータ膨大すぎるんだけど…

正確な障害解析には普段から多くの情報を取得しておく必要がありますが、その結果、解析に時間がかかったり、そもそも普通のマシンでは処理ができなかったりといったことが発生します。
私の所属するプロジェクトでは、ログをBigQueryとElasticSearchに格納し、Re:dashやKibanaで可視化できる仕組みを構築しています。

普段はマクロなインフラメトリクス(や、売上情報等)を表示するために使っていますが、障害解析時はクエリを書くことで簡単に情報を絞り込んだり、可視化することができ、便利です。nginxのタイムスタンプ問題もクエリを書くことで簡単に解決できます。

(※ すべてBigQuery+Re:dash に統一化したいなぁ)

さいごに

障害解析は「より少ない情報、より少ない時間で原因を特定する」エクストリームスポーツではありませんし、そうあってはなりません。
エンジニアにエスパーの力を求めるのは間違っています。
また、「システムの癖を知った、長年の経験のあるエンジニアにしかできない作業」であってもなりません。

障害解析のために十分な情報を集めることや、スピーディに解析できる環境を用意することは言うほど簡単ではありませんし、コストもかかりますが、安定したサービスを提供するには必要不可欠なものです。

堅牢なシステムの構築は1日にして成らず、頑張っていきましょう。

続きを読む

tsort について

はじめに

Ruby Advent Calendar 2017 5日目の記事です。

この記事では、Ruby標準ライブラリにある tsort について、説明します。
tsort を使うことで、依存関係を解決して、順番に処理することなどが簡単にできます。

今回の内容は Meguro.rb #9 での発表資料をベースにしています。

トポロジカルソートとは

  • グラフ理論でのアルゴリズムの1つ
  • 依存関係を順に処理したいときに使える
  • Ruby 標準ライブラリの tsort でトポロジカルソートができる

トポロジカルソートの利用例

Set#divide

標準ライブラリの Set#divide は内部実装で tsort を使っています。

その前に Set#divide について説明しましょう。
以下、るりまでの説明です。

元の集合をブロックで定義される関係で分割し、その結果を集合として返します。

ブロックパラメータが 1 個の場合、block.call(o1) == block.call(o2) が真
ならば、o1 と o2 は同じ分割に属します。

ブロックパラメータが 2 個の場合、block.call(o1, o2) が真ならば、
o1 と o2 は同じ分割に属します。
この場合、block.call(o1, o2) == block.call(o2, o1)
が成立しないブロックを与えると期待通りの結果が得られません。

o1 と o2 が同じ分割に属し、 o2 と o3 が同じ分割に属する場合、 o1 と o3 がたとえブロックの評価結果が偽であっても、 o1 と o3 は同じ分割に属することになります。

下記の例をみると分かりやすいでしょう。

require 'set'
numbers = Set[1, 3, 4, 6, 9, 10, 11]
set = numbers.divide { |i,j| (i - j).abs == 1 }
p set     # => #<Set: {#<Set: {1}>,
          #            #<Set: {11, 9, 10}>,
          #            #<Set: {3, 4}>,
          #            #<Set: {6}>}>

9 と 11が同じ分割に属しています。

Rubygems

Rubygems は内部で依存関係を解決する処理があり、そのために tsort を利用しています。

Bundler

Bundler も Rubygems と同様に内部で依存関係を解決するため、tsort を使っています。

Ruby on Rails

Ruby on Rails の Railtie では tsort を使っています。

ドキュメント によると、

Railtie is the core of the Rails framework and provides several hooks to extend Rails and/or modify the initialization process.

すべての Rails のコンポーネント(Action Mailer, Action Controller, Action View and Active Record)は Railtie です。

Ruby on Rails の Initializer では、:before:after オプションを渡すことで、特定の Initializer の前後に順に実行するように指定することができます。

この Initializer の実行順を解決するために内部的に tsort が使われています。

tsort の適用例

今回、tsort が必要だった理由

  • AWS の運用費用を売上管理と同じ分類で管理したかった。
  • AWS では、リソースの費用をタグごとに分類して管理する機能がある
  • とはいえ、やってみたところ未分類のコストが多かった
    • 理由: タグがついていないリソースが多く、それらのコストが未分類として計上されていた
    •   EC2 にはタグがついていたが、EBS、スナップショット、AMI 等にはタグがなかった
  • EC2 に付与されたタグを自動的に関連するリソースにも付与するスクリプトを作ろうと思った

AWS のリソース間の関連

  • EC2 を起点として、リソース間の関連性は下図のようになっています。

    • それぞれのリソースの詳細については、今回の主題と異なるので説明を割愛します。

image.png

処理内容

詳細は、今回説明しませんが、下記の処理を作成しました。

  1. aws describe-instances コマンドで、EC2 と EBS と ENI の関係を取得・EC2 に付与されたタグをすべて取得
  2. aws describe-snapshots コマンドで、EBS と スナップショット ID の関係を取得
  3. aws describe-images コマンドで、AMI と スナップショットID の関係を取得
  4. ①~③の結果生成されたグラフ構造を元に、関連する EC2 に付与されたタグを EBS、ENI、スナップショット、AMI にも同様に付与する

有向グラフの構築

有向グラフを構築する処理の例です。

上記の図を Tree と見立てた場合、ハッシュ @edges の key が親ノード、 value が配列で子ノードとなっています。

@edges = Hash.new{|h, key| h[key] = []}
each_tag_ebs_eni do |instance_id, tags, ebs, eni|
  @edges[instance_id].push *ebs
  @edges[instance_id].push *eni
  
end
each_ebs_snapshot do |volume_id, snapshot_id|
  @edges[volume_id].push snapshot_id
end
each_snapshot_ami do |snapshot_id, ami_id|
  @edges[snapshot_id].push ami_id
end

TSort 利用のための準備

TSort モジュールを include する場合は、tsort_each_nodetsort_each_child メソッドを実装する必要があります。
tsort_each_node ではグラフのすべてのノードに順にアクセスする処理を実装します。
tsort_each_child では、引数で与えられたノードの直接の子のノードに順にアクセスする処理を実装します。
このように、使う側で必要なメソッドを実装するという仕様になっていることで、TSort モジュールは特定のデータ構造に依存しない、柔軟性が高い設計になっています。

class AwsTSort
  include TSort
  def tsort_each_node
    (snip)
  end

  def tsort_each_child(node)
    @edges.fetch(node, []).each do |child|
      yield child
    end
  end
  
end

今回の例では、tsort_each_node が必要なメソッドを利用していませんので、省略しています。

TSort のメソッドの呼び出し

TSort#each_strongly_connected_component_from という少々長い名前のメソッドを使うと、そのノードの子孫を順に処理することができます。
本稿の例では、EC2 に関連した EBS、ENI などのリソースを順に取得できます。

def resources_from(start)
  [].tap do |resources|
    each_strongly_connected_component_from(start) do |nodes|
      resources.push *nodes # nodes は Array
    end
    resources.delete(start) # start自身を除外
  end
end

後日談

TSort を使うことで、無事、各リソースにタグ付けする点については見事実現できました。
しかしながら、それでもなお未分類のコストが一定程度残っている状態です。
AWS的なベストプラクティスは、アカウントの分離だそうですが、今から取り組むのはハードルが高いと感じています。

詳しい人教えてください。

続きを読む

CloudFront → ELB → EC2(Rails App)な構成でTurnoutのallowed_ipsをいい感じに機能させる

株式会社LITALICOでWebエンジニアをやっています, @Takuan_Oishiiです。
この記事は『LITALICO Advent Calendar 2017』4日目の記事です.

はじめに

Turnout, 気軽にメンテ画面に移行できてめっちゃ便利ですよね. パスやアクセス元IPを指定してメンテ画面を出さないようにしたりできるので結構重宝します.
ただし, タイトルの通り「ユーザーからのリクエストをまずCloudFrontで受けてオリジンであるELBに転送してELBの下にRailsのアプリがいる」という構成を取っている場合, アクセス元IPを指定してメンテ画面を表示しないようにするという機能(allowed_ips)がうまく動きません.

この記事では, Turnoutのallowed_ipsをいい感じに機能させるモンキーパッチを紹介します.
ついでにTurnout, Rack::Request, ActionDispatch::Requestの中身がどうなっているのかについて軽く解説します.

環境: ruby 2.4.2 rails 5.1.4, turnout 2.4.1

結論

以下のモンキーパッチなファイルをconfig/initializers以下に配置する.

turnout.rb
require 'ipaddr'

module Turnout
  class Request
    private

    def ip_allowed?(allowed_ips)
      begin
        ip = IPAddr.new(rack_request.client_ip.to_s)
      rescue ArgumentError
        return false
      end

      allowed_ips.any? do |allowed_ip|
        IPAddr.new(allowed_ip).include? ip
      end
    end
  end
end
rack_client_ip
module Rack
  class Request
    module Helpers
      def client_ip
        split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')).first
      end
    end
  end
end

解説

X-Forwarded-Forの値

CloudFront → ELB → EC2(Rails App)という構成にしている場合, EC2に到達するリクエストのX-Forwarded-Forヘッダーは アクセス元のclient-ip, cloudfront-ip という値になります.
これは, CloudFrontもELBも以下のような挙動でリクエストを転送するためです.
カスタムオリジンの場合のリクエストとレスポンスの動作 – Amazon CloudFront

ビューワーがリクエストを CloudFront に送信し、X-Forwarded-For リクエストヘッダーを含めない場合、CloudFront は TCP 接続からビューワーの IP アドレスを取得して、IP アドレスが含まれた X-Forwarded-For ヘッダーを追加し、リクエストをオリジンに転送します。たとえば、CloudFront が TCP 接続から IP アドレス 192.0.2.2 を取得する場合、以下のヘッダーをオリジンに転送します。

X-Forwarded-For: 192.0.2.2

HTTP ヘッダーおよび Classic Load Balancer – Elastic Load Balancing

クライアントからのリクエストに既に X-Forwarded-For ヘッダーが含まれている場合、Elastic Load Balancing はヘッダー値の末尾にクライアントの IP アドレスを追加します。この場合、リストの最後の IP アドレスが、クライアントの IP アドレスです。たとえば、次のヘッダーには、クライアントによって追加された 2 つの IP アドレス (信頼できない可能性があります) と、Elastic Load Balancing によって追加されたクライアント IP アドレスがあります。

X-Forwarded-For: ip-address-1, ip-address-2, client-ip-address

Turnoutのallowed_ip指定がうまくいかない理由

Turnoutはメンテナンス画面を表示するかどうかを, allowed?メソッドによって判定しています.
さらに, ip_allowed? メソッドはアクセス元IPアドレスが, 設定ファイル等に記述されたallowed_ipsに含まれているかどうかを判定しています.
ここで言うアクセス元IPアドレスは, rack_request.ip.to_sによって取得されています.

https://github.com/biola/turnout/blob/master/lib/turnout/request.rb

turnout/lib/turnout/request.rb
require 'ipaddr'

module Turnout
  class Request
    #(中略)

    def allowed?(settings)
      path_allowed?(settings.allowed_paths) || ip_allowed?(settings.allowed_ips)
    end

    private

    attr_reader :rack_request

    #(中略)

    def ip_allowed?(allowed_ips)
      begin
        ip = IPAddr.new(rack_request.ip.to_s)
      rescue ArgumentError
        return false
      end

      allowed_ips.any? do |allowed_ip|
        IPAddr.new(allowed_ip).include? ip
      end
    end
  end
end

rack_request.ipは, rack/lib/rack/request.rbのipメソッドです.
ポイントはX_FORWARDED_FORの最後の値を返り値としていることです.

https://github.com/rack/rack/blob/master/lib/rack/request.rb

rack/lib/rack/request.rb
require 'rack/utils'
require 'rack/media_type'

module Rack
  # Rack::Request provides a convenient interface to a Rack
  # environment.  It is stateless, the environment +env+ passed to the
  # constructor will be directly modified.
  #
  #   req = Rack::Request.new(env)
  #   req.post?
  #   req.params["data"]

  class Request
  #(中略)

    module Helpers
    #(中略)

      def ip
        remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
        remote_addrs = reject_trusted_ip_addresses(remote_addrs)

        return remote_addrs.first if remote_addrs.any?

        forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))

        return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR")
      end

    #(中略)
    end
    include Env
    include Helpers
  end
end

今回想定する構成では, X-Forwarded-Forが前述した状態になるため, rack_request.ipはCloudFrontのIPアドレスを返すことになります.
当然ですが, Turnoutのallowed_ipsで判定したいのはCloudFrontのIPアドレスではありません.
一番最初にリクエストを送った普通のユーザーのIPアドレスで判定を行いたくて, allowed_ipsにも特別扱いしたい普通のユーザーのIPアドレスを設定しているはずです.

以上の理由で, Turnoutのallowed_ipsの機能は今回想定する構成では期待した動作になりません.

解決策

ip_allowed?メソッドがrack_request.ipではなく rack_request.client_ipを使ってallowed_ipsとの比較を行うようにオーバーライドしてしまいました.

turnout.rb(再掲)
require 'ipaddr'

module Turnout
  class Request
    private

    def ip_allowed?(allowed_ips)
      begin
        ip = IPAddr.new(rack_request.client_ip.to_s)
      rescue ArgumentError
        return false
      end

      allowed_ips.any? do |allowed_ip|
        IPAddr.new(allowed_ip).include? ip
      end
    end
  end
end

client_ipなんてメソッドはRackには存在しませんので, 自分で作ってしまいます.
ipメソッドとは逆で, X_FORWARDED_FORの先頭の値を返すようにしています.

rack_client_ip(再掲)
module Rack
  class Request
    module Helpers
      def client_ip
        split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')).first
      end
    end
  end
end

これでめでたくallowed_ipsに指定したIPアドレスからのリクエストでメンテナンス画面を表示しないようにできました🍣

おまけ

Railsのコントローラ等ではおもむろにrequest.ipとかrequest.remote_ipのメソッドでIPアドレスを取得することができますが, これは実はRack::Requestのipメソッドではありません.
これらはActionDispatch::Requestのメソッドです.

さらに, 少々めんどくさいソースコードを追っていくと, remote_ipメソッドの返り値がActionDispatch::RemoteIp::GetIpcalculate_ipメソッドによって決定されていることがわかります.
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb

結局このメソッドも最終的にはX_FORWARDED_FORの末尾の値を返すようになっています.
そもそもTurnoutはActionDispatchとは一切関係ないので, 「ip_allowed?メソッドがrequest.remote_ip呼べばいいじゃん!」 というのは通用しませんし, remote_ipを呼べたとしてもipと同じ結果が返ってくるだけです.

さらにもうちょっと解説すると, ActionDispatchはtrusted_proxiesという値を設定可能です.
この値に指定したIPアドレスはremote_ipの返り値候補から除外されるので, いい具合に使うこともできそうです.
参考 https://qiita.com/yasu/items/da7ebdb01cb3209583df

ちなみにActionDispatch::Requestは今回client_ipメソッドを定義したRack::Request::Helpersをincludeしているため, Railsのコントローラ等でおもむろにrequest.client_ipなどと呼ぶことができます.
副産物ですが有効活用できるシーンもあるかもしれませんね.

おわりに

そもそもこんなもんどうしてもちょっと乱暴なやりかたになっちゃうんだよなあ
ちょっとしたモンキーパッチで快適なメンテ生活できるよ!

ちなみに当然ですがX_FORWARDED_FORは信用ならないヘッダーなので, そのあたりは気をつけて使用しましょう.

明日の『LITALICO Advent Calendar 2017』は@negiさんの「Webアクセシビリティ対応を考えてみた話」です. お楽しみに.

続きを読む

TerraformとAWSに同時入門する

GMOペパボ、ムームードメインのエンジニア@litencattです。

昨日は@dp42による、“Hello, World.”の次としてのチャットボットでした。

今日は、入社以来専らWeb開発がメインでやってきたけど、最近興味のあるAWSやTerraformによるインフラ構築について読んだ本をベースにやってみたことについて書いていきます。

やること

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版のインフラ環境や手順を参考に、AWS環境構築をTerraformを用いて行います。

なお今回はApacheなどEC2インスタンスに対する各種インストールは直接インスタンス内に入ってコマンド実行しています。

ゴール

AWS上に以下の環境構築を行ないます

  • EC2インスタンス x 2台

    • Webサーバ

      • インターネットゲートウェイを持つ
      • Apache(httpd)上でWordPressが動作している
    • DBサーバ
      • NATゲートウェイを通してインターネットに接続する
      • MySQLが起動している

具体的には以下のAWSリソースを扱います。

  • EC2
  • VPC
  • サブネット
  • ルートテーブル
  • インターネットゲートウェイ
  • セキュリティグループ
  • Elastic IP
  • NATゲートウェイ

今回参考にした本について

ネットワークにつての基礎的な用語について詳しい説明があったり、AWSで構築していく環境についての図がとてもわかりやすいのでこのあたり初めてな人にはおすすめな本だと思いました!

Terraformとは

  • AWSなど様々なサービスProviderに対して、サーバの構築・変更・管理を行うためのツール
  • HashiCorpのプロダクト
  • Enterprise版もあるみたい

ペパボではプライベートクラウドとしてOpenStackを自社運用しています。
TerraformはOpenStackにも対応しており、最近は各サービスのインフラ管理がTerraformで行われるようになってきています。

今回はこのTerraformをAWSに対して使っていきます。
https://www.terraform.io/docs/providers/aws/index.html

今回のコードのレポジトリ

https://github.com/litencatt/terraform-aws-templates

準備

Terraformのインストール(Mac)

$ brew install terraform

使用バージョン

2017/12/3の執筆時点での最新リリースバージョンをつかいます

$ terraform version
Terraform v0.11.1
+ provider.aws v1.5.0

https://github.com/hashicorp/terraform/blob/master/CHANGELOG.md#0111-november-30-2017

主にVim向け

HCL扱う場合は入れとくと便利そうです
https://github.com/hashivim/vim-hashicorp-tools

AWSの準備

terraform.tfvarsの設定

今回はAWSのアクセスキーなどの秘匿情報をterraform.tfvarsに持つようにしています。
ここに作成したアクセスキーとシークレットアクセスキーを設定してください。
リージョンなども変更したい場合は必要に応じて変更してください。

terraform.tfvars
access_key = "AWS_ACCESS_KEY"
secret_key = "AWS_SECRET_KEY"
region     = "ap-northeast-1"
key_name   = "KEY_PAIR_NAME"

:warning:実際のアクセスキーなどが書かれたterraform.tfvarsはレポジトリには登録しないよう注意ください

EC2インスタンスへのログイン時に必要な鍵ファイルについて

今回はAWSのダッシュボード上のキーペアで鍵を作成し、それをEC2インスタンス作成時に使用するように指定しています。そのため、key_nameには作成したキーペア名を設定してください。

VPC作成

まずはVPCを作成します
https://github.com/litencatt/terraform-aws-templates/pull/1

main.tf
+variable "access_key" {}
+variable "secret_key" {}
+variable "region" {}
+
+provider "aws" {
+  access_key = "${var.access_key}"
+  secret_key = "${var.secret_key}"
+  region     = "${var.region}"
+}
+
+resource "aws_vpc" "vpc-1" {
+  cidr_block = "10.0.0.0/16"
+  tags {
+    Name = "vpc-1"
+  }
+}

main.tfファイル作成後$ terraform initを実行し、AWSのpluginを取得します。

その後、$ terraform applyを実行して成功するとVPCが作成され、AWSのVPCダッシュボードのVPCページでも確認することが出来ます。
image

ちなみにv0.11.0より$ terraform applyした場合は下記のようにyesを入力しないとapplyが実行されないように変更されています。

$ terraform apply

(省略)

Plan: 15 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

initせずにplanなどを実行した場合のエラー

$ terraform initを先に実行してください

$ terraform plan
Plugin reinitialization required. Please run "terraform init".
Reason: Could not satisfy plugin requirements.
...(略

以降の作業

すべて記事内に書くと結構なボリュームになりそうなので、以降の作業内容については各PRを参照ください。
PRを作成して手順や差分をわかりやすくし、必要に応じて説明を入れています:nerd:

WordPressページの表示確認

NATゲートウェイの作成のPR内の作業までをすべて完了後、
WebサーバのURLに対してブラウザよりアクセスするとWordPressのスタートページの表示を確認することが出来ます。
image

:warning:この記事で作成したインスタンスは既にdestroy済みなのでPR上のURLにはアクセスできませんのでご注意ください

TerraformでAWSの環境構築してみて

今後のやっていき

最後に

今回、WordPress環境をAWS上にTerraformを主に使って構築してみましたが、
こんなWordPress環境だけでなく、Node.jsやRails環境などがなんと約10秒で出来上がってしまうというロリポップ!マネージドクラウドのβ版が現在無料公開中ですので、そちらも是非宜しくお願いします:smile:
https://mc.lolipop.jp/

参考にさせて頂いたサイト

など多数

続きを読む

AWS FargateでとりあえずMastodonを動かしてみる

先日こんな記事を見かけて、

Web, Worker, Websocketと要素が揃っているMastodonのスタックをXXで動かしてみる、というのは感触をつかむのによいなと思いました。

で、じゃあAWS Fargateではどうかなと、このようにしてみました。

Amazon ECS .png

事前準備

Dockerコンテナ以外で、マネージドサービスで揃えられるものは優先的にそちらを使用します。

  • AWSで用意

    • VPS (既存でも新規でも)
    • RDS (Postgresで起動)
    • ACMで証明書 (ALBに適用する)
    • ALB
      • TargetGroup-web (HTTP/3000)
      • TargetGroup-stream (HTTP/4000)
    • S3バケット (PAPERCLIP Wikiのポリシー適用が楽)
  • Redis => Redis Labs / 30MBまでならFreeプランでOK
    • ElastiCacheが使いたければそちらでも
  • MailGunアカウント
    • 専用の認証情報をつくるとよいです

ALBは以下のルーティングを最後に割り当てます。

  • / (default)=> TargetGroup-web
  • /api/v1/streaming* TargetGroup-stream

こんな感じです。

EC2 Management Console 2017-12-03 14-04-04.png

Fargateを考慮したTask definitions

事前準備の内容が揃っていれば、Dockerのコンテナとして動かすのは次の3要素。

  • Web(Rails/puma)
  • Sidekiq(Ruby)
  • Streaming(Node.js)

これらをTask definitionsに起こすとして、全部のコンテナ定義を一つにまとめるとスケール変更が大げさになるので、それぞれを別のECSサービスとして登録できるようにバラします。
compose感はなくなりますが、Fargateならその方が良いように思います。

各タスクはmastodonリポジトリのdocker-compose.ymlをベースに、kubedonを参考にしつつアレンジ。

.env.production

大筋として本家のdocker-composeを使いまわすので、もともと用意されている.env.productionから環境変数という仕組みをほぼ踏襲します。
今回このようにしました。

.env.production
RAILS_ENV=production
NODE_ENV=production
REDIS_URL=redis://:<RedisLabsのパスワード>@<RedisLabsのエンドポイント>

DB_HOST=<RDSのエンドポイント>
DB_USER=<RDSのユーザ>
DB_NAME=<RDSのDB名>
DB_PASS=<RDSのパスワード>
DB_PORT=5432

LOCAL_DOMAIN=<ALBに向けるドメイン>
LOCAL_HTTPS=true

PAPERCLIP_SECRET=<rake secretで作成>
SECRET_KEY_BASE=<rake secretで作成>
OTP_SECRET=<rake secretで作成>

VAPID_PRIVATE_KEY=<rake mastodon:webpush:generate_vapid_key で作成>
VAPID_PUBLIC_KEY=<rake mastodon:webpush:generate_vapid_key で作成>

DEFAULT_LOCALE=ja

SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=465
SMTP_LOGIN=< MAILGUNの認証ユーザ >
SMTP_PASSWORD=< MAILGUNのパスワード >
SMTP_FROM_ADDRESS=noreply@< MAILGUNのドメイン >
SMTP_TLS=true

S3_ENABLED=true
S3_BUCKET=< S3のバケット >
AWS_ACCESS_KEY_ID=< S3用のIAMユーザのキー >
AWS_SECRET_ACCESS_KEY=< S3用のIAMユーザのシークレットキー >
S3_REGION=< S3のリージョン >
S3_PROTOCOL=https
S3_HOSTNAME=< S3のリージョン別エンドポイント >

STREAMING_CLUSTER_NUM=1

web

web用にdocker-compose.web.yml, ecs-param.web.ymlを用意します。ecs-cliの取り回しを考えると、ディレクトリ分けて標準のファイル名を使う方が楽だったかも。
また、汎用性を考えたらloggingは分けて、–fileで追加指定してもよいはずです。

docker-compose.web.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails db:migrate && rails assets:precompile && rails s -p 3000 -b 0.0.0.0"]
    ports:
      - "3000:3000"
    mem_limit: 2GB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: web

assign_public_ipはイメージの取得にDockerhubを使う場合ENABLEDでないとPullできません。(※他にルートを用意できればおそらく可)
SGはALBとさえ通信できればOKなので、特にきにせずENABLEDで良いでしょう。

ecs-param.web.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 512
    mem_limit: 2GB
  services:
    web:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

webは他に比べてスペック高、ほぼasset:precompileのため(メモリが足りないと落ちる)です。

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_WEB=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose 
  --file docker-compose.web.yml 
  --ecs-params ecs-param.web.yml 
  --project-name mstdn-fargate-web 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE 
  --container-name web 
  --container-port 3000 
  --target-group-arn ${TARGET_GROUP_WEB}
  # --timeout 0

--target-group-arnは後から変更できないので、変更が必要な場合はサービスを一旦消すか別名で作成です。

なお実行の際、CPUをケチる(<1024)と--timeout 0を付与しておかないとデフォルト待ちの5分ではassets生成が終わりません。
このあたり考慮すると、実際に運用する場合は起動時間のことも考えて、よそでprecompileしてpublicを別に用意するのが得策でしょう。

streaming

streaming用にdocker-compose.streaming.yml, ecs-param.streaming.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.streaming.yml
version: '2'
services:
  streaming:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["npm", "run", "start"]
    ports:
      - "4000:4000"
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: streaming

streamingのパラメータ、サイズは最小。FargateはCPUとメモリの組み合わせに制限があるようです。

ecs-param.streaming.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    streaming:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。webとほぼおなじですね。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_STREAMING=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose 
  --file docker-compose.streaming.yml 
  --ecs-params ecs-param.streaming.yml 
  --project-name mstdn-fargate-streaming 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE 
  --container-name streaming 
  --container-port 4000 
  --target-group-arn ${TARGET_GROUP_STREAMING}

sidekiq

sidekiq用にdocker-compose.sidekiq.yml, ecs-param.sidekiq.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.sidekiq.yml
version: '2'
services:
  sidekiq:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["sidekiq", "-q", "default", "-q", "mailers", "-q", "pull", "-q", "push"]
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: sidekiq

こちらも最小でOK。

ecs-param.sidekiq.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    sidekiq:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

CLUSTER_NAME=既存ECSクラスタの名前に置き換えで。

$ ecs-cli compose 
  --file docker-compose.sidekiq.yml 
  --ecs-params ecs-param.sidekiq.yml 
  --project-name mstdn-fargate-sidekiq 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE

ECS状況

$ ecs-cli ps --cluster ${CLUSTER_NAME}
Name                                            State                                                                               Ports                          TaskDefinition
775a399c-19e1-4e2a-bd3d-98f8b4d7304e/streaming  RUNNING                                                                             34.204.191.189:4000->4000/tcp  mstdn-fargate-streaming:1
921f2d1a-0e4f-49a6-a5b5-434e2b91cf11/sidekiq    RUNNING                                                                                                            mstdn-fargate-sidekiq:2
edb1ba2e-533d-4afd-aaee-39db59ee616a/web        RUNNING                                                                             34.204.174.122:3000->3000/tcp  mstdn-fargate-web:15

$ aws ecs list-services --cluster ${CLUSTER_NAME} 
{
    "serviceArns": [
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-web", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-sidekiq", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-streaming"
    ]
}

それぞれ別のECSサービスとして動かせたので、個別にコンテナ数を変更できて良い感じのはず。

Mastodon 2017-12-03 14-02-01.png

Mastdon admin登録

さて、Mastodonの管理者になるにはmastodon:make_adminタスクを実行すればよいのですが、Fargate上のDockerコンテナで何か実行(docker exec相当)できるのだろうか。

さっとアタッチはできないぽいので、これもtask definitionからやりますか。

yamlをかいてー

docker-compose.web_make_admin.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails mastodon:make_admin USERNAME=sawanoboly"]
    mem_limit: 512MB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: railstask

ecs-cli compose upと。

$ ecs-cli compose 
  --file docker-compose.web_make_admin.yml 
  --ecs-params ecs-param.web.yml 
  --project-name mstdn-fargate-makeadmin 
  --cluster ${CLUSTER_NAME} 
  up --launch-type FARGATE

で、管理メニューアクセスオープン。

レポート - Mastodon 2017-12-03 14-34-44.png

少々手間だが、できるのでよいです。

おわりに

これまではスケール変更を柔軟にしようと思ったら結局ECSクラスタなりk8s、docker swarmなどのノードを確保しておく必要があったことを考えると、リソース配分が目に見える分だけになるのでシンプルです。

Fargateで起動するコンテナと通常のEC2インスタンスを比較すると、数字上で同等のCPU/メモリのインスタンスを維持するよりFargateのほうがちょっと割高になるはずです。
しかし、EC2ではサービス用のプロセス以外にも色々動かしたりケアする場所も多いため、コンテナより余分に性能やらディスクやらが必要になります。Fargateは下限ギリギリを攻めてもよい分、トータルでどっちがよいかは動かしたいものによるでしょう。

スケールアウトの対応速度は比較にならないほどFargateの方がはやいです。※この記事の例での構成ではコンテナイメージの最適化をしてないので少々もたもたしますが。

Fargate専用のSpotFleetでないかなあ。

続きを読む