AWS Lambdaから実行したEC2上のPythonでモジュールがimportできない

S3に動画ファイルが配置されたことを契機にLambdaでEC2上のPythonをキックし、
ffmpy経由でffmpegを起動し動画を処理するようなプログラムを作っていたのですが、
どうもffmpyが正常にimportできないので、現状と解決した際の情報共有に書き残します。

環境

実行用コードは下記のようにec2-userのホームディレクトリ配下に配置されています。

/home/ec2-user/hoge/
├── MAIN1_KICKED_BY_LAMBDA.py # メインメソッド
├── fuga
│   └── hogefuga.py           # 動画編集用モジュール
...

下記の設定をrootユーザに対して行いました。

  • pyenv -> Anaconda3.4.1 (Python 3.6)
  • pip install ffmpy
  • ffmpegをインストール (下記スクリプト)
ffmpegcpl.sh
#!/bin/sh

sudo yum -y install autoconf automake cmake freetype-devel gcc gcc-c++ git libtool make mercurial nasm pkgconfig zlib-devel

mkdir ~/ffmpeg_sources

#Yasm
cd ~/ffmpeg_sources
git clone --depth 1 git://github.com/yasm/yasm.git
cd yasm
autoreconf -fiv
./configure --prefix="$HOME/ffmpeg_build" --bindir="$HOME/bin"
make
make install
make distclean

#libx264
cd ~/ffmpeg_sources
git clone --depth 1 git://git.videolan.org/x264
cd x264
PKG_CONFIG_PATH="$HOME/ffmpeg_build/lib/pkgconfig" ./configure --prefix="$HOME/ffmpeg_build" --bindir="$HOME/bin" --enable-static
make
make install
make distclean

#libx265
cd ~/ffmpeg_sources
hg clone https://bitbucket.org/multicoreware/x265
cd ~/ffmpeg_sources/x265/build/linux
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX="$HOME/ffmpeg_build" -DENABLE_SHARED:bool=off ../../source
make
make install

#libfdk_aac
cd ~/ffmpeg_sources
git clone --depth 1 git://git.code.sf.net/p/opencore-amr/fdk-aac
cd fdk-aac
autoreconf -fiv
./configure --prefix="$HOME/ffmpeg_build" --disable-shared
make
make install
make distclean

#libmp3lame
cd ~/ffmpeg_sources
curl -L -O http://downloads.sourceforge.net/project/lame/lame/3.99/lame-3.99.5.tar.gz
tar xzvf lame-3.99.5.tar.gz
cd lame-3.99.5
./configure --prefix="$HOME/ffmpeg_build" --bindir="$HOME/bin" --disable-shared --enable-nasm
make
make install
make distclean

#libopus
cd ~/ffmpeg_sources
git clone http://git.opus-codec.org/opus.git
cd opus
autoreconf -fiv
./configure --prefix="$HOME/ffmpeg_build" --disable-shared
make
make install
make distclean

#libogg
cd ~/ffmpeg_sources
curl -O http://downloads.xiph.org/releases/ogg/libogg-1.3.2.tar.gz
tar xzvf libogg-1.3.2.tar.gz
cd libogg-1.3.2
./configure --prefix="$HOME/ffmpeg_build" --disable-shared
make
make install
make distclean

#libvorbis
cd ~/ffmpeg_sources
curl -O http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.4.tar.gz
tar xzvf libvorbis-1.3.4.tar.gz
cd libvorbis-1.3.4
LDFLAGS="-L$HOME/ffmeg_build/lib" CPPFLAGS="-I$HOME/ffmpeg_build/include" ./configure --prefix="$HOME/ffmpeg_build" --with-ogg="$HOME/ffmpeg_build" --disable-shared
make
make install
make distclean

#libvpx
cd ~/ffmpeg_sources
git clone --depth 1 https://chromium.googlesource.com/webm/libvpx.git
cd libvpx
./configure --prefix="$HOME/ffmpeg_build" --disable-examples
make
make install
make clean

#FFmpeg
cd ~/ffmpeg_sources
git clone http://source.ffmpeg.org/git/ffmpeg.git
cd ffmpeg
PKG_CONFIG_PATH="$HOME/ffmpeg_build/lib/pkgconfig" ./configure --prefix="$HOME/ffmpeg_build" --extra-cflags="-I$HOME/ffmpeg_build/include" --extra-ldflags="-L$HOME/ffmpeg_build/lib" --bindir="$HOME/bin" --pkg-config-flags="--static" --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265
make
make install
make distclean
hash -r

現状・試したこと

実行するコードについては、公開を控えさせてください。。。
Lambdaから実行したときのログは下記のとおりで、どうやらec2-userのpyenv(存在しない)を見にいって落ちているようです。

Traceback (most recent call last):
  File "/home/ec2-user/hoge/MAIN1_KICKED_BY_LAMBDA.py", line 88, in <module>
    hogefuga.exeExtractWav(TMP_DIRECTORY2 + '/', nameonly)
  File "/home/ec2-user/hoge/fuga/hogefuga.py", line 175, in exeExtractWav
    extractWav(filename)
  File "/home/ec2-user/hoge/fuga/hogefuga.py", line 162, in extractWav
    ff.run()
  File "/home/ec2-user/.pyenv/versions/anaconda3-4.3.1/lib/python3.6/site-packages/ffmpy.py", line 99, in run
    raise FFExecutableNotFoundError("Executable '{0}' not found".format(self.executable))
ffmpy.FFExecutableNotFoundError: Executable 'ffmpeg' not found

SSH接続しrootで直接実行した際はroot側のpyenvを見にいき問題なく実行されるのですが、解決策としては以下の2つでしょうか。

  • ec2-user環境にも同様にpyenv, ffmpeg環境を構築する
  • 実行ファイル類をrootのホーム配下へ移動する

そもそも直接実行とLambda実行でパスが変わる原因を明らかにしないとすっきりしませんが・・・。

(2017.05.23 17:12追記)

  • ec2-user環境にも同様にpyenv, ffmpeg環境を構築する
  • 実行ファイル類をrootのホーム配下へ移動する

両方試したものの、下記のエラーが出て終了。

----------ERROR-------
failed to run commands: exit status 1
Traceback (most recent call last):
  File "MAIN1_KICKED_BY_LAMBDA.py", line 17, in <module>
    import formatchg
  File "/root/fuga/hoge/fugahoge.py", line 10, in <module>
    from ffmpy import FFmpeg
ImportError: No module named ffmpy

続きを読む

AWS IoTとは

AWS IoTに関する基本的な内容をまとめてみたものです。AWS IoTに関する、Web上にすでにある解説コンテンツをまとめたサイトの抜粋です。
AWS IoTとは

AWS IoTとは

AWS IoSサービスにより、さまざまなデバイスを AWS の各種 Services や他のデバイスに接続し、データと通信を保護し、デバイスデータに対する処理やアクションを実行することが可能になります。

AWS IoTデバイスSDK
AWS IoT には、ハードウェアデバイスやモバイルアプリケーションを簡単に、すばやく接続できるようサポートする SDK が準備されています。AWS IoT デバイス SDK を使用すれば、AWS IoT との間で MQTT、HTTP、または WebSockets プロトコルを介した接続、認証、メッセージ交換が可能になります。このデバイス SDK では C、JavaScript および Arduino がサポートされており、クライアントライブラリ、開発者ガイドおよびメーカー向けの移植ガイドが付属しています。

認証と認可
AWS IoT では、接続するすべてのポイントでの相互認証と暗号化が提供されており、デバイスと AWS IoT 間では身元が証明されたデータのみが交換されます。

コミュニケーションプロトコル
AWS IoT とデバイスの間では MQTT、HTTP、または WebSockets プロトコルを介した接続、認証、メッセージ交換が可能です。MQTTは、M2M/IoTでよく使用されるOASISスタンダードのプロトコル。ライトウェイトでリソースや回線帯域が限られているデバイスでよく利用されます。MQTTはhttpsと比較して、スループットで93倍、メッセージ送信の消費電力で1/12、メッセージ受信の消費電力で1/180となります。

ルールエンジンによって、AWS Lambda、Amazon Kinesis、Amazon S3、Amazon Machine Learning、Amazon DynamoDB、Amazon CloudWatch、および Amazon Elasticsearch Service (組み込みの Kibana と統合されている) などの AWS エンドポイントへのメッセージのルーティングも行えます。

AWS IoTのコンポーネント

デバイスゲートウェイ
接続されたデバイスから、指定されたトピックについて複数の受信者にデータをブロードキャストすることができます。デバイスゲートウェイでは MQTT、WebSocket、および HTTP 1.1 プロトコルがサポートされており、専用プロトコルやレガシープロトコルのサポート実装も容易に行えます。デバイスゲートウェイは、インフラストラクチャのプロビジョニングが不要でありながら、10 億台以上のデバイスにも対応できるよう自動的にスケールされます。

レジストリ
レジストリによって、デバイスの ID が確定され、デバイスの属性や機能といったメタデータが追跡されます。各デバイスには、デバイスのタイプや接続方法にかかわらず、一貫した形式の一意の ID がレジストリによって割り当てられます。

シャドウ
AWS IoT では、それぞれのデバイスについて「シャドウ」を作成できます。シャドウにはデバイスの最新の状態が保存されるため、アプリケーションや他のデバイスからのメッセージの読み出し、およびデバイスとの通信が実行できます。デバイスのシャドウには、デバイスがオフライン状態のときでも、各デバイスについて最後に報告された状態と、希望する今後の状態が保持されます。最後に報告された時点の状態の取得や、希望する今後の状態の設定は、API またはルールエンジンによって実行できます。

ルールエンジン
ルールエンジンによって、接続されたデバイスによって生成されるデータを収集し、処理し、分析し、データに基づいたアクションを実行するアプリケーションを構築することが可能になります。ルールエンジンでは、お客様が定義したビジネスルールに基づいて、AWS IoT に向けて発行された入力メッセージが評価、変換され、別のデバイスやクラウドサービスへと配信されます。1 つのデバイスからのデータにも、多数のデバイスからのデータにも同じルールを適用でき、アクションを単独で実行することも、多数のアクションを並行して実行することも可能です。

ルールエンジンによって、AWS Lambda、Amazon Kinesis、Amazon S3、Amazon Machine Learning、Amazon DynamoDB、などの AWS エンドポイントへのメッセージのルーティングも行えます。AWS Lambda、Amazon Kinesis および Amazon Simple Notification Service (SNS) を使用して外部のエンドポイントに届けることも可能です。

KinesisとIoTの違い

Kinesisを使ってIoTのシステムを実現している例もありますが、IoTサービスはより包括的にデバイスとクラウドを接続するためのサービスを用意していますので、まずはデバイスとの入り口をIoTサービスで対応して、Kinesisサービスにつなぐという構成が、AWSでのIoTシステムの構成かと思います。
具体的なKinesisとIoTの違いとして一番明確なのは、デバイスとの間のコミュニケーションプロトコルです。IoTはMQQTに対応していますが、Kinesisはhttpsのみとなります。IoTはシャドウのような仕組みも持っており、IoTシステムを構築するためのトータルなシステムを組み上げる仕組みを包括的に用意しています。

デバイスデータに対するさまざまな処理を簡単に実現する仕組み

AWS IoTでは、「ルールエンジン」に定義したビジネスルールに基づいて、デバイスデータを迅速にフィルタリング、変換、活用することができます。各種処理は、AWS Lambda、Amazon Kinesis、Amazon S3、Amazon Machine Learning、Amazon DynamoDB、Amazon CloudWatch、および Amazon Elasticsearch Service といった AWS の各種サービスを呼び出すことにより実行させます。

各種AWSのサービスには、「ルールエンジン」がAWS IoTにPublishされたメッセージを評価し、ルールに基づいて配信されます。

また、Amazon Kinesis、AWS Lambda、Amazon S3などのサービスを経由して、外部のエンドポイントを呼び出すことも可能です。

(例) センサデータをKinesisを経由してエンタプライズアプリケーションへ

・各種デバイスからのセンサデータをIoTにて受信
・ルールエンジンにて、受信したセンサデータへのフィルタリングおよびデータ変換を実施
・AWS Kinesisにストリームデータとしてパブリッシュ
・Kinesisで収集されたデータをデータベースやBIなどの分析アプリケーションに転送

例) デバイスデータをIoTからAmazon SNSを経由してスマホにプッシュ通知

・各種デバイスからのデータやイベント通知をIoTで受信
・スマホに通知すべき条件判定をIoTのルールエンジンにて実行
・通知条件に合致する場合には、Amazon SNSを経由して、アップル社APNS、Google社のGCMに
・配信リクエストを送信することにより、スマホへのプッシュ通知として、デバイスのデータやイベントの通知を実現

続きを読む

CloudFormation で Cognito

これまで

Amazon CognitoAWS CloudFormation でのリソース作成と管理に対応しておらず、リソース作成を自動化したい場合は AWS CLIAWS SDK を使った自作スクリプトを使う必要がありました。

しかし、この方法で冪等性を担保するのは難しく、 CloudFormation Custom Resource を作った猛者もいました。

また、 IAM Role の Assume Policy に Identity Pool の ID を含めないといけない点が特に面倒でした。

これから

2017年4月28日、 Cognito の各種リソースが CloudFormation で作成・管理できるようになりました。

これが待ち望んでいたものです。

User Pool 内のユーザーやグループまでリソースとして用意されているのが面白いですね。

IAM Role も同じスタック内で作ってしまえば Assume Policy の中で Identity Pool の ID も参照できるので、手動で ID をコピペする手間が省けて最高です。

テンプレート例

AWSTemplateFormatVersion: "2010-09-09"
Description: "Example template including Cognito Identity Pool and User Pool."
Parameters:
  EmailIdentityArn:
    Type: String
Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: 
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - Users
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
      AliasAttributes:
      - email
      - preferred_username
      AutoVerifiedAttributes:
      - email
      EmailConfiguration:
        SourceArn:
          Ref: EmailIdentityArn
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      Schema:
      - Name: email
        AttributeDataType: String
        DeveloperOnlyAttribute: false
        Mutable: true
        Required: true
      - Name: preferred_username
        AttributeDataType: String
        DeveloperOnlyAttribute: false
        Mutable: true
        Required: false
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: 
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - Users-client
      GenerateSecret: false
      RefreshTokenValidity: 7
      UserPoolId:
        Ref: UserPool
  IdentityPool:
    Type: AWS::Cognito::IdentityPool
    Properties:
      AllowUnauthenticatedIdentities: true
      IdentityPoolName: 
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - Users
      CognitoIdentityProviders:
      - ClientId:
          Ref: UserPoolClient
        ProviderName:
          Fn::Join:
          - ""
          - - cognito-idp.
            - Ref: "AWS::Region"
            - .amazonaws.com/
            - Ref: UserPool
  UnauthenticatedPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action:
          - mobileanalytics:PutEvents
          - cognito-sync:*
          Resource:
          - "*"
  UnauthenticatedRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action: "sts:AssumeRoleWithWebIdentity"
          Principal:
            Federated: cognito-identity.amazonaws.com
          Condition:
            StringEquals:
              "cognito-identity.amazonaws.com:aud":
                Ref: IdentityPool
            ForAnyValue:StringLike:
              "cognito-identity.amazonaws.com:amr": unauthenticated
      ManagedPolicyArns:
      - Ref: UnauthenticatedPolicy
  AuthenticatedPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action:
          - mobileanalytics:PutEvents
          - cognito-sync:*
          - cognito-identity:*
          Resource:
          - "*"
  AuthenticatedRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action: "sts:AssumeRoleWithWebIdentity"
          Principal:
            Federated: cognito-identity.amazonaws.com
          Condition:
            StringEquals:
              "cognito-identity.amazonaws.com:aud":
                Ref: IdentityPool
            ForAnyValue:StringLike:
              "cognito-identity.amazonaws.com:amr": authenticated
      ManagedPolicyArns:
      - Ref: AuthenticatedPolicy
  RoleAttachment:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId:
        Ref: IdentityPool
      Roles:
        unauthenticated:
          Fn::GetAtt:
          - UnauthenticatedRole
          - Arn
        authenticated:
          Fn::GetAtt:
          - AuthenticatedRole
          - Arn
Outputs:
  UserPool:
    Value:
      Ref: UserPool
  UserPoolClient:
    Value:
      Ref: UserPoolClient
  IdentityPool:
    Value:
      Ref: IdentityPool
  UnauthenticatedRole:
    Value:
      Ref: UnauthenticatedRole
  AuthenticatedRole:
    Value:
      Ref: AuthenticatedRole

その他の活用方法

Serverless Framework は CloudFormation のリソーステンプレートを埋め込めるので、 User Pool のトリガーで使う Lambda ファンクションを同時作成するのにうってつけです。

その場合は AWS::Cognito::UserPool の LambdaConfig プロパティ を指定してトリガーを設定します。また、 AWS::Lambda::Permission も忘れずに作る必要があります(Principalcognito-idp.amazonaws.com)。

続きを読む

boto3でec2をdescribeして値を取り出す

どうもLambda(Python)初心者の若松です。

唐突ですが、みなさんはLambda使っていますか?

サーバが無くてもコードが動く。
魅力的ですよねぇ。

やっぱり時代はServerlessだと。
インフラでも言語の一つくらい使えるようになれと。

そんな流れに逆らわないゆとり世代ど真ん中の私は、とうとうLambdaに踏み込んだわけです。

前置きが長くなりましたが、インフラ屋が四苦八苦しながら戯言言ってんなぁぐらいの気持ちで生暖かく見ていただければと思います。

やりたいこと

特定のIPを持っているインスタンスのインスタンスIDを取得したい。

環境

AWS Lambda
Python 3.6

第一形態

コード

とりあえずインスタンス情報を全部取り出してみようかと思い、以下のコードを書きました。

import boto3

def lambda_handler(event, context):

    ec2 = boto3.client('ec2')

    responce = ec2.describe_instances()

    return responce

結果

returnで以下のエラーが返って失敗

{
  "errorMessage": "An error occurred during JSON serialization of response",
  "errorType": "datetime.datetime(2017, 5, 11, 5, 15, 59, tzinfo=tzlocal()) is not JSON serializable"
}

結果そのままではJSONとして扱えないみたいです。

調査

boto3のリファレンスを呼んでみると、どうやら describe_instances の返り値は dict(辞書)型 というもののようです。
とりあえずprintで出力したほうがよさげなので、コードを変更することにしました。

第二形態

コード

とりあえず出力をプリント文に変更

import boto3

def lambda_handler(event, context):

    ec2 = boto3.client('ec2')

    responce = ec2.describe_instances()

    print(responce)

    return

結果

ログに以下が出力されました。
AWSCLIの結果でよく見るあれですね。

{
    "Reservations": [
        {
            "OwnerId": "xxxxxxxxxxxx", 
            "ReservationId": "r-xxxxxxxxxxxxxxxxx", 
            "Groups": [], 
            "Instances": [
                {
                    "Monitoring": {
                        "State": "disabled"
                    }, 
                    "PublicDnsName": "ec2-xxx-xxx-xxx-xxx.ap-northeast-1.compute.amazonaws.com", 
                    "Platform": "xxxxxxx", 
                    "State": {
                        "Code": 80, 
                        "Name": "stopped"
                    }, 
                    "EbsOptimized": false, 
                    "LaunchTime": "xxxx-xx-xxxxx:xx:xx.xxxx", 
                    "PublicIpAddress": "xxx.xxx.xxx.xxx", 
                    "PrivateIpAddress": "xxx.xxx.xxx.xxx", 
                    "ProductCodes": [], 
                    "VpcId": "vpc-xxxxxxx", 
                    "StateTransitionReason": "", 
                    "InstanceId": "i-xxxxxxxxxxxxxxxxx", 
                    "EnaSupport": true, 
                    "ImageId": "ami-xxxxxxxx", 
以下略

調査

次はIPで絞りたいと思います。
AWSCLIでいうところの--filterはboto3にもあるらしいのでそれを使うことにしました。

第三形態

コード

特定のIPを持っているインスタンスに絞るため、Filtersを追加

import boto3

def lambda_handler(event, context):

    ec2 = boto3.client('ec2')

    responce = ec2.describe_instances(
        Filters=[{'Name':'network-interface.addresses.private-ip-address','Values':["xxx.xxx.xxx.xxx"]}]
    )

    print(responce)

    return

結果

指定したIPアドレスを持つインスタンスに絞れた。

調査

次は返り値をインスタンスIDに絞ります。
イメージとしてはAWSCLIでいうところの--queryだが、boto3にはないようです。
ここでJSONに変換するだのなんだのと、ネットの情報に踊らされてかなり苦戦しました。
結果的には、dict型はオブジェクトの後に [“hoge”][“fuga”] と書くことで値を取り出せるらしいことがわかりました。

最終形態

コード

import boto3

def lambda_handler(event, context):

    ec2 = boto3.client('ec2')

    responce = ec2.describe_instances(
        Filters=[{'Name':'network-interface.addresses.private-ip-address','Values':["xxx.xxx.xxx.xxx"]}]
    )["Reservations"][0]["Instances"][0]["InstanceId"]

    print(responce)

    return

結果

めでたくインスタンスIDの取得に成功しました。
リストある際には[0]と明示的に0を書く必要があるというところに気をつけたいところです。
※AWSCLIの--queryでは[]と省略して書けるため

まとめ

いかがでしたでしょうか。
やってる内容は非常に初歩的な内容ですが、いくつかハマった箇所があったので備忘録的にまとめました。

意外と簡単に動かせるので、是非Tryしてみてください。
Lambda怖くない。

続きを読む