AWS IoT SDK のオフラインキューイングとQoSの関係

AWS IoTでは、MQTTクライアントのNode.js実装であるMQTT.jsをラップしたaws-iot-device-sdk-jsというライブラリを提供しています。
このライブラリではオフラインキューイングの仕組みが備わっているのですが、データ送信時に指定するQoSレベルと、さらには内部で使用しているMQTT.jsにもオフラインキューイングの仕組みが備わっているため、これらの関係がどうなっているかを調べてみました。

今回テストしたコード。
offlineQueueingをtrue/falseにすることで、オフラインキューイングを有効/無効にすることができます。

'use strict'

const awsIot = require('aws-iot-device-sdk')
require('dotenv').config()

const device = awsIot.device({
  keyPath: process.env.KEY_FILE,
  certPath: process.env.CERT_FILE,
  caPath: process.env.CA_FILE,
  host: process.env.IOT_HOST,
  clientId: 'test-device',
  offlineQueueing: true,  // オフラインキューイング設定
  debug: true
})

const topic = `test`

let timer = null

device.on('reconnect', () => {
  console.info(new Date(), 'reconnect')
})

device.on('close', () => {
  console.info(new Date(), 'closed')
})

device.on('offline', () => {
  console.info(new Date(), 'offline')
})

device.on('error', (err) => {
  console.error(new Date(), 'error', err)
})

device.on('connect', () => {
  console.info(new Date(), 'connected')

  if (timer !== null) {
    return
  }

  let count = 0
  timer = setInterval(() => {
    count++
    console.info(new Date(), `publishing {count: ${count}}`)
    device.publish(topic, JSON.stringify({count: count}), (err) => {
      if (err) {
        return console.error(new Date(), err)
      }
      console.info(new Date(), 'published')
    })
  }, 5000)
})

実行ログ。

{ keyPath: './certs/private.pem.key',
  certPath: './certs/certificate.pem.crt',
  caPath: './certs/root-CA.crt',
  clientId: 'test-device',
  host: 'xxxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com',
  offlineQueueing: false,
  debug: true,
  reconnectPeriod: 1000,
  fastDisconnectDetection: true,
  protocol: 'mqtts',
  port: 8883,
  key: <Buffer 2d ... >,
  cert: <Buffer 2d ...>,
  ca: <Buffer 2d ... >,
  requestCert: true,
  rejectUnauthorized: true }
attempting new mqtt connection...
2017-08-23T08:28:05.391Z 'connected'
2017-08-23T08:28:10.398Z 'publishing {count: 1}'
2017-08-23T08:28:10.407Z 'published'
2017-08-23T08:28:15.415Z 'publishing {count: 2}'
2017-08-23T08:28:15.418Z 'published'
2017-08-23T08:28:20.424Z 'publishing {count: 3}'
2017-08-23T08:28:20.425Z 'published'
2017-08-23T08:28:25.426Z 'publishing {count: 4}'
2017-08-23T08:28:25.427Z 'published'
2017-08-23T08:28:30.433Z 'publishing {count: 5}'
2017-08-23T08:28:30.435Z 'published'
2017-08-23T08:28:35.440Z 'publishing {count: 6}'
2017-08-23T08:28:35.441Z 'published'
2017-08-23T08:28:40.447Z 'publishing {count: 7}'
2017-08-23T08:28:40.448Z 'published'
2017-08-23T08:28:45.454Z 'publishing {count: 8}'
2017-08-23T08:28:45.456Z 'published'
2017-08-23T08:28:50.462Z 'publishing {count: 9}'
2017-08-23T08:28:50.463Z 'published'
2017-08-23T08:28:55.469Z 'publishing {count: 10}'
2017-08-23T08:28:55.470Z 'published'
2017-08-23T08:29:00.476Z 'publishing {count: 11}'
2017-08-23T08:29:00.478Z 'published'
2017-08-23T08:29:05.484Z 'publishing {count: 12}'
2017-08-23T08:29:05.485Z 'published'
2017-08-23T08:29:10.491Z 'publishing {count: 13}'
2017-08-23T08:29:10.492Z 'published'
2017-08-23T08:29:15.498Z 'publishing {count: 14}'
2017-08-23T08:29:15.501Z 'published'
2017-08-23T08:29:20.508Z 'publishing {count: 15}'
2017-08-23T08:29:20.510Z 'published'
2017-08-23T08:29:25.515Z 'publishing {count: 16}'
2017-08-23T08:29:25.516Z 'published'
2017-08-23T08:29:30.522Z 'publishing {count: 17}'
2017-08-23T08:29:30.523Z 'published'
2017-08-23T08:29:35.529Z 'publishing {count: 18}'
2017-08-23T08:29:35.531Z 'published'
2017-08-23T08:29:40.536Z 'publishing {count: 19}'
2017-08-23T08:29:40.538Z 'published'
2017-08-23T08:29:45.544Z 'publishing {count: 20}'
2017-08-23T08:29:45.545Z 'published'
2017-08-23T08:29:50.551Z 'publishing {count: 21}'
2017-08-23T08:29:50.552Z 'published'
2017-08-23T08:29:55.559Z 'publishing {count: 22}'
2017-08-23T08:29:55.560Z 'published'
2017-08-23T08:30:00.520Z 'offline'
connection lost - will attempt reconnection in 1 seconds...
2017-08-23T08:30:00.523Z 'closed'
2017-08-23T08:30:00.561Z 'publishing {count: 23}'
2017-08-23T08:30:01.525Z 'reconnect'
2017-08-23T08:30:05.566Z 'publishing {count: 24}'
2017-08-23T08:30:10.573Z 'publishing {count: 25}'
2017-08-23T08:30:15.579Z 'publishing {count: 26}'
2017-08-23T08:30:20.585Z 'publishing {count: 27}'
2017-08-23T08:30:25.591Z 'publishing {count: 28}'
2017-08-23T08:30:30.597Z 'publishing {count: 29}'
connection lost - will attempt reconnection in 2 seconds...
2017-08-23T08:30:31.549Z 'closed'
2017-08-23T08:30:33.550Z 'reconnect'
2017-08-23T08:30:35.601Z 'publishing {count: 30}'
2017-08-23T08:30:39.428Z 'connected'
2017-08-23T08:30:40.603Z 'publishing {count: 31}'
2017-08-23T08:30:40.604Z 'published'
2017-08-23T08:30:45.609Z 'publishing {count: 32}'
2017-08-23T08:30:45.610Z 'published'
2017-08-23T08:30:50.616Z 'publishing {count: 33}'
2017-08-23T08:30:50.617Z 'published'
2017-08-23T08:30:55.628Z 'publishing {count: 34}'
2017-08-23T08:30:55.631Z 'published'
2017-08-23T08:31:00.640Z 'publishing {count: 35}'
2017-08-23T08:31:00.643Z 'published'

MQTTコネクション断の検知に時間がかかる

上記のテストコードを実行し、AWS IoTに接続していくつかのデータが送信されたことを確認したあと、意図的にネットワークを遮断という方法で実験を行いました。
注意すべきなのは、aws-iot-device-sdk-jsはネットワークが遮断されてすぐにオフライン(コード内部ではinactive)状態に入るわけではなく、1分ほど立ってからコネクションの断を検知してオフラインになるということです。
上記のログでいうと、{count: 10}を送信した直後にネットワークを遮断していますが、ライブラリがオフライン状態に入ったのはそこから約1分後です。
その後、{count: 30}を送信した直後にネットワークを復活させました。
コネクション断の検知は、MQTT.jsがネットワークソケットのcloseイベントに反応することによって行われていると思われますが、このcloseイベントが実際のコネクション断のあとなかなか発火されないのがなぜなのかはわかってません。

オフラインキューイング設定とQoSの関係

aws-iot-device-sdk-jsのオフラインキューイング設定と、publishメソッドに渡すQoSレベルの関係をまとめました。
QoSの設定は、上記コードにおいて、device.publishメソッドの第3引数に{qos: 1}を渡すことによって可能です。何も指定しない場合はQoS0になります。MQTTではQoS0から2までを定義していますが、AWS IoTではQoS0と1のみ利用可能です。

# オフラインキューイング QoS データの送信結果
1 false 0 実際のコネクション断から再接続までのデータがロストする
2 true 0 ライブラリがコネクション断を検知してから再接続するまでのデータがキューイングされ、再送される。実際のコネクション断からライブラリが検知するまでのデータはロストする。
3 false 1 ライブラリがコネクション断を検知してから再接続するまでのデータのみロストするという不思議な動き。
4 true 1 全てのデータがキューイングされ、再送される。データロストはない。

#2の根拠

ライブラリがオフラインになってからキューイングが始まり、接続再開と同時にキューイングしたデータを送信するという動きになります。上述したように、実際のネットワーク断からライブラリがオフラインだと判断するまでに時間がかかるので注意が必要です。

#3の根拠

この不思議な動きはaws-iot-device-sdk-jsの実装を見るとわかります。
データロストが起きているのは、ライブラリがオフライン状態に入っているときです。
まず、オフラインキューイングはfalseなため最初のif文はパスします。次に、_filing()はライブラリがオフライン状態だとみなしているときにtrueになり、ここではオンラインであることを期待しているので、このif文もパスしてしまい、結果何も処理をされないままメソッドが終わってしまうことになります。

this.publish = function(topic, message, options, callback) {
   //
   // If filling or still draining, push this publish operation 
   // into the offline operations queue; otherwise, perform it
   // immediately.
   //
   if (offlineQueueing === true && (_filling() || drainingTimer !== null)) {
      if (_trimOfflinePublishQueueIfNecessary()) {
         offlinePublishQueue.push({
            topic: topic,
            message: message,
            options: options,
            callback: callback
         });
      }
   } else {
      if (offlineQueueing === true || !_filling()) {
         device.publish(topic, message, options, callback);
      }
   }
};

#1の根拠

単純にネットワークが遮断されている間のデータ送信が失敗しているということです。
MQTT.jsはデフォルトではQoS0のデータをオフラインキューイングするようになっていますが、上記のロジックによってMQTT.jsのpublishも呼ばれないため、このような動きになります。

#4の根拠

aws-iot-device-sdk-jsは実際のコネクション断に気づかずデータを送信するが、実際の送信処理を行っているMQTT.jsがQoSレベル1に従った動きをしているため、データのロストは起きません。つまり、データの送信は行うがAWS IoTからpubackが返ってこないために再送処理をしているということです。

まとめ

オフラインキューイングとQoSをどのように設定するかは送信するデータの重要度によって変わってくるので、どの組合せが良いかは一概には言えませんが、上記#2で指摘したようにオフラインキューイングが直感的な動きと少し異なる点には注意が必要です。

続きを読む

AnsibleでAWS操作 Certificate Manager編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編
  5. Simple Email Service編
  6. Certificate Manager編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • ACMにて独自ドメインへのSSL証明書を発行
  • SSL証明書をCloudFrontに設定
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み
  • 独自ドメイン取得済み
  • SESの設定済み
  • CFとS3の設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. SSL証明書発行リクエストの作成

command
ansible-playbook -i inventory/production create-aws-acm-request.yml

戻り値の値を控えます。

2. SESに保存された認証確認メールから認証用URLを取得し、ブラウザからapproveする

command
ansible-playbook -i inventory/production view-aws-acm-notice-mail-url.yml

戻り値のURLにブラウザからアクセスする必要あり

3. CloudFrontの設定を更新して、httpsでアクセス出来るようにする

command
ansible-playbook -i inventory/production update-aws-cf.yml


## ディレクトリ構成
```text

├── ansible.cfg
├── create-aws-acm-request.yml
├── templates
│   └── production
│       └── cf
│           └── update.j2
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── create-aws-acm-request
│   │   └── tasks
│   │       └── main.yml
│   ├── update-aws-cf
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-acm-notice-mail-url
│       └── tasks
│           └── main.yml
├── update-aws-cf.yml
├── view-aws-acm-notice-mail-url.yml
└── vars
    └── all.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
AWS:
  ACM:
    ARN: ${ACMリクエストのARN}
    REGION: us-west-2
    VERIFICATION_MAIL_OBJECT: ${認証メールオブジェクト名}
  CF:
    DISTRIBUTION:
      ID: ${ディストリビューションID}
      ORIGIN_ID: ${オリジンID}
      E_TAG: ${Eタグ}
  S3:
    BUCKET:
      NAME: ${バケット名}
    END_POINT: ${S3バケットの静的ホスティングエンドポイント}
DOMAIN:
  MAIN:
    NAME: ${ドメイン名}
  SUB:
    NAME: ${サブドメイン名}

templates

production/cf/update.j2
{
  "DistributionConfig": {
    "Comment": "Https Bucket.",
    "CacheBehaviors": {
      "Quantity": 0
    },
    "IsIPV6Enabled": true,
    "Logging": {
      "Bucket": "",
      "Prefix": "",
      "Enabled": false,
      "IncludeCookies": false
    },
    "WebACLId": "",
    "Origins": {
      "Items": [
        {
          "OriginPath": "",
          "CustomOriginConfig": {
            "OriginSslProtocols": {
              "Items": [
                "TLSv1",
                "TLSv1.1",
                "TLSv1.2"
              ],
              "Quantity": 3
            },
            "OriginProtocolPolicy": "http-only",
            "OriginReadTimeout": 30,
            "HTTPPort": 80,
            "HTTPSPort": 443,
            "OriginKeepaliveTimeout": 5
          },
          "CustomHeaders": {
            "Quantity": 0
          },
          "Id": "{{ AWS.CF.ID }}",
          "DomainName": "{{ AWS.S3.END_POINT }}"
        }
      ],
      "Quantity": 1
    },
    "DefaultRootObject": "index.html",
    "PriceClass": "PriceClass_All",
    "Enabled": true,
    "DefaultCacheBehavior": {
      "TrustedSigners": {
        "Enabled": false,
        "Quantity": 0
      },
      "LambdaFunctionAssociations": {
        "Quantity": 0
      },
      "TargetOriginId": "{{ AWS.CF.ID }}",
      "ViewerProtocolPolicy": "redirect-to-https",
      "ForwardedValues": {
        "Headers": {
          "Quantity": 0
        },
        "Cookies": {
          "Forward": "all"
        },
        "QueryStringCacheKeys": {
          "Quantity": 0
        },
        "QueryString": true
      },
      "MaxTTL": 604800,
      "SmoothStreaming": false,
      "DefaultTTL": 604800,
      "AllowedMethods": {
        "Items": [
          "HEAD",
          "GET"
        ],
        "CachedMethods": {
          "Items": [
            "HEAD",
            "GET"
          ],
          "Quantity": 2
        },
        "Quantity": 2
      },
      "MinTTL": 604800,
      "Compress": false
    },
    "CallerReference": "2017-07-29_06-25-01",
    "ViewerCertificate": {
      "SSLSupportMethod": "sni-only",
      "ACMCertificateArn": "{{ AWS.ACM.ARN }}",
      "MinimumProtocolVersion": "TLSv1",
      "Certificate": "{{ AWS.ACM.ARN }}",
      "CertificateSource": "acm"
    },
    "CustomErrorResponses": {
      "Quantity": 0
    },
    "HttpVersion": "http2",
    "Restrictions": {
      "GeoRestriction": {
        "RestrictionType": "none",
        "Quantity": 0
      }
    },
    "Aliases": {
      "Items": [
        "{{ DOMAIN.MAIN.NAME }}"
      ],
      "Quantity": 1
    }
  },
  "Id": "{{ AWS.CF.DISTRIBUTION.ID }}",
  "IfMatch": "{{ AWS.CF.DISTRIBUTION.E_TAG }}"
}

playbook

create-aws-acm-request.yml
- hosts: cliservers
  roles:
    - create-aws-acm-request
  vars_files:
    - vars/all.yml
update-aws-cf.yml
- hosts: cliservers
  roles:
    - update-aws-cf
  vars_files:
    - vars/all.yml
view-aws-acm-notice-mail-url.yml
- hosts: cliservers
  roles:
    - view-aws-acm-notice-mail-url
  vars_files:
    - vars/all.yml

tasks

role/create-aws-acm-request/tasks/main.yml
- name: "Create Certificate"
  shell: |
    aws acm request-certificate 
    --domain-name {{ DOMAIN.MAIN.NAME }} 
    --subject-alternative-names {{ DOMAIN.SUB.NAME }} 
    --domain-validation-options DomainName={{ DOMAIN.SUB.NAME }},ValidationDomain={{ DOMAIN.MAIN.NAME }} 
    --region={{ AWS.ACM.REGION }} 
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-acm-notice-mail-url/tasks/main.yml
- name: Download Verification Mail Object
  shell: |
    aws s3 cp 
    s3://${AWS.S3.BUCKET.NAME}/{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }} 
    {{ TEMP.DIRECTORY }}.{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

- name: View URL
  shell: |
    grep https {{ TEMP.DIRECTORY }}.{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }} | grep context
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/update-aws-cf/tasks/main.yml
- name: Create Json
  template: 
    src={{ ENV }}/cf/update.j2
    dest={{ TEMP.DIRECTORY }}/update.json
  tags:
    - always

- name: Update Distribution
  shell: |
    aws cloudfront update-distribution 
    --cli-input-json file://files/{{ ENV }}/cf/update.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

これで今までhttpでアクセスしてたS3バケット(CloudFront経由)にアクセスすると、httpsにリダイレクトされた上で正常に表示されると思います。

AWS+CloudFrount+ACM+S3を使えばほぼ無料でSSL証明証の発行や紐付けが出来、自動更新の設定や複数ドメインのSSL証明書をまとめて管理することが出来ます。

自分は以下のようなWordpressプラグインとAWSサービスで静的サイトを運用していますが、今のところ問題なく運用出来ています。

Wordpress+staticpress+staticpresss3+S3+CloudFront+ACM

Wordpressを静的サイト化することで一部機能が制限されてしまいますが、セキュリティ観点がかなり向上します。

また、制限される部分も特に不要な機能だと思いますし、JavaScript等である程度保管することも可能なので動的サイトを切り捨てて静的サイトにするメリットは十分あると思いますので、興味のある方は是非お試し下さい♪

じゃあの。

続きを読む

AWS Lambda(C#)からAuroraにつないでみる

Serverless Meetup Tokyo #4に参加してきたら

  • AWS SAから「ServerlessにはAuroraは不向き」的な話をされた
  • ENI作成時間が気になるなら、定期的にLambdaの暖機が必要

のような逆風にも負けずLambdaからAuroraに接続するアツい話を聞いて、無性にLambda(C#)からAuroraに接続したくなったので試してみました。

そう言えば「LambdaならC#よりnode.js/Pythonの方がオススメ」的な話も聞いたことがあるような…

開発環境

  • Mac
  • .Net Core 2.0.0 / .Net Core 1.0.5共存環境

本来Lambdaの作成には.Net Core 2.0.0は不要ですが
 ・自分の環境は1.0と2.0の共存環境で
 ・2.0.0になってdotnet restoreの振る舞いが変わった
ため、dotnetコマンドのバージョンが2.0.0の場合で記載します。

作業概要

  1. .Net Coreのインストール
  2. Auroraの作成
  3. C#コード作成
  4. デプロイ用パッケージの作成
  5. Lambdaの実行

1. .Net Coreのインストール

MicrosoftのサイトからMac用をダウンロードしてインストールします。

2.0.0
1.0.5 with SDK 1.0.4

2. Auroraの作成

AWSマネジメントコンソールからAuroraを構築します。

※留意点

  • VPC内からアクセスするため、Publicly Accessible:Noにします
  • お試しなら、インスタンスタイプは一番料金が安いdb.t2.smallがオススメです

3. C#コード作成

適当な作業ディレクトリを作成し、その配下に.csファイルを作成します。
またAuroraに接続するために、下記値を設定します。

  • クラスタエンドポイント
  • ユーザ / パスワード
  • データベース名
MyFunction.cs
using Amazon.Lambda.Core;
using MySql.Data.MySqlClient;

namespace sample_aurora
{
    public class MyFunction
    {
        const string ConnectionString = "<<クラスタエンドポイント>>;user=<<ユーザ>>;password=<<パスワード>>;port=3306;database=<<データベース名>>;SslMode=None";

        [LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
        public string MyHandler(LambdaEvent lambdaEvent, ILambdaContext context)
        {
            string command = lambdaEvent.Command;
            LambdaLogger.Log("Command: " + command + "n");
            switch(command){
                case "select":
                    Select();
                    break;
                case "insert":
                    Insert();
                    break;
                case "init":
                    Init();
                    break;
                default:
                    LambdaLogger.Log("Do nothing.n");
                    break;

            }
            return "Sample function was executed.";
        }

        private void Select()
        {
            MySqlConnection connection = new MySqlConnection(ConnectionString);
            connection.Open();

            MySqlCommand command = new MySqlCommand("select id from test_tbl;", connection);

            string message = "Data: ";

            using (MySqlDataReader reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    string row = $"{reader["id"]}";
                    message = message + "," + row;
                }
            }

            connection.Close();

            LambdaLogger.Log(message + "n");
        }

        private void Insert()
        {
            MySqlConnection connection = new MySqlConnection(ConnectionString);
            connection.Open();

            MySqlCommand command = new MySqlCommand("insert into test_tbl values ();", connection);

            command.ExecuteNonQuery();

            connection.Close();

            LambdaLogger.Log("A record has been inserted.n");
        }

        private void Init()
        {
            MySqlConnection connection = new MySqlConnection(ConnectionString);
            connection.Open();

            MySqlCommand command = new MySqlCommand("create table if not exists test_tbl (id int auto_increment primary key);", connection);

            command.ExecuteNonQuery();

            connection.Close();

            LambdaLogger.Log("A table has been created.n");
        }
    }

    public class LambdaEvent
    {
        public string Command { get; set; }
    }
}

4. デプロイ用パッケージの作成

4-1. .csprojファイルの作成

同じ作業ディレクトリに.csprojファイルを作成します。

sample_aurora.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.TestUtilities" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="MySql.Data.Core" Version="7.0.4-IR-191" />
  </ItemGroup>
</Project>

4-2. デプロイパッケージの作成

作業ディレクトリ内で以下コマンドを実行します。

$ dotnet restore
$ dotnet publish --output publish
$ cp ~/.nuget/packages/system.data.sqlclient/4.1.0/runtimes/unix/lib/netstandard1.3/System.Data.SqlClient.dll publish/
$ cp ~/.nuget/packages/system.diagnostics.tracesource/4.0.0/runtimes/unix/lib/netstandard1.3/System.Diagnostics.TraceSource.dll pulish/
$ cd publish
$ zip ../sample_aurora.zip *

※留意点

  • System.Data.SqlClient.dllとSystem.Diagnostics.TraceSource.dllはdotnet publishでは含まれなかったため、手動でコピーしています。(無いとLambda実行時にエラーになります)

4-3. Lamdbaのデプロイ

AWSマネジメントコンソールからLambdaを選んで、「sample_aurora.zip」をデプロイします。

  • VPC対応にします
  • Auroraに接続できるsecurity groupを指定します
  • もしくは、指定したsubnet groupがaurora側のsecurity groupで許可されていれば、lamdbaで指定するsecurity grouopはダミーで構いません

5. Lambdaの実行

AWSマネジメントコンソールからLambdaを実行します。

5-1. テーブル作成

test eventに以下のjsonを指定してLambdaを実行します。

{
  "Command": "init"
}

Log outputに以下出力されます。

Command: init
A table has been created.

5-2. Insert

test eventに以下のjsonを指定してLambdaを実行します。

{
  "Command": "insert"
}

Log outputに以下出力されます。

Command: insert
A record has been inserted.

実行した回数分テーブルにレコードが追加されます。

5-3. Select

test eventに以下のjsonを指定してLambdaを実行します。

{
  "Command": "select"
}

Log outputに以下出力されます。

Command: select
Data: ,1,2,3

雑感

案外簡単にC# LambdaからAuroraへ接続できました。

続きを読む

現在のインスタンス料金を取得する script: EC2 編

http://qiita.com/bells17/items/5326d11edc6acc4feea2
の EC2 版です

注意点として OS を Linux、 tenancy を Shared に絞っています

ec2.rb
require 'json'
require 'bigdecimal'

results = {}
json_data = open(ARGV[0]) {|io| JSON.load(io) }

# product 情報を取得
json_data['products'].keys.each do |skuNo|
    product = json_data['products'][skuNo]

    if (product['productFamily'] == 'Compute Instance' and
          product['attributes']['locationType'] == 'AWS Region' and
          product['attributes']['location'] == 'Asia Pacific (Tokyo)' and
          product['attributes']['operatingSystem'] == 'Linux' and
          product['attributes']['tenancy'] == 'Shared')

        results[product['sku']] = {
            sku: product['sku'],
            location: product['attributes']['location'],
            instanceType: product['attributes']['instanceType'],
            instanceTypePrefix: product['attributes']['instanceType'].split('.')[0],
            instanceFamily: product['attributes']['instanceFamily'],
            vcpu: product['attributes']['vcpu'],
            memory: product['attributes']['memory'],
            storage: product['attributes']['storage'],
            clockSpeed: product['attributes']['clockSpeed'],
            networkPerformance: product['attributes']['networkPerformance'],
            ecu: product['attributes']['ecu'],
            currentGeneration: product['attributes']['currentGeneration'],
            price_unit: 'USD'
        }

    end
end


# price

## on demand
json_data['terms']['OnDemand'].keys.each do |skuNo|
    if (results[skuNo])
        results[skuNo][:price_per_hour] = Proc.new {
            skuTerm = json_data['terms']['OnDemand'][skuNo][json_data['terms']['OnDemand'][skuNo].keys[0]]
            priceInfo = skuTerm['priceDimensions'][skuTerm['priceDimensions'].keys[0]]
            BigDecimal(priceInfo['pricePerUnit']['USD']).floor(2).to_f.to_s
        }.call
        results[skuNo][:price_per_day] = (BigDecimal(results[skuNo][:price_per_hour]) * BigDecimal("24")).floor(2).to_f.to_s
        results[skuNo][:price_per_month] = (BigDecimal(results[skuNo][:price_per_day]) * BigDecimal("30")).floor(2).to_f.to_s
    end
end

## reserved 
json_data['terms']['Reserved'].keys.each do |skuNo|
    if (results[skuNo])

        plans = json_data['terms']['Reserved'][skuNo].values.select do |plan|
            plan['termAttributes']['PurchaseOption'] == "All Upfront" # "All Upfront" のものだけ取得したい
        end

        results[skuNo][:price_reserved_1year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '1yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

        results[skuNo][:price_reserved_3year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '3yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

    end
end


# sort
sorted_result = {}
results.values.each do |row|
    sorted_result[row[:currentGeneration]] ||= {}
    sorted_result[row[:currentGeneration]][row[:instanceFamily]] ||= {}
    sorted_result[row[:currentGeneration]][row[:instanceFamily]][row[:instanceTypePrefix]] ||= []
    sorted_result[row[:currentGeneration]][row[:instanceFamily]][row[:instanceTypePrefix]].push row
end

results = []
['Yes', 'No'].each do |currentGeneration| # 現行世代のものから並べる
    sorted_result[currentGeneration].keys.sort.each do |instanceFamily| # インスタンスファミリー毎に並べる
        sorted_result[currentGeneration][instanceFamily].keys.sort.each do |instanceTypePrefix|
            results.concat sorted_result[currentGeneration][instanceFamily][instanceTypePrefix].sort_by { |row| row[:price_per_hour] }
        end
    end
end

p results.to_json

上記を保存して以下のように実行する

curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json > price-AmazonEC2.json
ruby ec2.rb price-AmazonEC2.json | sed -e s/^"// | sed -e s/"$// | sed -e 's/\"/"/g' | jq .

以下のような感じで結果が取れる

[
  {
    "sku": "Q4QTSF7H37JFW9ER",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "c3.large",
    "instanceTypePrefix": "c3",
    "instanceFamily": "Compute optimized",
    "vcpu": "2",
    "memory": "3.75 GiB",
    "storage": "2 x 16 SSD",
    "clockSpeed": "2.8 GHz",
    "networkPerformance": "Moderate",
    "ecu": "7",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.12",
    "price_per_day": "2.88",
    "price_per_month": "86.4",
    "price_reserved_1year_purchased_all_upfront": "753",
    "price_reserved_3year_purchased_all_upfront": "1528"
  },
  {
    "sku": "HTNXMK8Z5YHMU737",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "c3.xlarge",
    "instanceTypePrefix": "c3",
    "instanceFamily": "Compute optimized",
    "vcpu": "4",
    "memory": "7.5 GiB",
    "storage": "2 x 40 SSD",
    "clockSpeed": "2.8 GHz",
    "networkPerformance": "Moderate",
    "ecu": "14",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.25",
    "price_per_day": "6.0",
    "price_per_month": "180.0",
    "price_reserved_1year_purchased_all_upfront": "1505",
    "price_reserved_3year_purchased_all_upfront": "3032"
  },
  {
    "sku": "YR67H6NVBRN37HRZ",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "c3.2xlarge",
    "instanceTypePrefix": "c3",
    "instanceFamily": "Compute optimized",
    "vcpu": "8",
    "memory": "15 GiB",
    "storage": "2 x 80 SSD",
    "clockSpeed": "2.8 GHz",
    "networkPerformance": "High",
    "ecu": "28",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.51",
    "price_per_day": "12.24",
    "price_per_month": "367.2",
    "price_reserved_1year_purchased_all_upfront": "3012",
    "price_reserved_3year_purchased_all_upfront": "8132"
  },
...
]

続きを読む

RDSとS3でファイルのやり取りを行う

データベースサーバ上にファイルを置いて、PL/SQLのUTL_FILE経由で読み書きするような処理があった場合、RDSに移行しようとすると、データベースサーバにファイルが置けないなあ・・・などという場合に、S3を間に置く方法があります。

前提

EC2 <-file-> S3 <-file-> RDS上のファイル
というやり取りについて記載しています。
また、以下の情報は2017年8月時点のものです。

Oracle on Amazon RDSでの制限

前提として、Oracle on RDSでできることを整理しましょう。S3にアクセスするにはUTL_HTTPパッケージが必要です。ユーザーガイドの「utl_http、utl_tcp、utl_smtp の使用」にサポートされる旨が記載されています。
次に、UTL_FILEによるアクセスです。UTL_FILEを用いるにはディレクトリオブジェクトを扱える必要があります。こちらについてもユーザーガイドの「主要データストレージ領域で新しいディレクトリを作成する」にて、rdsadmin.rdsadmin_util.create_directoryプロシージャを使用して可能なことが記載されています。

使用するライブラリ

UTL_HTTPを使ったS3へのアクセスを全て自分で書くのは大変なので、alexandria-plsql-utilsのAMAZON_AWS_S3_PKGを用いることにします。

準備手順

EC2, S3のバケット、RDSをすべて新規で構成する手順を見ていきましょう。順序としては以下のようになります。
1. RDSを作成する。
2. S3にバケットを作成する。
3. バケットに含まれるオブジェクトへのアクセス権を持つポリシーを作成する。
4. 3で作成したポリシーを持つロールを付与したEC2を作成する。
5. 3で作成したポリシーを持つユーザを作成する。
6. RDSにディレクトリを作成し、S3へのアクセスに必要な権限と5で作成したユーザのアクセス情報を設定する。
7. EC2とS3のやり取りを行ってみる。
8. RDSとS3のやり取りを行ってみる。

1. RDSの作成

まず、いきなりRDSを作成するのではなく、先にRDSのメニューから「オプショングループ」を選択し、「apex」という名前でAPEX及びAPEX-DEVを含むオプショングループを作成して下さい。これは、AMAZON_AWS_S3_PKGが内部でデコード関連でAPEXのライブラリを使用しているためです(APEXを実際に起動する必要はありません)。以下のようになります。
スクリーンショット 2017-08-19 17.32.40.png

apexオプショングループを用いてRDSを作成して下さい。指定箇所はパラメータグループの下にあります。次のようになります。
スクリーンショット 2017-08-19 17.34.53.png
あとは通常のRDSの作成と同様です。RDSの作成については以下を参照して下さい。
RDSユーザーガイド-Oracle DB インスタンスを作成して Oracle DB インスタンス上のデータベースに接続する

2. S3バケットの作成

特に特筆すべきことはありません。グローバルで一意になる名前でS3にバケットを作成しましょう。
S3入門ガイド-バケットの作成

3. ポリシーの作成

IAMから2で作成したバケットに含まれるオブジェクトへのアクセス権限を持つポリシーを作成します。
IAM -> ポリシーで「ポリシーの作成」を押したら、「独自のポリシーの作成」を選びましょう。
スクリーンショット 2017-08-19 17.46.37.png
ポリシー名には「allow-rds-s3-policy」などとし、ポリシードキュメントには以下のJSONを記述します。

S3-rds-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<手順2で作成したS3バケットの名前>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::<手順2で作成したS3バケットの名前>/*"
            ]
        }
    ]
}

このポリシーは2で作成したS3バケット、及びオブジェクトに対する権限を付与します。これをEC2、及びRDSのPL/SQLアクセス時に有効にすれば、EC2 <-> S3 <-> RDS上のPL/SQLでファイルをやり取りできます。

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

3の手順で作成したポリシーを付与したEC2用のIAMロールを作成します。IAMサービスから
ロール -> 新しいロールの作成 -> EC2ロールタイプ と選択し、
スクリーンショット 2017-08-20 0.33.30.png

3の手順で作成したポリシーを付与して
スクリーンショット 2017-08-20 0.34.37.png

名前をつければ完了です。
スクリーンショット 2017-08-20 0.37.17.png

あとはこのロールを指定してEC2インスタンスを作成します。
スクリーンショット 2017-08-20 0.39.29.png
1の手順で作成したRDSに1521ポートで接続可能なサブネットに作成して下さい。
EC2インスタンスの作成については以下も参照して下さい。
インスタンスの作成

5. PL/SQL用IAMユーザの作成

PL/SQLにIAMロールは付与できないので3の手順で作成したポリシーを付与したユーザをPL/SQL用に作成します。IAMサービスから
ユーザー -> ユーザーの追加
を選択し、ユーザー名を入力して「プログラムによるアクセス」を有効にします。
スクリーンショット 2017-08-20 0.45.29.png
アクセス権限の設定画面では、「既存のポリシーを直接アタッチ」を選択し、3の手順で作成したポリシーをチェックします。
スクリーンショット 2017-08-20 0.48.06.png
作成が完了した際に得られる「アクセスキー ID」と「シークレットアクセスキー」をPL/SQL側で使用することになります。

6. RDS上での設定

RDS上ではディレクトリの作成と、アクセス権限の設定を行います。
設定を行う前に、4の手順で作成したEC2インスタンスにログインし、必要なツールを入れます。

SQL*Plus

OTNのInstant Client Downloads for Linux x86-64から、basic及びsqlplusの2つのパッケージをブラウザ経由でダウンロードし、EC2インスタンスに転送してインストールして下さい(オラクル社のSSOログインが要求されますので、ブラウザで実施する必要があります)。
以下のように12.2のrpmをインストールした場合には、OCIライブラリやSQL*Plusのバイナリは/usr/lib/oracle/12.2/client64にインストールされています。

SQL*Plusのインストール
$ sudo rpm -i oracle-instantclient12.2-basic-12.2.0.1.0-1.x86_64.rpm
$ sudo rpm -i oracle-instantclient12.2-sqlplus-12.2.0.1.0-1.x86_64.rpm
$ ls /usr/lib/oracle/12.2/client64/bin/
adrci  genezi  sqlplus
$ ls /usr/lib/oracle/12.2/client64/lib/
glogin.sql             libmql1.so       libocijdbc12.so   libsqlplusic.so
libclntsh.so.12.1      libnnz12.so      libons.so         ojdbc8.jar
libclntshcore.so.12.1  libocci.so.12.1  liboramysql12.so  xstreams.jar
libipc1.so             libociei.so      libsqlplus.so
$

以下のように.bash_profileを設定しておきましょう。これでいつでもRDSにログインできます。

~/.bash_profile
...
ORACLIENT=/usr/lib/oracle/12.2/client64
export PATH=$PATH:$HOME/.local/bin:$HOME/bin:$ORACLIENT/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLIENT/lib

alias sql="sqlplus '<DBユーザー>@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=<DB名>.<エンドポイント>.ap-northeast-1.rds.amazonaws.com)(PORT=1521))(CONNECT_DATA=(SID=<DBのSID>)))'"

alexandria-plsql-utils

Gitでクローンし、AMAZON_AWS_S3_PKGをインストールします。

alexandria-plsql-utilsのインストール
$ sudo yum install git
...
完了しました!
$ git clone https://github.com/mortenbra/alexandria-plsql-utils.git
Cloning into 'alexandria-plsql-utils'...
remote: Counting objects: 447, done.
remote: Total 447 (delta 0), reused 0 (delta 0), pack-reused 447
Receiving objects: 100% (447/447), 382.00 KiB | 0 bytes/s, done.
Resolving deltas: 100% (184/184), done.
Checking connectivity... done.
$ cd alexandria-plsql-utils/
$ ls
README.md  alexandria-logo.jpg  demos  doc  extras  ora  setup
$ ls setup/
$ sql      #前節で設定したエイリアスでRDSへ接続
...
SQL> @install_core
...
SQL> show errors
No errors
SQL> @install_inet
...
SQL> show errors
No errors
SQL> @install_amazon
...
SQL> show errors
No errors
SQL> exit
$

ACLの設定

明示的にACLを設定しない限りUTL_HTTPによるアウトバウンドのアクセスはOracleにより全て拒否されます。次のようにDBMS_NETWORK_ACL_ADMINパッケージを用いて自ユーザから手順2で作成したs3バケットに対してのみアクセスを許可します。

create_acl.sql
declare
   l_myuser varchar(32);
begin
   select user into l_myuser from dual;
   dbms_network_acl_admin.create_acl(
     acl         => 's3',
     description => 's3 acl',
     principal   => l_myuser,
     is_grant    => true,
     privilege   => 'connect'
   );
   dbms_network_acl_admin.add_privilege(
     acl         => 's3',
     principal   => l_myuser,
     is_grant    => true,
     privilege   => 'resolve'
   );
   dbms_network_acl_admin.assign_acl(
     acl         => 's3',
     host        => '<手順2で作成したバケット名>.s3.amazonaws.com'
   );
end;
/

ディレクトリの作成

RDS側の格納先であるメインデータストレージ領域上のディレクトリを作成します。以下では2つ作成しています。テーブル名などと同じく、Oracleのデータベース・オブジェクト名となるので引用符で囲まなければ大文字となります。
データベース・オブジェクト名および修飾子

create_directory.sql
begin
  rdsadmin.rdsadmin_util.create_directory('EC2');
  rdsadmin.rdsadmin_util.create_directory('S3');
end;
/

アクセス

EC2からS3へのアクセス

テスト用ファイルとしてAWSのEC2オファーファイルを使い、EC2とS3のオファーファイルをそれぞれ異なるディレクトリに配置しておきましょう。

テスト用ファイルのダウンロード
$ mkdir ~/files
$ cd ~/files
$ mkdir ec2 s3
$ wget -O ec2/ec2-price.csv https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.csv
...
ec2/ec2-price.csv   100%[===================>]  92.92M  29.9MB/s    in 3.1s    
...
$ wget -O s3/s3-price.csv https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonS3/current/index.csv
...
s3/s3-price.csv     100%[===================>] 870.02K  --.-KB/s    in 0.05s   
...
$

EC2からS3へのコピー

EC2ではAWS CLIが使え、手順4でS3への権限をロールで与えているので、以下のコマンドを打てば完了です。

S3へのupload
$ cd ~/files
$ aws s3 cp ec2/ec2-price.csv s3://<手順2で作成したバケット名>/ec2/ec2-price.csv
upload: ec2/ec2-price.csv to s3://<手順2で作成したバケット名>/ec2/ec2-price.csv
$ aws s3 cp s3/s3-price.csv s3://<手順2で作成したバケット名>/s3/s3-price.csv
upload: s3/s3-price.csv to s3://<手順2で作成したバケット名>/s3/s3-price.csv
$

S3からEC2へのコピー

以下のコマンドを打てば完了です。

S3からのdownload
$ cd ~/files
$ rm -R */*.csv #ファイルを消しておきます
$ aws s3 cp s3://<手順2で作成したバケット名>/ec2/ec2-price.csv ec2/ec2-price.csv
download: s3://<手順2で作成したバケット名>/ec2/ec2-price.csv to ec2/ec2-price.csv
$ aws s3 cp s3://<手順2で作成したバケット名>/s3/s3-price.csv s3/s3-price.csv
download: s3://<手順2で作成したバケット名>/s3/s3-price.csv to s3/s3-price.csv       
$ ls -R
.:
ec2  s3

./ec2:
ec2-price.csv

./s3:
s3-price.csv
$

RDSからS3へのアクセス

AMAZON_AWS_S3_PKGはBLOBとS3オブジェクトをインタフェースします。
認証とやり取りのためのBLOBとして1つテーブルを用意しておきます。

rds_s3_config.sql
create table rds_s3_config (
  key varchar2(32) primary key,
  value varchar2(128),
  tmpblob blob
);
insert into rds_s3_config (key, value) values ('aws_id', '<手順5で得たアクセスキーID>');
insert into rds_s3_config (key, value) values ('aws_key', '<手順5で得たシークレットアクセスキー>');
insert into rds_s3_config (key, value) values ('aws_s3_bucket', '<手順2で作成したS3バケット名>');
insert into rds_s3_config (key, tmpblob) values ('temporary_blob', empty_blob());
commit;
実行結果
SQL> @rds_s3_config

Table created.


1 row created.


1 row created.


1 row created.


1 row created.


Commit complete.

SQL> 

S3からRDSへのコピー

S3からオブジェクトをBLOBで取り出し、BLOBをファイルに書き込みます。次のようなプロシージャを作成しておきます。

copy_s3_to_local.sql
create or replace procedure copy_s3_to_local(
  p_s3_bucket varchar2,
  p_s3_key varchar2,
  p_local_dir varchar2,
  p_local_file varchar2
) is
  l_aws_id      varchar2(128);
  l_aws_key     varchar2(128);

  l_blob        blob;
  l_length      integer;
  l_index       integer := 1;
  l_bytecount   integer;
  l_tempraw     raw(32767);
  l_file        utl_file.file_type;
  l_dir         varchar2(128);
begin
  select value into l_aws_id from rds_s3_config where key = 'aws_id';
  select value into l_aws_key from rds_s3_config where key = 'aws_key';
  amazon_aws_auth_pkg.init(l_aws_id, l_aws_key);

  l_blob := amazon_aws_s3_pkg.get_object(p_s3_bucket, p_s3_key);
  -- エラーレスポンスかどうかを粗く判定
  if utl_raw.cast_to_varchar2(dbms_lob.substr(l_blob,256,1)) like '%<Error>%' then
    raise NO_DATA_FOUND;
  end if;

  l_length := dbms_lob.getlength(l_blob);
  l_file  := utl_file.fopen(p_local_dir, p_local_file, 'wb', 32767);

  while l_index <= l_length
  loop
      l_bytecount  := 32767;
      DBMS_LOB.read(l_blob, l_bytecount, l_index, l_tempraw);
      utl_file.put_raw(l_file, l_tempraw);
      l_index      := l_index + l_bytecount;
  end loop;
  utl_file.fflush(l_file);
  utl_file.fclose(l_file);
end;
/
show errors

実行結果
SQL> @copy_s3_to_local

Procedure created.

No errors.
SQL> 

テストしてみましょう。

copy_s3_to_local_test.sql
set serveroutput on
begin
  copy_s3_to_local('<手順2で作成したS3バケット名>', 'ec2/ec2-price.csv', 'EC2', 'ec2-price.csv');
  copy_s3_to_local('<手順2で作成したS3バケット名>', 's3/s3-price.csv', 'S3', 's3-price.csv');
end;
/
テスト:S3からRDSへのダウンロード
SQL> @copy_s3_to_local_test

PL/SQL procedure successfully completed.

SQL> 

RDSからS3へのコピー

ファイルからテーブル上のBLOBに書き込み、S3にアップロードします。次のようなプロシージャを作成しておきます。

copy_local_to_s3.sql
create or replace procedure copy_local_to_s3(
  p_local_dir varchar2,
  p_local_file varchar2,
  p_s3_bucket varchar2,
  p_s3_key varchar2
) is
  l_aws_id      varchar2(128);
  l_aws_key     varchar2(128);

  l_blob        blob;
  l_handle      bfile;
  l_dir         varchar2(128);
  l_doffset     pls_integer := 1;
  l_soffset     pls_integer := 1;
begin
  select value into l_aws_id from rds_s3_config where key = 'aws_id';
  select value into l_aws_key from rds_s3_config where key = 'aws_key';
  amazon_aws_auth_pkg.init(l_aws_id, l_aws_key);

  select tmpblob into l_blob from rds_s3_config where key = 'temporary_blob' for update;
  l_handle := bfilename(p_local_dir, p_local_file);
  dbms_lob.fileopen(l_handle, dbms_lob.file_readonly);
  dbms_lob.loadblobfromfile(l_blob, l_handle, dbms_lob.getlength(l_handle), l_doffset, l_soffset);
  -- このサンプルはContent-TypeをCSVに固定
  amazon_aws_s3_pkg.new_object(p_s3_bucket, p_s3_key, l_blob, 'text/csv');
  dbms_lob.fileclose(l_handle);
  rollback;
end;
/
show errors

テストしてみましょう。

copy_local_to_s3_test.sql
set serveroutput on
begin
  copy_local_to_s3('EC2', 'ec2-price.csv', '<手順2で作成したS3バケット名>', 'ec2/ec2-price.csv');
  copy_local_to_s3('S3', 's3-price.csv', '<手順2で作成したS3バケット名>', 's3/s3-price.csv');
end;
/
テスト
$ aws s3 rm s3://<手順2で作成したS3バケット名>/ec2/ec2-price.csv #ファイルを削除
delete: s3://<手順2で作成したS3バケット名>/ec2/ec2-price.csv
$ aws s3 rm s3://<手順2で作成したS3バケット名>/s3/s3-price.csv   #ファイルを削除
delete: s3://<手順2で作成したS3バケット名>/s3/s3-price.csv
$ aws s3 ls s3://<手順2で作成したS3バケット名>/ec2/ #空であることを確認
$ aws s3 ls s3://<手順2で作成したS3バケット名>/s3/  #空であることを確認
$ sql
... 
SQL> @copy_local_to_s3_test

PL/SQL procedure successfully completed.

SQL> exit
...
$ aws s3 ls s3://<手順2で作成したS3バケット名>/ec2/ #アップロードされたことを確認
2017-08-21 13:44:18   97438744 ec2-price.csv
$ aws s3 ls s3://<手順2で作成したS3バケット名>/s3/  #アップロードされたことを確認
2017-08-21 13:44:20     890903 s3-price.csv
$

まとめ

以上、EC2とS3のファイルのやり取り、そしてS3とRDSのファイルのやり取りについて見てきました。
より本格的に処理するには、特に紹介したPL/SQLプロシージャにおいて、S3へのアップロードのContent-Typeを適切に選択したり、エラーレスポンス(XMLドキュメントが返される)の判定を厳密にしたりなどが必要となるでしょう。

続きを読む

現在のインスタンス料金を取得する script: RDS 編

http://qiita.com/bells17/items/5326d11edc6acc4feea2
の RDS 版です

注意点として取得するデータのエンジンを Aurora に絞ってます

rds.rb
require 'json'
require 'bigdecimal'

results = {}
json_data = open(ARGV[0]) {|io| JSON.load(io) }

# product 情報を取得
json_data['products'].keys.each do |skuNo|
    product = json_data['products'][skuNo]

    if (product['productFamily'] == 'Database Instance' and
          product['attributes']['locationType'] == 'AWS Region' and
          product['attributes']['location'] == 'Asia Pacific (Tokyo)' and
          product['attributes']['databaseEngine'] == 'Amazon Aurora') # Aurora だけに絞ってます (エンジンが mysql か postgresql かは無いっぽい??)

        results[product['sku']] = {
            sku: product['sku'],
            location: product['attributes']['location'],
            instanceType: product['attributes']['instanceType'],
            instanceFamily: product['attributes']['instanceFamily'],
            vcpu: product['attributes']['vcpu'],
            physicalProcessor: product['attributes']['physicalProcessor'],
            clockSpeed: product['attributes']['clockSpeed'],
            memory: product['attributes']['memory'],
            networkPerformance: product['attributes']['networkPerformance'],
            currentGeneration: product['attributes']['currentGeneration'],
            price_unit: 'USD'
        }

    end
end


# price

# on demand
json_data['terms']['OnDemand'].keys.each do |skuNo|
    if (results[skuNo])
        results[skuNo][:price_per_hour] = Proc.new {
            skuTerm = json_data['terms']['OnDemand'][skuNo][json_data['terms']['OnDemand'][skuNo].keys[0]]
            priceInfo = skuTerm['priceDimensions'][skuTerm['priceDimensions'].keys[0]]
            BigDecimal(priceInfo['pricePerUnit']['USD']).floor(2).to_f.to_s
        }.call
        results[skuNo][:price_per_day] = (BigDecimal(results[skuNo][:price_per_hour]) * BigDecimal("24")).floor(2).to_f.to_s
        results[skuNo][:price_per_month] = (BigDecimal(results[skuNo][:price_per_day]) * BigDecimal("30")).floor(2).to_f.to_s
    end
end

## reserved 
json_data['terms']['Reserved'].keys.each do |skuNo|
    if (results[skuNo])

        plans = json_data['terms']['Reserved'][skuNo].values.select do |plan|
            plan['termAttributes']['PurchaseOption'] == "All Upfront" # "All Upfront" のものだけ取得したい
        end

        results[skuNo][:price_reserved_1year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '1yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

        results[skuNo][:price_reserved_3year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '3yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

    end
end

# sort
sorted_result = {}
results.values.each do |row|
    sorted_result[row[:currentGeneration]] ||= {}
    sorted_result[row[:currentGeneration]][row[:instanceFamily]] ||= []
    sorted_result[row[:currentGeneration]][row[:instanceFamily]].push row
end

results = []
['Yes', 'No'].each do |currentGeneration| # 現行世代のものから並べる
    next unless sorted_result[currentGeneration]
    sorted_result[currentGeneration].keys.sort.each do |instanceFamily| # インスタンスファミリー毎に並べる
        results.concat sorted_result[currentGeneration][instanceFamily].sort_by { |row| row[:price_per_hour] }
    end
end

p results.to_json

上記を保存して以下のように実行する

curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonRDS/current/index.json > price-AmazonRDS.json
ruby rds.rb price-AmazonRDS.json | sed -e s/^"// | sed -e s/"$// | sed -e 's/\"/"/g' | jq .

以下のような結果が取れる

[
  {
    "sku": "H7JQN46Z6VDZ3K5V",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.t2.small",
    "instanceFamily": "General purpose",
    "vcpu": "1",
    "physicalProcessor": "Intel Xeon Family",
    "clockSpeed": "Up to 3.3 GHz",
    "memory": "2 GiB",
    "networkPerformance": "Low to Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.06",
    "price_per_day": "1.44",
    "price_per_month": "43.2",
    "price_reserved_1year_purchased_all_upfront": "403",
    "price_reserved_3year_purchased_all_upfront": "776"
  },
  {
    "sku": "MK8ETWDCPSK52PEV",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.t2.medium",
    "instanceFamily": "General purpose",
    "vcpu": "2",
    "physicalProcessor": "Intel Xeon Family",
    "clockSpeed": "Up to 3.3 GHz",
    "memory": "4 GiB",
    "networkPerformance": "Low to Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.12",
    "price_per_day": "2.88",
    "price_per_month": "86.4",
    "price_reserved_1year_purchased_all_upfront": "792",
    "price_reserved_3year_purchased_all_upfront": "1530"
  },
  {
    "sku": "8Z6GS5F6NKX37Q5E",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.large",
    "instanceFamily": "Memory optimized",
    "vcpu": "2",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "15.25 GiB",
    "networkPerformance": "Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.35",
    "price_per_day": "8.4",
    "price_per_month": "252.0",
    "price_reserved_1year_purchased_all_upfront": "1704",
    "price_reserved_3year_purchased_all_upfront": "3433"
  },
  {
    "sku": "PQP78BGE4C2HXDQF",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "4",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "30.5 GiB",
    "networkPerformance": "Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.7",
    "price_per_day": "16.8",
    "price_per_month": "504.0",
    "price_reserved_1year_purchased_all_upfront": "3408",
    "price_reserved_3year_purchased_all_upfront": "6867"
  },
  {
    "sku": "2WTMTR9HDDT7AA73",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.2xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "8",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "61 GiB",
    "networkPerformance": "High",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "1.4",
    "price_per_day": "33.6",
    "price_per_month": "1008.0",
    "price_reserved_1year_purchased_all_upfront": "6815",
    "price_reserved_3year_purchased_all_upfront": "13733"
  },
  {
    "sku": "VRNJP9SPPRH2KM8M",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.4xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "16",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "122 GiB",
    "networkPerformance": "High",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "2.8",
    "price_per_day": "67.2",
    "price_per_month": "2016.0",
    "price_reserved_1year_purchased_all_upfront": "13631",
    "price_reserved_3year_purchased_all_upfront": "27466"
  },
  {
    "sku": "NC3BZ293ZJFBVUT5",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.8xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "32",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "244 GiB",
    "networkPerformance": "10 Gigabit",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "5.6",
    "price_per_day": "134.4",
    "price_per_month": "4032.0",
    "price_reserved_1year_purchased_all_upfront": "27261",
    "price_reserved_3year_purchased_all_upfront": "54932"
  }
]

続きを読む

AnsibleでAWS操作 Simple Email Service編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編
  5. Simple Email Service編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • SSL証明書発行時のドメイン認証メールをSESにて受信
  • 受信メールをS3バケットに保存
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み
  • 独自ドメイン取得済み
  • Route53の設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. ドメイン認証用のトークンを発行

command
ansible-playbook -i inventory/production create-aws-ses-token.yml

※戻り値の値を控えます

2. Route53のレコードセットの更新

command
ansible-playbook -i inventory/production update-aws-route53-record-set.yml

3. Route53の承認ステータスの確認

command
ansible-playbook -i inventory/production view-aws-ses-verification-status.yml

VerificationStatusがSuccessになることを確認します。
※多少時間がかかる場合があります

4. メール保存用のS3バケットを作成

command
ansible-playbook -i inventory/production setup-aws-s3-bucket.yml

5. SESルール周りのセットアップの作成

command
ansible-playbook -i inventory/production setup-aws-ses-rule.yml

ディレクトリ構成


├── ansible.cfg
├── create-aws-ses-token.yml
├── templates
│   └── production
│       ├── route53
│       │   └── record_set.j2
│       ├── s3api
│       │   └── s3-policy.j2
│       └── ses
│           └── rule-set.j2
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── active-aws-ses-rule-set
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-rule
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-rule-set
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-token
│   │   └── tasks
│   │       └── main.yml
│   ├── setup-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── update-aws-route53-record-set
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-ses-verification-status
│       └── tasks
│           └── main.yml
├── setup-aws-ses-rule.yml
├── setup-aws-s3-bucket.yml
├── update-aws-route53-record-set.yml
├── view-aws-ses-verification-status.yml
└── vars
    └── all.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
SERVER_IP: ${IPアドレス}
TEMP:
  DIRECTORY: /temp
DOMAIN:
  MAIN:
    NAME: ${ドメイン名}
  SUB:
    NAME: ${サブドメイン名}
AWS:
  ROUTE53:
    HOSTED_ZONE_ID: ${ホストゾーンID}
  S3:
    BUCKET:
      NAME: ${バケット名}
  SES:
      TOKEN: ${認証トークン}
      REGION: us-west-2
    RULE:
      NAME: ${ルール名}
      SET:
        NAME: ${ルールセット名}

templates

json/production/ses/rule-set.j2
{
  "Name": "{{ AWS.SES.RULE.NAME }}",
  "Enabled": true,
  "TlsPolicy": "Optional",
  "Recipients": [
    "admin@{{ DOMAIN.MAIN.NAME }}",
    "admin@{{ DOMAIN.SUB.NAME }}"
  ],
  "Actions": [
    {
      "S3Action": {
        "BucketName": "{{ AWS.S3.BUCKET.NAME }}"
        }
    }
  ],
  "ScanEnabled": true
}
s3-policy.j2
{
  "Version":"2012-10-17",
  "Statement":[{
      "Sid":"AddPerm",
      "Effect":"Allow",
        "Principal": "*",
      "Action":["s3:PutObject"],
      "Resource":["arn:aws:s3:::{{ AWS.S3.SES.NAME }}/*"]
    }
  ]
}
record_set
{
  "Comment": "DomainRecords",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "EvaluateTargetHealth": false,
          "DNSName": "{{ DOMAIN.MAIN.NAME }}"
        },
        "Type": "A",
        "Name": "{{ DOMAIN.MAIN.NAME }}"
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "{{ DOMAIN.SUB.NAME }}",
        "Type": "A",
        "TTL": 1200,
        "ResourceRecords": [
          {
            "Value": "{{ SERVER_IP }}"
          }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "_amazonses.{{ DOMAIN.MAIN.NAME }}.",
        "Type": "TXT",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": ""{{ AWS.SES.TOKEN }}""
          }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "{{ DOMAIN.MAIN.NAME }}.",
        "Type": "MX",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "10 inbound-smtp.{{ AWS.SES.REGION }}.amazonaws.com."
          }
        ]
      }
    }
  ]
}

playbook

create-aws-ses-token.yml
- hosts: cliservers
  roles:
    - create-aws-ses-token
  vars_files:
    - vars/all.yml
setup-aws-ses-rule.yml
- hosts: cliservers
  roles:
    - create-aws-ses-rule-set
    - create-aws-ses-rule
    - active-aws-ses-rule-set
  vars_files:
    - vars/all.yml
setup-aws-s3-bucket.yml
- hosts: cliservers
  roles:
    - create-aws-s3-bucket
    - setup-aws-s3-bucket
  vars_files:
    - vars/all.yml
update-aws-route53-record-set.yml
- hosts: cliservers
  roles:
    - update-aws-route53-record-set
  vars_files:
    - vars/all.yml
view-aws-ses-verification-status.yml
- hosts: cliservers
  roles:
    - view-aws-ses-verification-status
  vars_files:
    - vars/all.yml

tasks

role/active-aws-ses-rule-set/tasks/main.yml
- name: Active Receipt Rule Set
  shell: |
    aws ses set-active-receipt-rule-set 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-s3-bucket/tasks/main.yml
- name: Create Bucket
  shell: "aws s3 mb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-rule-set/tasks/main.yml
- name: Create Receipt Rule Set
  shell: |
    aws ses create-receipt-rule-set 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }} 
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-rule/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/ses/rule-set.j2
    dest={{ TEMP.DIRECTORY }}/rule-set.json
  tags:
    - always

- name: Create Receipt Rule
  shell: |
    aws ses create-receipt-rule 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }} 
    --rule file://{{ TEMP.DIRECTORY }}/rule-set.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-token/tasks/main.yml
- name: "Create SES Token"
  shell: |
    aws ses verify-domain-identity  
    --domain "{{ DOMAIN.MAIN.NAME }}" 
    --region={{ AWS.SES.REGION }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/setup-aws-s3-bucket/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/s3api/s3-policy.j2
    dest={{ TEMP.DIRECTORY }}/s3-policy.json
  tags:
    - always

- name: Create Policy
  shell: |
    aws s3api put-bucket-policy 
    --bucket {{ AWS.S3.BUCKET.NAME }} 
    --policy file://{{ TEMP.DIRECTORY }}/s3-policy.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/update-aws-route53-record-set/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/route53/record_set.j2
    dest={{ TEMP.DIRECTORY }}/record_set.json
  tags:
    - always

- name: Update Record Set
  shell: |
    aws route53 change-resource-record-sets 
    --hosted-zone-id {{ AWS.ROUTE53.HOSTED_ZONE_ID }} 
    --change-batch file://{{ TEMP.DIRECTORY }}/record_set.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-ses-verification-status/tasks/main.yml
- name: View Verification Status
  shell: |
    aws ses get-identity-verification-attributes 
    --identities ${DOMAIN.MAIN.NAME}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

上記作業が正常に終了すれば、SSL証明書発行時のドメイン認証メールをSESにて受信及びS3バケットへの保存が可能になりました。

ただ、S3バケット上で保存している電子メールは そのままだと解読が出来ない ので、オブジェクトをローカル等にダウンロードした上で内容を表示する必要があります。(Lambdaを使えば転送とかもできるかも?)

ここまでの設定をすればACMにて無料のSSL証明書を発行する準備が出来たので、
CloudFront+S3+ACMによる無料SSLサイトを構築することが可能 です。

AWSサービス同士はとても連携しやすく、同サービス内だからこそのメリットもたくさんあるので他にも良い組み合わせがあったら記事にまとめていこうと思います♪

じゃあの。

続きを読む

aws-sdk for Rubyを使ってCloudFrontのinvalidationをリクエストする

S3にjsonをあげて、取得するときはCloudFront経由にする構成を作っています。
jsonを更新/削除する際にCloudFrontのキャッシュを削除したかったのですが、余り記事が引っかからなかったので備忘までにメモしておきます。

前提

  • Ruby 2.3.0
  • aws-sdk 2.10.22

※ sdkのversionが1の場合は、Clientのクラスやapiのパラメータが異なりますので、こちらをご覧ください。

コード

Railsアプリケーションで削除するclassを作ったので、そのまま貼ります。

lib/clients/cloud_front.rb
module Clients
class CloudFront
  include Singleton

  @@instance = Aws::CloudFront::Client.new(
    region: ENV['AWS_DEFAULT_REGION'],
    access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
  )

  # CloudFrontのInvalidationを作成する
  # @see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFront/Client.html#create_invalidation-instance_method
  #
  # @param [Array[String]] paths キャッシュオブジェクトのパスの配列
  # @return [String] 結果のメッセージ
  def self.create_invalidation(paths=[])
    # 非同期のAPIをcall
    ret = @@instance.create_invalidation({
      distribution_id: ENV['AWS_CLOUD_FRONT_DISTRIBUTION_ID'],
      invalidation_batch: {
        paths: {
          quantity: paths.size,
          items: paths
        },
        # api callを同定するためのユニークな文字列
        # @see http://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateInvalidation.html#API_CreateInvalidation_RequestSyntax
        caller_reference: Time.now.to_s
      }
    })

    # invalidationの作成に成功したときは、
    # レスポンスのinvalidation.statusにInProgressが入る
    if ret.blank? or ret.invalidation.blank? or ret.invalidation.status != 'InProgress'
      Rails.logger.warn "Fail to delete cache of #{paths.join(', ')}"
      Rails.logger.warn "Response from CloudFront: #{ret.inspect}"
      return 'キャッシュの削除に失敗しました。'
    end

    'キャッシュの削除を開始しました。しばらく待って確認してください。'
  end

end
end

補足1: メソッドのレスポンスについて

invalidationのcallは元々非同期ですし、削除に失敗したからと言ってアプリケーションを止めたくなかったので、私の場合はメッセージを返してコントローラーでflashに入れるようにしています。場合によっては例外出してもいいかもしれません。

補足2: メソッドの引数について

引数経由で渡しているオブジェクトのpathですが、同じversionのS3クライアント(Aws::S3::Resource)やWeb consoleとは異なり、冒頭の’/’が必須になるのでご注意ください。

  • NG

    • v1/hoge.json
  • OK
    • /v1/hoge.json

続きを読む

IAMポリシーをAnsibleで管理する

はじめに

以前、AnsibleでIAMユーザおよびグループを管理するPlaybookをご紹介しましたが、今回はAnsibleでIAMポリシーを管理してみたいと思います。

やること

  • グループにインラインポリシーアタッチ
  • グループに管理ポリシーアタッチ

ポイント

  • Ansibleのモジュールは管理ポリシーの操作に対応していないので、AWS CLIにて実装

注意

ポリシーのデタッチには対応していません。
ユーザへのアタッチ、ロールについては今回はフォローしていません。

前提

  • AWS関連のモジュール実行にはbotoが必要です。
  • AWS CLIが必要です。
  • credential情報は環境変数かaws configureでセットしてある必要があります。

sample

以下のグループにインラインポリシーとAWS管理ポリシーをアタッチします。
ポリシー内容はサンプルなので適当です。

  • ansible

    • インラインポリシー

      • SourceIpを制限したAdminポリシー
    • AWS管理ポリシー
      • CloudWatchReadOnlyAccess
      • AmazonEC2ReadOnlyAccess

ディレクトリ構成

ディレクトリ構成
site.yml
roles/
|--iam/
|  |--tasks/
|  |  |--main.yml
|  |--templates/
|  |  |--admin_policy.json.j2
group_vars/
|--group.yml

vars

こんな感じに変数を定義します。

group_vars/group.yml
---
my_vars:
  aws:
    iam:
      inline_policies:
        - group_name: ansible
          policy:
            - name: admin_ip_restricted
              template: admin_policy.json.j2
              params:
                condition: '{"IpAddress": {"aws:SourceIp": ["XX.XX.XX.XX/32"]}}'
      managed_policies:
        - group_name: ansible
          policy:
            - arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess
            - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess

Role

インラインポリシーは、jsonのテンプレートを読み込んで定義します。

管理ポリシーのアタッチについては、shellモジュールでAWS CLIを実行しています。
重複実行してもエラーとはなりませんが、毎回ステータスがchangedになってしまうので、アタッチ済みポリシーと突き合わせます。

aws iam list-attached-group-policiesによりターゲットグループにアタッチされている管理ポリシーのARNを取得し、結果のjsonをfrom_jsonフィルタを通してset_factモジュールに渡すと、そのままグループごとに<<グループ名>>_policiesというARNのリストが生成されます。

whenにより、上記リストに追加したい管理ポリシーARNがあるかどうかを判定しています。

roles/iam/tasks/main.yml
---
- name: IAM Inline-Policy作成
  iam_policy:
    profile: "{{ lookup('env', 'AWS_DEFAULT_PROFILE') }}"
    iam_type: group
    iam_name: "{{ item.0.group_name }}"
    policy_name: "{{ item.1.name }}"
    state: present
    policy_json: "{{ lookup( 'template', item.1.template ) }}"
  with_subelements:
    - "{{ my_vars.aws.iam.inline_policies }}"
    - policy

- name: Get managed-policy list
  shell: >-
    aws iam list-attached-group-policies 
     --group-name {{ item.group_name }} 
     --query 'AttachedPolicies[].PolicyArn'
  changed_when: no
  with_items: "{{ my_vars.aws.iam.managed_policies }}"
  register: iam_managed_policies

- name: Create managed-policy list
  set_fact:
    "{{ item.item.group_name }}_policies": "{{ item.stdout | from_json }}"
  with_items: "{{ iam_managed_policies.results }}"
  when: not ansible_check_mode

- name: Attach managed-policy
  shell: >-
    aws iam attach-group-policy 
     --group-name {{ item.0.group_name }} 
     --policy-arn {{ item.1 }}
  with_subelements:
    - "{{ my_vars.aws.iam.managed_policies }}"
    - policy
  when: "'{{ item.1 }}' not in {{ item.0.group_name }}_policies"

templates

roles/iam/templates/admin_policy.json.j2
{% set params = item.1.params %}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
{% if params.condition is defined %}
      "Resource": "*",
      "Condition": {{ params.condition }}
{% else %}
      "Resource": "*"
{% endif %}
    }
  ]
}

まとめ

これでマネコンからだと分かりづらいポリシーを管理しやすくなるかと思います。
こちらのRoleの後ろに連結して一つのRoleとしても使えますのでお試しください。

参考

AnsibleでAWSリソースを管理するシリーズ

続きを読む

現在のインスタンス料金を取得する script: Redshift 編

今までリザーブド購入のために料金をいちいち AWS の料金ページまでアクセスして料金を確認してたんだけど AWS Price List API (http://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/price-changes.html) を利用して現在の料金の取得を自動化したかったので現在のインスタンス料金を取得する script を書いてみた

1111111料金_-_Amazon_Redshift___AWS.png

とりあえずインスタンスの種類が少なくて楽そうだったので今回は Redshift のものを対象に取得

前提条件として

  • Tokyo リージョンだけわかればよかったので Tokyo リージョンだけに絞ってる
  • 時間あたりの料金がわかる
  • リザーブドインスタンスの時の料金がわかる
  • インスタンスのスペックがわかる

あたりがわかるように書いた

とりあえず動いたものを貼ってるのでコードはきれいじゃない

redshift.rb
require 'json'
require 'bigdecimal'

results = {}
json_data = open(ARGV[0]) {|io| JSON.load(io) }

# product 情報を取得
json_data['products'].keys.each do |skuNo|
    product = json_data['products'][skuNo]

    if (product['productFamily'] == 'Compute Instance' and
          product['attributes']['locationType'] == 'AWS Region' and
          product['attributes']['location'] == 'Asia Pacific (Tokyo)')

        results[product['sku']] = {
            sku: product['sku'],
            location: product['attributes']['location'],
            instanceType: product['attributes']['instanceType'],
            instanceFamily: product['attributes']['instanceType'].split('.')[0],
            vcpu: product['attributes']['vcpu'],
            memory: product['attributes']['memory'],
            storage: product['attributes']['storage'],
            io: product['attributes']['io'],
            ecu: product['attributes']['ecu'],
            currentGeneration: product['attributes']['currentGeneration'],
            price_unit: 'USD'
        }

    end
end


# price

## on demand
json_data['terms']['OnDemand'].keys.each do |skuNo|
    if (results[skuNo])
        results[skuNo][:price_per_hour] = Proc.new {
            skuTerm = json_data['terms']['OnDemand'][skuNo][json_data['terms']['OnDemand'][skuNo].keys[0]]
            priceInfo = skuTerm['priceDimensions'][skuTerm['priceDimensions'].keys[0]]
            BigDecimal(priceInfo['pricePerUnit']['USD']).floor(2).to_f.to_s
        }.call
        results[skuNo][:price_per_day] = (BigDecimal(results[skuNo][:price_per_hour]) * BigDecimal("24")).floor(2).to_f.to_s
        results[skuNo][:price_per_month] = (BigDecimal(results[skuNo][:price_per_day]) * BigDecimal("30")).floor(2).to_f.to_s
    end
end


## reserved 
json_data['terms']['Reserved'].keys.each do |skuNo|
    if (results[skuNo])

        plans = json_data['terms']['Reserved'][skuNo].values.select do |plan|
            plan['termAttributes']['PurchaseOption'] == "All Upfront" # "All Upfront" のものだけ取得したい
        end

        results[skuNo][:price_reserved_1year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '1yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

        results[skuNo][:price_reserved_3year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '3yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

    end
end


# sort
sorted_result = {}
results.values.each do |row|
    sorted_result[row[:currentGeneration]] ||= {}
    sorted_result[row[:currentGeneration]][row[:instanceFamily]] ||= []
    sorted_result[row[:currentGeneration]][row[:instanceFamily]].push row
end

results = []
['Yes', 'No'].each do |currentGeneration| # 現行世代のものから並べる
    sorted_result[currentGeneration].keys.sort.each do |instanceFamily| # インスタンスファミリー毎に並べる
        results.concat sorted_result[currentGeneration][instanceFamily].sort_by { |row| row[:price_per_hour] }
    end
end

# sorted_result.values.each do |targetGenerationInstances|
#   targetGenerationInstances.keys.sort.each { |instanceFamily|
#       # binding.pry
#       results.concat targetGenerationInstances[instanceFamily].sort_by { |row| row[:price_per_hour]}
#   }
# end

p results.to_json

上記を保存して以下のように実行する

curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonRedshift/current/index.json > price-AmazonRedshift.json
ruby redshift.rb price-AmazonRedshift.json | sed -e s/^\"// | sed -e s/\"$// | sed -e 's/\\"/"/g' | jq .

以下のような結果が取れる

[
  {
    "sku": "6REDMMEE7FXXH5Y6",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "dc1.large",
    "instanceFamily": "dc1",
    "vcpu": "2",
    "memory": "15 GiB",
    "storage": "0.16TB SSD",
    "io": "0.20 GB/s",
    "ecu": "7",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.31",
    "price_per_day": "8",
    "price_per_month": "240",
    "price_reserved_1year_purchased_all_upfront": "1645",
    "price_reserved_3year_purchased_all_upfront": "2885"
  },
  {
    "sku": "CNP4R2XZ8N7RJJA8",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "dc1.8xlarge",
    "instanceFamily": "dc1",
    "vcpu": "32",
    "memory": "244 GiB",
    "storage": "2.56TB SSD",
    "io": "3.70 GB/s",
    "ecu": "104",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "6.09",
    "price_per_day": "147",
    "price_per_month": "4410",
    "price_reserved_1year_purchased_all_upfront": "33180",
    "price_reserved_3year_purchased_all_upfront": "46160"
  },
  {
    "sku": "YWHTRJBA2KAFS857",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "ds2.xlarge",
    "instanceFamily": "ds2",
    "vcpu": "4",
    "memory": "31 GiB",
    "storage": "2TB HDD",
    "io": "0.40 GB/s",
    "ecu": "14",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "1.19",
    "price_per_day": "29",
    "price_per_month": "870",
    "price_reserved_1year_purchased_all_upfront": "6125",
    "price_reserved_3year_purchased_all_upfront": "7585"
  },
  {
    "sku": "Q8X9U7UKTJV2VGY8",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "ds2.8xlarge",
    "instanceFamily": "ds2",
    "vcpu": "36",
    "memory": "244 GiB",
    "storage": "16TB HDD",
    "io": "3.30 GB/s",
    "ecu": "116",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "9.52",
    "price_per_day": "229",
    "price_per_month": "6870",
    "price_reserved_1year_purchased_all_upfront": "49020",
    "price_reserved_3year_purchased_all_upfront": "60630"
  },
  {
    "sku": "ZURKE2HZ3JZC6F2U",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "ds1.xlarge",
    "instanceFamily": "ds1",
    "vcpu": "2",
    "memory": "15 GiB",
    "storage": "2TB HDD",
    "io": "0.30 GB/s",
    "ecu": "4.4",
    "currentGeneration": "No",
    "price_unit": "USD",
    "price_per_hour": "1.19",
    "price_per_day": "29",
    "price_per_month": "870",
    "price_reserved_1year_purchased_all_upfront": "6125",
    "price_reserved_3year_purchased_all_upfront": "7585"
  },
  {
    "sku": "PDMPNVN5SPA5HWHH",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "ds1.8xlarge",
    "instanceFamily": "ds1",
    "vcpu": "16",
    "memory": "120 GiB",
    "storage": "16TB HDD",
    "io": "2.40 GB/s",
    "ecu": "35",
    "currentGeneration": "No",
    "price_unit": "USD",
    "price_per_hour": "9.52",
    "price_per_day": "229",
    "price_per_month": "6870",
    "price_reserved_1year_purchased_all_upfront": "49020",
    "price_reserved_3year_purchased_all_upfront": "60630"
  }
]

続きを読む