CLOVER🍀

That was when it all began.

LocalStackの提供するAWS SAMを使って、LocalStackにAmazon API Gateway+AWS Lambdaをデプロイしてみる

これは、なにをしたくて書いたもの?

前に、LocalStackの提供するAWS CLIを使ってLocalStackを操作してみました。

LocalStackの提供するAWS CLIを使って、LocalStackを操作する - CLOVER🍀

こちらと類似のもので、LocalStackの提供するAWS SAM(aws-sam-cli-local)もあったので、こちらを試してみることに
しました。

GitHub - localstack/aws-sam-cli-local: Simple wrapper around AWS SAM CLI for use with LocalStack

これはLocalStackの提供するAWS CLIawslocal)と似たようなもので、エンドポイントをLocalStackに向けた
AWS SAMのラッパーです。

環境

今回の環境は、こちら。

$ localstack --version
0.12.17.5


$ python3 -V
Python 3.8.10


$ aws --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off


$ awslocal --version
aws-cli/2.2.35 Python/3.8.8 Linux/5.4.0-81-generic exe/x86_64.ubuntu.20 prompt/off

LocalStack自体のインストールは割愛します。AWS Lambdaの実行には、docker-reuseを指定して起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

aws-sam-cli-localをインストールして、Amazon API GatewayAWS Lambdaを動かしてみる

まずはaws-sam-cli-localをインストール。

$ pip3 install aws-sam-cli-local==1.1.0.1

AWS SAMのラッパーなのですが、aws-sam-cli-localの依存関係に含まれているので明示的にインストールしなくても
大丈夫です。自分でインストールしていても問題ありません。

バージョン。

$ samlocal --version
SAM CLI, version 1.30.0

では、こちらに沿ってAmazon API GatewayAWS LambdaのHello Worldアプリケーションを作成して、LocalStackに
デプロイしてみましょう。

チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model

init。

$ samlocal init

アプリケーション名はsam-app、ランタイムはPython 3.8とします。

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.8
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-app/README.md

生成されたディレクトリに移動。

$ cd sam-app

こんな感じにディレクトリツリーができています。

$ tree
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

5 directories, 13 files

Lambda関数。

hello_world/app.py

import json

# import requests


def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format

        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------
    API Gateway Lambda Proxy Output Format: dict

        Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    """

    # try:
    #     ip = requests.get("http://checkip.amazonaws.com/")
    # except requests.RequestException as e:
    #     # Send some context about this error to Lambda Logs
    #     print(e)

    #     raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
            # "location": ip.text.replace("\n", "")
        }),
    }

Amazon API Gatewayを通したレスポンス形式に沿っていますね。

テンプレート。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

ビルド。

$ samlocal build

次はデプロイなのですが、今回はS3バケットを先に作成しておきます。

$ awslocal s3 mb s3://my-bucket

デプロイ。AWS CloudFormationのスタック名は任意の名称で。--regionを明示しているのはAWS_REGION環境変数
設定していないからのようです。aws configureもしていないのですが、実施していたらよいのかもしれませんね。
--s3-bucketでは、先ほど作成したS3バケットを指定。

$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket

実行結果。

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                        
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldApi                                                                                                                                              
Description         API Gateway endpoint URL for Prod stage for Hello World function                                                                                           
Value               https://uvpkw1p552.execute-api.us-east-1.amazonaws.com/Prod/hello/                                                                                         

Key                 HelloWorldFunction                                                                                                                                         
Description         Hello World Lambda Function ARN                                                                                                                            
Value               arn:aws:lambda:us-east-1:000000000000:function:my-stack-HelloWorldFunction-23bdfa27                                                                        

Key                 HelloWorldFunctionIamRole                                                                                                                                  
Description         Implicit IAM Role created for Hello World function                                                                                                         
Value               arn:aws:iam::000000000000:role/cf-my-stack-HelloWorldFunctionRole                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

あっさり作成されました。これで、LocalStack上にデプロイされています。

Amazon API Gatewayのエンドポイント、AWS LambdaのARN、IAMロールのARNが得られます。

awslocalでも、作成されたAmazon API GatewayREST APIのidを確認。

$ awslocal apigateway get-rest-apis

REST APIのidは環境変数に保存しておきましょう。

$ REST_API_ID=.....

ステージは、Prod以外にもStageがあるようです。

$ awslocal apigateway get-stages --rest-api-id $REST_API_ID
{
    "item": [
        {
            "deploymentId": ".....",
            "stageName": "Stage",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {}
        },
        {
            "deploymentId": ".....",
            "stageName": "Prod",
            "description": "",
            "cacheClusterEnabled": false,
            "methodSettings": {},
            "variables": {},
            "tags": {}
        }
    ]
}

LocalStack上にデプロイされたLambda関数を、Amazon API Gateway越しに呼び出してみます。

$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message": "hello world"}

LocalStackで動作させた時のAmazon API GatewayのURLのルールは、こちらに記載があります。

LocalStack / Invoking API Gateway

使えそうですね。あっさり確認OKです。

動作確認というと、sam local invokeをしてみては?という話もあるのですが、これをやるんだったら素のAWS SAMを
そのまま使えばよいのではと思うので書きません。
※動かしはしました…というか、動かした瞬間に「そのまま使うのと同じでは」と思いました…

aws-sam-cli-localの仕組み

最初に書いた通り、aws-sam-cli-localAWS SAMのラッパーです。

aws-sam-cli-localPythonスクリプト内で、エンドポイントをLocalStack向けに設定してsamコマンドを
呼び出しているものです。

https://github.com/localstack/aws-sam-cli-local/blob/64a41c2c8e46a584c6fb4b13abfc8bb62e136d48/bin/samlocal

ハマったところ

実は、ひとつだけハマったところがありまして。

sam buildした後に、sam deploy --guidedをサジェストされるのですが。

こちらに素直に従うと

$ samlocal deploy --guided --region us-east-1

S3バケット名が解決できなくてエラーになります。

Error: Unable to upload artifact HelloWorldFunction referenced by CodeUri parameter of HelloWorldFunction resource.
Parameter validation failed:
Invalid bucket name "!Ref SamCliSourceBucket": Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$" or be an ARN matching the regex "^arn:(aws).*:(s3|s3-object-lambda):[a-z\-0-9]*:[0-9]{12}:accesspoint[/:][a-zA-Z0-9\-.]{1,63}$|^arn:(aws).*:s3-outposts:[a-z\-0-9]+:[0-9]{12}:outpost[/:][a-zA-Z0-9\-]{1,63}[/:]accesspoint[/:][a-zA-Z0-9\-]{1,63}$"

これを回避したくて、先にS3バケットを作成したのでした…。

Apache Spark 3.1(PySpark)で、Pythonの実行パスを指定する

これは、なにをしたくて書いたもの?

Apache SparkでPythonプログラムを扱う時(要はPySpark)に、どのPythonを使用するのかがちょっと気になりまして。

調べてみることにしました。

環境

今回の環境は、こちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux


$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ python3 -V
Python 3.8.10

Ubuntu Linux 20.04 LTSです。

Apache Sparkは3.1.2を使います。

また、pythonというコマンドはインストールされていません。

$ python

コマンド 'python' が見つかりません。もしかして:

  command 'python3' from deb python3
  command 'python' from deb python-is-python3

Apache Sparkをインストールする

まずは、Apache Sparkをインストールします。Pythonで扱う場合はpipでもインストール可能ですが、今回はふつうに
tar.gzファイルをダウンロードしてくることにします。

$ curl -OL https://dlcdn.apache.org/spark/spark-3.1.2/spark-3.1.2-bin-hadoop3.2.tgz
$ tar xf spark-3.1.2-bin-hadoop3.2.tgz
$ cd spark-3.1.2-bin-hadoop3.2

PySparkを起動。

$ bin/pyspark
Python 3.8.10 (default, Jun  2 2021, 10:49:15) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
21/09/06 22:21:10 WARN Utils: Your hostname, ubuntu2004.localdomain resolves to a loopback address: 127.0.0.1; using 192.168.121.6 instead (on interface eth0)
21/09/06 22:21:10 WARN Utils: Set SPARK_LOCAL_IP if you need to bind to another address
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/path/to/spark-3.1.2-bin-hadoop3.2/jars/spark-unsafe_2.12-3.1.2.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
21/09/06 22:21:11 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.2
      /_/

Using Python version 3.8.10 (default, Jun  2 2021 10:49:15)
Spark context Web UI available at http://192.168.121.6:4040
Spark context available as 'sc' (master = local[*], app id = local-1630934476222).
SparkSession available as 'spark'.
>>> 

この時点でpython3コマンドを使っているような雰囲気がありますね。

バージョンを確認。

>>> import sys
>>> sys.version
'3.8.10 (default, Jun  2 2021, 10:49:15) \n[GCC 9.4.0]'

実際、このあたりを見ているとpython3コマンドを使うようになっているみたいですね。

https://github.com/apache/spark/blob/v3.1.2/bin/pyspark#L42

https://github.com/apache/spark/blob/v3.1.2/bin/find-spark-home#L38

とりあえず、README.mdでWord Countして動作確認。

>>> spark = SparkSession.builder.getOrCreate()
>>> df = spark.read.text('README.md')
>>> counts = df.rdd.map(lambda x: x[0]).flatMap(lambda x: x.split(' ')).map(lambda x: (x, 1)).reduceByKey(lambda a, b: a + b).collect()
>>> for (word, count) in sorted(counts, reverse=True, key=lambda x: x[1]):
...   print(f'{word}: {count}')

結果の抜粋。

: 73
the: 23
to: 16
Spark: 14
for: 12
a: 9
and: 9
##: 9
is: 7
on: 7
run: 7
can: 6
in: 5
also: 5
of: 5
an: 4
including: 4
if: 4
you: 4
*: 4
Please: 4

〜省略〜

PySparkで使うPythonの実行パスを指定する

先ほどのpysparkスクリプトを見ると気づくのですが、PySparkで使うPythonの実行パスを環境変数で指定することが
できます。

PYSPARK_PYTHONを使うと、まるっと切り替えられるようですね。これ、ドキュメントには記載がなさそうな感じです。

試しにと、Python 3.9をインストール。

$ sudo apt install python3.9

バージョン確認。

$ python3.9 -V
Python 3.9.5

PYSPARK_PYTHONpython3.9を指定して実行。

$ PYSPARK_PYTHON=python3.9 bin/pyspark

すると、Pythonのバージョンが変わりました。

Python 3.9.5 (default, May 19 2021, 11:32:47) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
21/09/06 22:28:30 WARN Utils: Your hostname, ubuntu2004.localdomain resolves to a loopback address: 127.0.0.1; using 192.168.121.6 instead (on interface eth0)
21/09/06 22:28:30 WARN Utils: Set SPARK_LOCAL_IP if you need to bind to another address
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/path/to/spark-3.1.2-bin-hadoop3.2/jars/spark-unsafe_2.12-3.1.2.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
21/09/06 22:28:30 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.2
      /_/

Using Python version 3.9.5 (default, May 19 2021 11:32:47)
Spark context Web UI available at http://192.168.121.6:4040
Spark context available as 'sc' (master = local[*], app id = local-1630934912371).
SparkSession available as 'spark'.

sys.versionでも確認。

>>> import sys
>>> sys.version
'3.9.5 (default, May 19 2021, 11:32:47) \n[GCC 9.3.0]'

ちなみに、タスクを実行するExecutorが使用するPythonと、タスクのアサインを指示するDriverが使用するPython
別々のものを指定することができます。

Executorの方はPYSPARK_PYTHON環境変数で、Driverの方はPYSPARK_DRIVER_PYTHON環境変数ですね。

$ export PYSPARK_PYTHON=...
$ export PYSPARK_DRIVER_PYTHON=...

PYSPARK_DRIVER_PYTHON環境変数を指定していない場合は、PYSPARK_PYTHON環境変数の値が使われます。
そして、PYSPARK_PYTHONのデフォルト値はPySparkの起動スクリプトの中でpython3となります。

コメントを見るとわかるのですが、PYSPARK_DRIVER_PYTHONの方はipythonなどでの指定を想定していそうな
感じですね。

https://github.com/apache/spark/blob/v3.1.2/bin/pyspark#L27-L31

ただし、ExecutorとDriverそれぞれで使うPythonのメジャーバージョン、マイナーバージョンが異なることは許容されないので
その点には注意です。

https://github.com/apache/spark/blob/v3.1.2/python/pyspark/worker.py#L472-L477

pipでインストールした場合は?

ここまでtar.gzファイルを使ってインストールしたApache Sparkで話をしてきましたが、pipでインストールした場合は
どうでしょう?

試してみます。

$ pip3 install pyspark==3.1.2

この場合、pysparkコマンドにパスが通っているので、そのまま起動できます。

$ pyspark
Python 3.8.10 (default, Jun  2 2021, 10:49:15) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
21/09/06 23:07:28 WARN Utils: Your hostname, ubuntu2004.localdomain resolves to a loopback address: 127.0.0.1; using 192.168.121.6 instead (on interface eth0)
21/09/06 23:07:28 WARN Utils: Set SPARK_LOCAL_IP if you need to bind to another address
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/path/to/venv/lib/python3.8/site-packages/pyspark/jars/spark-unsafe_2.12-3.1.2.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
21/09/06 23:07:28 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.2
      /_/

Using Python version 3.8.10 (default, Jun  2 2021 10:49:15)
Spark context Web UI available at http://192.168.121.6:4040
Spark context available as 'sc' (master = local[*], app id = local-1630937250516).
SparkSession available as 'spark'.
>>> 

ですが、PYSPARK_PYTHON環境変数を使うとSPARK_HOMEの探索がずれるみたいでうまく動きません。
※仮想環境(venv)で動かしています

$ PYSPARK_PYTHON=python3.9 pyspark
Traceback (most recent call last):
  File "path/to/venv/bin/find_spark_home.py", line 86, in <module>
    print(_find_spark_home())
  File "path/to/venv/bin/find_spark_home.py", line 52, in _find_spark_home
    module_home = os.path.dirname(find_spec("pyspark").origin)
AttributeError: 'NoneType' object has no attribute 'origin'
/path/to/venv/bin/pyspark: 行 24: /bin/load-spark-env.sh: そのようなファイルやディレクトリはありません
/path/to/venv/bin/pyspark: 行 68: /bin/spark-submit: そのようなファイルやディレクトリはありません

どうしたものかな?と思ったのですが、そもそもこのケースの場合は使うpipに使うPython自体を切り替えればいい
気がしますね。

$ sudo apt install python3.9-venv
$ . venv/bin/activate

pip3.9でPySparkをインストール。

$ pip3.9 install pyspark==3.1.2

これで、切り替わりました。

$ pyspark
Python 3.9.5 (default, May 19 2021, 11:32:47) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
21/09/06 23:18:13 WARN Utils: Your hostname, ubuntu2004.localdomain resolves to a loopback address: 127.0.0.1; using 192.168.121.6 instead (on interface eth0)
21/09/06 23:18:13 WARN Utils: Set SPARK_LOCAL_IP if you need to bind to another address
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/path/to/venv/lib/python3.9/site-packages/pyspark/jars/spark-unsafe_2.12-3.1.2.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
21/09/06 23:18:13 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.2
      /_/

Using Python version 3.9.5 (default, May 19 2021 11:32:47)
Spark context Web UI available at http://192.168.121.6:4040
Spark context available as 'sc' (master = local[*], app id = local-1630937895507).
SparkSession available as 'spark'.
>>>