(JSON取得によるRedmineチケット駆動) – Qiita

AWS-CLIによるAMI作成と削除(JSON取得によるRedmineチケット駆動) – Qiita. #!/bin/bash #JSON形式でチケット情報を取得(終了チケットを含めて取得) JSON=$(curl -v -H “Content-Ty… 続きを表示 #!/bin/bash #JSON形式でチケット情報を取得(終了チケットを含めて取得) JSON=$(curl -v -H “Content-Type: … 続きを読む

AWS AppSyncとReactでToDoアプリを作ってみよう (0)はじめに

昨年12月のAWS re:Invent 2017で発表されたAWS AppSyncのパブリックプレビュー申請が通ったので、ToDoアプリの作成を例にして、AppSync + ReactでWebアプリを構築する際の流れを紹介していきたいと思います。

この記事で取り上げる内容

この記事では、あえてGraphQLスキーマの定義からReactでのクライアント側の実装までを順を追って説明していきます。
ToDoアプリのスキーマについては、AWS公式ドキュメントのDesigning a GraphQL APIに記載されている内容に沿って作成していきます。

とりあえずサクッとサンプルを動かしたいという方は、次のような記事の内容を参考に AWS公式のサンプルを動作させてみるのも良いと思います。(自分も最初はこのサンプルを動かしてみて雰囲気を掴みました)

目次

今回は、記事が長くなりそうなので、何回かに分けて内容を紹介していきます。

  1. AWS AppSyncとReactでToDoアプリを作ってみよう (0)はじめに ←今回はこの記事
  2. AWS AppSyncとReactでToDoアプリを作ってみよう (1)GraphGLスキーマの定義
  3. AWS AWS AppSyncとReactでToDoアプリを作ってみよう (2)DataSourceとResolverの設定
  4. AWS AppSyncとReactでToDoアプリを作ってみよう (3)Reactアプリの作成

事前知識

GraphQL

AWS AppSyncでは、クライアントからAppSyncで作成したAPIへのクエリの際にGraphQLを利用します。

次の記事などを参考にしてサラッと概要を掴んでおけば、問題ないかと思います。

React

今回の記事では、AWSからクライアントライブラリが出ていて、一番簡単に実装できそうなReactでクライアント側の実装を行います。
使用するライブラリaws-appsyncが、内部的にReduxを使用しているため、React + Reduxに触ったことがあるとよいかと思います。

注意点

この記事は、AWS AppSyncのパブリックプレビュー時点(2018/01/22)の内容を元に紹介している内容です。

続きを読む

AWS AppSyncとReactでToDoアプリを作ってみよう (2)DataSourceとResolverの設定

はじめに

前回の記事で定義したGraphQLスキーマはAPIのインターフェースの定義でした。
このインターフェースを使ってAPIにアクセスがあった際に、データを保存する先のリソース(DataSource)の作成と連携の部分を設定していきます。
AWS AppSyncでは、Resolverを作成することで、DataSourceとGraphQLスキーマとの紐付けを行います。

リソース(DataSource)の作成

今回の例では、GraphQLスキーマに定義したTodo型からDynamoDBのテーブルを作成します。
まずは、コンソール画面から、「AWS AppSync > 作成したプロジェクト > Schema」を開き、画面右上の「Create Recources」をクリックします。

CreateRecouces.png

次の画面では、定義済みのGraphQLスキーマから使用する型を選択します。
今回はTodoを選択しました。

スクリーンショット 2018-01-22 6.07.41.png

使用する型を選択すると、自動でテーブル名、テーブルの構成が入力されます。
今回は、idをプライマリキーとしました。

スクリーンショット 2018-01-22 7.05.49.png

ここまでの項目を入力すると、画面最下部にテーブル定義を元に、自動で追加されるスキーマ定義のプレビューが表示されます。
最後に、「Create」ボタンをクリックすると、DynamoDBテーブルが作成されます。

スクリーンショット 2018-01-22 7.05.59.png

Resolverの追加と修正

前の手順では、GraphQLスキーマから自動でDynamoDBとResolverのプロビジョニングを行いましたが、一部、定義元のスキーマと紐付いていない点があるので、追加・修正していきます。

getTodos

getTodosには、Resolverが紐付けられていないので、新規で追加します。

「Schema > Query > getTodos: [Todo]」の「Attach」をクリックします。
次の画面では、作成済みのDataSourceから紐付けるテーブルを選択します。
今回はTodoTabeを選択しました。

スクリーンショット 2018-01-22 6.42.08.png

次に、リクエストのマッピングを行います。
「Paginated scan」のテンプレートをベースに、次のように変更しました。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "Scan"
}

レスポンスのマッピングは次のようになります。

ResponseMappingTemplate
$utils.toJson($context.result.items)

最後に、画面右下の「Save」をクリックしてResolverの設定を保存します。

getTodo

これは、自動で追加されたQueryです。
今回のToDoアプリでは、item一件ごとに取得するGetItem操作は不要なので、削除します。

allTodo

これは、自動で追加されたQueryです。
こちらは、getTodosと処理が重複するため削除します。
(ページネーション処理も自動で設定済みのようなので、こちらを使うのがよさそうですが…)

addTodo

自動で追加されたMutation、putTodoの内容をこちらに移行すれば良さそうです。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key": {
        "id": { "S" : "${context.arguments.id}"}
    },
    "attributeValues" : {
        "id": {  "S": "${context.arguments.id}" },
        "title": {  "S": "${context.arguments.title}" },
        "description": {  "S": "${context.arguments.description}" },
        "completed": {  "B": "${context.arguments.completed}" }
    }
}
ResponseMappingTemplate
$utils.toJson($context.result)

updateTodo

updateTodoには、Resolverが紐付けられていないので、新規で追加します。
getTodosで行った新規追加手順と同様に操作を行い、次のテンプレートを設定します。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key": {
        "id": { "S" : "${context.arguments.id}"}
    },
    "update" : {
        "expression" : "SET title = :title, description = :description, completed = :completed",
        "expressionValues" : {
            ":title" : { "S": "${context.arguments.title}" },
            ":description" : { "S": "${context.arguments.description}" },
            ":completed" : { "BOOL": ${context.arguments.completed} }
       }
    }
}
ResponseMappingTemplate
$utils.toJson($context.result)

更新操作は、DynamoDBの更新式の指定が必須となっているようでうです。

deleteTodo

こちらは、自動でしっかりとマッピングされているので、そのまま使用します。

putTodo

これは、自動で追加されたMutationです。
こちらは、addTodoと処理が重複するため削除します。

以上で、Resolverの設定が終わりました。

APIの動作確認

作成したAPIの動作確認を行ってみます。
コンソール画面から、「AWS AppSync > 作成したプロジェクト > Queries」を開き、画面左側のエディターエリアにクエリを入力し「▶」をクリックしてクエリを実行します。

スクリーンショット 2018-01-22 22.37.05.png

MutationとQueryそれぞれの項目を動作確認してみたいと思います。

addTodo

まずは、データを追加してみます。

Query
mutation addTodo {
  addTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
    title: "髪を切る"
    description: "来週までには"
    completed: false
  ) {
    id
    title
    description
    completed
  }
}

次のようなレスポンスが返ってくれば成功です。

Response
{
  "data": {
    "addTodo": {
      "id": "0651ed86-9314-4267-9bcf-7143b785f173",
      "title": "髪を切る",
      "description": "来週までには",
      "completed": false
    }
  }
}

getTodos

事前に何件かデータを追加した状態で以下のクエリを実行します。

Query
query {
  getTodos {
    id
    title
    description
    completed
  }
}
Response
{
  "data": {
    "getTodos": [
      {
        "id": "f163372a-8b54-4da4-9237-911a64067517",
        "title": "豆腐を食べる",
        "description": "腐りそう",
        "completed": false
      },
      {
        "id": "0cbab86a-ad72-41b4-a63d-9ce3f9a7d552",
        "title": "Qiita書く",
        "description": "あと2本",
        "completed": false
      },
      {
        "id": "0651ed86-9314-4267-9bcf-7143b785f173",
        "title": "髪を切る",
        "description": "来週までには",
        "completed": false
      }
    ]
  }
}

updateTodo

Query
mutation updateTodo {
  updateTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
    title: "部屋を掃除する"
    description: "さらっと済ます"
    completed: false
  ) {
    id
    title
    description
    completed
  }
}

※ Responseは省略

deleteTodo

Query
mutation deleteTodo {
  deleteTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
  ) {
    id
    title
    description
    completed
  }
}

対象のitemのidを指定して、削除を行います。

※ Responseは省略

最後に

今回は、定義したGraphQLスキーマから自動でリソースを作成しましたが、AppSyncのコンソールからは、全て手動でリソースを用意することもできます。必要に応じて使い分けると良さそうです。
次回は、作成したGraphQL APIと連携するReactフロントエンドの実装を行っていく予定です。

参考

Attaching a Data Source -AWS AppSync
Provision from Schema -AWS AppSync
Resolver Mapping Template Reference for DynamoDB -AWS AppSync

続きを読む

Amazon LightsailでもAWS Lambdaで運用して( )できた話 – Qiita

Amazon LightsailでもAWS Lambdaで運用して(◜◡◝)できた話 – Qiita. はじめにある案件で、社内のユーザーが使用する数台のサーバを提供しているものがありましてその案件… 続きを表示 はじめにある案件で、社内のユーザーが使用する数台のサーバを提供しているものがありましてその案件のテストサーバで先ごろ特殊 … 続きを読む

Amazon S3 に Git LFS サーバを超簡単に立てる

元ネタはこちら。
Serverless Git LFS for Game Development – Alan Edwardes
Amazon API Gateway + Amazon Lambda + Amazon S3 を使うんだそうです。

準備: AWS のアカウントをとる

AWSのアカウントがなければとります。
作成の流れは 公式のドキュメント でだいじょうぶです。

アカウントをとったら初期設定しておきます。以下のエントリが参考になりました。
AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ – Qiita

サーバ作成手順

Screen_Shot_2018-01-21_at_22_29_22-fullpage.png

  • 「次へ」をクリックします → オプションの設定ページはスルーで「次へ」をクリックします。
  • 「AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。」にチェックして「作成」をクリックします。

Screen_Shot_2018-01-21_at_23_05_08-fullpage.png

  • スタックのリストに新しいスタックができて(スタックがなければリロードして下さい)、「CREATE_COMPLETE」と表示されたら成功です。

Screen_Shot_2018-01-21_at_23_10_18-fullpage.png

  • スタックの詳細画面に入って、出力を確認すると、Git LFSのエンドポイントのURLが表示されているので、リンクをコピーします。

Screen_Shot_2018-01-21_at_22_34_53-fullpage.png

Git LFS に lfs.url を設定

Git LFS は導入してあることとします。

以下のコマンドで、 .lfsconfig を作成します。

git config -f .lfsconfig lfs.url <表示されたエンドポイントのURL>

git lfs env で確認して、Endpoint= の行が設定したURLになってたらOKです。

git push すると、S3の方にLFSオブジェクトがpushされるようになります。

問題点

URL を見ればわかると思いますが、この方法で作成するとリージョンが 欧州 (アイルランド) に作成されます。
東京リージョンにS3を立てたいと思って、 URL の region=eu-west-1 の部分を region=ap-northeast-1 に変えてみましたが、リージョンを変えると成功しないみたいです。

alanedwardes/Estranged.Lfs – Github

どなたか検証お願いします。 m(_ _)m

追記

わかった気がします。

同じリージョン内のでしかオブジェクトが参照できないぽいので、
自分のS3に東京リージョンでバケット作って、 git-lfs.yaml と git-lfs-lambda.zip をコピーしました。
パブリックアクセス許可にしたので、下のURLで誰か試してください。

https://console.aws.amazon.com/cloudformation/home#/stacks/new?region=ap-northeast-1&templateURL=https://s3-ap-northeast-1.amazonaws.com/kanemu-infrastructure-ap-northeast-1/git-lfs/git-lfs.yaml

続きを読む

AWS IoT device managementのJob機能の動きをデバイス視点で見てみる

はじめに

公式ページはこちらになります。(2018/1/20時点では英語のマニュアルのみとなっております)
同じく、投稿時点では、javascript IoT Device SDKはjobの処理が入っているようですが、pythonなどはまだSDKに入っていないようですので、本説明にあわせてご自分でシーケンスをコードで表現する必要があるようです。
デバイス側でどういったことを考えてシーケンスを書くかや、request/responseのjsonについての理解が深まるようにまとめようと思います。

デバイスが意識すべきjob用のtopic

以下、topoic中の{}で区切ったものは変数となるものです。 device名やjobIdなど。

job情報取得で使えるtopic

新規job待受用のsubscribe topic

  • $aws/things/{thingName}/jobs/notify
  • $aws/things/{thingName}/jobs/notify-notify

新規jobが作成されると下記2つにpublishされます。つまり新規jobを受け取るために、動作中のデバイスがsubscribeしたままにすべきtopicということになります。2つのtopicの情報の差を含めて見ましょう。

$aws/things/{thingName}/jobs/notify
=> job一覧入ってきます。jobDocumentはありません。

{
  "timestamp": unixtime,
  "jobs": {
    "IN_PROGRESS": [
      {
        "jobId": "作成したjob名",
        "queuedAt": unixtime,
        "lastUpdatedAt": unixtime,
        "startedAt": unixtime,
        "executionNumber": 1,
        "versionNumber": 3
      }
    ],
    "QUEUED": [
      {
        "jobId": "作成したjob名",
        "queuedAt": unixtime,
        "lastUpdatedAt": unixtime,
        "startedAt": unixtime,
        "executionNumber": 1,
        "versionNumber": 1
      }
    ]
  }
}

$aws/things/{thingName}/jobs/notify-next
=> 次にやるべきjob情報のみが、通知されます。
jobDocumentにある、test:testは job生成時に指定したS3のjob fileの中身(json)が入っています。

{
  "timestamp": unixtime,
  "execution": {
    "jobId": "job名",
    "status": "QUEUED",
    "queuedAt": unixtime,
    "lastUpdatedAt": unixtime,
    "versionNumber": 1,
    "executionNumber": 1,
    "jobDocument": {
      "test": "test"
    }
  }
}

注意:通知対象のdeviceに”IN_PROGRESS”のstatusがある場合、notify-nextは発行されません。
複数の処理を通知して、デバイス側でupdate処理が重複しないための考慮だと思います。

この場合、デバイスが “testjob-20180120-00” のtaskの終了通知(後述のupdate通知をSUCCEEDEDでpublish)すると、AWS IoTがnotify-nextへ通知します。

能動的にjob statusを確認するためのtopic

起動時などにjobを確認する用途 publishすると、acceptedへ返却される

  • $aws/things/{thingName}/jobs/get (publish)
  • $aws/things/{thingName}/jobs/get/accepted (publish前にsubscribeしておく)

上記の通り、シーケンスとしては acdepted topicを先にsubscribeしてから、get topicへpublishすることとなります。acceptedへのsubscribeへの返却は以下のようなjsonとなります。先に説明したnotifyと少しjsonフォーマットが異なりますが、取得できる内容はほぼ同じです。
こちらはデバイスが起動したばかりなどで前回の終了状態が分からない場合や電源off時などにupdateが来ていないかなど、最初にチェックするような用途になるかと思います。

{
  "timestamp": unixtime,
  "inProgressJobs": [],
  "queuedJobs": [
    {
      "jobId": "Job名",
      "queuedAt": unixtime,
      "lastUpdatedAt": unixtime,
      "executionNumber": 1,
      "versionNumber": 1
    }
  ]
}

deviceがAWS IoTにreportするためのtopic

$aws/things/thingName/jobs/jobId/update
requestのjson format

{
  "status": ["IN_PROGRESS", "FAILED", "SUCCEEDED", "REJECTED"],
  "statusDetails": {
    "progress": "0%"
  },
  "expectedVersion":"",
  "clientToken":""
}

statusは上記の4つの状態のみを指定可能
statusDetailsのjsonについては自由に記載できます。ここでは例として”progress”としています。
管理者視点で見ると、統計などが見れる機能があるので、デバイス側のupdate処理内で定期的に呼び出して置くと良いかと思います。

AWS IoTのテスト画面を使っての動作確認

device想定のsubscribe設定

AWSのマネージメントコンソールから、AWS IoTを選択し左側メニューの “Test” を選択します。
Subscription topicへ”$aws/things/+/jobs/#”を入力し、subscribe to topicを押下して、受信状態のままにしておきます。

意味を説明すると$aws/things/(+ = すべてのthingName)/jobs/(# = ここ以下すべてのレイヤtopic)
を受信することになります。

スクリーンショット 2018-01-21 21.39.56.png

test Jobの作成

事前作業

AWS IoTの作成

今回は、AWS IoTのテスト画面しか使わないので名前だけあれば良いです。(証明書やpolicyなどは使いません)
本投稿では qiita-thing としています。

job通知のjsonドキュメント配置

S3へjsonドキュメントを置きます。このjsonドキュメントがjobのjobDocumentとして通知されます。
ここでは、 {“test”:”test”}としておいておきます。

AWS IoTからjob作成

先程の受信状態のコンソールをそのままにして別タブなどでもう一つ AWS IoT画面を開き、Manage => Jobsを選択します。画面右上にあるCreateを押下して、jobの作成をしてみます。

Select a job で、Create custom jobを選択。次の画面で以下を設定
– Job IDに qiita-test
– device updateに、先程作ったthingを一つ選択、
– job fileに 先程おいたS3のjson fileを選択
– job typeに snapshotを選択
createを実行。createを実行するとjob通知が自動で発行されます。

通知の確認とupdateで通知

AWS IoTのtest画面の方に戻ります。
先程のjob作成でsubscribeにnotifyとnotify-nextの通知がきています。
スクリーンショット 2018-01-21 22.09.31.png

本test画面の Publish の topicを以下とします。
$aws/things/qiita-thing/jobs/qiita-test/update

updateのpayloadに以下を設定

{
  "status": "IN_PROGRESS",
  "statusDetails": {
    "progress": "0%"
  },
  "expectedVersion":"",
  "clientToken":""
}

状態を確認

Publishのtopicを以下に変更して、publishを実行します。
$aws/things/qiita-thing/jobs/get
mqttのpayloadは設定しなくともよいです。

test画面のsubscribeの画面に以下が表示されました。想定通り、queueからinProgressへ変化しています。

{
  "timestamp": 1516540819,
  "inProgressJobs": [
    {
      "jobId": "qiita-test",
      "queuedAt": 1516540026,
      "lastUpdatedAt": 1516540699,
      "startedAt": 1516540699,
      "executionNumber": 1,
      "versionNumber": 2
    }
  ],
  "queuedJobs": []
}

AWS IoTコンソールのjob詳細画面もin progressとなっています。
スクリーンショット 2018-01-21 22.28.08.png

複数jobを通知してみる

notifyとnotify-nextの動きを確認するために、再度jobを作成して送信してみます。
jobIdは qiita-test-2としてみます。それ以外は先程同じにしています。

説明済みですが、notifyのみが通知されました。

{
  "timestamp": 1516541383,
  "jobs": {
    "IN_PROGRESS": [
      {
        "jobId": "qiita-test",
        "queuedAt": 1516540026,
        "lastUpdatedAt": 1516540699,
        "startedAt": 1516540699,
        "executionNumber": 1,
        "versionNumber": 2
      }
    ],
    "QUEUED": [
      {
        "jobId": "qiita-test-2",
        "queuedAt": 1516541383,
        "lastUpdatedAt": 1516541383,
        "executionNumber": 1,
        "versionNumber": 1
      }
    ]
  }
}

最初のjobを完了させてみる

updateの通知と同様の手順で以下のtopicに payloadを”SUCCEEDED”として送信してみます。

topic : $aws/things/qiita-thing/jobs/qiita-test/update

{
  "status": "SUCCEEDED",
  "statusDetails": {
    "progress": "100%"
  },
  "expectedVersion":"",
  "clientToken":""
}

完了通知に合わせて自動でnotifyとnotify-nextが送られてきました。
ということで、notify-nextをsubscribeしておけば良いことがわかります。
スクリーンショット 2018-01-21 22.37.56.png

まとめ

ということで、AWS IoTのdevice managementのjob機能についてdeviceの観点で簡単に整理してみました。実際にはjob documentのデザイン(プログラムDLのS3 urlをセット)やプログラム更新をする部分というところがdevice updateの本質なので、主にシーケンス/request方法についての説明となりますが、AWS IoT側でこのくらい作られていれば、device updateプログラムの開発に集中出来るのではないでしょうか?

免責

本投稿は、個人の意見で、所属する企業や団体は関係ありません。
ご自身でもドキュメントの確認や、動きを確認してからの利用をおすすめします。

続きを読む

scrapy でクローラーを実装し、画像を収集してみる

AWS Rekognition を使う時にクローラーも使ってなんかできないかなと思い scrapy を利用してみました。とりあえず今回はドメインと画像収集のところまで。いかがわしいことには絶対利用しないでください

今回はスタートのページからどんどんリンクを辿り、ドメイン名のフォルダごとに、辿った時のページの画像を保存します。今度そのフォルダごとに画像を AWS Rekognition に投げて、そのドメインがどんなドメインなのかを画像から判別しようと考えています。

前提

  • scrapy 1.5.0
  • python3
  • scrapy インストール済み

参考サイト

Spider のコード

クローラーの肝となる部分です。参考サイトではCrawlSpiderクラスを継承して利用している場合が多かったです。そっちの方が大抵の場合は楽だと思います。

WebSpider.py
# -*- coding: utf-8 -*-
  import scrapy
  from tutorial.items import TutorialItem
  import re
  from scrapy.exceptions import NotSupported
  from urllib.parse import urlparse


  class WebSpider(scrapy.Spider):
      name = 'web'
      # 見つけたドメインを入れる
      tracked_domains = []
      # 全てを対象
      allowed_domains = []
      # 最初に見に行くサイト
      start_urls = ['http://XXXXXXXXXXXXX']

      # response を毎回処理する関数
      def parse(self, response):
          try:
              # データ処理
              # この関数内の処理が終わると続きを実行する
              # dataPipeline を利用した場合もここに戻って来る
              yield self.parse_items(response)

              # リンクを辿る
              for link in response.xpath('//@href').extract():
                  if re.match(r"^https?://", link):
                      yield scrapy.Request(link, callback=self.parse)
          except NotSupported:
              # GET のレスポンスが txt じゃなかった場合
              # Spiders の logging 機能が用意されているのでそれを利用
              self.logger.info("Raise NotSupported")

      # ドメインごとにページに表示された画像を保存する
      def parse_items(self, response):
          # domain の抽出
          url = response.url
          parsed_url = urlparse(url)
          domain = parsed_url.netloc

          # 同じ Domain は一回しかチェックしない
          if domain in self.tracked_domains:
              return

          self.tracked_domains.append(domain)

          item = TutorialItem()
          item['domain'] = domain

          # title の抽出
          title = response.xpath(r'//title/text()').extract()
          if len(title) > 0:
              item['title'] = title[0]
          else:
              item['title'] = None

          # 画像 URL をセット
          item["image_urls"] = []
          for image_url in response.xpath("//img/@src").extract():
              if "http" not in image_url:
                  item["image_urls"].append(response.url.rsplit("/", 1)[0]
                         + "/" + image_url)
              else:
                  item["image_urls"].append(image_url)

          # item を返すと datapipeline に渡される
          return item

start_urlsに設定したURLを元にクローラーが動きます。
私はそんなことはしませんが、ここにいかがわしいサイトを指定するとリンクを辿って新しいいかがわしいサイトのドメインが見つかるかもしれません。私はそんなことしませんが。

基本的にはparse()関数が scrapy のレスポンスごとに呼ばれて処理を行います。
今回は途中でparse_items()を呼び出し、まだ保存していないドメインであればフォルダを作成してそのページ上の画像を保存します。

settings.py で scrapy の設定を記述

parse_items()itemを return すると、ImagePipeline に渡されます。
その設定は以下の通りです。自作 Pipeline の説明は後述。

settings.py
  # 自作 pipeline に繋げる
  ITEM_PIPELINES = {'tutorial.pipelines.MyImagesPipeline': 1}
  # データの保存場所
  IMAGES_STORE = '/Users/paper2/scrapy/tutorial/imgs'
  # リンクを辿る深さを指定
  DEPTH_LIMIT = 5
  # LOG_LEVEL = 'ERROR'
  DOWNLOAD_DELAY = 3

pipelines.py で pipeline をカスタマイズ

デフォルトの ImagePipeline ですとドメインごとのフォルダを作成して、、、などといった追加の作業ができないので継承して自分で作成します。

pipeliens.py
# -*- coding: utf-8 -*-
from scrapy.pipelines.images import ImagesPipeline
import os
from tutorial import settings
import shutil


class MyImagesPipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        # DL できたファイルのパス
        file_paths = [x['path'] for ok, x in results if ok]

        # ドメインごとのフォルダに move
        for file_path in file_paths:
            img_home = settings.IMAGES_STORE
            full_path = img_home + "/" + file_path
            domain_home = img_home + "/" + item['domain']

            os.makedirs(domain_home, exist_ok=True)
            # DL した結果同じファイルのことがある
            if os.path.exists(domain_home + '/' + os.path.basename(full_path)):
                continue
            shutil.move(full_path, domain_home)

        # parse() の続きに戻る
        return item

これで完成です。

実際に回してみる

しばらく回すと色々なドメインから画像が集まりました。
Screen Shot 2018-01-20 at 20.07.47.png

うん?よくみたらいかがわしいサイトのドメインが混ざっているぞ、、、画像もいかがわしいものが、、、、ということで次回はこのいかがわしいドメインを取り除く (逆にそれだけ残す??)のを AWS Rekognition でやってみようと思います。

続きを読む

AWS無料期間、Elastic IPを割り当てているとインスタンス停止してても課金される

結論

次の条件下では、わずかだが料金が発生する。
・ Elastic IP アドレスを割り当てているインスタンスを「停止させている」
→解決方法 : インスタンスを動かしたままにしておくか、Elastic IPを解放する

経緯

みんな大好きAWS。簡単にサーバを用意できて大変ありがたい。そんな便利なサービスですが、はじめのうちは無償利用枠でやりくりしたいひとも少なくないはず。

かくいう私もそのひとりで、作成したインスタンスにひとしきりデプロイした後、しばらく使わないからとEC2インスタンスを停止させました。

😎「停止させてれば課金されへんやろ」

しかしある日、Amazonから突然の請求メールが…

Billing Management Console.png

😎「!?」

“Elastic IP address not attached to a running instance” とな…

AWSのユーザガイドを覗くと…

AWS 請求情報とコスト管理 予想外の料金の回避

Elastic IP アドレス
終了したインスタンスにアタッチされたすべての Elastic IP アドレスはアタッチを解除されますが、割り当ては続行されます。その IP アドレスが不要になった場合は、追加料金が発生しないように解放してください。詳細については、『Linux インスタンス用 Amazon EC2 ユーザーガイド』の「Elastic IP アドレスを解放する」を参照してください。

貴重なIPをそんなもったいない使い方しないで、ってことですかね。

また、料金が発生したときに備えて、CloudWatchでBillingAlarmを設定するのも重要ですね!

参考にさせて頂いたページ

AWSで無料枠を利用していたつもりなのに請求が来ていた件 | Bamboo lath 日々の記録

AWS 運用 料金 メモ – Qiita

AWSで無料枠を利用し「Elastic IP」も全て解放済みのはずなのに料金が発生した件の原因と対応 – わかったつもりになってない?
→複数リージョンのインスタンスを使ってるときもまた注意が必要ですね

続きを読む

mapを使った読みやすいterraform variablesの書き方

スクリーンショット 2018-01-19 0.51.28.png

ディレクトリ構成

以下のように、terraform/provider/aws/env/stgとしました。
変数ファイルであるvariables.tfもメイン処理をするec2.tfも同じディレクトリに置きます。

環境ごとに完全にファイルを分断するイメージで、tfstateファイルも環境ごとに別にします。(保管場所はS3です)
なお、module化はしません。

 %tree 
.
└── provider
    └── aws
        └── env
            └── stg
                ├── backend.tf
                ├── ec2.tf
                └── variables.tf

変数の書き方

Before

これまでterraformの変数ファイルはこんな感じで書いてました。

before_variables.tf
variable "ami" {
  default = "ami-4af5022c"
}

variable "instance_type" {
  default = "t2.micro"
}

variable "instance_key" {
  default = "id_rsa"
}

実際のEC2のパラメータはこれだけではありません。もっとたくさんある上に、AWSのリソースは他にもあるとなるとかなり長い変数ファイルとなります。

特徴としては、variableが毎回並んでしまって行数が増える。読みにくい見にくい探しづらい。

これを解決するのにmapを使いました。

A map value is a lookup table from string keys to string values. This is useful for selecting a value based on some other provided value.
A common use of maps is to create a table of machine images per region, as follows:

sample.tf
variable "images" {
  type    = "map"
  default = {
    "us-east-1" = "image-1234"
    "us-west-2" = "image-4567"
  }
}

After

一つのvariableに対してリソースの値をまとめます。
map型の宣言は省略可能みたいです。

after_variables.tf
variable "ec2_config" {
  type = "map" #省略化
  default = {
    ami = "ami-4af5022c" 
    instance_type = "t2.micro" 
    instance_key = "id_rsa" 
  }
}

こうすることで、リソースごとに値がまとまるので読みやすくなりました。
ではメイン処理をするec2.tfを見ていきましょう。

map型でのec2.tfの書き方

Before

これまでのリソース定義の書き方はこちらです。

before_ec2.tf
resource "aws_instance" "vtryo-web01" {
  ami              = "${var.ami}"
  instance_type    = "${var.instance_type}"
  instance_key     = "${var.instance_key}"

    tags {
    Name = "vtryo-web01"
  }
}

そしてafter版に対するvariableに対応するリソース定義の仕方です。
map型で変数を格納したので、lookup関数を使って値を参照させます。

beforeのときよりやや長くなったように見えますが、ルールを覚えれば(後述)そこまで複雑ではない上、柔軟性は良くなっています。

after_ec2.tf
resource "aws_instance" "vtryo-web" {
  ami              = "${lookup(var.ec2_config, "ami")}" 
  instance_type    = "${lookup(var.ec2_config, "instance_type")}" 
  key_name         = "${lookup(var.ec2_config, "instance_key")}" 

  tags {
    Name = "vtryo-${format("web%02d", count.index + 1)}" 
  }

注意点

resource定義内のami, instance_type, key_nameはterraform側で名前が決まっています。variables.tf内の変数名は任意ですが、こちらは自由には決められないので注意しましょう。
aws_instance

lookup

上記の書き方は、lookup関数でKeyを直接指定する方法を取っています。
こちらを参考にしています。
Terraformのoutputでmapを利用する方法

たとえば以下であれば
ami = "${lookup(var.ec2_config, "ami")}"

variables.tf内のec2_configamiの値を参照する」という意味になります。

format

最終行の"vtryo-${format("web%02d", count.index + 1)}"についてですが、変数格納後の値はvtryo-web01になります。
ちなみに"web%02d"を、"web%01d"にするとvtryo-web1になってしまうので注意です。

terraform init

さて実行です。

% terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.7.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.7"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform plan

 % terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

変数が格納されました!

ちょっと応用

さきほどのec2.tfでNameには"vtryo-${format("web%02d", count.index + 1)}"を書きました。
この出力結果はvtryo-web01です。
これをstg-vtryo-web01のように、環境を先頭に入れる方法を書いておきます。

variable “env”

variables.tfにvariable "env { }"を書きます。

variables.tf
variable "env" { }

variable "ec2_config" {
  default = {
    ami           = "ami-4af5022c"
    instance_type = "t2.micro"
    instance_key  = "id_rsa"
  }
}

{ }の中にdefaultを入れてもよいです。その場合は以下のように書きます。

variables.tf
variable "env" { 
  default = "text message..."
 }

${var.env}

一方でec2.tfには以下を書きます。

resource "aws_instance" "vtryo-web" {
  ami                      = "${lookup(var.ec2_config, "ami")}"
  instance_type            = "${lookup(var.ec2_config, "instance_type")}"
  key_name                 = "${lookup(var.ec2_config, "instance_key")}"
  tags {
    Name = "${var.env}-vtryo-${format("web%02d", count.index + 1)}"
  }
}

${var.env}をつけることで、terraform planしたときに入力を求められます。
入力した内容が、${var.env}に格納されるという仕組みです。

terraform plan

Enter a valueを求められるので今回はstgにしました。
すると、stgという文字列が格納されます。

% terraform plan
var.env
  Enter a value: stg

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "stg-vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

ちなみにterraform plan -var "env=stg"とすることで入力なしで格納することが可能です。

今回の補足というか余談

「terraformのbest-practice、わかりづらくない?」

開発者が推奨している構成だとはわかっていますが、素直にそんな感想がありました。
terraform best-practice
(もちろんQiitaにかかれている記事も読んでいます
Terraform Best Practices in 2017

ファイルの中に何度も宣言をしないといけないようですし、どうにも読みづらい印象があります。
また、「Environmentで環境を分ける」構成になっていますが、公式曰く

Workspaces can be used to manage small differences between development, staging, and production, but they should not be treated as the only isolation mechanism.

とあります。我らがGoogle翻訳を使うとこんなことを言ってます。

ワークスペースは、開発、ステージング、およびプロダクションの小さな違いを管理するために使用できますが、唯一の分離メカニズムとして扱うべきではありません。

初見で解読するには骨がいる内容です。何度もterraformを使い込んだらわかるんでしょうか。
ま、単純に私の技術力が追いついていない可能性の方が高いですが(爆)

とはいえ誰でも彼でもterraformの技術力が高いわけではないので、より読みやすくメンテナンスしやすい書き方をしても良いと思っています。

terraformは一歩間違えるとインフラそのものが削除されることがあるので、それを踏まえて確実な方が良いだろうというきもちです。

今回はQiitaの記事にだいぶ助けられたこともあったので、Qiitaに書くことにしました。

シンプルに書きたい欲

terraformにはmodule化が出来る機能があって、それがやや難易度を上げているように思います。

変数をmodule化できることでメリットもありますが、利用ができなければ意味がない。terraformにはmoduleのレジストリがありますが、仕組みを理解しないとやっぱり使うのは大変です。

学習コストばかりかかるくらいなら、いっそ使わずにシンプルにコードを書いて行こうということでした。

best-practiceに固執しない

重要なのは可読性ととっつきやすさだと思ったので、あえて固執せずにディレクトリを構成し、terraformを見やすく書く方法を考えました。
誰かの参考になれば幸いです。

参考

Terraformのoutputでmapを利用する方法 – Qiita
Terraform Best Practices in 2017 – Qiita
TerraformでのAWS環境構築の設定を分ける – Qiita
terraform
さらに一歩楽しむterraform. moduleでIAM UserとPolicy管理を簡素化しよう
本記事はこちらのクローンです。

続きを読む