20191219のAWSに関する記事は30件です。

AI Platform(GCP)でGPU 100個同時に使いテンションあがった

aiplatform_jobs.gif

ひゃっはー

概要

機械学習基盤系のサービスとしてAWSのSageMakerが有名かと思いますが
本投稿ではGCPの類似サービスAI Platformを使ってGPU 100並列同時に動かした話をします。

AI Platformでできること

SageMakerと似た感じで、AI Platformでは以下のようにうたっています。
公式ドキュメントより引用)

AI Platform を使用して、ML ワークフローの次の段階を管理できます。
ml-workflow.png

  • データで ML モデルをトレーニングする。
    • モデルをトレーニング
    • モデルの精度を評価する
    • ハイパーパラメータを調整する
  • トレーニング済みモデルをデプロイする。
  • モデルに予測リクエストを送信する。
    • オンライン予測
    • バッチ予測(TensorFlow のみ)
  • 継続的に予測をモニタリングする。
  • モデルとモデル バージョンを管理する。

いわゆる機械学習基盤として使えますよ便利ですね、ってサービスなんですが
単に時間のかかるバッチジョブ(MLの場合、特に学習ジョブ)をサーバレスで走行する環境として使うだけでも割とその恩恵を受けられます。
というかマジメに機械学習基盤として使うにはちょっとまだ機能連携が足りていないような...

GPUをGCEなどのIaaSで利用するとインスタンスの起動・停止がめんどいので
学習が走行されている時間だけ課金されるマネージドなジョブ実行ができるのはありがたいです。

バッチを走行するだけなら他にも方法あるやん、って思われるかもしれないですが
意外と「GPUが使える」かつ「時間制限がない」ってサービスはあまりなくて
他の選択肢だとGKEでk8s jobくらいなのかな、と思います(他にもあったらごめんなさい...)

なので、GKEよりは手軽にML学習のジョブを走らせられるのは、それだけでもメリットと感じます。

GPUの同時利用上限

AI Platformには同時に利用できるGPU数に上限が設けられています。

  • Tesla K80 GPUs: 30
  • Tesla P100 GPUs: 30
  • Tesla V100 GPUs: 8

SageMakerのデフォルト上限は下記のようです。(最新版は公式ドキュメントを参照してください)

インスタンスタイプ GPU 同時利用インスタンス数上限
ml.p2.xlarge K80 * 1 1
ml.p2.8xlarge K80 * 8 1
ml.p2.16xlarge K80 * 16 1
ml.p3.2xlarge V100 * 1 2
ml.p3.8xlarge V100 * 4 2
ml.p3.16xlarge V100 * 8 2

合計のGPU数はさほどAI Platformと変わらないように思えますが
1つのインスタンスにGPUが複数搭載されていても、GPU分散で学習させないと意味がないです。
そういう意味だと、1GPUのジョブを何個も同時に実行したい場合にはSageMakerのデフォルトではちょっと足りないです。
(もちろんSageMakerでも上限を増やす申請はできます)

上限を増やす

通常、GCPの上限を増やす際の申請は、GCPコンソール上の「IAMと管理」=>「割り当て」から行うのですが
AI PlatformのGPU上限はなぜか別管理されており、ここのGoogleフォームから行います。
公式ドキュメント参照)

で、私はここで "70 P100s", "30 V100s" と申請し、無事通りました。
(申請から承認まで大体1週間くらいかかりました)

これでGPUめっちゃ同時に使う環境が手に入りました。

ユースケース

ところでGPUを同時にたくさん利用したいケースってどんなんでしょうか。

  • モデル実装、パラメータ、データ、等のバリエーションを短期間で試したいとき
  • GPU分散で学習させたいとき
  • 上記のようなタスクが複数人同時に発生するとき
  • Qiitaで釣りっぽいタイトルを書きたいとき

とかですかね。用法・用量を守って正しく使いましょう。

ちなみに私はデータのバリエーションがたくさんあって、一気に複数のモデルを作って比較したい
というちゃんとした理由は一応ありました。

学習ジョブの実行

AI Platformでの学習ジョブ実行手順はざっくり以下です。

  1. 「pythonのコマンドとして実行できる形式でパッケージ」 or 「Dockerイメージを作成」
  2. 「パッケージをGCSにアップロード」 or 「DockerイメージをGCRにpush」
  3. 学習データをGCSにアップロード
  4. gcloudコマンドでAI Platformジョブを開始

以前はDockerコンテナでの実行はベータ版だったのですが
今はGAになっていますし、Dockerがやりやすいでしょう。

以下ではDockerコンテナを用いた手順を記載
と思ったのですが、 時間がないので 公式チュートリアルが充実しているので
そちらにリンクを貼っておき、「?」となりそうなところだけ補足します。(みんな大好きMNISTです)
基本的にチュートリアルのコピペで動くはずです。

また、単にGPUを使ったジョブ実行を体験したいだけなら、↑のチュートリアルすべてを実施する必要はありません。

具体的には

だけで可能です。

Dockerfile

チュートリアルなんで素直にそのまま使えばいいのですが、ちょっとだけ補足。

FROM nvidia/cuda:9.0-cudnn7-runtime

古い。けどまぁ無視していいかと。
例えばTensorflow 1.12以上を使いたいときとかはCUDA 10のイメージを指定しましょう。

RUN wget -nv \
    https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz && \
    mkdir /root/tools && \
    tar xvzf google-cloud-sdk.tar.gz -C /root/tools && \
    rm google-cloud-sdk.tar.gz && \
    /root/tools/google-cloud-sdk/install.sh --usage-reporting=false \
        --path-update=false --bash-completion=false \
        --disable-installation-options && \
    rm -rf /root/.config/* && \
    ln -s /root/.config /config && \
    # Remove the backup directory that gcloud creates
    rm -rf /root/tools/google-cloud-sdk/.install/.backup

# Path configuration
ENV PATH $PATH:/root/tools/google-cloud-sdk/bin
# Make sure gsutil will use the default service account
RUN echo '[GoogleCompute]\nservice_account = default' > /etc/boto.cfg

Dockerコンテナではなく、Pythonパッケージをアップロードする方法でやる場合は
すでに色々とインストールされているインスタンスで実行されるのですが
Dockerコンテナ内では当然必要なツール等は自分でインストールしておく必要があります。

AI PlatformはGCSを入出力ファイル置き場として使うので、
ここではそれに必要なgcloud SDKのインストール、およびgsutilコマンドの認証設定を行っています。

ジョブの実行

DockerイメージをGCRにpushしたら、あとはgcloudコマンドでAI Platformのジョブを実行するだけです。かんたんですね。

$ export MODEL_DIR=pytorch_model_gpu_$(date +%Y%m%d_%H%M%S)
$ export REGION=us-central1
$ export JOB_NAME=custom_container_job_gpu_$(date +%Y%m%d_%H%M%S)

と変数を定義しておいて、下記を実行。

$ gcloud ai-platform jobs submit training $JOB_NAME \
  --scale-tier BASIC_GPU \
  --region $REGION \
  --master-image-uri $IMAGE_URI \
  -- \
  --epochs=5 \
  --model-dir=gs://$BUCKET_NAME/$MODEL_DIR
  • --scale-tier
    ジョブを実行するインスタンスのスペックを指定します。(スケール階層と呼ばれてます)
    BASIC_GPU はk80が1個アサインされたインスタンスが使われます。
    指定できるスケール階層は こちら

  • --region
    ジョブを実行するリージョンを選びます。
    アクセスするGCSと同一リージョンを指定しなければならない、という謎制約があります。(ファイル転送の速度を考慮してくれてる、というポジティブ解釈)
    また、リージョンによっては使えるGPUの種類が違ったりします。

  • -- 以降の引数は、gcloudコマンドの引数ではなく、実行するpythonコマンドの引数として与えられます。
    チュートリアルのソースコードを見ればわかるのですが
    --epochs--model-dir という引数がコマンドに渡されて実行されることになります。
    自作コマンドを作るときも、こんな感じで可変なパラメータを引数化します。

上記のgcloudコマンドを実行して

jobId: custom_container_job_gpu_20191219_142126
state: QUEUED

と出力されれば、ジョブ発行は成功です。

チュートリアルでは

$ gcloud ai-platform jobs describe $JOB_NAME
$ gcloud ai-platform jobs stream-logs $JOB_NAME

でジョブの進捗を確認しよう、となっていますが
GCPコンソール上で AI Platformのジョブ一覧を見たほうが簡単かもです。

スクリーンショット 2019-12-19 23.27.50.png

あとはGPUの数だけジョブを発行しまくれば、ひゃっはーできます。

最後に

タイトルから釣りっぽい投稿になってしまい、反省しています。

そもそもGPUを100個同時に使うケースはあまりないにしても、
GPUを使いたいとき使いたいだけ利用できるサーバレスタイプの環境として、
という観点だけでもAI Platformを使ってみる価値はあるかもです。

また、今回取り上げませんでしたがハイパーパラメータの自動調整機能を使うと、複数GPUを一気に使えるメリットもあるってもんです。
MNISTを100並列で動かしてどうすんねん、という感じなので、もっと有効に使っていきましょう。
(一応私ももっとマジメに使ってます)

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

めちゃくちゃ簡単 Amazon Rekognition

AIとか画像解析とか聞くとなんか難しそうだけど、Amazonが提供しているサービスを使うと簡単に実装できるので、試してみた!

Amazon Rekognitionとは

Amazon Rekognition では、イメージ分析とビデオ分析をアプリケーションに簡単に追加することができます。Amazon Rekognition API にイメージやビデオを指定するだけで、このサービスによってモノ、人物、テキスト、シーン、アクティビティを識別できます。不適切なコンテンツも検出できます。また、Amazon Rekognition では、高精度な顔分析および顔認識も可能です。顔の検出、分析、比較は、ユーザー検証、カタログ作成、人数計数、公共安全など、多岐にわたって活用できます。

ひとまず実装してみる

環境を作成するのが手間なのでLambdaを使って実装。
プログラムはLambdaで提供があるPython 3.7。

顔認識

人物が写っている2枚の写真から同一人物かどうかを判定する。
S3にアップされている写真を比較するサンプルプログラム。

from boto3 import Session

def lambda_handler(event, context):
    session = Session(region_name="ap-northeast-1")
    rekognition = session.client("rekognition")

    response=rekognition.compare_faces(
        SourceImage={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}},
        TargetImage={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}},
        SimilarityThreshold=80)

    return response

簡単にプログラムの解説。

from boto3 import Session

AWSの各種サービスを簡単に仕様できるライブラリboto3を使って実装。
※ライブライはC++、Go、Java、JavaScript,.Net、Node.js、PHP、Rubyがある

    session = Session(region_name="ap-northeast-1")
    rekognition = session.client("rekognition")

サービスを使用するためには必ずSessionというAWSとの接続情報が必要。
リージョンはアジア東京を指定。
rekognitionを使用するためのオブジェクト取得。

    response=rekognition.compare_faces(
        SourceImage={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}},
        TargetImage={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}},
        SimilarityThreshold=80)

これがAmazonから提供されている「顔認証」関数。
API仕様 こちらから

SourceImageに認識したい顔だけが写っている写真
TargetImageに解析したい写真
SimilarityThresholdに認識度。高いほど厳しいチェックになる。0-100で指定。

responseは
FaceMatches に同一人物を判断した顔の情報
UnmatchedFaces に違うと判断した顔の情報

Confidenceがその判断の信頼度

{
  "SourceImageFace": {
    "BoundingBox": {
      "Width": 0.30209746956825256,
      "Height": 0.21446102857589722,
      "Left": 0.319732129573822,
      "Top": 0.23807010054588318
    },
    "Confidence": 99.99998474121094
  },
  "FaceMatches": [
    {
      "Similarity": 99.37952423095703,
      "Face": {
        "BoundingBox": {
          "Width": 0.21386872231960297,
          "Height": 0.16094279289245605,
          "Left": 0.6417301297187805,
          "Top": 0.6225799322128296
        },
        "Confidence": 99.99987030029297,
        "Landmarks": [
          {
            "Type": "eyeLeft",
            "X": 0.7082532644271851,
            "Y": 0.6811327934265137
          },
          {
            "Type": "eyeRight",
            "X": 0.7683542370796204,
            "Y": 0.7155389189720154
          },
          {
            "Type": "mouthLeft",
            "X": 0.6812899112701416,
            "Y": 0.7383925914764404
          },
          {
            "Type": "mouthRight",
            "X": 0.7310847640037537,
            "Y": 0.7682420015335083
          },
          {
            "Type": "nose",
            "X": 0.6889581680297852,
            "Y": 0.7370006442070007
          }
        ],
        "Pose": {
          "Roll": 38.84079360961914,
          "Yaw": -19.29507827758789,
          "Pitch": -41.210079193115234
        },
        "Quality": {
          "Brightness": 44.140357971191406,
          "Sharpness": 78.64350128173828
        }
      }
    }
  ],
  "UnmatchedFaces": [
    {
      "BoundingBox": {
        "Width": 0.01333685964345932,
        "Height": 0.013453098013997078,
        "Left": 0.9252238869667053,
        "Top": 0.4759874939918518
      },
      "Confidence": 97.7153091430664,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.9277710914611816,
          "Y": 0.4811204969882965
        },
        {
          "Type": "eyeRight",
          "X": 0.933621883392334,
          "Y": 0.4823104441165924
        },
        {
          "Type": "mouthLeft",
          "X": 0.9270203113555908,
          "Y": 0.4870522618293762
        },
        {
          "Type": "mouthRight",
          "X": 0.931785523891449,
          "Y": 0.4880983531475067
        },
        {
          "Type": "nose",
          "X": 0.9278116226196289,
          "Y": 0.484792023897171
        }
      ],
      "Pose": {
        "Roll": 13.193720817565918,
        "Yaw": -30.132854461669922,
        "Pitch": -10.473925590515137
      },
      "Quality": {
        "Brightness": 75.9936752319336,
        "Sharpness": 73.32209777832031
      }
    },
    {
      "BoundingBox": {
        "Width": 0.2887393832206726,
        "Height": 0.32251936197280884,
        "Left": 0.1142626702785492,
        "Top": 0.45462948083877563
      },
      "Confidence": 99.99995422363281,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.1551831066608429,
          "Y": 0.5991319417953491
        },
        {
          "Type": "eyeRight",
          "X": 0.25697842240333557,
          "Y": 0.6217619776725769
        },
        {
          "Type": "mouthLeft",
          "X": 0.14137881994247437,
          "Y": 0.7185965180397034
        },
        {
          "Type": "mouthRight",
          "X": 0.22245647013187408,
          "Y": 0.7383744716644287
        },
        {
          "Type": "nose",
          "X": 0.14210543036460876,
          "Y": 0.6684256792068481
        }
      ],
      "Pose": {
        "Roll": 17.434614181518555,
        "Yaw": -41.510498046875,
        "Pitch": -21.136119842529297
      },
      "Quality": {
        "Brightness": 41.628482818603516,
        "Sharpness": 95.51618957519531
      }
    },
    {
      "BoundingBox": {
        "Width": 0.007370786275714636,
        "Height": 0.008144599385559559,
        "Left": 0.6609883904457092,
        "Top": 0.49813687801361084
      },
      "Confidence": 83.50224304199219,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.6622864603996277,
          "Y": 0.5042642951011658
        },
        {
          "Type": "eyeRight",
          "X": 0.6650356650352478,
          "Y": 0.5022475719451904
        },
        {
          "Type": "mouthLeft",
          "X": 0.6650884747505188,
          "Y": 0.5060529112815857
        },
        {
          "Type": "mouthRight",
          "X": 0.6673902273178101,
          "Y": 0.5043438076972961
        },
        {
          "Type": "nose",
          "X": 0.6656265258789062,
          "Y": 0.505027174949646
        }
      ],
      "Pose": {
        "Roll": -43.10660934448242,
        "Yaw": -4.3733038902282715,
        "Pitch": -33.09136962890625
      },
      "Quality": {
        "Brightness": 73.02571868896484,
        "Sharpness": 53.330047607421875
      }
    },
    {
      "BoundingBox": {
        "Width": 0.009758301079273224,
        "Height": 0.010332607664167881,
        "Left": 0.966498851776123,
        "Top": 0.46405404806137085
      },
      "Confidence": 61.05154037475586,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.9691260457038879,
          "Y": 0.4726991653442383
        },
        {
          "Type": "eyeRight",
          "X": 0.9677429795265198,
          "Y": 0.4707190990447998
        },
        {
          "Type": "mouthLeft",
          "X": 0.9723362326622009,
          "Y": 0.4712704122066498
        },
        {
          "Type": "mouthRight",
          "X": 0.9711670279502869,
          "Y": 0.4696311056613922
        },
        {
          "Type": "nose",
          "X": 0.9699736833572388,
          "Y": 0.4713567793369293
        }
      ],
      "Pose": {
        "Roll": -119.80672454833984,
        "Yaw": -8.657889366149902,
        "Pitch": 21.94735336303711
      },
      "Quality": {
        "Brightness": 84.52898406982422,
        "Sharpness": 73.32209777832031
      }
    },
    {
      "BoundingBox": {
        "Width": 0.018424568697810173,
        "Height": 0.020794881507754326,
        "Left": 0.7489200830459595,
        "Top": 0.47518208622932434
      },
      "Confidence": 87.87468719482422,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.7618342638015747,
          "Y": 0.48698189854621887
        },
        {
          "Type": "eyeRight",
          "X": 0.7628127336502075,
          "Y": 0.4866960644721985
        },
        {
          "Type": "mouthLeft",
          "X": 0.7631105184555054,
          "Y": 0.4888738989830017
        },
        {
          "Type": "mouthRight",
          "X": 0.7637985944747925,
          "Y": 0.4886566698551178
        },
        {
          "Type": "nose",
          "X": 0.7641363143920898,
          "Y": 0.48711803555488586
        }
      ],
      "Pose": {
        "Roll": -34.46097946166992,
        "Yaw": 60.46308135986328,
        "Pitch": -14.032490730285645
      },
      "Quality": {
        "Brightness": 67.51453399658203,
        "Sharpness": 83.14741516113281
      }
    },
    {
      "BoundingBox": {
        "Width": 0.011087420396506786,
        "Height": 0.011487020179629326,
        "Left": 0.9760779142379761,
        "Top": 0.4641396105289459
      },
      "Confidence": 97.86558532714844,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.978825032711029,
          "Y": 0.469203919172287
        },
        {
          "Type": "eyeRight",
          "X": 0.9811084270477295,
          "Y": 0.4689272344112396
        },
        {
          "Type": "mouthLeft",
          "X": 0.9797573685646057,
          "Y": 0.47199031710624695
        },
        {
          "Type": "mouthRight",
          "X": 0.9815858006477356,
          "Y": 0.47177186608314514
        },
        {
          "Type": "nose",
          "X": 0.979856014251709,
          "Y": 0.47067731618881226
        }
      ],
      "Pose": {
        "Roll": -11.149984359741211,
        "Yaw": -42.128475189208984,
        "Pitch": -1.7187515497207642
      },
      "Quality": {
        "Brightness": 79.50706481933594,
        "Sharpness": 73.32209777832031
      }
    },
    {
      "BoundingBox": {
        "Width": 0.008290473371744156,
        "Height": 0.009163682349026203,
        "Left": 0.6165273785591125,
        "Top": 0.5080556273460388
      },
      "Confidence": 73.62310791015625,
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.6195554137229919,
          "Y": 0.5123717188835144
        },
        {
          "Type": "eyeRight",
          "X": 0.6210446357727051,
          "Y": 0.5128311514854431
        },
        {
          "Type": "mouthLeft",
          "X": 0.6189116835594177,
          "Y": 0.5142983794212341
        },
        {
          "Type": "mouthRight",
          "X": 0.6200852394104004,
          "Y": 0.5146732926368713
        },
        {
          "Type": "nose",
          "X": 0.6193997263908386,
          "Y": 0.5135700702667236
        }
      ],
      "Pose": {
        "Roll": 20.73882293701172,
        "Yaw": -33.038211822509766,
        "Pitch": -11.055394172668457
      },
      "Quality": {
        "Brightness": 41.28157424926758,
        "Sharpness": 67.22731018066406
      }
    }
  ],
  "ResponseMetadata": {
    "RequestId": "d638e263-eeb5-4cbf-807d-aa6328f8096b",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "content-type": "application/x-amz-json-1.1",
      "date": "Thu, 19 Dec 2019 12:20:36 GMT",
      "x-amzn-requestid": "d638e263-eeb5-4cbf-807d-aa6328f8096b",
      "content-length": "5545",
      "connection": "keep-alive"
    },
    "RetryAttempts": 0
  }
}

導入事例

千株式会社の写真販売サービス「はいチーズ」。幼稚園や保育園でのイベントの写真販売の際に、スマホから「自分の子どもが映った写真」だけを選んで抽出できるサービスを提供。

テキスト検出

from boto3 import Session

def lambda_handler(event, context):
    session = Session(region_name="ap-northeast-1")
    rekognition = session.client("rekognition")

    response=rekognition.detect_text(Image={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}})

    return response

簡単にプログラムの解説。

from boto3 import Session

AWSの各種サービスを簡単に仕様できるライブラリboto3を使って実装。
※ライブライはC++、Go、Java、JavaScript,.Net、Node.js、PHP、Rubyがある

    session = Session(region_name="ap-northeast-1")
    rekognition = session.client("rekognition")

サービスを使用するためには必ずSessionというAWSとの接続情報が必要。
リージョンはアジア東京を指定。
rekognitionを使用するためのオブジェクト取得。

    response=rekognition.detect_text(Image={'S3Object':{'Bucket':"XXXXX",'Name':"XXXXX"}})

これがAmazonから提供されている「テキスト検出」関数。
API仕様 こちらから

Imageに解析したい画像を渡すだけ。

demo2_car.jpeg

この画像を解析した結果。
行と単語ごとに解析される。
「J389 NLT」は行(line)で「J389 NLT」、単語で「J389」「NLT」。
DetectedTextが検出したテキスト
Confidenceが信頼度

{
  "TextDetections": [
    {
      "DetectedText": "C",
      "Type": "LINE",
      "Id": 0,
      "Confidence": 72.02649688720703,
      "Geometry": {
        "BoundingBox": {
          "Width": 0.04000072553753853,
          "Height": 0.022570611909031868,
          "Left": 0.25833332538604736,
          "Top": 0.1862500011920929
        },
        "Polygon": [
          {
            "X": 0.25833332538604736,
            "Y": 0.1862500011920929
          },
          {
            "X": 0.2983340620994568,
            "Y": 0.18744535744190216
          },
          {
            "X": 0.29803428053855896,
            "Y": 0.21001596748828888
          },
          {
            "X": 0.2580335736274719,
            "Y": 0.20882061123847961
          }
        ]
      }
    },
    {
      "DetectedText": "J389 NLT",
      "Type": "LINE",
      "Id": 1,
      "Confidence": 99.67211151123047,
      "Geometry": {
        "BoundingBox": {
          "Width": 0.16278892755508423,
          "Height": 0.054150763899087906,
          "Left": 0.47471365332603455,
          "Top": 0.7062126398086548
        },
        "Polygon": [
          {
            "X": 0.47471365332603455,
            "Y": 0.7062126398086548
          },
          {
            "X": 0.6375026106834412,
            "Y": 0.7274553775787354
          },
          {
            "X": 0.6343620419502258,
            "Y": 0.781606137752533
          },
          {
            "X": 0.4715730845928192,
            "Y": 0.7603633999824524
          }
        ]
      }
    },
    {
      "DetectedText": "C",
      "Type": "WORD",
      "Id": 2,
      "ParentId": 0,
      "Confidence": 72.02649688720703,
      "Geometry": {
        "BoundingBox": {
          "Width": 0.040019527077674866,
          "Height": 0.022515427321195602,
          "Left": 0.25833332538604736,
          "Top": 0.1862500011920929
        },
        "Polygon": [
          {
            "X": 0.25833332538604736,
            "Y": 0.1862500011920929
          },
          {
            "X": 0.2983333468437195,
            "Y": 0.1875
          },
          {
            "X": 0.29750001430511475,
            "Y": 0.20999999344348907
          },
          {
            "X": 0.25833332538604736,
            "Y": 0.20874999463558197
          }
        ]
      }
    },
    {
      "DetectedText": "J389",
      "Type": "WORD",
      "Id": 3,
      "ParentId": 1,
      "Confidence": 99.63874053955078,
      "Geometry": {
        "BoundingBox": {
          "Width": 0.08508982509374619,
          "Height": 0.05266508460044861,
          "Left": 0.4749999940395355,
          "Top": 0.706250011920929
        },
        "Polygon": [
          {
            "X": 0.4749999940395355,
            "Y": 0.706250011920929
          },
          {
            "X": 0.559166669845581,
            "Y": 0.71875
          },
          {
            "X": 0.5550000071525574,
            "Y": 0.7712500095367432
          },
          {
            "X": 0.4716666638851166,
            "Y": 0.7587500214576721
          }
        ]
      }
    },
    {
      "DetectedText": "NLT",
      "Type": "WORD",
      "Id": 4,
      "ParentId": 1,
      "Confidence": 99.70547485351562,
      "Geometry": {
        "BoundingBox": {
          "Width": 0.07040064036846161,
          "Height": 0.05260571464896202,
          "Left": 0.5674999952316284,
          "Top": 0.7200000286102295
        },
        "Polygon": [
          {
            "X": 0.5674999952316284,
            "Y": 0.7200000286102295
          },
          {
            "X": 0.637499988079071,
            "Y": 0.7275000214576721
          },
          {
            "X": 0.6341666579246521,
            "Y": 0.7799999713897705
          },
          {
            "X": 0.5649999976158142,
            "Y": 0.7724999785423279
          }
        ]
      }
    }
  ],
  "ResponseMetadata": {
    "RequestId": "b4280fa0-2982-4ac7-8ce3-02beb83b986f",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "content-type": "application/x-amz-json-1.1",
      "date": "Thu, 19 Dec 2019 13:26:41 GMT",
      "x-amzn-requestid": "b4280fa0-2982-4ac7-8ce3-02beb83b986f",
      "content-length": "2131",
      "connection": "keep-alive"
    },
    "RetryAttempts": 0
  }
}

料金

API を使用して画像を分析するたびに料金が発生。
だいたい、 1 か月あたり 100 万枚分析して 1000 USD。

料金例は こちらから

捕捉

ACCESS_KEY、SECRET_KEYを指定せずに正常にsessionが取得できているのはLambdaを使っているからですね。
ポリシーを作成し、Lambda実行するロールに割り当ている。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rekognition:DetectText",
                "rekognition:CompareFaces",
                "s3:*"
            ],
            "Resource": "*"
        }
    ]
}

rekognition:DetectText、rekognition:CompareFacesの使用とs3への全ての操作を許可。

これがないと下記のエラーが出る。

 "errorMessage": "An error occurred (AccessDeniedException) when calling the CompareFaces operation: User: arn:aws:sts::294326415501:assumed-role/testRekognitionPythonFace-role-jcjxzbmq/testRekognitionPythonFace is not authorized to perform: rekognition:CompareFaces",
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Terraform で CodePipeline を管理する場合の注意点

CodePipeline を作っていてでハマったことの備忘録。

Terraform で管理する場合の注意点

  • OAuth Token の差分が plan/apply のたびに発生する
    • API get-CodePipeline が OAuth Token を取得できないため、Terraform や他のどんなツールを使っても冪等にできない
      • aws codepipeline get-codepipeline を実行すると、 **** という表示になって隠されている
      • 根本的な解決方法がなく、今も Open Issue になっている[1]
  • GitHub webhook も定義する場合は、GitHub で発行した OAuth Token の scope に admin:repo_hook も必要[2]
  • GitHub webhook は update できない
    • 一度作成したリソースを修正したい場合は、 taint してから apply する
  • Custom Action を定義できない
    • これも API の制約で、 custom action を updade/get する手段がなく、 create/delete をする手段しか提供されていないため、どんなツールを使っても冪等に保つことができない
    • ユースケース) Action Provider に Jenkins Custom を定義する場合など
    • したがってコンソールか CLI をつかって定義することになる

AWS Console or CLI で管理する場合の注意点

  • AWSコンソール上から作業できないワークフローがある
    • (GitHub など外部の CVS の) OAuth トークンの登録 [3]
  • 裏で暗黙的に作成されるリソースがある
    • GitHub webhook
      • GitHub のリポジトリに push されたときに AWS の Webhook 用エンドポイントを呼び出すためのもの
    • CodePipeline の webhook
      • GitHub => AWS CodePipeline を起動させるための Webhook エンドポイント
        • https://codepipeline.ap-northeast-1.amazonaws.com/...
      • aws codepipeline list-webhooks で確認できる。試行錯誤しているとゴミが残りがち

共通の注意点

  • CodePipeline の定義を更新すると、実行中の action はすべてキャンセルされる
  • (定義を変更せずに) CodePipeline で実行中のアクションを中止する方法はない
    • アクションを中止したい場合は CodePipeline の定義を変更する[4], [5]

参考

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

超ミニマムなチュートリアルを通してCodeDeployを理解する2(AutoScaling編)

前回記事の続きです。

超ミニマムなチュートリアルを通してCodeDeployを理解する。

本記事で取り上げた通り、CodeDeployによるデプロイは以下の要領で行われます。

  • appspec.ymlとデプロイしたいファイルをzip化してS3に格納しておく。
  • CodeDeployではデプロイ先と対象のファイルや設定を指定する。

今回は、ロードバランサ配下のAutoScalingGroup(ASG)のインスタンスを対象としてindex.htmlをデプロイすることを試してみます。

1. ASGを用意する。

別記事にまとめたので参考としてください。
独自VPCでEC2のAutoScalingくらい秒で組めるようになりたい

ただし、注意すべき変更箇所があります。
前回のCodeDeployの記事のStepにも含めましたが、デプロイさせるインスタンスにはCodeDeployのAgentが導入されている必要があります。
起動設定の中の高度な詳細から以下のシェルを起動時に実行してもらうようにしましょう。

この設定をしない状態(=Agentが入っていない状態)で後述のデプロイのコマンドを叩くと長い間「進行中」となった後にエラー終了します。

高度な詳細
#!/bin/sh

sudo yum update -y
sudo yum install ruby -y
sudo yum install wget -y
cd /home/ec2-user
wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto
sudo yum install httpd -y
sudo service httpd start
sudo chkconfig httpd on

ここではAgent導入とApacheの導入・起動を設定しております。
(なお、このAgent導入方法はAmazonLinuxが対象です。AmazonLinux2は少し記述が違います。この辺りは公式記載あり)

インスタンスが立ち上がったらどれか1つにsshで入って確認をしましょう。

1. Agentの確認
以下のコマンドでAgentが入っているかを確認します。

$ sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID 14295

2. httpdの確認
ブラウザから直接インスタンスのIPにアクセスするor動いてるプロセスを確認する。

$ ps ax | grep httpd
14371 ?        Ss     0:00 /usr/sbin/httpd
14374 ?        S      0:00 /usr/sbin/httpd
14375 ?        S      0:00 /usr/sbin/httpd
14376 ?        S      0:00 /usr/sbin/httpd
14377 ?        S      0:00 /usr/sbin/httpd
14378 ?        S      0:00 /usr/sbin/httpd
14379 ?        S      0:00 /usr/sbin/httpd
14380 ?        S      0:00 /usr/sbin/httpd
14382 ?        S      0:00 /usr/sbin/httpd
14492 pts/0    S+     0:00 grep --color=auto httpd

3. ロードバランサとのリンクの確認
ロードバランサのDNS名をブラウザからアクセスしてApacheのデフォルトの画面が表示されればOKです。

これで、ELB + AutoScaingの準備は完了です。index.htmlをデプロイしていきましょう。
なお、今回は説明のためにCodeDeployASGという名のAutoScalingGroupを用意しました。
前回と同様に2台のインスタンス構成です。

2. デプロイするファイルを用意する。

ローカルでやることは超ミニマムなチュートリアルを通してCodeDeployを理解する。の記事と同じです。
実行済みの場合はスキップしてOKです。

  • AWS CLIを入れるorアップデートする。
  • デプロイ対象のindex.htmlを作成する。
  • CodeDeployのデプロイ設定を記述したappspec.ymlを作成する。

3. S3バケットを準備する。

超ミニマムなチュートリアルを通してCodeDeployを理解する。の記事と同じです。
実行済みの場合はスキップしてOKです。

4. ASGへのデプロイ

ここからが本題です。

現在、testCodeDeployディレクトリの中身は以下のようになっているはずです。

$ ls
appspec.yml index.html

アプリケーションの作成

まずは、アプリケーションを作成します。ここではTestASGとします。

$ aws deploy create-application --application-name TestASG
{
    "applicationId": "f33128d3-81bd-49f7-9b97-28f635995xxx"
}

リビジョンの紐付け

デプロイ対象をzip化して、まるごと先ほど作成したS3バケットへ放り込みます。(1つのコマンドでできる。)
ここではファイル名をtestCodeDeploy.zipとします。
ただし、この操作は、前回記事のS3バケットにすでに用意してある場合はzip化put-objectの操作はいらなくなり、register-application-revisionのみ行えば大丈夫です。

S3が空の場合
$ aws deploy push \
--application-name Test \
--s3-location s3://testcodedeploy/testCodeDeploy.zip \
--ignore-hidden-files
To deploy with this revision, run:
aws deploy create-deployment --application-name Test --s3-location bucket=testcodedeploy,key=testCodeDeploy.zip,bundleType=zip,eTag=2dbc9aab3836f21317a9eed7a36f1xxx --deployment-group-name <deployment-group-name> --deployment-config-name <deployment-config-name> --description <description>
S3に既にzipがある場合
$ aws deploy register-application-revision \
--application-name TestASG \
--s3-location bucket=testcodedeploy,key=testCodeDeploy.zip,bundleType=zip

特に何も返値はないので不安な気持ちになりますがあってます。大丈夫。

デプロイメントグループの作成

次は、デプロイメントグループの作成です。ここではTestASGGroupと命名します。前回は--ec2-tag-filtersでデプロイ対象のインスタンスを名指しで指定したのに対し、今回は--auto-scaling-groupsで指定します。
service-role-arnには、AWSがデフォルトで提供しているロールCodeDeployServiceRoleを使用します。

$ aws deploy create-deployment-group \
--application-name TestASG \
--auto-scaling-groups CodeDeployASG \
--deployment-group-name TestASGGroup \
--deployment-config-name CodeDeployDefault.OneAtATime \
--service-role-arn arn:aws:iam::xxxxxxxxxxxx:role/CodeDeployServiceRole
{
    "deploymentGroupId": "91350391-950f-497b-87c4-95abd8454xxx"
}

デプロイ実行

いよいよデプロイです。

$ aws deploy create-deployment \
--application-name TestASG \
--deployment-config-name CodeDeployDefault.OneAtATime \
--deployment-group-name TestASGGroup \
--s3-location bucket=testcodedeploy,bundleType=zip,key=testCodeDeploy.zip 
{
    "deploymentId": "d-QHW602861"
}

これで実際に対象のEC2の/var/www/html下にindex.htmlが置かれているはずです。
先ほどアクセスしたEC2のIPアドレスにブラウザからアクセスしてみましょう。Hello World!が表示されれば成功です。

実際にCodeDeployコンソールからデプロイメントを見てみると成功となっているでしょう。
インスタンスの終了とかがあるので時間がかかるかと想像していましたが、デプロイは10秒ほどで完了しました。

デプロイ方式の種類

  • CodeDeployDefault.AllAtOnce
    • 一度に全てのインスタンスへデプロイ。
  • CodeDeployDefault.HalfAtATime
    • 一度に半分ずつデプロイ。
  • CodeDeployDefault.OneAtATime
    • 一度に1つずつデプロイ。

詳しくは公式を参考に→https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/deployment-configurations.html

今後試したいこと

  • EC2へのデプロイ
  • AutoScalingGroupへのデプロイ
  • lambdaへのデプロイ
  • CodeBuild等との一連の連携
  • appspec.ymlのhooksの利用
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2とロードバランサでwebサービスを公開する

はじめに

学生がAWSからサーバを借りてウェブサービスを公開する時、EC2 + 固定IPで公開する場合が多いかと思います。
ですが、AWSの夏インターンに参加した際にAWSにはELB(Elastic Load Balancing)というサービスがあり、必ずそれを設置していました。
ここでは、EC2単体ではない一歩進んだWebサービス公開の方法を紹介します。

ロードバランサ設置のメリット

WEBサーバが1台でもELBをおいた方がいい理由はこちら書かれています
- https://dev.classmethod.jp/cloud/aws/benefit_elb_with_one_ec2/

負荷分散目的で設置されるロードバランサですが、次の理由でサーバが1台でもロードバランサを設置した方が良いらしいです。

  • インスタンス(仮想マシンのこと)の差し替えが容易
  • ELB(ロードバランサのこと)のヘルスチェックで死活監視が可能。別のサービスで通知も可能に
  • 無料SSL証明書(ドメイン証明書)が利用できる
  • などなど

AWSアカウントの準備

これまでは、クレジットカードが無ければAWSのアカウントを作ることはできませんでした。
でももし、あなたが学生ならばAWS Educateで、クレジットカード不要で無料枠のサービスを使うことができます。

EC2インスタンスの準備

  1. AWSのEC2コンソールを開く
  2. 「インスタンスの作成」をクリック! スクリーンショット 2019-12-19 13.00.51.png
  3. 無料枠で使えるインスタンスを選択(ここでは、Amazon Linux 2 AMI (HVM), SSD Volume Type) スクリーンショット 2019-12-19 13.02.35.png
  4. インスタンスタイプはここではt2.microを選択 スクリーンショット 2019-12-19 13.04.36.png
  5. 次のステップはデフォルトで「次のステップ: ストレージの追加」をクリック
  6. ステップ4: ストレージの追加 も設定はデフォルトで「次のステップ: タグの追加」をクリック
  7. 以下のタグを設定したうえで、「次の手順: セキュリティグループの設定」をクリック
キー
Name (任意の名前)
  1. セキュリティグループの設定を以下のようにする(ルールの追加で追加できる) セキュリティーの警告が出るかもしれないが、「起動」を行う
タイプ プロトコル ポート範囲 ソース
SSH TCP 22 0.0.0.0/0
HTTP TCP 80 0.0.0.0/0
  1. 新しいキーペアの作成で、SSHのためのキーペアを作成するを選択し、インスタンスを作成

ロードバランサの設置

  1. EC2コンソールを開く
  2. 左側のメニューバーから「ロードバランサー」選択
  3. 「ロードバランサーの作成」を選択する
  4. 「Application Load Balancer」の「作成」をクリック
  5. 名前は何か任意につけ、「リスナー」でHTTP 80番ポートとなっていることを確認。アベイラビリティーゾーン(AZ)では、先ほどのEC2があるところを選択。進むわからない場合はあとでも追加できるのでとりあえずどれかを選択。「セキュリティグループの設定」をクリック

6.「新しいセキュリティグループを作成する」 を選択し、任意のセキュリティーグループ名と、次のように設定し、「次の手順: ルーティングの設定」 をクリック

タイプ プロトコル ポート範囲 ソース
HTTP TCP 80 0.0.0.0/0

7.以下のように設定し、残りの設定はデフォルトのままで、「次の手順: ターゲットの登録」をクリック

ターゲットグループ 名前 ターゲットの種類
新しいターゲットグループ 先ほどのセキュリティーグループ名 インスタンス

8.最初作ったEC2インスタンスを選択し、「登録済みに追加」。次の手順に進む
9 「作成」!

サーバーアプリケーションのインストール

  1. 先ほど作ったインスタンスにSSH接続
    • 「EC2ダッシュボード」->「接続」にssh接続の方法が書いてあります
  2. 以下のコマンドでApacheをインストール・再起動
sudo yum -y install httpd
sudo service httpd start

アクセス

  1. まずはEC2のパブリックIP(EC2コンソールから確認)にアクセスしてみましょう
  2. 次にロードバランサのDNS名でアクセスしてみましょう
  3. /var/www/html/が公開ディレクトリです.何か置いてみましょう

Route53を使用してELBにドメインを設定

というのをしたかったのですが、学生用のAWS Educateでの、Route 53で発行できる無料の新規ドメイン名がわからなかったので、やっていません。

終わりに

N番煎じだったり、セキュリティのあたりなど様々な設定をもっとしていかないといけないと思いますが、ひとまず一連の流れはこのようになります。

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

AWS CLIでAMIリージョン間コピーを行うコマンド

AWS CLIの初期設定は終わっているものとします。

EC2:インスタンスID確認コマンド

まずはAMIを作成するEC2インスタンスを確認。

タグ:Name、値がMyInstanceのインスタンスの情報を取得する場合

aws ec2 describe-instances --filters "Name=tag:Name,Values=MyInstance"

インスタンスIDを指定して取得する場合

aws ec2 describe-instances --instance-ids {instance-id}

AMI作成コマンド

aws ec2 create-image --instance-id {instanceID} --name {ami-name} --description {description} --reboot

{ami-name}は任意のAMI名。
AMI作成時はデータの整合性を保証するために再起動を行うのがデフォルトの動きになっている。--no-rebootを指定すると再起動しないが、データの整合性は保証されない。
コマンド実行すると新しいAMIのIDが返ってくる。

AMIにタグを作る

aws ec2 create-tags --resources {ami-id} --tags Key={key-name},Value={key-value}

AMIリージョン間コピー

aws ec2 copy-image --source-image-id {ami-id} --source-region {source-region-name} --region {region-name} --name {ami-name}

コマンドを実行すると新しいAMIのIDが返ってくる。

AMI一覧確認コマンド

自分で作成したAMIの一覧

aws ec2 describe-images --owners self

AMI削除コマンド

aws ec2 deregister-image --image-id {ami-id}

補足

別リージョンのAMIを操作する場合はコマンドに--region {RegionName} を付加しないとエラーとなる(指定しない場合aws configureで設定したデフォルトリージョンとなるため)。

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

openssl コマンドを使用して AWSのsmtp サーバからメールを送信する

前提条件

  • 送信元 From メールアドレスも送信先 To メールアドレスも SES で認証済みであること

メール送信できる IAM ユーザーを作成する

policy
{
"Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ses:SendRawEmail",
            "Resource": "*"
        }
    ]
}

SMTP ユーザ名、パスワードを作成する

  1. SMTP ユーザー名は AWS アクセスキー ID と同じ

  2. SMTP パスワードを IAM シークレットアクセスキーから生成する

  3. 以下のコードで変換する(Bash)

  4. export AWS_SECRET_ACCESS_KEY={IAM シークレットアクセスキー} 後、bash実行

    #!/usr/bin/env bash
    
    # These variables are required to calculate the SMTP password.
    VERSION='\x02'
    MESSAGE='SendRawEmail'
    
    # Check to see if OpenSSL is installed. If not, exit with errors.
    if ! [[ -x "$(command -v openssl)" ]]; then
    echo "Error: OpenSSL isn't installed." >&2
    exit 1
    # If OpenSSL is installed, check to see that the environment variable has a
    # length greater than 0. If not, exit with errors.
    elif [[ -z "${AWS_SECRET_ACCESS_KEY}" ]]; then
    echo "Error: Couldn't find environment variable AWS_SECRET_ACCESS_KEY." >&2
    exit 1
    fi
    
    # If we made it this far, all of the required elements exist.
    # Calculate the SMTP password.
    (echo -en $VERSION; echo -n $MESSAGE \
    | openssl dgst -sha256 -hmac $AWS_SECRET_ACCESS_KEY -binary) \
    | openssl enc -base64
    
  5. 生成されたSMTP パスワードを確認する

AWS アクセスキー ID と生成した SMTP パスワードを base64 エンコードする

  • AWS アクセスキー ID を base64 エンコード

    • echo -n "{AWS アクセスキー ID}" | base64
  • SMTP パスワードを base64 エンコード

    • echo -n "{SMTPパスワード}" | base64
  1. AWS SMTP サーバへの TLS 接続をテスト

    • openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.us-east-1.amazonaws.com:587
  2. 接続後、メール送信をテスト

AUTH LOGIN
{base64 エンコード後AWS アクセスキー ID}
{base64 エンコード後AWS シークレットアクセスキー}
  • 235 Authentication successful. と表示後、以下を入力
MAIL FROM: {送信元メールアドレス}
RCPT TO: {送信先メールアドレス}
DATA
from:{送信元メールアドレス}
to:{送信先メールアドレス}
Subject: Test Subject
Test Mail
.
QUIT
  1. メールが受信できることを確認する

参考リンク

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

Lambda関数をそのままAWS Batchに乗せ換える(Python3.6限定)

この記事は アラタナアドベントカレンダー2019 19日目の記事です。

Serverlessでサービスを展開していると、どうしてもRDBを使いたくなったり、どこかでバッチ処理をしたくなったりしますよね。

RDBについては、RDS ProxyがLambdaから使用可能になりそうでいい兆しですね。

Amazon RDS Proxy

バッチ処理についてはかつてからAWS Batchというのが用意されています。

これはECRに用意したDockerを使って処理を実行できる環境です。Queueも用意されているのでかなり自由なバッチ設計ができると思います。

さて、Serverlessで環境を構築している場合、まず選択肢として、AWSBatchよりもLambdaを考えると思います。

StepFunctionやQueue、S3などのイベント、並行処理を駆使して構築する場合もあるでしょう。

ただ、Lambdaの実行制限は現状15分でそれを超える場合は何か手を考えなくてはいけません。

処理を分割する、並行処理を考える等あると思いますが、選択肢としてAWS Batchへの移行も入ってくるかと。

しかし、問題はECR上にDockerを用意しなくてはいけない点です。

AWS Batchを使用する場合、Lambdaで使っているソースコードをわざわざDockerに乗せて、ECRのレポジトリに突っ込まなくてはいけない。

これは非常に面倒臭いです。

そこでServerlessFrameworkを使っている場合限定ですが、いいPluginを見つけました。

serverless-aws-batch

※ ただしこちらは不具合が多いので注意。。。

このプラグインはServerlessFrameworkからデプロイする際に、Lambdaで使用するソースをそのままDockerに乗せ、ECRにPushしてくれて、
Batchの設定まで行ってくれる夢のようなPluginです。

イベントや環境変数もそのまま使えます(Batchを起動するLambdaが自動で作られ、イベントや環境変数の設定も引き継がれる)。
serverless.ymlをちょっと書き換えるだけで全部構築してくれます。

ただ、先の大元のPluginはちょっとバグが多かったので、せめてPython3.6で動くように手を入れたものがこちらになります。

serverless-aws-batch

※ 大元をForkしてまだプルリクを出していないのですが、取り込まれたらリンクを修正します。

使い方はGithubのReadmeに詳しく書いてあります。
基本serverless.ymlでiamにbatchを許可するのと、該当のfunctionにbatchの項目を付け足すだけで済むはずです。

ぜひ使ってみてください\(^o^)/

Python3.6以外では動く保証はないですが、同じような修正をすればいけると思うので、ForkしてPRしてもらえればいいかと思います。

あらためて、大元のjustinram11さんありがとうございます!ナイスアイデアです!

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

[Datadog]IntegrationがないツールでもDatadogからアラート通知したい

はじめに

こんにちは、なじむです。
今回はこれまでのCFnの記事からは離れて、Datadog Advent Calendar 2019の12/20(金)分の記事です。DatadogにはたくさんのIntegrationがありますが、たまに無いものもあります。「えーイベントの通知は全部Datadogでやりたい」という時に使えるかもしれない「なんちゃってIntegration」です。今回はコンテナセキュリティツールであるSysdigを使用します。

やりたいこと

障害発生時の運用として以下を考えていました。

障害発生時に確認するサイト(ダッシュボード)はDatadogに一本化したい!

いくつかの監視ツールを使用していると「ホスト障害の時はDatadogを見る」、「コンテナ障害の時はSysdigを見る」というように障害の起きた個所で見るべきサイトが異なってしまう場合があります。その運用だと負荷が高くなったり(特に夜間の寝ぼけた時はミスしやすい…)、後でイベントの一覧を見るときに見るべきサイトを間違えたりするため、最初に見るべきサイトはDatadogに統一したいと考えました。

前提

以下の環境を想定しています。

  • DatadogでSNS Integrationを有効にしていること
  • アラートを検知した場合の通知先(今回はSysdigの通知先)でSNSが使用できること

実装の流れ

通知までの大きな流れとしては以下です(絵を描けば良かった…)

  1. Sysdigでアラート発生→SNSにアラートを通知
  2. SNSでDatadogのイベントを作成
  3. 作成されたイベントを基にDatadogのMonitorをトリガー→メールやチャットに通知

実装方法

今回の肝は「SNSでDatadogのイベントを作成」のため、その方法を書いていきます。やり方は2つ考えました。SNS Integrationを使用した簡易な方法と、Lambdaを使用した柔軟な通知が行う方法です。

  1. SNS Integrationを使用する方法
  2. Lamdbdaを使用する方法

SNS Integrationを使用する方法

この方法はとても簡単で、以下の手順を実施すれば実現可能です。

  1. SNSトピックを作成する(参考:Sysdig公式
  2. SNSサブスクリプションを作成する(参考:Datadog公式

サンプルコード(CloudFormation)

ではいつも通りCloudFormationでSNSを設定していきます。

---
AWSTemplateFormatVersion: 2010-09-09
Description: SNS - post Datadog event from sysdig

#------------------------------
# Parameters: Set your argv.
#------------------------------
Parameters:
  DatadogWebhookURL:
    Type: String
    Description: Datadog webhook URL (ex. https://app.datadoghq.com/intake/webhook/sns?api_key=xxxxxxxxx)

#------------------------------
# Resources: Your resource list
#------------------------------
Resources:
  # SNS Resource
  ## Topic
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: sysdig2datadog # ここは任意の名前に変更してください
      Subscription:
      - Endpoint: !Ref DatadogWebhookURL
        Protocol: https
      TopicName: sysdig2datadog # ここは任意の名前に変更してください

  ## TopicPolicy
  SNSTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        {
          "Version": "2008-10-17",
          "Id": "__default_policy_ID",
          "Statement": [
            {
              "Sid": "__default_statement_ID",
              "Effect": "Allow",
              "Principal": {
                "AWS": "*"
              },
              "Action": [
                "SNS:GetTopicAttributes",
                "SNS:SetTopicAttributes",
                "SNS:AddPermission",
                "SNS:RemovePermission",
                "SNS:DeleteTopic",
                "SNS:Subscribe",
                "SNS:ListSubscriptionsByTopic",
                "SNS:Publish",
                "SNS:Receive"
              ],
              "Resource": {"Ref": "SNSTopic"},
              "Condition": {
                "StringEquals": {
                  "AWS:SourceOwner": {"Ref": "AWS::AccountId"}
                }
              }
            },
            {
              "Sid": "__console_pub_0",
              "Effect": "Allow",
              "Principal": {
                "AWS": [
                  "273107874544"
                ]
              },
              "Action": "SNS:Publish",
              "Resource": {"Ref": "SNSTopic"}
            }
          ]
        }
      Topics: 
      - !Ref SNSTopic

実行結果

SNSトピック、SNSサブスクリプションができました。
20191218_163835.jpg

動作確認

実際にSysdigでアラートを検知して、それがDatadogで表示されるかを確認してみましょう。

  1. Sysdigでアラートを出力する
    Sysdigにはテストアラートを発生させる機能があるので、今回はこの機能を使用してテストアラートをSNSに通知します。
    20191218_130736.jpg

  2. Datadog側で正常にアラートを受け取ったことを確認。めでたし!
    20191218_165611.jpg

Lamdbdaを使用する方法

メッセージ内容をより詳細にカスタマイズしたい場合はhttps(webhook)ではなく、Lambdaを使用してカスタマイズしてください。Lambdaを使用すると細かいパラメータ(アラートタイプや件名、メッセージの内容等)が指定できるのがとても良いです。

サンプルコード(lambda)

簡単なコード(python3)としては以下のようになります。APIKEYなどはLambdaの環境変数から引っ張ってきています。

from datadog import initialize, api
import os

APIKEY = os.environ['APIKey']
APPKEY = os.environ['APPKey']

options = {
  'api_key': APIKEY,
  'app_key': APPKEY
}

initialize(**options)

def lambda_handler(event, context):
  title      = event['Records'][0]['Sns']['Subject']
  text       = event['Records'][0]['Sns']['Message']
  alert_type = 'error'
  tags       = ['source:sysdig']

  api.Event.create(title=title, text=text, alert_type=alert_type, tags=tags)

まとめ

「何かあったら先ずはDatadogを見る!」とした場合、Integrationがない時は自分でイベントを作成して通知できるよ、という記事でした。
DatadogはIntegrationが豊富で、機能もどんどん追加されています。そして何といっても安い。Freeプランもあるため、個人で触ってみるのが気軽にできるのも良いですね。
書いた後で、SNSじゃなくてもDatadogのAPIが叩ければ良いので「SNSが使用できること」が前提じゃなくても良いよなぁと思いました。ただ、お手軽に実装する場合はSNS Integrationを使うと楽かなと思います。
何かの参考になれば幸いですノシ

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

VXLAN を使って、EC2 で Broadcast を試してみる

たまーに、VPC内でブロードキャストを使いたい時があるのですが、対応していないので自力でなんとかする必要があります。
方法の一つとして VXLAN を利用できるので必要最小限のメモを残しておきます。

構成

  • シンプルに EC2(10.0.0.0/24) を2台用意
  • VXLAN を使って Overlay Network(192.168.1.0/24) を構築
  • 192.168.1.0/24 でブロードキャストをためす
  • OS は Amazon Linux 2

EC2-A

  • ENI IP: 10.0.12.137/24
  • vxlan interface ip: 192.168.1.1/24
  • AZ: us-east1-1c

EC2-B

  • ENI IP: 10.0.12.99/24
  • vxlan interface ip: 192.168.1.2/24
  • AZ: us-east1-1c

設定

本当に必要最小限の設定だけします。
それぞれのEC2で、vxlan interface を作成し、IPをつけます。

EC2-A
[ec2-user@ip-10-0-12-137 ~]$ sudo ip link add vxlan0 type vxlan id 10 dev eth0
[ec2-user@ip-10-0-12-137 ~]$ sudo ip addr add 192.168.1.1/24 broadcast 192.168.1.255 dev vxlan0
[ec2-user@ip-10-0-12-137 ~]$ sudo ip link set vxlan0 up
[ec2-user@ip-10-0-12-137 ~]$ sudo bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.0.12.99
[ec2-user@ip-10-0-12-137 ~]$ bridge fdb show
33:33:00:00:00:01 dev eth0 self permanent
01:00:5e:00:00:01 dev eth0 self permanent
33:33:ff:cd:b4:59 dev eth0 self permanent
00:00:00:00:00:00 dev vxlan0 dst 10.0.12.99 self permanent
EC2-B
[ec2-user@ip-10-0-12-99 ~]$ sudo ip link add vxlan0 type vxlan id 10 dev eth0
[ec2-user@ip-10-0-12-99 ~]$ sudo ip addr add 192.168.1.2/24 broadcast 192.168.1.255 dev vxlan0
[ec2-user@ip-10-0-12-99 ~]$ sudo ip link set vxlan0 up
[ec2-user@ip-10-0-12-99 ~]$ sudo bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.0.12.137
[ec2-user@ip-10-0-12-99 ~]$ bridge fdb show
33:33:00:00:00:01 dev eth0 self permanent
01:00:5e:00:00:01 dev eth0 self permanent
33:33:ff:5b:d5:2f dev eth0 self permanent
00:00:00:00:00:00 dev vxlan0 dst 10.0.12.137 self permanent

boradcast に ping 投げて動作確認しようと思うので "/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts" の値を変更しておきます。ただ、Smurf攻撃の対策のためにも、検証後は設定を戻しておきましょう。

Smurf攻撃
https://ja.wikipedia.org/wiki/Smurf%E6%94%BB%E6%92%83

EC2-A
[ec2-user@ip-10-0-12-137 ~]$ sudo sh -c 'echo 0 >/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts'
EC2-B
[ec2-user@ip-10-0-12-99 ~]$ sudo sh -c 'echo 0 >/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts'

動作確認

とりあえず、 ふつーに ping でユニキャストの通信確認してみます。
問題なく Overlay Network(192.168.1.0/24) で通信できてます。

EC2-B
[ec2-user@ip-10-0-12-99 ~]$ ping -b 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=255 time=0.281 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=255 time=0.222 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=255 time=0.205 ms
64 bytes from 192.168.1.1: icmp_seq=4 ttl=255 time=0.185 ms

最後に、本題のブロードキャスト

EC2-B
[ec2-user@ip-10-0-12-99 ~]$ ping -b 192.168.1.255
WARNING: pinging broadcast address
PING 192.168.1.255 (192.168.1.255) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=255 time=0.017 ms
64 bytes from 192.168.1.1: icmp_seq=1 ttl=255 time=0.184 ms (DUP!)
64 bytes from 192.168.1.2: icmp_seq=2 ttl=255 time=0.027 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=255 time=0.293 ms (DUP!)
64 bytes from 192.168.1.2: icmp_seq=3 ttl=255 time=0.029 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=255 time=0.203 ms (DUP!)

お片づけ

/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts" の値を元に戻す

EC2-A
[ec2-user@ip-10-0-12-137 ~]$ sudo sh -c 'echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts'
EC2-B
[ec2-user@ip-10-0-12-99 ~]$ sudo sh -c 'echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts'

以上です。

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

AWS Developer Associate 合格記録

どした

AWS Certified Developer Associate を取得したので
どんなことやったのかなど、いつものように記録します。

about me

インフラエンジニアで、開発知識は少ないです。(2019/12時点)
運用、開発者系のAWS資格では、SysOps Administrator Associateのみ保持しています。

今まで、ネットワーキング設計やサーバー構築など、いわゆるAWSでいう
ソリューションアーキテクト側の経験を実質1年以上 積んでいます。

今回の対象分野で言えば、ElasticBeanstalk, Lambda, CloudFormation等の経験はありますがまだ浅い状態です。
つまり、この記事はあくまでインフラエンジニア目線での学習記録になります。

about DVA試験

AWS 認定開発者 – アソシエイト
https://aws.amazon.com/jp/certification/certified-developer-associate/

・AWSクラウドの基本的な考え方、主要なサービス、ベストプラクティス、使用方法などの理解
・AWSサービスを利用したアプリケーションの開発、デプロイ、デバッグに関する各サービス知識
が必要です。

何をやったのか

試験ガイドとサンプル問題

こちらにAWS公式から「AWS 認定試験に備える」情報が記載されています。
「試験ガイドとサンプル問題」は、はじめに必ず確認ですね。
https://aws.amazon.com/jp/certification/certification-prep/

また、サンプル問題で言えば、以下のQiita記事が助かりました。
AWS認定デベロッパー アソシエイト 練習問題を解こう
https://qiita.com/Junpei_Takagi/items/9fa884e35990768c66de

サンプル問題に対して根拠などをシンプルに記載されており、学習のヒントを見つけることができました。
サンプルと言えど、答えしかないとなかなかどこから入ればいいか時間がかかったりしますからね。。
※ちなみに模擬試験は、答えもワカラナイ

結果、サンプル問題を解いて確認してことで、「いま何が足りないか」を明確にできた気がします。

模擬試験

下調べをした際に、試験を受けた皆さんが各種記事、ブログでよく書かれていたことは
Solution Architectなどの他のAssociate試験を受けたことがある人は、
まず模擬試験を受けてみるべきだと言うことでした。

はい、その通りでした。(いわゆる腕試し的な)
結果は50%代で、足りないのはAPIやSQSのオプション機能、そしてDynamoDBの超基本DB系知識だということが判明したので、そこから始められました。

書籍

まず、以下の本で基本的なサービスの機能、動作を主に図で覚えました。
現状で足りない基本知識の補完作業です。

以前他のAssociate本でも活用しましたが、今回が一番役に立ちました。
SQS, Lambda, DynamoDBで特に助かりました。やはり図があるとスピーディに理解ができますね。

AWS認定アソシエイト3資格対策~ソリューションアーキテクト、デベロッパー、SysOpsアドミニストレーター~
https://www.amazon.co.jp/dp/4865941991
※直リンク

問題集

こちらは定番で、AWS問題集サイトです。
DVAに関しても少し問題が出ています。
https://aws.koiwaclub.com/

問題数も少ないので知識的な意味でも完璧にしてしまいます。

udemy

有名なサイトですが、私が購入したのは以下2点です。

・Ultimate AWS Certified Developer Associate 2020
https://www.udemy.com/course/aws-certified-developer-associate-dva-c01/
・AWS 認定デベロッパー アソシエイト模擬試験問題集
https://www.udemy.com/course/aws-31955/

1つ目は、AWSドキュメントの長い文章を読む前に図示された説明をみる事で、頭に入りやすくするためです。
2つ目の問題集は、単純に演習がやりたかったからです。
この問題集は本番よりもレベルが高い問題もあると感じたので、あくまで演習や知識補完と考えていいと思います。
私はProfessionalも受ける予定なので、むしろ今後に使えるかな、と感じました。

結果&まとめ

無事合格しました。
手応え的にはギリギリだったかもしれません。

今回の全体の感想はこんな感じです。

・学習してきた内容は本当にコアな内容だったから、やってよかった
・概念的な知識は、Solution Architectの学習で十分(CloudWatchなど、開発者も使用するような被るサービスもある)
・アプリケーションデプロイ時の用件に対して、DBやSQS、デプロイ系サービスのどのオプションを使えばいいのか、単純に知識がまだ少し足りない部分があった
・Code系サービス(CodeDeployなど)経験が少ないため、少しでも具体的に聞かれるとわからない事がある
ECSをもっと色々使おう...
・ElasticBeanstalkって意外と色々ある
Professionalがもう心配

また、最近よくAWS試験を受けていますが試験当日のルーチンは大事だなと強く思いました。

今後は、AWS資格に関しては残りのDevOps Engineer Professionalを取得してから、Specialtyへ進みます。
また、色々できなかったAWSの検証系をどんどんやってみて、公開しようと思います。(ツッコミ、welcome)

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

NoSQL×PaaSで運用するナレッジベース+WebAPI

これはなに

NoSQLとPaaSでナレッジベース+WebAPIを構築した際のノウハウのまとめです。

文字列タグ指向無向グラフ型ナレッジベース というアーキテクチャを実装する場合に、
Heroku + Redis + FastAPI で構築した例と、
AWS(DynamoDB + Lambda + API Gateway) で構築した例を紹介します。

コード部分は全て Python3.8.0 を使用しています。

※ 現在AWS編が未完成です。ごめんなさい。

ナレッジベースとは?

ナレッジベースには様々な定義がありますが、この記事では
「知識をコンピュータが読み取り可能な形式で格納したデータベース」を指します。
知識ベース ナレッジデータベース KB などの呼ばれ方もあります。

参考リンク(クリックで開く)

文字列タグ指向無向グラフ型ナレッジベース

今回例として構築するナレッジベースです。
名前だけでは分かりにくいと思うので、イメージ図を用意しました。
(可視化は未実装のためマインドマップツール coggle で作成)

スクリーンショット 2019-12-03 20.00.32.png

なお、このアーキテクチャは流通しているものではなく、
GraphQLから着想を得て独自に設計したものです。
(サーベイはしていないのでもしかしたら既存かもしれません)

「文字列タグ指向」について

このナレッジベースでは文字列(とその集合)データのみを扱い、
全ての文字列をタグとして扱います。

上の図の例では、
Webサービス名 アカウントID URL 記事タイトル 概念 プログラミング言語
などの各文字列を1つのタグとして扱っています。

仕様上、文字列には空白や改行文字などは含まないものとします。

「無向グラフ型」について

このナレッジベースでは、関係のあるタグを結び付けるようにします。

例えば フレームワーク というタグには
Rails Laravel Django Flask
というタグが紐付いているというデータが取得可能で、

スクリーンショット 2019-12-03 20.43.44.png

例えば QiitaPython のどちらのタグも紐付いているのは
https://qiita.com/1ntegrale9/items/94ec4437f763aa623965
というタグ(QiitaのPythonに関する記事URL)、のようにデータを取得できます。

スクリーンショット 2019-12-03 20.44.33.png

上の図では頂点(文字列)がタグ、辺が関係を表しています。
そして無向なので双方参照が可能です。
また包含関係などは考慮しないので、重み付けはありません。

参考記事:グラフ理論の基礎 - Qiita

このナレッジベースの役割

「2つの文字列を格納する」という単純な操作の繰り返しだけで、
辞書的な知識体系(集合知)を形成することです。

そして爆速で育て上げるためにWebAPIが必要になります。

構築例:Redis + FastAPI + Heroku

手軽に無料で運用したい場合はこちらを採用します。

Heroku の初期設定や Redis の基本操作はこちらで解説しています。
Heroku×Redis×Python で始める NoSQL DB 入門 - Qiita

Redis

オンメモリで読み書きが高速なKVSです。永続化にも対応しています。
1つのタグに複数のタグを紐付けたいので、集合型のみを使います。

ライブラリのインストール

Python で扱うため、redis-py を利用します。

python3 -m pip install redis hiredis

hiredis-py は C 実装の高速なパーサのラッパーです。
redis-py 側が hiredis を検知してパーサを切り替えてくれるので入れておきます。

Redisに接続

以下のコードで接続の初期化を行います。
Heroku Redis が自動で設定してくれる環境変数 REDIS_URLを使います。

import redis, os
conn = redis.from_url(os.environ['REDIS_URL'], decode_responses=True)

デフォルトだと日本語の表示に不具合があるので、
decode_responses=True は必須です。

全てのタグを取得

keys() を使って取得します。

def get_all_tags():
    return sorted(conn.keys())

タグが一覧で見れると便利なので用意しておきます。
ただ規模が大きくなると負荷が高くなるので注意が必要です。

紐付くタグを取得する

smembers(key) 使って取得します。

def get_related_tags(tag):
    r.smembers(tag) if r.exists(tag) else []

念のため、存在しないタグを指定された場合は空配列を返すようにします。
存在確認には exists(key) を使います。

2つのタグを紐付けて格納する

sadd(key, value) を使って集合型データを格納します。
双方向に紐付けたいので、key-value を入れ替えて2回実行します。

def set_relation_tags(tag1, tag2):
    return conn.pipeline().sadd(t1, t2).sadd(t2, t1).execute()

Redis はトランザクションをサポートしており、redis-py の場合は
pipeline() から execute() までのチェーンにすることで、
トランザクション内での一括実行ができます。

また、pipeline メソッドによるアトミックな実行は、個別実行よりも高速のようです。
PythonでRedisを効率的に使う(redis-pyのパフォーマンスをあげるには) - [Dd]enzow(ill)? with DB and Python

FastAPI

FastAPI は Python の Web フレームワークの1つで、
シンプルな WebAPI を少ないコードで実装できるのと、
API ドキュメントを設定なしで自動生成してくれるのが特徴です。

Flask Responder Starlette DRF などはオーバースペックで、
Bottle は逆に機能が足りず、FastAPIがちょうどよくハマりました。

ライブラリのインストール

python3 -m pip install fastapi uvicorn email-validator

Uvicorn は高速なASGIサーバーです。FastAPIの起動に使います。
Gunicorn の typo ではないです。

email-validator は入れておかないと起動時に怒られます。何故か。

アプリケーションの初期化

非常にシンプルです。

main.py
from fastapi import FastAPI
app = FastAPI()

引数の titlediscription を設定すると、
上記画像のような自動生成される API Doc にタイトルと説明部が反映されます。

main.py
app = FastAPI(
    title='collective-intelligence',
    description='文字列タグ指向無向グラフ型ナレッジベース',
)

また、docs_url を指定すると、API Doc のURLを変更できます。
デフォルトでは /docs ですが、ルートにしておくのも良いでしょう。

main.py
app = FastAPI(docs_url='/')

全てのタグを取得

シンプルに HTTPメソッド(GET) と URL と返り値を書くだけです。
リストか辞書を返り値にすることで JSON レスポンスになります。

main.py
@app.get('/api')
def read_all_tags():
    return get_all_tags()

この定義が API Doc に自動で反映されます。
更に右上の Try it out からリクエストを実行できます。

指定のタグに紐付くタグを取得

タグには記号も含む任意の文字列を想定しており、
クエリストリングでは対応できないため、POST にしておきます。

main.py
@app.post('/api/pull')
def read_related_tags(tag: str):
    return get_related_tags(tag)

引数に指定した tag: str をリクエストボディから受け付けます。
型アノテーションを付けていますが、これを利用してリクエストのバリデーションを行います。
適合しない場合、422 Validation Error がレスポンスになります。

2つのタグを紐付けて格納する

FastAPI は pydantic という、
型アノテーションを活用するためのライブラリを内包しています。
これを利用して独自の型を定義し、バリデーションに利用します。

main.py
from pydantic import BaseModel

class Tags(BaseModel):
    tag1: str
    tag2: str

@app.post('/api/push')
def create_tags_relationship(tags: Tags):
    set_tags_relationship(tags.tag1, tags.tag2)
    return {tag: get_related_tags(tag) for _, tag in tags}

定義した型は Schema として API Doc に反映されます。

FastAPI の起動

先ほど紹介した Uvicorn で起動します。
main.py 内の app で初期化した場合は main:app と指定します。
--reload オプションにより、ファイル変更時にリロードして反映してくれます。

$ uvicorn main:app --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [57749]
INFO:     Started server process [57752]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

http://127.0.0.1:8000 または http://127.0.0.1:8000/docs にアクセスすると、
API Doc が表示されることが確認できると思います。

Heroku

Webアプリケーションを手軽にデプロイできるPaaSです。
多くの言語・フレームワークに対応しており、
PostgreSQL や Redis も一定枠まで無料でホスティングしてくれます。

最初に以下の手順が必要です。

必要なファイルの用意

以下のファイルが必要になります。
これを GitHub リポジトリに用意します。

$ tree
.
├── main.py          # アプリケーション
├── Procfile         # プロセス実行コマンド定義ファイル
├── requirements.txt # 依存ライブラリ定義ファイル
└── runtime.txt      # Pythonバージョン定義ファイル
Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT
requirements.txt
fastapi
email-validator
uvicorn
redis
hiredis
runtime.txt
python-3.8.0

実際のディレクトリ も参考にしてください。

アプリケーションのデプロイ

Dashboard の Deploy タブからデプロイ作業を行います。
GitHub と連携してリポジトリを紐付け、Manual Deploy を実行します。
Automatic deploys も設定しておくと、master への push 時に自動でデプロイしてくれます。

スクリーンショット 2019-12-19 14.45.06.png

build が無事に完了したら、
登録されたプロセスを Configure Dynos から ON にしておきます。

スクリーンショット 2019-12-19 14.48.21.png

Dashboard 右上の Open app からデプロイされたアプリケーションを確認できます。

構築例:AWS(DynamoDB + Lambda + API Gateway)

執筆中のため公開をお待ちください

スケーラビリティを意識するならこちらを採用します。
柔軟にデータ構造を変えることも可能です。

初めての、LambdaとDynamoDBを使ったAPI開発 - Qiita
API Gateway + Lambda + DynamoDB - Qiita

Amazon DynamoDB

RDBと同様に1テーブル1プライマリキーが基本です。
プライマリキーはデータを一意に識別するためのキーで、「パーティションキー」または
「パーティションキーとソートキーの複合キー」のことです。
ソートキーを追加することによってパーティションキーのユニーク制限を緩和できます。

開始方法 - Amazon DynamoDB | AWS
初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~ | Developers.IO
無料枠で頑張るためにDynamoDBのキャパシティを理解する - ITと筋トレの二刀流

テーブルの設計

パーティションキー:タグ
ソートキー:timestamp

テーブルの作成

AWS Lambda

初めてのサーバーレスアプリケーション開発 ~LambdaでDynamoDBの値を取得する~ | Developers.IO
GitHub Actionsを使ってAWS Lambdaへ自動デプロイ (詳説+デモ手順付きver) - Qiita

2つのタグを紐付けて格納する

Lambda が呼び出されると lambda_handler 関数が実行される

import boto3, time
from decimal import Decimal

def lambda_handler(event, context):
    timestamp = Decimal(time.time())
    table = boto3.resource('dynamodb').Table('collective-intelligence')
    with table.batch_writer() as batch: # 複数putする場合はbatch_writerを使うと良い
        batch.put_item(Item={
            'tag': event['tag1'],
            'related_tag': event['tag2'],
            'timestamp': timestamp
        })
        batch.put_item(Item={
            'tag': event['tag2'],
            'related_tag': event['tag1'],
            'timestamp': timestamp
        })
    return {'statusCode': 201}

指定のタグに紐付くタグを取得

import boto3
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):
    table = boto3.resource('dynamodb').Table('collective-intelligence')
    response = table.query(KeyConditionExpression=Key('tag').eq(event['tag'])) # tag指定で検索
    tags = set(item['related_tag'] for item in response['Items']) # set型に格納して重複を削る
    return {'statusCode': 200, 'body': list(tags)} # JSONレスポンスのためlist型にキャスト

Amazon API Gateway

WebAPIの作成と管理をしてくれる

初めてのサーバーレスアプリケーション開発 ~API GatewayからLambdaを呼び出す~ | Developers.IO
ゼロから作りながら覚えるAPI Gateway環境構築 | Developers.IO
Amazon API Gateway チュートリアル - Amazon API Gateway

リソースとメソッドの作成

/push と /pull で POST を作成

リクエストの検証を設定

Lambdaを実行する前に弾けるとコストが下がってよい

  • モデル(JSON Schema)の定義
  • 設定->リクエストの検証に「本文の検証」を設定
  • リクエスト本文にモデルを設定

JSON Schema Tool
リクエストおよびレスポンスマッピングのモデルおよびマッピングテンプレートを作成する - Amazon API Gateway
APIGatewayの新機能Request Validationを試してみた - エムティーアイ エンジニアブログ

メソッドの作成

create-method.png

メソッドの選択

create-post.png

メソッド管理画面

do-pull.png

PULLモデルの作成

model-pull.png

PUSHモデルの作成

model-push.png

リクエストの検証を設定

request-pull.png

PULL API のテスト

test-pull.png

PUSH API のテスト

test-push.png

利用料金に関して

Billing 画面の請求書から確認します。

まだ本稼働しているわけではないですが、
テストで数百件のリクエスト/レスポンスの送受信を行った結果0円だったので、
お試しで使う分には恐れることはなさそうです。

スクリーンショット 2019-12-05 19.39.49.png

GCP vs AWS

GCP(Firestore)とAWS(DynamoDB)で悩みましたがDynamoDBを採用しました。

GCP側で選択する場合、4つのデータストアサービスから悩むことになると思いますが、
趣味で使うならFirestore一択だと思います。
データベースを選択: Cloud Firestore または Realtime Database  |  Firebase

終わりに

これらはほぼ独学で得た内容ですが、
新しい技術を習得するスキルは会社のモダンな環境で得られたものだと思います。
強いエンジニアが新技術をガンガン使っていく現場で働けるというのは最高の体験です。

また、Heroku側の構成のものを公開しています。
公開時点でデータは空ですが、自由に触ってみてください。
https://collective-intelligence.herokuapp.com/

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

AWS初心者がFirehoseを使ってみたので感想

Fringe81アドベントカレンダー2019の20日目です。
tuaaasa a.k.a ちゃりがお送りします。

始めに述べておきますが、私は半年ほど前からAWSを使い始めた初心者です。

そんな私が先日業務内で、
AWS Kinesisの機能の一つであるKinesis Data Firehoseに触れました。
今回はその感想をまとめます。
CloudFormationを用いてデプロイしたので、合わせてそちらも話します。

Kiensis

https://aws.amazon.com/jp/kinesis/
より以下抜粋

ストリーミングデータをリアルタイムで収集、処理、分析することが簡単になる

また、完全マネージドサービスなので、ストリームデータをKinesisに任せちゃえばいい感じにやってくれます。

本題のFirehose

https://aws.amazon.com/jp/kinesis/data-firehose/
より以下抜粋

ストリーミングデータをデータレイクやデータストア、分析ツールに確実にロードする最も簡単な方法です。
image.png

今回私はRedshiftにロードしました。

嬉しいこと

完全マネージドサービスのおかげで、難しいことを考える必要がなかった

データロードの重複防止とかエラー時のリトライとか、煩わしいことをFirehoseがやってくれる点非常に助かりますね。
また、エラーログも残してくれるのもうれしいです。

つらいこと

Redshiftへのパブリックアクセスを可能にしてあげなければいけない

Amazon Redshift クラスターへの VPC アクセス

これですね。

不正データが1件でも入ってくると直近の正常なデータも全て欠損してしまう

FirehoseでRedshiftにロードする場合、

  1. 一旦S3にアップロード
  2. manifestという複数のデータを一つにまとめたデータの集合を作成
  3. manifestをCOPYコマンドでロード

という形をとっています。
これのおがげで不正データが含まれるmanifestはエラー対象となりロードされないのですね。

対処法

1回COPYコマンドあたり、1万件まではエラー処理させないオプションを追加することで正常なデータのみロードさせました。

スクリーンショット 2019-12-19 16.52.21.png

ただ、1万1件目はエラーとなってしまうのと、
不正データの内容がFirehoseでロギングできないことには注意が必要です。

おまけ

CloudFormationで設定できない項目がある

RedshiftにロードするためのS3アップロードとは別に、
ストリームデータのバックアップができる。

現在()この機能をCloudFormationでは設定できない模様。(以下2019年10月15日に返答されたサポートメールの内容)

Screen Shot 2019-12-03 at 18.06.33.png

おわり

実際に稼働していないので、実績ができたらまた報告しようかなと思います。
ありがとうございました。

参考

CloudWatch LogsのログをprivateなVPCにあるRedshiftに投入
AWS Kinesis Firehoseでログ欠損しないために

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

AWSの起動 templateをCLIで更新

サーバの実行環境はDockerコンテナで管理するが当たり前になった昨今ですが、数年前にサービスインして保守フェーズの案件ではEC2インスタンスで直接サービスを動かす構成は、まだまだ、現役です。

今回は、オートスケーリンググループで起動させるEC2インスタンスを起動テンプレートで管理しており、起動テンプレートのバージョンを上げて、対象のAMIを新しいものに変更する作業を AWS CLIで行います。

使用する環境変数名

今回は AMI と EBSのスナップショットIDを新しくします。

環境変数名 値の意味
TEMPLATE_NAME 更新したいテンプレート名
TEMPLATE_ID TEMPLATE_NAME の launch-template-id
PREV_TEMPLATE_VER 作業時点でのデフォルトバージョン
TEMPLATE_DESC 起動テンプレートの新しいバージョンの説明
AMI_ID 起動テンプレートの新しいバージョンが利用する AMI の ID
EBS_SNAPSHOT_ID 起動テンプレートの新しいバージョンが利用する EBSのスナップショットID

作業コマンド

# 起動テンプレートのIDを取得
$ export TEMPLATE_NAME="my_template"
$ export TEMPLATE_ID=$( aws ec2 describe-launch-templates--launch-template-names ${TEMPLATE_NAME} | \
    jq -r '.LaunchTemplates[].LaunchTemplateId' \
)
$ echo $TEMPLATE_ID
lt-1234567890abcdef
# 起動テンプレートのデフォルトバージョンを取得
$ export PREV_TEMPLATE_VER=$( aws ec2 describe-launch-templates --launch-template-names ${TEMPLATE_NAME} | \
    jq -r '.LaunchTemplates[].DefaultVersionNumber' \
)
$ echo $PREV_TEMPLATE_VER
7
# 今の起動テンプレートをファイルに保存しておく
$ aws ec2 describe-launch-template-versions \
    --launch-template-id ${TEMPLATE_ID} \
    --versions ${PREV_TEMPLATE_VER} \
    > launch-template-befor.json
# AMIを指定して起動テンプレートの新しいバージョンを作成する
export NEW_TEMPLATE_JSON=$(aws ec2 create-launch-template-version \
    --launch-template-id ${TEMPLATE_ID} \
    --version-description "${TEMPLATE_DESC}" \
    --source-version ${PREV_TEMPLATE_VER} \
    --launch-template-data '{"BlockDeviceMappings":[{"DeviceName": "/dev/xvda","Ebs":{"DeleteOnTermination": true,"SnapshotId":"'${EBS_SNAPSHOT_ID}'","VolumeSize": 30,"VolumeType": "gp2"}}],"ImageId":"'${AMI_ID}'"}')
# 新しいバージョン番号を取得
export NEW_TEMPLATE_VER=$(echo ${NEW_TEMPLATE_JSON} | jq -r '.LaunchTemplateVersion.VersionNumber')
# 新しいバージョンをファイルに保存しておく
$ aws ec2 describe-launch-template-versions --launch-template-id ${TEMPLATE_ID} --versions ${NEW_TEMPLATE_VER} > launch-template-after.json

aws ec2 create-launch-template-version の引数 --launch-template-data の値は 公式ドキュメント を参照してください。

差分確認

作業前後で aws ec2 describe-launch-template-versions の結果を比較して、想定した変更差分であることを確認します。

$ diff launch-template-befor.json launch-template-after.json 
6,8c6,8
<             "VersionNumber": 7,
<             "VersionDescription": "change instance to t2.medium",
<             "CreateTime": "2018-12-14T04:19:43.000Z",
---
>             "VersionNumber": 10,
>             "VersionDescription": "update app. ver 1.2.3",
>             "CreateTime": "2019-12-19T06:23:15.000Z",
10c10
<             "DefaultVersion": true,
---
>             "DefaultVersion": false,
21c21
<                             "SnapshotId": "snap-01234567890123456",
---
>                             "SnapshotId": "snap-abcdefabcdefabcde",
40c40
<                 "ImageId": "ami-01234567890123456",
---
>                 "ImageId": "ami-abcdefabcdefabcde",
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Lambda@Edgeについてまとめる

はじめに

Lambda@Edgeを初めて使い、色々戸惑うことが多かったため調べたことをまとめる。
また、都度追加していく。

Lambda@Edgeとは

Lambda@EdgeとはCloudFrontの機能の1つで、CloudFrontのエッジサーバでコードを実行するLambda関数のこと。ユーザーに近い場所でコードが実行されることでアプリケーションのパフォーマンスが向上する。
CloudFrontのイベントをトリガーとしてコードを実行する。

トリガー

上述の通り、CloudFrontのイベントをトリガーとし、イベントがあったCloudFrontと同リージョンで起動する。指定できるCloudFrontのイベントは以下の4つ。

1.ビューワーリクエスト
2.ビューワーレスポンス
3.オリジンリクエスト
4.オリジンレスポンス
スクリーンショット 2019-12-17 19.53.34.png
Lambda@Edgeのトリガーをビューワーリクエストに設定すると、CloudFrontがキャッシュを保有しているかに関わらずビューワーからリクエストがある度にLambdaが起動する。キャッシュがある場合に起動させたくない場合はトリガーを オリジンリクエスト に設定する。

ログ

Lambda@EdgeのログはLambda同様CloudWatchLogsに保存されるが、Lambda関数が起動したエッジサーバがあるリージョンのCloudWatchLogsに保存される。

Lambda@Edgeの作成する際の注意

リージョン

Lambda@Edgeはバージニア北部リージョンでLambda関数を作成する。

作成方法はLambda関数と同様

恥ずかしい話ですが、Lmadba@Edgeを使用する前、Lambda@Edgeというサービスがあるものだと思っていたんですが、Lambda関数をバージニア北部リージョンで作成し、CloudFrontのイベントをトリガーとすることでLambda@Edgeになるということみたい。

IAMロール

Lambda@EdgeにアタッチするIAMロールは、サービスプリンシパル lambda.amazonaws.comedgelambda.amazonaws.com が引き受けることができるロールである必要があるため、以下の信頼ポリシーを記述したIAMロールを作成する。

ロールの信頼ポリシー
{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Principal": {
            "Service": [
               "lambda.amazonaws.com",
               "edgelambda.amazonaws.com"
            ]
         },
         "Action": "sts:AssumeRole"
      }
   ]
}

IAMポリシーを作成

IAMポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "lambda:GetFunction",
                "lambda:EnableReplication"
            ],
            "Resource": "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:<関数名>:<バージョン>"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:UpdateDistribution"
            ],
            "Resource":"arn:aws:cloudfront::xxxxxxxxxxxx:distribution/<ディストリビューションID>"
        }
    ]
}

関数の編集後はCloudFrontにデプロイする

関数を編集して保存後CloudFrontにデプロイして、編集内容が各エッジサーバに反映される。
ちなみに、Lambda関数はバージョン管理しておりデプロイする際に自動でバージョンアップされ、それがデプロイされる。

参考

Lambda@Edge
Lambda@Edge 用の IAM アクセス権限とロールの設定

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

【執筆中】Aurora Data API を TypeScript + typeorm から 使う物語

リンク先として先んじて公開した、執筆中の記事です

概要

特定のツイートのリツイート情報をクローリングするツールを、以下の構成で実装した。
その過程で踏み抜いた、5000兆個くらいある落とし穴の倒し方を書き残す。
踏みすぎて疲れた。

前提と関連記事

Aurora Serverless DB を作って Node.js(TS) から使う」 を前提としています。
Aurora Serverless MySQL(5.6) で日本語データを扱えるようにする」 も設定しています。

typeormとは

TypeScript の class として Entity を定義すると、
自分でSQL書かなくても一通りなんでもできる OR Mapper だよ。
べんりだね!

環境

  • Aurora エンジン Aurora (MySQL)-5.6.10a
  • AWS SDK 2.590.0
  • Node.js v10.15.0
  • npm 6.6.0
  • typeorm 0.2.21
  • typeorm-aurora-data-api-driver 1.1.8
  • Serverless Framework

↓で作った serverless.yml

serverless create --template aws-nodejs-typescript

package.json

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

Aurora Data API を TypeScript + typeorm から 使う物語

概要

Rest API を経由してツイッターのリツイート情報を取得し、
Aurora DB に格納するサンプルツール twitterer を、以下の構成で実装した。

その過程で 5000兆個くらいある落とし穴を踏み抜いたので倒し方を書こうと思ったが、
1日経ったら過程をほとんど忘れたのでちゃんと動く結果を主に書き残す。

前提と関連記事

Aurora Serverless DB を作って Node.js(TS) から使う」 を前提としています。
Aurora Serverless MySQL(5.6) で日本語データを扱えるようにする」 も設定しています。

typeormとは

TypeScript の class として Entity を定義すると、
自分でSQL書かなくても一通りなんでもできる OR Mapper だよ。

TypeORMはNode.js開発のスタンダードになるか?
こちらの紹介記事がわかりやすいと思いました。
べんりだね!

環境

  • Aurora エンジン Aurora (MySQL)-5.6.10a
  • AWS SDK 2.590.0
  • Node.js v10.15.0
  • npm 6.6.0
  • typeorm 0.2.21
  • typeorm-aurora-data-api-driver 1.1.8
  • Serverless Framework

プロジェクトの構成と解説

主要な構成

twitterer/
├── serverless.yml
├── ormconfig.js    // typeormの設定ファイル
├── package.json
├── webpack.config.js
|
├── twitterer-handler.ts
└── src/
    ├── twitterer-express.ts
    ├── twitterer-service.ts
    ├── entities/
    |   └── twitterer-types.ts
    ├── helpers/
    |   └── typeorm-helper.ts
    └── db/    // typeormにより出力されたスクリプトが蓄積される
        ├── migrations/
        └── subscribers/

serverless.yml

基本的には以下でしたときのまま。

serverless create --template aws-nodejs-typescript

ポリシーの設定

Lambda Role から Data API を使うために追加したポリシー。
必要なポリシーセットがわからず苦労していた

serverless.yml
provider:
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "secretsmanager:GetSecretValue"
        - "secretsmanager:PutResourcePolicy"
        - "secretsmanager:PutSecretValue"
        - "secretsmanager:DeleteSecret"
        - "secretsmanager:DescribeSecret"
        - "secretsmanager:TagResource"
      Resource: "arn:aws:secretsmanager:*:*:secret:*"
    - Effect: "Allow"
      Action:
        - "dbqms:CreateFavoriteQuery"
        - "dbqms:DescribeFavoriteQueries"
        - "dbqms:UpdateFavoriteQuery"
        - "dbqms:DeleteFavoriteQueries"
        - "dbqms:GetQueryString"
        - "dbqms:CreateQueryHistory"
        - "dbqms:DescribeQueryHistory"
        - "dbqms:UpdateQueryHistory"
        - "dbqms:DeleteQueryHistory"
        - "rds-data:ExecuteSql"
        - "rds-data:ExecuteStatement"
        - "rds-data:BatchExecuteStatement"
        - "rds-data:BeginTransaction"
        - "rds-data:CommitTransaction"
        - "rds-data:RollbackTransaction"
        - "secretsmanager:CreateSecret"
        - "secretsmanager:ListSecrets"
        - "secretsmanager:GetRandomPassword"
        - "tag:GetResources"
      Resource: "arn:aws:rds:ap-northeast-1:XXXXXXXXXXXX:cluster:XXXXXXXXXXXX"

webpack

そのまんまだと、なぜか typeorm-aurora-data-api-driver がpackされなくてハマったので追記

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: package.json
      forceInclude:
        - typeorm-aurora-data-api-driver

handler

express で受けるので、以下のように書く

serverless.yml
functions:
  twitterer:
    handler: twitterer-handler.v1
    events:
      - http: ANY /
      - http: "ANY /{proxy+}"

package.json

scripts

package.json
  "scripts": {
    "debug": "$(npm bin)/ts-node-dev --clear --respawn ./twitterer-handler.ts",
    "migration:generate": "ts-node $(npm bin)/typeorm migration:generate -n migration",
    "migration:run": "ts-node $(npm bin)/typeorm migration:run "
  },

dependencies

package.json
  "dependencies": {
    "aws-sdk": "^2.590.0",
    "body-parser": "^1.19.0", // expressで意図したrequest bodyを受け取るため
    "cors": "^2.8.5", // web viewから蹴ることも想定して
    "express": "^4.17.1", 
    "serverless-http": "^2.3.0",
    "source-map-support": "^0.5.10",
    "twitter": "^1.7.1",
    "typeorm": "^0.2.21",
    "typeorm-aurora-data-api-driver": "^1.1.8" // typeormからData APIを使える
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.17",
    "@types/express": "^4.17.2",
    "@types/node": "^10.12.18",
    "@types/twitter": "^1.7.0",
    "@typescript-eslint/eslint-plugin": "^2.11.0",
    "@typescript-eslint/parser": "^2.11.0",
    "copy-webpack-plugin": "^5.1.1", // ormconfig.js をpackするために使う
    "eslint": "^6.7.2",
    "eslint-config-prettier": "^6.7.0",
    "eslint-plugin-prettier": "^3.1.1",
    "fork-ts-checker-webpack-plugin": "^3.0.1",
    "prettier": "^1.19.1",
    "serverless-webpack": "^5.2.0",
    "ts-loader": "^5.3.3",
    "ts-node-dev": "^1.0.0-pre.44",
    "typescript": "^3.2.4",
    "webpack": "^4.29.0",
    "webpack-node-externals": "^1.7.2"
  }

webpack.config.js

そのままだと、 ormconfig.js がpackされなくてハマったので、以下のように追記

webpack.config.js
// import 部分
const CopyWebpackPlugin = require("copy-webpack-plugin");

// plugins 部分
  plugins: [
    new CopyWebpackPlugin(["ormconfig.js"]),
  ],

ormconfig.js

ormconfig.js
module.exports = {
  type: "aurora-data-api",

  region: "ap-northeast-1",
  // Aurora Serverless DB の arn
  resourceArn: "arn:aws:rds:ap-northeast-1:XXXXXXXXXXXX:cluster:XXXXXXXXXXXX",
  // DB にアクセスするために作った Secret の arn
  secretArn: "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:XXXXXXXXXXXX",
  // デフォルトでつなぐDB(schema)
  database: "twitterer",

  entities: [__dirname + "/src/entities/**/*.ts"],
  migrations: [__dirname + "/src/db/migrations/**/*.ts"],
  subscribers: [__dirname + "/src/db/subscribers/**/*.ts"],
  cli: {
    entitiesDir: "src/entities/",
    migrationsDir: "src/db/migrations/",
    subscribersDir: "src/db/subscribers/",
  },
};

twitterer-handler.ts

twitterer-handler.ts
import "source-map-support/register";
import { TwittererExpress } from "./src/twitterer-express";

const serverless = require("serverless-http");

export const v1 = serverless(TwittererExpress);

twitterer-express.ts

公開用に加工してます、エラーハンドリングとか適当なので許してちょ。

twitterer-express.ts
import bodyParser from "body-parser";
import cors from "cors";
import express, { Request, Router } from "express";
import { NextFunction, Response } from "express-serve-static-core";
import { RetweetEntity } from "./entities/twitterer-types";
import { TwittererService } from "./twitterer-service";

/**
 * initialize
 */
const { app, r } = (() => {
  TwittererService.init().then();

  const app = express();
  const r: Router = Router();

  app.use(cors());
  app.use(bodyParser.json());
  app.use(r);

  // ローカル実行用
  app.listen("8080", () => console.log(`Start listening on port 8080`));

  return { app, r };
})();
export const TwittererExpress = app;

/**
 * define routes
 */
const P = "/twitterer/v1";
r.post(`${P}/tweets/:tweetId/retweets/clawl`, clawlRetweet);
r.get(`${P}/tweets/:tweetId/retweets`, getRetweets);

/*****************************************************************/

async function clawlRetweet(req: Request, res: Response, next: NextFunction) {
  const { tweetId } = req.params;

  try {
    const retweets = await TwittererService.clawlRetweets(tweetId);
    res.status(200).send(retweets);
    next();
  } catch (e) {
    console.error(e);
    res.status(424 /* failed dependency */).send(JSON.stringify(e));
  }
}

async function getRetweets(req: Request, res: Response, next: NextFunction) {
  const { tweetId } = req.params;

  const retweets = await RetweetEntity.find({ where: { tweetId } });

  const resRetweets = retweets.map(e => ({
    retweetId: e.retweetId,
    userScreenName: e.userScreenName,
    userName: e.userName,
  }));

  res.status(200).send({
    count: retweets.length,
    retweets: resRetweets,
  });
  next();
}

twitterer-service.ts

createConnection() で利用するすべての entities を指定しないと、
ローカルでは動いてもデプロイ後動かなくなる

twitterer-service.ts
import Twitter from "twitter";
import { BaseEntity, Connection, createConnection, getConnection, getConnectionOptions } from "typeorm";
import { TypeormHelper } from "./typeorm-helper";
import { RetweetEntity } from "./entities/twitterer-types";

export class TwittererService {
  /**
   * init aurora connnection
   */
  static async init() {
    // 後述の
    TypeormHelper.patchBug(Connection);

    const connectionOptions = await getConnectionOptions();
    const conn = await createConnection({
      ...connectionOptions,
      // 利用するEntityをこうして書かないと、デプロイ後動かない。
      // (entityがrepositoryに見つかりませんよ、みたいなエラー出る)
      entities: [RetweetEntity],
    });
    BaseEntity.useConnection(conn);
  }

  static get client(): Twitter {
    return new Twitter({
      consumer_key: "XXXXXXXXXX",
      consumer_secret: "XXXXXXXXXX",
      access_token_key: "XXXXXXXXXX",
      access_token_secret: "XXXXXXXXXX",
    });
  }

  static async clawlRetweets(tweetId: string): Promise<RetweetEntity[]> {
    const clawledAt = new Date();

    // 本当は保持していたカーソルの読み込みとかいろいろやってる

    const twitterRes = await this.client.get(`statuses/retweets/${tweetId}.json`, {});
    const retweets: RetweetEntity[] = [];

    for (const e of twitterRes as any) {
      const retweet = new RetweetEntity();

      retweet.tweetId = e.retweeted_status.id_str;
      retweet.retweetId = e.id_str;
      retweet.userId = e.user.id_str;
      retweet.userName = e.user.name;
      retweet.userScreenName = e.user.screen_name;
      retweet.retweetedAt = new Date(e.created_at);
      retweet.clawledAt = clawledAt;
      retweet.rawJson = e;

      retweets.push(retweet);
    }

    await getConnection().transaction(async e => {
      await e.save(retweets);
      // 本当はカーソルの更新とかいろいろやってる
    });

    return retweets;
  }
}

typeorm-helper.ts

何故かデプロイ後動かない というエラーに悩まされた結果たどり着いたソリューション。
めっっっっちゃここでハマった。二度とハマりたくない
patch-package を使おうとしたけどwebpackとの組み合わせに難航したのでモンキーパッチ

ありがとうsdebaun

typeorm-helper.ts
import { EntityMetadata, EntitySchema } from "typeorm";

export class TypeormHelper {
  /**
   * デプロイすると動かなくなる糞バグのモンキーパッチ
   * https://github.com/typeorm/typeorm/issues/3427
   */
  static patchBug(typeormConnection: any) {
    // this is a copypasta of the existing typeorm Connection method
    // with one line changed
    // @ts-ignore
    typeormConnection.prototype.findMetadata = function(
      target: Function | EntitySchema<any> | string
    ): EntityMetadata | undefined {
      // @ts-ignore
      return this.entityMetadatas.find(metadata => {
        // @ts-ignore
        if (metadata.target.name === target.name) {
          // in latest typeorm it is metadata.target === target
          return true;
        }
        if (target instanceof EntitySchema) {
          return metadata.name === target.options.name;
        }
        if (typeof target === "string") {
          if (target.indexOf(".") !== -1) {
            return metadata.tablePath === target;
          } else {
            return metadata.name === target || metadata.tableName === target;
          }
        }

        return false;
      });
    };
  }
}

twitterer-types.ts

typeormではlength指定なしの文字列カラムは varchar(255) となる。
こちらの記事でも触れたように、このままではキーカラム767bytes制限に阻まれて使うことができない。

対策には、

  1. キーカラムの長さを短くするか、
  2. 上記記事の内容と合わせて テーブルに ROW_FORMAT=DYNAMIC を指定する必要がある。

前者はめんどかったので後者で実現しようとしたが、typeorm には ROW_FORMAT を指定できない、なんてことだ
ということでSQLインジェクションをつかって無理やり解決

twitterer-types.ts
import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm";

// typeormには ROW_FORMAT の指定オプションが無いため、 SQL インジェクションを使うクソリューション
@Entity({ name: "retweet", engine: "InnoDB ROW_FORMAT=DYNAMIC" })
export class RetweetEntity extends BaseEntity {
  @PrimaryColumn() tweetId: string;
  @PrimaryColumn() retweetId: string;

  @Column() userId: string;
  @Column() userName: string;
  @Column() userScreenName: string;
  @Column() retweetedAt: Date;
  @Column() clawledAt: Date;
  // AuroraServerless は MySQL5.6 しか使えないため、実際はTEXT型になる
  // (JSON は MySQL5.7から)
  @Column("simple-json") rawJson: any;
}

マイグレーションSQLの生成と実行

npm run migration:generate
npm run migration:run

generateでできるスクリプト

現状のDBの状態とEntityの定義を比較し、差分を埋めるために必要なSQLを作ってくれる。
テーブルのない状態で実行するとCREATE文が生成される
あとはこれをgitで管理して環境ごとに適用したりして便利につかうわけですね

さっきSQLインジェクションした ROW_FORMAT=DYNAMIC もしっかり入ってるね

import {MigrationInterface, QueryRunner} from "typeorm";

export class migration1576639222255 implements MigrationInterface {
    name = 'migration1576639222255'

    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.query("CREATE TABLE `retweet` (`tweetId` varchar(255) NOT NULL, `retweetId` varchar(255) NOT NULL, `userId` varchar(255) NOT NULL, `userName` varchar(255) NOT NULL, `userScreenName` varchar(255) NOT NULL, `retweetedAt` datetime NOT NULL, `clawledAt` datetime NOT NULL, `rawJson` text NOT NULL, PRIMARY KEY (`tweetId`, `retweetId`)) ENGINE=InnoDB ROW_FORMAT=DYNAMIC", undefined);
    }

    public async down(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.query("DROP TABLE `retweet`", undefined);
    }

}

ローカルで実行してみる

npm run debug
curl -X POST http://localhost:8080/twitterer/v1/tweets/<TWEET_ID>/clawl
curl -X GET http://localhost:8080/twitterer/v1/tweets/<TWEET_ID>

デプロイして確かめてみる

sls deploy
curl -X GET https://XXXXXXXX/twitterer/v1/tweets/<TWEET_ID>

まとめ

正直落とし穴踏みすぎて、全部網羅できたか覚えてない。
もし問題あれば教えて下さい。


普段はFirestoreとか使ってるんだけど

  • 小規模ツールでいちいちfirebase プロジェクト増やすのめんどいな
  • やっぱSQL使いたいときもあるよね

ってことで取り組んでみました。

typeorm使うと、自分でSQL書かなくていい!ちょう楽ちん!!
このテンプレートをつかって、今後の開発が爆速になりそう。

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

Datadogを全社的に導入した時にやったこと

本記事は、VISITS Advent Calendar 2019の21日目の記事です。

VISITS Technologiesでインフラエンジニアをしている@syogunです。
本日は、全社的にDatadogを導入したので、何をやったのか、何がよくなったのか等を振り返りたいと思います。
これから、Datadogを導入しようと考えている方に少しでも参考になればと考えています。
それでは参りましょう。

なぜ導入したのか?

私が使いたかったからです!本気です。
弊社エンジニア @ham さんの スタートアップに転職して思ったこと にもありましたが、
スタートアップ企業は、技術選定の自由度が高い印象です。(言うまでもなく個人的な見解です)
もちろん同僚や上司を納得させ、最後までケツを持つ覚悟が必要だと思います。

具体的な理由としては、Datadogだけでかなりの領域の監視がカバーできるという点です。
インフラレイヤー以外にも外形監視、APM、ログ監視等の機能もあり、何かあった時は基本的にDatadog上で調査できる点は非常に魅力的でした。

やったこと

ここからは私が導入時にやったことを振り返っていきます。

Integration

弊社ではAWSやGCPといったクラウドサービス上でシステムが動いています。
AWSしか触ったことのないサーバーエンジニアがGCPで本番運用してみた(前編)
AWSしか触ったことのないサーバーエンジニアがGCPで本番運用してみた(後編)

Datadogでは、AWSやGCPのインテグレーション設定を行うだけで、かなりのメトリクスが取得できます。
今のところすべてのインテグレーションは無料で、種類も豊富なのでおすすめです。

インテグレーション
Datadog インテグレーション一覧(めっちゃあるやん..!)

AWS,GCPのインテグレーション方法に関しては、以下の記事をご参照ください。
Datadog AWS インテグレーション手順
Datadog GCP インテグレーション手順

Multi Organization

Datadogには、Multi Organization機能があります。
弊社では、ideagramdesign thinking testといった複数プロダクトがあるんですが、各プロダクトごとにDatadogのOrganizationを作成して運用しています。
そうすることで、各プロダクトのエンジニアは、自身が開発するOrganizationに参加すればいい状態を作っています。
スクリーンショット 2019-12-12 14.28.17.png
Organization切り替え時に再度認証(私はGoogle認証)が必要なところは若干手間です。改善に期待。

Synthetics(外形監視)

Syntheticsという外形監視機能があります。弊社ではこのSyntheticsの実行結果をSLOの目標値として使用しています。
SyntheticsにはAPIテストブラウザテストの2種類があります。

APIテスト

これはシンプルな外形監視です。
監視対象のURL、ロケーション、コンディション(responstimeやstatus code等を記載)を設定するだけです。
コンディション設定は以下のようなイメージです。
スクリーンショット 2019-12-12 14.45.06.png
実行結果は以下のようなイメージです。
DNSの名前解決時間やTTFBなどの情報が確認できます。
スクリーンショット 2019-12-13 11.37.54.png

ブラウザテスト

ブラウザテストは、ブラウザテストしたいサービスの画面遷移をレコーディングするだけでセットアップできます。
スクリーンショット 2019-12-12 14.48.34.png
また、ユーザIDやパスワードといったアカウント情報もDatadog上に保存可能で、保存したアカウント情報をブラウザテストが使用してログインするといったことも可能です。
なお、ブラウザテストはAPIテストと比較してお値段が少々お高いので、実行回数にはお気をつけください。
料金はこちら

APM

APMを導入することで、インフラのメトリクスやログだけだと分からなかったアプリケーションレイヤーのボトルネックを特定するのに役立っています。
スクリーンショット 2019-12-12 15.17.54.png
弊社CTO @mayahアドベントカレンダーでもご紹介したのですが、 Kaizen Week と称して1週間を技術的負債を集中的に取り除く活動を定期的に開催しています。
そのKaizen Weekで弊社エンジニア @kshibata101 さんがDatadog APMを活用したパフォーマンスチューニングを行い、Best KAIZEN Awardを受賞しました。
また、APMはアプリケーションのエラーメッセージも確認できるので、トラブルシューティング時にとても役立ちます。
スクリーンショット 2019-12-16 17.14.39.png

APMの導入方法に関しては、以下の記事をご参照ください。
Instrument your application

また、APMに関しては私を含め弊社メンバーがQiitaに記事を投稿してます。お時間ありましたら、そちらも併せてご参照ください。
GoのアプリケーションにDatadogAPMを導入する。
GAEにDatadog APMを導入する

Infrastructure

イングレーションのところでもご紹介しましたが、ALBやRDSといったメトリクスがモリモリ取得できます。
こちらのメトリクスを基にアラートが柔軟に設定可能です。

Containers

一部環境ではAWS FargateやGKEを導入しているんですが、コンテナの監視に強いのもDatadogの魅力です。
GKEの場合は、DeaemonSetでDatadogをデプロイ、Fargateの場合はSideCar方式でデプロイすればそれぞれコンテナのメトリクスが取得できます。
スクリーンショット 2019-12-13 12.50.16.png

それぞれの導入方法に関しては、以下の記事をご参照ください。
How to monitor Google Kubernetes Engine with Datadog
Monitor AWS Fargate applications with Datadog

Serverless

Lambda等のメトリクスもインテグレーションを有効化するだけで自動で取得します。
インテグレーション設定1つでメトリクスが取得できるのは魅力ですね。
スクリーンショット 2019-12-13 12.59.44.png
ContainerやServerlessの料金は、こちら

Monitors

Managed Monitors

アラートはMonitorsから設定可能で、あらゆる情報(メトリクス、ログ、イベント等)からアラートの設定できます。
従来のシンプルな閾値監視(例えばCPU使用率90%以上とか)のみでなく、
Anomaly DetectionRorecast Alertといった機械学習を使用した閾値も設定できることも魅力です。
スクリーンショット 2019-12-13 11.45.15.png
閾値を固定化しづらいシチュエーションや、いつもと違う動きを検知したいといったニーズを満たせそうです。
このあたりは、
Anomaly Detection
Outliers Alert
Forecast Alert
に詳しく書いてありますので、よかったらご参照ください。
今後こういった技術がどんどんブラッシュアップされ、人間がアラート設定しなくなる未来が私には見えました。

SLO

Datadogを導入するにあたって、各プロダクトごとにSLI/SLOを決めて、それに沿った監視を行うことにしました。
SLI/SLOに関しては、本題から少し逸れるため詳しくは記載しませんが、

Syntheticsによる5分毎のAPI Testが以下の条件を満たすこと
・レスポンスタイム:Response Timeがxxx msで完了すること
・ステータスコード:200を返すこと
・上記条件を正常とみなし、可用率が99.9%以上となること。

といった感じで各プロダクトのエンジニアとPMと相談しながら決めました。(このあたりはまだ手探りです)

DatadogにはSLO機能があり、SLOの達成状況を可視化できるため、活用しています。
何をSLIにするかを設定し、目標とする稼働率(SLO)やSLO対象期間を設定することで、SLO達成状況が可視化できます。
また、SLOに基づいたError Budgetも出るので、リリース計画に参考にもできそうです。
例えば、「プロダクトAはError Budgetが残り少ないので今スプリントは、新規機能リリースはやめて改善タスクやバグチケットを中心にやっていこう」といった会話も可能できそうです。
スクリーンショット 2019-12-12 14.36.22.png

Log Management

Log Managementは、さまざまなログを集約できます。
なんといっても便利なのは、使いやすいフィルタリング機能ではないでしょうか。
ログはjson化しておけば、Datadog側でよろしくParseしてくれますし、必要に応じてDatadog側でParse処理も書けたりするので柔軟に対応可能です。
また、ログから独自のメトリクスも生成できるようなので便利ですね。
以下、いくつか活用事例をご紹介します。

ALBやGCLBのログ

AWSのALBやGCPのGCLBのアクセスログをLog Managemenで可視化できます。もちろんこのログを基にアラートを設定することも可能です。
スクリーンショット 2019-12-13 13.20.22.png
注意点としては、アクセス数が多いサービスの場合、S3->Datadogの転送料金が以外にかかってしまうので要注意です。
Datadog側でログをアーカイブするLog Archivesという機能があるので、保存期間を短くしてコストカットする手もあるかと思います。

導入方法に関しては、以下の記事をご参照ください。
Amazon Load Balancer
Collect Google Stackdriver logs with Datadog

アプリケーションのログ

アプリケーションのログもLog ManagemenでParseしてくれます。
例えばGAEを使用している場合、Stackdriverに出力されたログをそのままLog Managemenに投げるだけでいい感じにParseしてくれます。
スクリーンショット 2019-12-13 13.27.16.png
また各ログをクリックすると、こんな感じで詳細を確認できます。
スクリーンショット 2019-12-13 13.29.25.png

Slow Queryのログ

RDSのSlow QueryもParseしてくれます。
Log ManagemenにSlow Queryログを転送することによって

  • どういった種類のSlow Queryが一番多いのか?
  • 一番実行時間が長いSlow Queryはどれか?
  • どの時間帯にSlow Queryが発生しているのか?

といった情報が可視化できます。Queryの実行時間や種類でフィルタリングできるのですごく便利です。
スクリーンショット 2019-12-13 13.37.20.png

JavaScriptのログ

これはまだテスト運用中ですが、JavaScriptのログもLog Managemenに転送しています。
こうすることで、今まではDatadogで検知できなかったFrontendのエラーも検知できるようになります。
スクリーンショット 2019-12-13 13.40.22.png
JavaScriptの導入方法に関しては、以下の記事をご参照ください。
Monitor JavaScript console logs and user activity with Datadog

この記事を書いている途中でIntroducing Datadog Real User Monitoringなる機能がローンチされて、実はそっち方がいいじゃないかと思ってます。またさわる機会があれば記事書きたいと思います、

Dashboards

Datadogの魅力の一つはDashboardが柔軟に作れるという点ではないでしょうか。
例えば、イングレーション設定するだけで、以下のようなDashboardが自動的に作成されます。便利です。
スクリーンショット 2019-12-16 18.19.58.png
新規Dashboard作成は、結構センスのいる作業(そして私はセンスなし)ですが、一度作ってコツが掴めけばある程度は作れる用になる気がします。
DatadogのDashboardには、TimeboardScreenboardの2種類があります。
スクリーンショット 2019-12-13 13.56.15.png
これどういう使い分けをするのか?最初は分からなかったんですが、今は自分なりにこう理解しています。(私がそう思っているだけです)

Screenboard

ステータス確認用のDashboard。
具体的には、サービスが生きているのか死んでいるのか、KPIやSLOを達成ているのかしていないのか等。
Screenboardは、現在のステータスを一目で確認できるDashboardという位置付けにしています。
Screenboardでは、何か異常があった時に何かが起こっていることは分かりますが、具体的に何が原因なのかは分かりません。
弊社では以下のようなDashboardにしてます。
スクリーンショット 2019-12-13 14.01.37.png

Timeboard

Timeboardはトラブルシューティング用のDashboardで、Screenboardで何かが起こっていることを検知して、実際に何が起こったのかを調査するためのDashboardという位置付けにしています。
このDashboardはとても便利、ALB、EC2、RDSといった別々のメトリクスを一画面でかつ同じ時間軸で見れるので相関関係が確認しやすいです。
私のお気に入りは、Datadogがアラート名で検索すると、そのアラートが発生した時間とメトリクスをオーバーレイしてくれます。
スクリーンショット 2019-12-13 18.11.10.png
詳細は、以前私が書いたDatadogのDashboard(Timeboard)が便利!もご参照ください。

コード化(terraform化)

Datadogは、Terraformによってコード管理できます。
弊社ではGCPで似たような構成をしているプロダクトがいくつかあり、その場合Terraformでコード化しておくことで、MonitorやDashboardを簡単に複製できる仕組みを整えています。
このあたりは、気力があればまた書きたいと思います。

導入方法に関しては、以下の記事をご参照ください。
Managing Datadog with Terraform

社内布教活動

監視は一度作ったら終わりではなく、開発メンバーでPDCAを回して継続して改善していく必要があります。
私の場合、Datadogのセットアップは基本1人で進めてきたため、私以外のエンジニアがDatadogをさわる機会がなかったので、いくつか社内布教活動を行いました。
以下、事例です。

エンジニア勉強会

弊社では週1回、エンジニアの勉強会を行なっているため、そこで勉強会を開催しました。
Datadogは、多機能がゆえにメニューが豊富なので、どこを見ればいいんだ?ってなりがちです。
勉強会ではDatadogで見て欲しいポイントや機能を絞って、使い方を中心にレクチャーしました。

MTGでアラートを振り返る

週次開催されるプロダクトMTG等で、Datadogが発報したアラートから気になるものがあれば議題にあげて、開発メンバーに確認してもらうようにしています。(全プロダクトではないです)
このアラートは既知なのか?既知であれば対応中のものなのか?このままアラートとして出し続けて良いものなのか?等を議論し、アラートが狼少年化しないよう日々PDCAを回しています。

アラートが来たら騒ぐ

Datadogのアラートを設定した私は何のアラートなのか予想がつきますが、その他の開発メンバーは(特に最初の方は)内容を理解できていないことが多いと思います。
私の役目はDatadogからアラートがきたら、アラートの内容を確認して、確認した結果をコメントするようにしています。
そうすることで、(心優しい)開発メンバーがエラー内容について補足してくれたりして、今後対応方針をその場で決めたりしています。

KPT

ここからはKPTで振り返りを。

Keep

  • インフラからフロントエンドまで異常を検知できる仕組みが整いつつある。
  • アラートを日々改善することで徐々にノイズは減ってきている。(狼少年化しないことが大事)
  • 私以外のエンジニアも監視に携わる機会が増えてきた。
  • Datadogの開発速度が早く、新機能を試したくなる。楽しい。
  • 監視をDatadogに統一することで、学習コストが減った気がする。

Problem

  • (Datadogの話から少し逸れますが)SLOの運用がうまくいっていない。
  • Datadog自体のコストの監視ができていない。
  • アラート自体、まだまだ改善の余地はある。

Try

  • SLOのあり方や具体的な数値については、もう1度PMや開発メンバーとMTGした方が良さそう。またSLOの定期的な見直しや運用ルールの明確化が必要かも。
  • (特に)ログはアクセス数が増えるにしたがってコストがかさむので、何からコストをチェックできる仕組みを作る。
  • 継続してアラートのチューニングやアプリケーションの改修を行い、本当に必要なアラート(何かしらのアクションが必要なもの)だけが飛ぶようにしていく。

まとめ

今回、全社横断的にDatadogを導入したことで、各プロダクトで最低限の監視すべてきものがある程度標準化できたのではないかと考えています。そこから各プロダクト固有の監視を追加、アップデートしていけば、より良い監視に近づいていける気がします。
また、今では調査に必要な情報はDatadogに集約されているため、 「何かあればDatadogだけを見れば分かる」という状態ができつつあります。これはトラブルシューティング時の初動が早くなり、操作にも慣れているので問題の特定が早くなると言う効果があるんじゃなかろうか?とは感じています。
Datadog自体の開発速度も早く、監視がどんどん便利になっている感があり、よりクリエティブな開発に集中したい方にはオススメのツールでした。
まだまだ書きたいことはありましたが、またの機会にしたいと思います。
ではまた!!

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

Aurora Serverless MySQL(5.6) で日本語データを使えるようにする

文字コードを変えよう

Aurora Serverless を作ってみたものの、
latinという文字コード(character set)が標準で
日本語や?を扱えなかったので使えるようにする。

環境(2019/12/19)

  • Aurora Serverless
    • MySQL
    • aurora 5.6

設定方法

パラメータグループを作成

RDS > パラメータグループ
画像はAurora Serverless むけの設定なので注意。

パラメータを設定

上部の「パラメータを編集」を押下し、以下のように設定していく

image.png

character_set

ただの utf8 だとこういうふうになるので、utf8mb4 を選択。

character_set_client = utf8mb4
character_set_connection = utf8mb4
character_set_database = utf8mb4
character_set_results = utf8mb4
character_set_server = utf8mb4

index column size の拡張(optional)

utf8mb4 を設定して、 varchar(255) のキーカラムとか作ると
mysql Index column size too large. The maximum column size is 767 bytes.
と、こういう理由で怒られる。

そんな長いキーカラム作るなよって話もあるんだけど、
そういうの気にせずに typeorm から使いたいのでその制限を拡張する。

innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix = 1

パラメータグループを反映

対象のデータベースの「変更」から、作成したパラメータグループを選択して保存
しばらくすると反映されるはず

結果の確認

show variables like "character%" すると見える

なんか utf8mb4 じゃないけどまあ問題なく使えてるから大丈夫なんでしょう。。

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

Aurora Serverless MySQL(5.6) で日本語データを扱えるようにする

文字コードを変えよう

Aurora Serverless を作ってみたものの、
latinという文字コード(character set)が標準で
日本語や?を扱えなかったので使えるようにする。

環境(2019/12/19)

  • Aurora Serverless
    • MySQL
    • aurora 5.6

設定方法

パラメータグループを作成

RDS > パラメータグループ
画像はAurora Serverless むけの設定なので注意。

パラメータを設定

上部の「パラメータを編集」を押下し、以下のように設定していく

image.png

character_set

ただの utf8 だとこういうふうになるので、utf8mb4 を選択。

character_set_client = utf8mb4
character_set_connection = utf8mb4
character_set_database = utf8mb4
character_set_results = utf8mb4
character_set_server = utf8mb4

index column size の拡張(optional)

utf8mb4 を設定して、 varchar(255) のキーカラムとか作ると
mysql Index column size too large. The maximum column size is 767 bytes.
と、こういう理由で怒られる。

そんな長いキーカラム作るなよって話もあるんだけど、
そういうの気にせずに typeorm から使いたいのでその制限を拡張する。

innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix = 1

拡張したindex column sizeを使うために

CREATE文で、 ROW_FORMAT=DYNAMIC を指定する必要があった。

CREATE TABLE `retweet` (`tweetId` varchar(255) NOT NULL, ...) ENGINE=InnoDB ROW_FORMAT=DYNAMIC

パラメータグループを反映

対象のデータベースの「変更」から、作成したパラメータグループを選択して保存
しばらくすると反映されるはず

結果の確認

show variables like "character%" すると見える

なんか utf8mb4 じゃないけどまあ問題なく使えてるから大丈夫なんでしょう。。

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

AWSでYOLOv3を動かす

目的

AWS上でYOLOを動かす。
AWSの知識があまりないため、解決策の1例としてご覧ください。

使用したもの

[YOLO] https://github.com/ptxyasu/keras-yolo3
[AWS] EC2 https://aws.amazon.com/jp/ec2/

流れ

1.AWSに環境構築
2.AWSに必要なデータ設置
3.yolo実行

AWS EC2について

今回は開発環境と一致させるため、Ubuntu 16.04のAMIを選択。
インスタンスタイプは無料枠のt2.microとした。

問題1 ボリューム不足

環境構築を行い、keras-yolo3をcloneしようとするとボリュームがないというエラーが発生。

解決策1 EBSのボリュームの変更

ボリュームのサイズを8GiBから少しずつ変更。
6時間に1回程度しか変更できないことに注意。しかし、増やすだけ料金がかかることにも注意!!
結果的に20GiBまで増やした。

問題2 メモリ不足(おそらく)

エラー文を調べてもあまり情報は出てこず。

解決策2 インスタンスタイプの変更

t2.microの仮想CPU1つ、メモリ1GiBが問題かなと感じた。
そこでインスタンスタイプをt2.microからt3a.2xlargeへ変更。
これに関してはt2.largeやt2.xlargeへの変更でも良かったかも。
料金がすごく高くなってしまうので注意!

問題3 yolo実行でのエラー

エラー文を読むとおそらく以下が問題。
AWS内にfontが存在しないのだと思う。

yolo_video.py
font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
                    size=np.floor(8e-3 * image.size[1] + 0.5).astype('int32'))

解決策3 プログラムの変更

今回検出画像ではなく、クラス名とその座標が得られればいいので描写に関する部分は消すことで対応。

yolo_video.py
def detect_image(self, image):
        start = timer()

        if self.model_image_size != (None, None):
            assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required'
            assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required'
            boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
        else:
            new_image_size = (image.width - (image.width % 32),
                              image.height - (image.height % 32))
            boxed_image = letterbox_image(image, new_image_size)
        image_data = np.array(boxed_image, dtype='float32')

        print(image_data.shape)
        image_data /= 255.
        image_data = np.expand_dims(image_data, 0)  # Add batch dimension.

        out_boxes, out_scores, out_classes = self.sess.run(
            [self.boxes, self.scores, self.classes],
            feed_dict={
                self.yolo_model.input: image_data,
                self.input_image_shape: [image.size[1], image.size[0]],
                K.learning_phase(): 0
            })

        print('Found {} boxes for {}'.format(len(out_boxes), 'img'))

        for i, c in reversed(list(enumerate(out_classes))):
            predicted_class = self.class_names[c]
            box = out_boxes[i]
            score = out_scores[i]

            predicted_value = self.get_concentration(predicted_class)
            value = max(value,predicted_value)

            label = '{} {:.2f}'.format(predicted_class,score)

            top, left, bottom, right = box
            top = max(0, np.floor(top + 0.5).astype('int32'))
            left = max(0, np.floor(left + 0.5).astype('int32'))
            bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32'))
            right = min(image.size[0], np.floor(right + 0.5).astype('int32'))
            print(label, (left, top), (right, bottom))

            if top - label_size[1] >= 0:
                text_origin = np.array([left, top - label_size[1]])
            else:
                text_origin = np.array([left, top + 1])

            # My kingdom for a good redistributable image drawing library.

        end = timer()
        print(end - start)
        return image

    def close_session(self):
        self.sess.close()

問題4 物体が検出されない

これまでの方法を行うことで、yoloの実行まではできた。
しかし、 "Found 0 boxes for img" となり、物体が認識されない、、

解決策4 

サーバーに置く学習モデルのミス? → モデルには問題なし
画像やプログラムを開く際の権限? → 権限を与えても変化なし
開発環境が複数GPUに対し、AWSでは複数CPU → 本来関係なさそうだが少し影響あるのかも(要検証)

結果

実行までは至ったが、物体が検出されず。
ボリュームの拡大とインスタンスタイプの変更で料金が5000円程度も上がってしまったため、インスタンスを削除。

検出されない原因の解明と最適なインスタンスタイプの検討をしてからまた取り組みます。
エラーの心あたりある方いたらコメント欲しいです!お願いいたします。

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

WindowsとUbuntuでAWS Tools for PowerShellを構成する手順書

WindowsとUbuntu on WSL上でAWS Tools for PowerShellを構成したので、最小限の手順書にまとめてみた。基本的に公式情報を見てという感じだけど、順番にどこを見てなにをすればよいかを示し、OSごとにちょっと変わる部分(PowerShellのデフォルトの実行ポリシーとか)に言及したつもり。

公式のドキュメントではなく、現時点の情報を元にした内容であり、as isの形でご利用いただける方向けに投稿しておきます。右上の歯車マークから「Markdownで本文を見る」をすると元のテキストが採れるのでコピペに便利だと思います。

概要

PowerShellはMicrosoft社の開発したシェル環境およびスクリプト言語です。PowerShell 6からはオープンソース化され、LinuxやMac OSでも動作するようになりました。またMicrosoft Azure、Amazon Web Service、Google Cloud Platform、VMwareなどが公式の管理ツールをPowerShell用のモジュールとして提供しています。

ここでは、AWSをPowerShellから操作できるように、以下の流れでAWS Tools for PowerShellを構成します。

  1. OSごとの手順で、PowerShellをインストールします。
  2. PowerShell上で、AWS Tools for PowerShell( AWSPowerShell.NetCore モジュール)をインストールします。

PowerShellのインストール

以下のOSごとの手順で、PowerShellをインストールします。

WindowsへのPowerShellのインストール

WindowsへのPowerShellのインストール方法は「Windows への PowerShell Core のインストール - PowerShell | Microsoft Docs」を参照してください。
通常は、以下を行います。

  1. リリースページLatest releaseから PowerShell-x.x.x-win-x64.msi ファイルを取得
  2. 取得したファイルを実行し、ウィザードに従ってインストールを完了

LinuxへのPowerShellのインストール

LinuxへのPowerShellのインストール方法は、「Linux への PowerShell Core のインストール - PowerShell | Microsoft Docs」を参照してください。
例えば、Ubuntu 18.04では以下を行います。

  1. 上記ページの「Ubuntu 18.04」の項を参照する
  2. 記述に従い、Microsoftの universe リポジトリを使用可能にする
  3. 記述に従い、 sudo apt-get install -y powershell でPowerShellをインストールする

MacOSへのPowerShellのインストール

MacOSへのPowerShell のインストール方法は、「macOS への PowerShell Core のインストール - PowerShell | Microsoft Docs」を参照してください。

AWS Tools for PowerShellの構成

本手順は、PowerShellでAWSを管理する際に必要な準備です。

AWS Tools for PowerShellのインストール

PowerShellのコンソールを起動します。

  • Windowsでは、スタートメニューから PowerShell > PowerShell 6 (x64) を選択します。
  • MacOS、Linuxでは、シェル環境から pwsh を実行します。

まず外部から取得するスクリプトやモジュールを、署名付きのものであれば実行できるようになっているか、実行ポリシーを確認します。PowerShell上で Get-ExecutionPolicy を行います(なお PS> はPowerShellのプロンプトを表わしています)。

PS> Get-ExecutionPolicy
RemoteSigned

実行ポリシーが RestrictedAllSigned の場合はインターネットからダウンロードされたスクリプトおよび構成ファイルが実行できないので、以下を実行して RemoteSigned に変更します。すでに RemoteSigned またはそれより寛容なポリシー(例えばUbuntuでは通常 Unrestricted )の場合は、変更不要です。

PS> Set-ExecutionPolicy RemoteSigned

またAWS Tools for PowerShellの配布リポジトリであるPowerShell Galleryが信頼済みか確認します。

PS> Get-PSRepository                                                                                   
Name                      InstallationPolicy   SourceLocation
----                      ------------------   --------------
PSGallery                 Untrusted            https://www.powershellgallery.com/api/v2

Untrusted の時は、以下を実行して信頼済みにします。

PS> Set-PSRepository -Name PSGallery -InstallationPolicy Trusted

これでインストールできる環境が整ったので、Install-Module コマンドにより、AWS Tools for PowerShell Coreをインストールします。

PS> Install-Module -Name AWSPowerShell.NetCore -AllowClobber

AWS Tools for PowerShell Coreは、自動では読み込まれません。Import-Module コマンドにより読み込みます。その後 Get-Module によりAWS Tools for PowerShell Coreを読み込めていることを確認しておきます。

PS> Import-Module AWSPowerShell.NetCore
PS> Get-Module AWS*

ModuleType Version Name                  ExportedCommands
---------- ------- ----                  ----------------
Binary     4.0.1.1 AWSPowerShell.NetCore {Add-AASScalableTarget, Add-A…

なお、PowerShell環境ではコマンドレット名やファイル名はもちろん、引数でも補完機能が利用できること多くあります。 Import-Module ではモジュール名をすべて入力する代わりに AWS まで入力してTABキーを押下すると、このモジュール名が補完されて出てくるはずです。

参照

以下を元情報としています。本手順が古くなった場合や正しく動かないと思われたときには、こちらをご参照ください。

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

AWS CLIでAmazon Forecastの全リソース削除

備忘録的な投稿。順番に消していかないと削除できないのでCLIでやることにした。

Forecastを削除

for i in `aws forecast list-forecasts --max-items 100 --profile hoge | jq -r '.Forecasts[].ForecastArn'`
  do aws forecast delete-forecast --forecast-arn ${i} --profile hoge
done

jq -rじゃないとダブルクォートを認識してしまって上手く行かないので注意。

Predictorを削除

for i in `aws forecast list-predictors --max-items 100 --profile hoge | jq -r '.Predictors[].PredictorArn'`
  do aws forecast delete-predictor --predictor-arn ${i} --profile hoge
done

DatasetImportJobを削除

for i in `aws forecast list-dataset-import-jobs --max-items 100 --profile hoge | jq -r '.DatasetImportJobs[].DatasetImportJobArn'`
  do aws forecast delete-dataset-import-job --dataset-import-job-arn $i --profile hoge
done

Datasetを削除

for i in `aws forecast list-datasets --max-items 100 --profile hoge | jq -r '.Datasets[].DatasetArn'`
  do aws forecast delete-dataset --dataset-arn $i --profile hoge
done

DatasetGroupを削除

for i in `aws forecast list-dataset-groups --max-items 100 --profile hoge | jq -r '.DatasetGroups[].DatasetGroupArn'`
  do aws forecast delete-dataset-group --dataset-group-arn $i --profile hoge
done

これで全部消せたはず。なお、サクッとコンソール上で消せるのでS3に上げたデータは手で削除した。
参考ページ

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

Amazon SageMaker Studio が起動できなかった時の対処法

はじめに

Amazon SageMaker Studio が AWS re:Invent 2019 でプレビューリリースとなりました。
さあ、早速検証!と思いましたが、SageMaker Studio が開けないエラーが発生しました。
再起動を試そうにも、SageMaker Studio の削除方法がわからず...

実は、現時点(2019/12/18時点)では、コンソール上からSageMaker Studio の作成し直し等はできません。
そのため AWS CLI を利用して、SageMaker Studio を削除する必要があります。
domain の削除にもいくつか手順があったので、今回はその方法をご紹介いたします。

SageMaker Studio の削除方法

私の場合は以下の画面から動けなくなり、Restart Now を押しても、起動ができない状況でした。

2019-12-17_15h40_54.png

SageMaker Studio のドメインに関連するアプリケーションが存在するか確認し削除

関連するアプリケーションが存在することで、ドメインが削除できない場合があります。
以下のコードでアプリケーションの有無を確認します。

aws sagemaker list-apps --region us-east-2

もし、関連するアプリケーションがあった場合は、以下のコードでアプリケーションを削除します。

aws sagemaker delete-app --domain-id <domain-id> --user-profile-name <user-profile-name> --app-type <app-type> --app-name <app-name> --region us-east-2

SageMaker Studio のドメインに関連するユーザープロファイルが存在するか確認し削除

関連するユーザープロファイルが存在すると、ドメインが削除できません。
以下のコードで ユーザープロファイルの有無を確認します。
※ユーザープロファイルは、コンソールからも確認できます。

image.png

aws sagemaker list-user-profiles --region us-east-2

関連するユーザープロファイルがある場合には、以下のコマンドで削除します。

aws sagemaker delete-user-profile --domain-id <domain-id> --user-profile-name <user-profile-name> --region us-east-2

SageMaker Studioの削除

これでようやく、Sagemaker Studio 削除ができます。
以下のコマンドを実行し、SageMaker Studio を削除します。

aws sagemaker delete-domain --domain-id <domain-id> --region us-east-2

最後に

これで、SageMaker Studio を削除することができました。
SageMaker Studio をまた利用したい場合は新たに SageMaker Studio を立ち上げてください。

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

AWSアカウント内で特定CIDRが含まれているセキュリティグループを検索するCLIコマンド

これは何

  • 表題の通りです。

AWS CLIコマンド

https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-security-groups.html

以下のコマンド。ドキュメントに記載のある通り、 Values=xxx.xxx.xxx.xxx/yyには対象のCIDRを指定する。Values=xxx.xxx.xxx.xxx/yy,xxx.xxx.xxx.xxx/yyのように複数指定可能。

$ aws ec2 describe-security-groups \
    --filter "Name=ip-permission.cidr,Values=xxx.xxx.xxx.xxx/yy" \
    --query "SecurityGroups[*].[GroupName]" --output text

オプションの --filter--query--output については以下を参照。

https://dev.classmethod.jp/cloud/aws/aws-cli-filter-and-query-howto/
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-usage-output.html

--query の記法はJMESPath。以下を参照。

http://jmespath.org/

何をやっているのか

取得範囲のフィルタリング

--filter オプションで「特定CIDRが含まれているセキュリティグループ」する

グループ名の一覧を取得

--filter だけだと出力にセキュリティグループの各種属性が含まれたままになる。必要なのはセキュリティグループの名前だけなので、--query オプションで GroupName キーの値のみを抽出する。

出力形式の指定

--output text と指定すれば、プレーンテキストとしてデータが出力される。

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

AWSでサーバレスな定期バッチ環境を作るには結局どれ使えばいいの?(Lambda vs Fargate vs Batch)

LIFULLで技術マネージャーをしています。たまにはエンジニアぽいところを見せてほしいという社内からのプレッシャー激励に促されて、AWSにおける知見を整理して共有します。

はじめに

システムを運用する上で、日次や月次の決まったタイミングで動作する定期バッチを必要とするケースがあると思います。定期実行なので、インフラリソースをそのタイミングだけ利用するサーバレスなバッチを作るのが経済的にも地球環境的にも優しいですよね。

ということで、AWSのサービスを利用して実現しようというお話なんですが、選択肢の多さに結局どれを使えばいいんだっけ?ってことになりかねません。そこで考えられる選択肢として LambdaFargateBatch の3つを取り上げて、定期バッチ環境を作る上でのメリット、デメリットをまとめたいと思います。

サービス比較

記事投稿時点での特徴を独断でまとめたものがこちらです。

Lambda Fargate Batch
環境構築の容易さ
実行環境の拡張性 ×
マシンリソースの拡張性 ×
機密情報の扱いやすさ ×
スケジューリング設定
ロギング設定

それでは、一つずつ説明していきます。

環境構築の容易さ

Lambdaはコードをアップロードさえすれば、実行環境が構築でき、その導入障壁の低さが魅力と言えます。

FargateBatchはコンテナベースのコンピューティングサービスであり、事前にバッチアプリケーションを含んだDockerイメージを用意しておく必要があります。

実行環境の拡張性

Lambdaは標準で使用できるランタイム(プログラム言語とバージョン)に限りがあります。ただ主要言語はほぼサポートされており、カスタムランタイムも作成できるため、さほど不自由さを感じないのではないでしょうか。
また、そのままだと標準ライブラリしか使えないため、 Serverless FrameworkAWS SAM を使用して、ローカルの実行環境をパッケージ化してデプロイする運用が一般的かと思います。ただ、OS依存のネイティブライブラリの導入にはさらに手間がかかるなど、拡張性と利便性がトレードオフであるといえます。

FargateBatchは前述の通り、あらかじめ自前で用意したコンテナ上でアプリケーションを動作させるため、自由度の高い実行環境を構築することが可能です。

マシンリソースの拡張性

Lambdaは実行時間に制限があるというのは有名な話ですね。またメモリ割り当てなど、さまざまなリソースに制限が適用されており、変更することができません。

※ 詳しくは AWS Lambdaの制限 を参照してください。

FargateはCPUとメモリの割り当て可能な組み合わせを選択して利用します。現在の利用可能範囲は 0.25vCPU × 512MBメモリ 〜 4vCPU × 30GBメモリ となっています。
また、ストレージはコンテナのルートボリューム(10GB)とマウント利用するコンテナ間共有ボリューム(4GB)の2種類が利用可能ですが、この容量制限を変更することはできません。

※ 詳しくは AWS Fargte - タスクCPUとメモリ | AWS Fargte - タスクストレージ を参照してください。

Batchはあらかじめ設定したコンピューティング環境でジョブを実行するサービスですが、そのリソースはEC2インスタンスになります。こちらはT系以外の全てのインスタンスを選択可能です。
また、ストレージはデフォルトでコンテナのルートボリューム(10GB)が利用可能とFargateと同じです。これはDockerのベースサイズのデフォルト値が10GBであることに起因するのだと思いますが、この容量制限はFargateと違い拡張することが可能です。拡張方法は Qiita - AWS Batch で使う EC2 インスタンスのストレージ容量を増やす の記事を参考にしてください。

機密情報の扱いやすさ

3サービス共通して、環境変数を設定してアプリケーション側で利用できる仕組みを提供しています。ただし、パスワードやAWSアクセスキーなどの機密情報を環境変数としてプレーンテキストで取り扱うことは推奨されていません。ここでは機密情報の扱いやすさという軸で比較してみます。

LambdaAWS KMS による環境変数の暗号化と復号化の仕組みを提供しています。ただし、暗号化の設定がCouldFormationに対応していなかったり、アプリケーション側に復号化の実装が必要だったりと使い勝手がいいとは決して言えません。

FargateParameter Store に登録したSecureStringをタスク起動時に復号化して、環境変数に設定してくれるという非常に便利な機能を提供してくれています。これだけでもFargateを選択する価値はあると私は思っています。

※ 詳しくは Developers.IO - ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました! を参照してください。

Batchは機密情報を取り扱うための仕組みが今のところサポートされていないようです。自前でParameter Storeや AWS Secrets Manager に登録した機密情報をアプリケーション側で復号化して使用するといった作り込みが必要になります。

スケジューリング設定

3サービス共通して、CloudWatch Eventsと紐づけることで、定期実行するためのスケジューリング設定が可能です。またCloudFormationでのスケジューリング設定も可能です。(長らくBatchはスケジュール設定がCFn未対応でしたが、本記事を執筆している時点では有難いことに対応してました)

ロギング設定

Lambdaはアプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。ロググループは/aws/lambda/<function name>固定となります。

Fargateにはログドライバーの仕組みがあり、これによって柔軟にログ出力先を設定することができます。awslogsログドライバーを使用すれば、アプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。CloudWatch Logsのロググループ名も任意に指定できます。

※ 詳しくは AWS Fargate - ログ記録 を参照してください。

Batchはアプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。ロググループは/aws/batch/job固定となります。

まとめ

ユースケースに合わせてどのサービスを利用するかを決めるべきですが、上記のメリット、デメリットを踏まえて、定期バッチ環境の構築における取捨選択のポイントを独断でまとめてみました。

サービス 取捨選択のポイント
Lambda 使用するライブラリが限定的であり、小規模処理をとりあえず動かしたい場合
Fargate 機密情報を必要とするような、多少手の込んだ中規模処理を動かしたい場合
Batch 機械学習のようなマシンリソースが潤沢に必要な大規模処理を動かしたい場合

※ あくまで定期バッチをテーマにしたまとめとして取り扱ってください。(本記事では各サービスの備えている利点を全て取り上げた訳ではありません)

サンプル

定期バッチ環境を構築するサンプルコードを紹介して終わりにします。
今回共通して、AWS SAM用のテンプレートを使用します。(Lambda以外はCFnのテンプレートとほぼ同一です)

Lambdaサンプル

lambda_template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Lambda Sample Settings'

Parameters:
  BaseName:
    Type: String
    Default: lambda-sample
  LambdaEnv:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev
  LambdaSubnetId:
    Type : AWS::EC2::Subnet::Id
  LambdaSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

Resources:
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${BaseName}-function
      Handler: handler.sample
      Runtime: ruby2.5
      Role: !GetAtt LambdaIamRole.Arn
      Environment:
        Variables:
          LAMBDA_ENV: !Ref LambdaEnv
      VpcConfig:
        SubnetIds:
          - !Ref LambdaSubnetId
        SecurityGroupIds:
          - !Ref LambdaSecurityGroupId
      Events:
        SampleEvent:
          Type: Schedule
          Properties:
            Name: !Sub ${BaseName}-event1
            Schedule: cron(0 3 1 * ? *)

  LambdaIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole

  LambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LambdaFunction}

VPC内でLambda関数を実行するシンプルなサンプルです。 AWS::Logs::LogGroup を明示的に指定しなくても実行時にロググループを作成してくれますが、このようにCloudFormationのスタックと紐づけることで、個別に管理する必要がなくなるのでおすすめです。

以下の通り、事前にインストールしたsamコマンドを使用してデプロイします。サブネットIDやセキュリティグループのハードコーディングは避けたいので、parameter-overridesオプションで渡しています。

$ sam package --template-file lambda_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name lambda-sample --capabilities CAPABILITY_IAM --parameter-overrides LambdaSubnetId=subnet-***************** LambdaSecurityGroupId=sg-*****************

Fargateサンプル

fargate_template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Fargate Sample Settings'

Parameters:
  BaseName:
    Type: String
    Default: fargate-sample
  FargateEnv:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev
  ECSSubnetId:
    Type : AWS::EC2::Subnet::Id
  ECSSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id

Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${BaseName}-cluster

  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Cpu: 2048
      Memory: 4096
      Family: !Sub ${BaseName}-task
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      NetworkMode: awsvpc
      ContainerDefinitions:
        - Name: !Sub ${BaseName}-container
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BaseName}:latest
          WorkingDirectory: /root/workspace/
          Environment:
            - Name: FARGATE_ENV
              Value: !Ref FargateEnv
          Secrets:
            - Name: AWS_ACCESS_KEY
              ValueFrom: /fargate-secrets/AWS_ACCESS_KEY
            - Name: AWS_SECRET_KEY
              ValueFrom: /fargate-secrets/AWS_SECRET_KEY
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref BaseName
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref BaseName

  TaskScheduleEvents1:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${BaseName}-event1
      ScheduleExpression: cron(0 3 1 * ? *)
      State: ENABLED
      Targets:
        - Id: !Sub ${BaseName}-target
          Arn: !GetAtt ECSCluster.Arn
          RoleArn: !GetAtt ECSEventRole.Arn
          Input: !Sub '{"containerOverrides":[{"name":"${BaseName}-container","command":["ruby", "./handler.rb"]}]}'
          EcsParameters:
            TaskDefinitionArn: !Ref ECSTaskDefinition
            TaskCount: 1
            LaunchType: FARGATE
            NetworkConfiguration:
              AwsVpcConfiguration:
                AssignPublicIp: DISABLED
                SecurityGroups:
                  - !Ref ECSSecurityGroupId
                Subnets:
                  - !Ref ECSSubnetId

  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess

  ECSEventRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole

  FargateLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Ref BaseName

コンテナのWORKDIR直下にあるhandler.rbバッチを実行するサンプルです。

あらかじめ用意したDockerイメージのURIを AWS::ECS::TaskDefinition ContainerDefinitionImageで設定しています。また、機密情報であるAWS_ACCESS_KEYAWS_SECRET_KEYをParameter Storeに事前に設定して、環境変数として利用する想定です。

ポイントとして、バッチコマンドを AWS::Events::Rule TargetcontainerOverridesで上書き設定しています。AWS::Events::Ruleを複数定義することで、コンテナ上にある様々な定期バッチのスケジューリングが同一テンプレートファイルで可能になります。

こちらも同様にsamコマンドを使用してデプロイします。

$ sam package --template-file fargate_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name fargate-sample --capabilities CAPABILITY_IAM --parameter-overrides ECSSubnetId=subnet-***************** ECSSecurityGroupId=sg-*****************

Batchサンプル

batch_template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Batch Sample Settings'

Parameters:
  BaseName:
    Type: String
    Default: batch-sample
  BatchEnv:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev
  BatchSubnetId:
    Type : AWS::EC2::Subnet::Id
  BatchSecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  EC2KeyPair:
    Type: AWS::EC2::KeyPair::KeyName
  EC2AmiId:
    Type: AWS::EC2::Image::Id

Resources:
  BatchComputeEnv:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      Type: MANAGED
      ServiceRole: !GetAtt BatchServiceRole.Arn
      ComputeEnvironmentName: !Sub ${BaseName}-compute-env
      ComputeResources:
        MaxvCpus: 2
        MinvCpus: 0
        Subnets:
          - !Ref BatchSubnetId
        SecurityGroupIds:
          - !Ref BatchSecurityGroupId
        InstanceRole: !GetAtt EcsInstanceProfile.Arn
        Ec2KeyPair: !Ref EC2KeyPair
        Type: EC2
        InstanceTypes:
          - r5.large
        ImageId: !Ref EC2AmiId
      State: ENABLED

  BatchJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      JobQueueName: !Sub ${BaseName}-job-queue
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref BatchComputeEnv
      State: ENABLED
      Priority: 1

  BatchJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      JobDefinitionName: !Sub ${BaseName}-job-definition
      ContainerProperties:
        Command: ["Ref::SciptLang", "Ref::Command"]
        Memory: 15000
        Vcpus: 2
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BaseName}:latest
        Environment:
          - Name: BATCH_ENV
            Value: !Ref BatchEnv

  TaskScheduleEvents1:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${BaseName}-event1
      ScheduleExpression: cron(0 3 1 * ? *)
      State: ENABLED
      Targets:
        - Id: !Sub ${BaseName}-target
          Arn: !Ref BatchJobQueue
          RoleArn: !GetAtt BatchEventRole.Arn
          Input: '{"Parameters" : {"SciptLang": "ruby", "Command": "./handler.rb"}}'
          BatchParameters:
            JobDefinition: !Ref BatchJobDefinition
            JobName: !Sub ${BaseName}-job

  BatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: batch.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole

  EcsInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role

  EcsInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EcsInstanceRole

  BatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceEventTargetRole

Fargateと同様にコンテナのWORKDIR直下にあるhandler.rbバッチを実行するサンプルです。

あらかじめ用意したDockerイメージのURIを AWS::Batch::JobDefinition ContainerPropertiesImageで設定しています。また、マシンリソースが潤沢に必要な大規模処理を想定して、ストレージを拡張したAMI IDを AWS::Batch::ComputeEnvironment ComputeResourcesImageIdで設定しています。

一つ目のポイントとして、EC2 vCPUの最小数を AWS::Batch::ComputeEnvironment ComputeResourcesMinvCpusで0に設定しています。これによりバッチ実行時にEC2インスタンスが起動し、バッチ終了後に自動でEC2インスタンスが削除されるというサーバレスな運用を実現します。

二つ目のポイントとして、バッチコマンドを AWS::Batch::JobDefinition ContainerPropertiesCommand["Ref::SciptLang", "Ref::Command"]のように参照形式で設定しています。これは AWS::Events::Rule TargetParametersで実体を設定することで、コンテナ上にある複数の定期バッチのスケジューリングを同一テンプレートファイルで可能にしています。(Fargateと同じようにcontainerOverridesでシンプルにバッチコマンドを上書き設定できるかと思ってましたが、難しかったためこのようなハック的なやり方を採用しました)

こちらも同様にsamコマンドを使用してデプロイします。

$ sam package --template-file batch_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name batch-sample --capabilities CAPABILITY_IAM --parameter-overrides BatchSubnetId=subnet-***************** BatchSecurityGroupId=sg-***************** EC2KeyPair=********** EC2AmiId=ami-*****************

さいごに

以上で終了です。AWSでサーバレスな定期バッチ環境を作るうえでの、お役に立てれば幸いです。

実際のところ、Batchの定期バッチ運用に関しては、スケジューリングしたLambda経由でジョブをサブミットすることで実現してました。今回この記事を執筆するにあたり、CFnに対応しているやん!じゃあ試してみようとなったのですが、思いのほか茨の道で大変でした。なんとか動く状態になって良かったです。

では、またX年後くらいにお会いしましょう。

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

Capistranoで自動デプロイ時に起きるエラーについて〜AWS〜

はじめに

Capistranoを使って、AWSに自動デプロイをかけた時に出るエラーのお話
本件は主に再起動時に起きるエラーです
最後にデプロイ時に確認する事項をまとめていますので、そちらも軽くご参照いただければと思います

Erroe内容

bundle exec cap production deploy実行後

terminal
Mac_ikuy:"hogehoge" User$ bundle exec cap production deploy
#<Thread:0x00007f8b2b50bb18@/Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:10 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
        1: from /Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:11:in `block (2 levels) in execute'
/Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute': Exception while executing as ec2-user@18.177.78.202: Authentication failed for user ec2-user@xx.xxx.xxx.xxx (SSHKit::Runner::ExecuteError)
(Backtrace restricted to imported tasks)
cap aborted!
SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@xx.xxx.xxx.xxx: Authentication failed for user ec2-user@xx.xxx.xxx.xxx


Caused by:
Net::SSH::AuthenticationFailed: Authentication failed for user ec2-user@xx.xxx.xxx.xxx

Tasks: TOP => rbenv:validate
(See full trace by running task with --trace)

こんな感じのエラーが出ます
今回の話の結論からいうと、SSHキーが消えてしまっているので、.sshにて下記コマンドを入力します

terminal
$ cd ~/.ssh
.ssh $ ssh-add ~/.ssh/hoge.pem

hoge.pemにはインスタンスに対応したpemキーを入力してください

Error解決しないときは

エラー文に慣れない方はなんかいっぱい書いてあってうわぁって思うかもしれませんが、ポイントは"Caused by"です
これのせいで、ってことですね
"Exception while executing"を直訳すると「実行中の例外」です
何か例外が起きてしまったんだなってことですね
AWSへのデプロイ時の基本注意点としては、

  • mysql/nginexの再起動
  • unicorn killの確認

です
前者は

[ec2-user@ip-xxx ~]$ sudo service nginx restart
[ec2-user@ip-xxx ~]$ sudo service mysqld restart

後者は

[ec2-user@ip-xxx ~]$ ps aux | grep unicorn
ec2-user  4223  0.0  0.0 11xxxx   master
ec2-user  4175  0.0  0.0 11xxxx   ~~~~省略~~~~
ec2-user  4236  0.0  0.0 11xxxx   ~~~~省略~~~~
[ec2-user@ip-xxx ~]$ kill 4223 #masterのNO.

上記ができているにも関わらずデプロイできないときは、インスタンスを一度停止しましょう
スクリーンショット 2019-12-19 11.36.51.png
"停止"が完了したら"開始"をしましょう
大概のエラーはこれでで解決できます。

まとめ

解決方法のまとめです

  • "Net::SSH::AuthenticationFailed: Authentication failed"が出たときはsshキーが消えている可能性があるので、addしてやる
  • mysql/nginexの再起動
  • unicornのkill
  • インスタンスの再起動
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MSK(Amazon Managed Streaming for Apache Kafka)へのトピック登録

1.はじめに

 Amazon MSK(Amazon Managed Streaming for Apache Kafka)は、Apache Kafkaのマネージドサービスです。Amazon MSKの設定変更は、Amazon CLI経由で行うことができます。
 デフォルトでは、Kafka Brokerに対して、Kafka Producerはトピックを生成することができせん。なぜならば、auto.create.topics.enable = trueとなっていないためです。
 したがって、本投稿では、まずはAmazon CLI経由での設定手順について解説します。その後、トピックを登録します。

2.設定方法

2-1.セキュリティグループの設定

 Amazon CLI経由でAmazon MSKを操作するためには、Kafka Clusterを構築するさいに、Amazon CLIで操作する端末(Amazon CLI端末)をアクセス可能なセキュリティグループに所属させる必要があります。
 Kafka Clusterを構築するさいには、Amazon CLI端末が含まれるセキュリティグループをアクセス対象として含めるように注意してください。

2-2.arnの確認

 
 Amazon Management Consolから、Amazon MSKのarnを確認します。この投稿のためだけに作成したので、arnは既に有効値とはなっていないです。

aaaaa.PNG

この場合、arnは以下の値となります。

arn:aws:kafka:ap-northeast-1:196585472650:cluster/Amazon-MSK-Test/d5a6aebf-8858-43db-bfe4-4a7288d93775-2

2-3.bootstrap-brokers(Zookeeper)の接続先を確認

describe-clusterコマンドで、bootstrap-brokers(Zookeeper)の接続先を確認する。この場合、"z-1.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-3.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-2.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181"となります。

aws kafka describe-cluster --region ap-northeast-1 --cluster-arn arn:aws:kafka:ap-northeast-1:196585472650:cluster/Amazon-MSK-Test/d5a6aebf-8858-43db-bfe4-4a7288d93775-2
{
    "ClusterInfo": {
        "EncryptionInfo": {
            "EncryptionInTransit": {
                "ClientBroker": "TLS", 
                "InCluster": true
            }, 
            "EncryptionAtRest": {
                "DataVolumeKMSKeyId": "arn:aws:kms:ap-northeast-1:196585472650:key/d94c7909-3392-482a-908d-d25f9a31abb9"
            }
        }, 
        "BrokerNodeGroupInfo": {
            "BrokerAZDistribution": "DEFAULT", 
            "ClientSubnets": [
                "subnet-05e68568d43a7c6d3", 
                "subnet-091efbcc876bdefb1", 
                "subnet-05a73c6877da30604"
            ], 
            "StorageInfo": {
                "EbsStorageInfo": {
                    "VolumeSize": 1
                }
            }, 
            "SecurityGroups": [
                "sg-0204c59a2c6992390", 
                "sg-0fc8fe66209887218", 
                "sg-0c4b89523751d689c"
            ], 
            "InstanceType": "kafka.m5.large"
        }, 
        "ClusterName": "Amazon-MSK-Test", 
        "CurrentBrokerSoftwareInfo": {
            "KafkaVersion": "2.3.1", 
            "ConfigurationRevision": 1, 
            "ConfigurationArn": "arn:aws:kafka:ap-northeast-1:196585472650:configuration/SalesClusterConfiguration2/9c66b8dc-836e-4348-a2a3-b2bd19717b0b-2"
        }, 
        "Tags": {
            "Name": "Kafka-Cluster"
        }, 
        "CreationTime": "2019-12-19T02:09:06.63Z", 
        "NumberOfBrokerNodes": 3, 
        "ZookeeperConnectString": "z-1.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-3.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-2.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181", 
        "State": "ACTIVE", 
        "CurrentVersion": "K2EUQ1WTGCTBG2", 
        "ClusterArn": "arn:aws:kafka:ap-northeast-1:196585472650:cluster/Amazon-MSK-Test/d5a6aebf-8858-43db-bfe4-4a7288d93775-2", 
        "EnhancedMonitoring": "DEFAULT", 
        "OpenMonitoring": {
            "Prometheus": {
                "NodeExporter": {
                    "EnabledInBroker": false
                }, 
                "JmxExporter": {
                    "EnabledInBroker": false
                }
            }
        }
    }
}

2-4.設定ファイルの作成

 Amazon CLI端末の任意のディレクトリに、設定変更用のファイルを作成します。設定ファイルのフォーマットは、Amazon MSKが指定した形式にする必要があります。ここを参照してください。

/home/ec2-user/test/configuration.txt
auto.create.topics.enable = true
message.max.bytes = 73400320

2-5.Amazon MSK公式のpythonツールの準備

設定ファイルと同一ディレクトリ階層に、以下のpythonファイルを作成します。今回の場合は、configuration.txtと同一ディレクトリであるhome/ec2-user/に配置します。
pythonファイルの雛形は、ここを参照してください。

/home/ec2-user/unction.py
import boto3

client = boto3.client('kafka')

config_file = open('/home/ec2-user/test/configuration.txt', 'r')

server_properties = config_file.read()

response = client.create_configuration(
            #この値はAWS アカウントでユニークにする必要があります。
            Name='SalesClusterConfiguration3',
                Description='The configuration to use on all sales clusters.',
                    KafkaVersions=['1.1.1', '2.1.0','2.3.1'],
                        ServerProperties=server_properties
                        )
print(response)

 boto3ライブラリがない場合は、pipコマンドでインストールしてください。

pip install boto3

2-6.設定反映

pythonコマンドを実行することで設定を反映することができます。以下の通り、'HTTPStatusCode': 200が返信された場合、設定変更が可能です。

python function.py 
{u'LatestRevision': {u'CreationTime': datetime.datetime(2019, 12, 19, 2, 42, 53, 2000, tzinfo=tzlocal()), u'Description': u'The configuration to use on all sales clusters.', u'Revision': 1}, u'Arn': u'arn:aws:kafka:ap-northeast-1:196585472650:configuration/SalesClusterConfiguration3/30ffe529-2733-486d-bfed-1f1641db9edf-2', u'CreationTime': datetime.datetime(2019, 12, 19, 2, 42, 53, 2000, tzinfo=tzlocal()), u'Name': u'SalesClusterConfiguration3', 'ResponseMetadata': {'RetryAttempts': 0, 'HTTPStatusCode': 200, 'RequestId': '939d9052-a6f7-4fa4-b66f-91fe737a8057', 'HTTPHeaders': {'x-amzn-requestid': '939d9052-a6f7-4fa4-b66f-91fe737a8057', 'x-amz-cf-pop': 'NRT12-C2', 'content-length': '347', 'via': '1.1 4cb3df5349fbb69c930b315b7d0a5272.cloudfront.net (CloudFront)', 'x-cache': 'Miss from cloudfront', 'x-amz-apigw-id': 'E7iC8G5xNjMFeFw=', 'x-amzn-trace-id': 'Root=1-5dfae3ac-0cd2e93c1fe9319eb9166bc2;Sampled=0', 'connection': 'keep-alive', 'x-amz-cf-id': 'zSz8M9gaSNl4bIuNUdbDfY_Sm4cEWCoZF3RfuAX22aQBZxHJOUI4ug==', 'date': 'Thu, 19 Dec 2019 02:42:53 GMT', 'content-type': 'application/json'}}}

2-7.トピック登録

Kafka Producerからトピックを登録します。Created topic "XXXXXXX".と結果が返ってきたらトピック生成完了です。

kafka-topics --zookeeper "z-1.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-3.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181,z-2.amazon-msk-test.sxm7ay.c2.kafka.ap-northeast-1.amazonaws.com:2181" --create --topic first-test --partitions 3 --replication-factor 3
Created topic "first-test".

以上

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

OpsworksにEC2を登録できない問題への対応

事象

インスタンス登録コマンドを発行すると途中で接続が切れ登録が失敗する。

$ aws opsworks register --infrastructure-class ec2 --region us-east-1 --stack-id <STACK_ID> --ssh-username ec2-user --ssh-private-key <PRIVATE_KEY> <INSTANCE_ID>
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 7929k  100 7929k    0     0  47.5M      0 --:--:-- --:--:-- --:--:-- 47.5M
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Initializing AWS OpsWorks environment
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Checking if OS is supported
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Running on supported OS
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Setup motd
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Executing: ln -sf --backup /etc/motd.opsworks-static /etc/motd
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Using assets bucket from environment: 'opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com'.
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Installing Ruby for the agent
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Executing: /tmp/opsworks-agent-installer.XbghhcTVDCDTtrGW/opsworks-agent-installer/opsworks-agent/bin/installer_wrapper.sh -r -R opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com
Connection to <Elastic IP> closed.

調査

登録対象のEC2インスタンス側でログを確認する。

[root@ip-172-30-1-37 ec2-user]# tail -f /var/log/aws/opsworks/user-data.log
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Checking if OS is supported
/tmp/opsworks-agent-installer.XbghhcTVDCDTtrGW/opsworks-agent-installer/boot-registration: line 48: lsb_release: command not found
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Running on supported OS
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Setup motd
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Executing: ln -sf --backup /etc/motd.opsworks-static /etc/motd
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Using assets bucket from environment: 'opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com'.
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Installing Ruby for the agent
[Thu, 19 Sep 2019 08:14:44 +0000] opsworks-init: Executing: /tmp/opsworks-agent-installer.XbghhcTVDCDTtrGW/opsworks-agent-installer/opsworks-agent/bin/installer_wrapper.sh -r -R opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com
[Thu, 19 Sep 2019 08:14:44 +0000] installer-wrapper: Using opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com for assets.
[Thu, 19 Sep 2019 08:14:44 +0000] installer-wrapper: Unsupported OS version: amazon 2018.03

OS versionが未サポートと言われるが、そんなはずはない。(https://aws.amazon.com/jp/about-aws/whats-new/2019/01/aws-opsworks-stacks-now-supports-amazon-linux-2--amazon-linux-20/)

原因が分からないのでググると以下のフォーラムにたどり着く。

https://forums.aws.amazon.com/thread.jspa?threadID=250571

Opsworks Agentが古いのでは?というコメントが見つかる。

対応

以下の設定を確認すると2016年付近のAgentを利用していたので、最新のバージョンを利用するように変更。

Stack Settings > OpsWorks Agent version ---> Latest version

再度登録コマンド発行。

$ aws opsworks register --infrastructure-class ec2 --region us-east-1 --stack-id <STACK_ID> --ssh-username ec2-user --ssh-private-key <PRIVATE_KEY> <INSTANCE_ID>
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 7933k  100 7933k    0     0  44.0M      0 --:--:-- --:--:-- --:--:-- 44.0M
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Initializing AWS OpsWorks environment
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Checking if OS is supported
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Running on supported OS
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Setup motd
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Executing: ln -sf --backup /etc/motd.opsworks-static /etc/motd
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Using assets bucket from environment: 'opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com'.
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Installing Ruby for the agent
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Executing: /tmp/opsworks-agent-installer.YmkVCDV5L5hS1Ok3/opsworks-agent-installer/opsworks-agent/bin/installer_wrapper.sh -r -R opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com
[Thu, 19 Sep 2019 08:22:23 +0000] opsworks-init: Starting the installer
Instance successfully registered. Instance ID: <Instance ID>
Connection to <Elastic IP> closed.

登録できた。

参考までにEC2側のログは以下の通り。

[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Initializing AWS OpsWorks environment
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Checking if OS is supported
/tmp/opsworks-agent-installer.YmkVCDV5L5hS1Ok3/opsworks-agent-installer/boot-registration: line 47: lsb_release: command not found
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Running on supported OS
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Setup motd
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Executing: ln -sf --backup /etc/motd.opsworks-static /etc/motd
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Using assets bucket from environment: 'opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com'.
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Installing Ruby for the agent
[Thu, 19 Sep 2019 08:22:21 +0000] opsworks-init: Executing: /tmp/opsworks-agent-installer.YmkVCDV5L5hS1Ok3/opsworks-agent-installer/opsworks-agent/bin/installer_wrapper.sh -r -R opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com
[Thu, 19 Sep 2019 08:22:21 +0000] installer-wrapper: Using opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com for assets.
[Thu, 19 Sep 2019 08:22:21 +0000] installer-wrapper: Downloading agent ruby package from https://opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com/packages/amazon/2018.03/opsworks-agent-ruby-2.2.3-1.x86_64.rpm
[Thu, 19 Sep 2019 08:22:21 +0000] downloader: Successfully created temporary download directory. (/tmp/opsworks-downloader.I6n5yywV)
[Thu, 19 Sep 2019 08:22:21 +0000] downloader: File size test passed.
[Thu, 19 Sep 2019 08:22:21 +0000] downloader: Checksum proof skipped.
[Thu, 19 Sep 2019 08:22:21 +0000] downloader: Successfully downloaded https://opsworks-instance-assets-ap-northeast-1.s3.amazonaws.com/packages/amazon/2018.03/opsworks-agent-ruby-2.2.3-1.x86_64.rpm
[Thu, 19 Sep 2019 08:22:21 +0000] installer-wrapper: Installing agent ruby
Preparing...                          ########################################
Updating / installing...
opsworks-agent-ruby-2.2.3-1           ########################################
[Thu, 19 Sep 2019 08:22:23 +0000] installer-wrapper: Cleaning up
[Thu, 19 Sep 2019 08:22:23 +0000] opsworks-init: Starting the installer

まとめ

https://docs.aws.amazon.com/ja_jp/opsworks/latest/userguide/registered-instances-register.html

登録フローの中でAgent Packageをダウンロードする部分があるが、そのダウンロードするパッケージのバージョンが古いため、登録コマンド(ローカル)のバージョンと不整合を起こしていたと思われる。
ログからは全く読めなかったので結構ハマってしまった。

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

CloudFormation、RDS部分でnot emptyエラーに振り回された話

CloudFormation初めました・・・

VPC/EC2/ALB/Subnet/EIP/IGW/NAT/ これらの作成はわりとさくさくいきました。
ここにRDS Auroraを足そうとしたときに、スタック作成時のエラー解決ができず、気づいたら累計30時間ぐらい使っていた話です。

結論から言うと、GUIからだと自動で作ってくれる Type: AWS::RDS::DBClusterParameterGroup の記述が漏れていました。

CloudFormationの公式ドキュメントや、有志の記事を参考にyamlを書いて、トライ&エラーを繰り返していました。

公式ドキュメント:
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html

スタック作成時にAllocatedStorage not empty.エラーが表示されました。
ドキュメントでは必須:いいえ だし、GUIではAuroraの場合ここを設定する必要ないので、ドキュメントかAPIのバグで、実は必須なんだと判断し、追記しました。

このエラーはでなくなりましたが、次々に必須:いいえであるはずの項目のエラーが・・・どんどん追記していきます。

最終的にType: AWS::RDS::DBInstanceのStorageType でどの有効な値: standard | gp2 | io1を入れても、not valiedのエラーがでるようになり、わけわからん状態に。

社内や有志のRDS Auroraを使うyamlと差分を見ていったところ、自分のyamlにType: AWS::RDS::DBClusterParameterGroupが無いことに気づき、追記して解消しました。

ドキュメントでは必須:いいえ だし、GUIではAuroraの場合ここを設定する必要ない

CloudFormationのTypeのレベルは、自動ではやってくれなくて、記述が必要ということですね(たぶん)

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