Ansibleのserverlessモジュールを利用したServerless Frameworkのデプロイ

はじめに

こんにちは、中山です。

Ansibleの次期リリースバージョンである2.3でserverlessというServerless Framework用モジュールがマージされました。このモジュールを利用することでAnsibleからServerless Frameworkをデプロイ可能になります。もちろん以前でも shellcommand モジュールを利用すればデプロイできましたが、よりAnsibleネイティブな形で利用できるようになったという訳です。ソースコードはこちらです。Extraモジュールとして登録されているので、野良モジュールのように自分で明示的にインストールする必要がないという点は微妙にうれしいですね。早速使ってみたので本エントリでご紹介したいと思います。

なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。

  • Serverless Framework: 1.6.1
  • Ansible: 2.3.0(f75ffe46db)

serverless モジュールのメリット

「わざわざ」AnsibleからServerless Frameworkを扱う利点はどういったものがあるでしょうか。PRのコメントでも触れられていますが、以下の用途には便利かもしれません。

  • デプロイ前後で何らかの処理をしたい
  • その処理をシェルスクリプトで書くのはシンドい
  • Serverless Frameworkのプラグインはあまり使いたくない
  • Serverless Frameworkで作成したスタックの情報をcloudformation_factsモジュールから参照して利用したい

AnsibleにはAWS関連のモジュールが充実しています(詳細な内容は弊社ブログのAnsibleカテゴリをご覧いただくと分かるかと思います)。Serverless Frameworkで作成したスタックの結果をこれらAWS関連モジュール内で利用したい、といった用途では確かに便利かもしれません。AWS CLIでも同じことはできなくないですが、シェルスクリプトの場合冪等性を自分で担保しなければならず、あまり使い勝手が良くないと思います。

使ってみる

説明が長くなりました。それでは早速使ってみましょう。

インストール

執筆時点(2017/02/04)ではまだ2.3がリリースされていません。ソースコードからインストールする必要があります。詳細はドキュメントを参照していただくとして、基本的には以下のコマンドでインストール可能です( PATH を一時的に変更しているだけなので、シェルのセッションが変わると結果が変わります)。

$ git clone git://github.com/ansible/ansible.git --recursive
$ cd ansible
$ source ./hacking/env-setup
$ ansible --version
ansible 2.3.0 (devel f75ffe46db) last updated 2017/02/04 16:22:37 (GMT +900)
  config file =
  configured module search path = Default w/o overrides

私の環境特有の問題なのかもしれませんが、Playbookの作成中に各種モジュールで利用しているPythonモジュールが見つからないとエラーが出てしまいました。一応以下のように PYTHONPATH へ該当のPythonモジュールをダウンロードしておけば回避可能です。これが正攻法なのか微妙ですが。。。

$ pip install pyyaml boto3 -t ${PYTHONPATH%:}

Playbookの実行

サンプルとなるコードをGitHubに作成しました。ご自由にお使いください。

今回はこのリポジトリを元に説明していきます。Playbookのディレクトリ構造は以下のようにしました。

$ tree -I ansible
.
├── README.md
├── ansible.cfg
├── handler.py
├── host_vars
│   └── localhost.yml
├── hosts
├── localhost.yml
├── roles
│   ├── cloudformation
│   │   └── tasks
│   │       ├── cloudformation.yml
│   │       └── main.yml
│   └── serverless
│       ├── defaults
│       │   └── main.yml
│       └── tasks
│           ├── main.yml
│           └── serverless.yml
├── serverless.yml
└── site.yml

7 directories, 13 files

以下に各種ファイルの内容を解説します。

  • roles/serverless/tasks/serverless.yml
---
- name: Deploy all functions
  serverless:
    service_path: "{{ service_path }}"
    stage: "{{ stage }}"
    region: "{{ region }}"
    state: present

service_pathserverless.yml 設置ディレクトリを指定しています。今回は ansible-playbook コマンドの実行ディレクトリ上に該当ファイルを設置しているため、「.(ドット)」でカレントディレクトリを指定しています( roles/serverless/defaults/main.yml で定義)。 statepresent を指定することで、 serverless deploy が実行されます。その他指定可能なオプションについてはドキュメントを参照してください。

  • roles/cloudformation/tasks/cloudformation.yml
---
- name: Obtain facts from Serverless Framework Stack
  cloudformation_facts:
    region: "{{ region }}"
    stack_name: "{{ service_name }}-{{ stage }}"
    stack_resources: true

cloudformation_facts モジュールでServerless Frameworkで作成したスタックの情報を参照しています。今回は単純に参照しているだけですが、この値を利用することでより複雑な設定が可能です。Serverless Frameworkではスタック名が「<サービス名>-<ステージ名>」となるので、 host_vars/localhost.yml で定義しておいた変数を参照してスタック名を指定しています。

Serverless Framework関連の設定は以下のようにしました。単純に1つのLambda関数を定義しているだけです。

  • serverless.yml
frameworkVersion: ">=1.6.0"

service: serverless-ansible-demo

provider:
  name: aws
  runtime: python2.7
  stage: dev
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
  • handler.py
def hello(event, context):
    return 'Invoked with this event: {}'.format(event)

Ansibleの実行は以下のコマンドで可能です。当たり前ですが、Playbookを実行するホスト上でServerless Frameworkを事前にインストールしておく必要があります。

$ ansible-playbook site.yml -vvv
<snip>
TASK [serverless : Deploy all functions] ****************************************************************************************************
<snip>
changed: [localhost] => {
    "changed": true,
    "command": "serverless deploy --region ap-northeast-1 --stage dev ",
    "invocation": {
        "module_args": {
            "deploy": true,
            "functions": null,
            "region": "ap-northeast-1",
            "service_path": ".",
            "stage": "dev",
            "state": "present"
        },
        "module_name": "serverless"
    },
    "out": "Serverless: Creating Stack...\nServerless: Checking Stack create progress...\n.....\nServerless: Stack create finished...\nServerless: Packaging service...\nServerless: Uploading CloudFormation file to S3...\nServerless: Uploading service .zip file to S3 (80.19 MB)...\nServerless: Updating Stack...\nServerless: Checking Stack update progress...\n..................\nServerless: Stack update finished...\nService Information\nservice: serverless-ansible-demo\nstage: dev\nregion: ap-northeast-1\napi keys:\n  None\nendpoints:\n  None\nfunctions:\n  serverless-ansible-demo-dev-hello\n",
    "service_name": "serverless-ansible-demo-dev",
    "state": "present"
}
<snip>
TASK [cloudformation : Obtain facts from Serverless Framework Stack] ************************************************************************
<snip>
ok: [localhost] => {
    "ansible_facts": {
        "cloudformation": {
            "serverless-ansible-demo-dev": {
                "stack_description": {
                    "capabilities": [
                        "CAPABILITY_IAM",
                        "CAPABILITY_NAMED_IAM"
                    ],
                    "creation_time": "2017-02-04T09:31:25.610000+00:00",
                    "description": "The AWS CloudFormation template for this Serverless application",
                    "disable_rollback": false,
                    "last_updated_time": "2017-02-04T09:33:06.079000+00:00",
                    "notification_arns": [],
                    "outputs": [
                        {
                            "description": "Current Lambda function version",
                            "output_key": "HelloLambdaFunctionQualifiedArn",
                            "output_value": "arn:aws:lambda:ap-northeast-1:************:function:serverless-ansible-demo-dev-hello:5"
                        },
                        {
                            "output_key": "ServerlessDeploymentBucketName",
                            "output_value": "serverless-ansible-demo-serverlessdeploymentbuck-8za2ealdohqa"
                        }
                    ],
                    "stack_id": "arn:aws:cloudformation:ap-northeast-1:************:stack/serverless-ansible-demo-dev/b2ce3af0-eabc-11e6-a2e3-50d5ca9ff48e",
                    "stack_name": "serverless-ansible-demo-dev",
                    "stack_status": "UPDATE_COMPLETE",
                    "tags": [
                        {
                            "key": "STAGE",
                            "value": "dev"
                        }
                    ]
                },
                "stack_outputs": {
                    "HelloLambdaFunctionQualifiedArn": "arn:aws:lambda:ap-northeast-1:************:function:serverless-ansible-demo-dev-hello:5",
                    "ServerlessDeploymentBucketName": "serverless-ansible-demo-serverlessdeploymentbuck-8za2ealdohqa"
                },
                "stack_parameters": {},
                "stack_resource_list": [
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:33:40.789000+00:00",
                        "LogicalResourceId": "HelloLambdaFunction",
                        "PhysicalResourceId": "serverless-ansible-demo-dev-hello",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::Lambda::Function"
                    },
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:33:45.931000+00:00",
                        "LogicalResourceId": "HelloLambdaVersion3Wtot8RivaqAqz38IQm0sOEEUYgyO4U3h6t1XgtiNw",
                        "PhysicalResourceId": "arn:aws:lambda:ap-northeast-1:************:function:serverless-ansible-demo-dev-hello:5",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::Lambda::Version"
                    },
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:33:12.576000+00:00",
                        "LogicalResourceId": "HelloLogGroup",
                        "PhysicalResourceId": "/aws/lambda/serverless-ansible-demo-dev-hello",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::Logs::LogGroup"
                    },
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:33:25.098000+00:00",
                        "LogicalResourceId": "IamPolicyLambdaExecution",
                        "PhysicalResourceId": "serve-IamP-1M54LIGT43WG",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::IAM::Policy"
                    },
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:33:20.491000+00:00",
                        "LogicalResourceId": "IamRoleLambdaExecution",
                        "PhysicalResourceId": "serverless-ansible-demo-dev-ap-northeast-1-lambdaRole",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::IAM::Role"
                    },
                    {
                        "LastUpdatedTimestamp": "2017-02-04T09:31:52.388000+00:00",
                        "LogicalResourceId": "ServerlessDeploymentBucket",
                        "PhysicalResourceId": "serverless-ansible-demo-serverlessdeploymentbuck-8za2ealdohqa",
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "AWS::S3::Bucket"
                    }
                ],
                "stack_resources": {
                    "HelloLambdaFunction": "serverless-ansible-demo-dev-hello",
                    "HelloLambdaVersion3Wtot8RivaqAqz38IQm0sOEEUYgyO4U3h6t1XgtiNw": "arn:aws:lambda:ap-northeast-1:************:function:serverless-ansible-demo-dev-hello:5",
                    "HelloLogGroup": "/aws/lambda/serverless-ansible-demo-dev-hello",
                    "IamPolicyLambdaExecution": "serve-IamP-1M54LIGT43WG",
                    "IamRoleLambdaExecution": "serverless-ansible-demo-dev-ap-northeast-1-lambdaRole",
                    "ServerlessDeploymentBucket": "serverless-ansible-demo-serverlessdeploymentbuck-8za2ealdohqa"
                }
            }
        }
    },
    "changed": false,
    "invocation": {
        "module_args": {
            "all_facts": false,
            "aws_access_key": null,
            "aws_secret_key": null,
            "ec2_url": null,
            "profile": null,
            "region": "ap-northeast-1",
            "security_token": null,
            "stack_events": false,
            "stack_name": "serverless-ansible-demo-dev",
            "stack_policy": false,
            "stack_resources": true,
            "stack_template": false,
            "validate_certs": true
        },
        "module_name": "cloudformation_facts"
    }
}
<snip>
PLAY RECAP **********************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0

Lambda関数を実行してみると正常にデプロイされていることが確認できます。

$ sls invoke -f hello -d "$(jo hoge=fuga)" -l
"Invoked with this event: {u'hoge': u'fuga'}"
--------------------------------------------------------------------
START RequestId: bce34cae-eabd-11e6-a27c-9b3fcffd5acf Version: $LATEST
END RequestId: bce34cae-eabd-11e6-a27c-9b3fcffd5acf
REPORT RequestId: bce34cae-eabd-11e6-a27c-9b3fcffd5acf  Duration: 0.22 ms       Billed Duration: 100 ms         Memory Size: 1024 MB    Max Memory Used: 9 MB

まとめ

いかがだったでしょうか。

AnsibleからServerless Frameworkをデプロイする serverless モジュールについてご紹介しました。正直に言うと個人的にはあまり使うこともないかなと思いましたが、上述した部分にメリットを感じる環境であれば便利かもしれません。簡単に利用できるので一度利用されてみてはいかがでしょうか。

本エントリがみなさんの参考になれば幸いに思います。