AnsibleでAWS Lambda(python)をdeployしてみる

# なぜansible？
ansible2.2からaws lambdaがデプロイできるようになったので試してみました

### 良かったこと
- 自分がansibleを普段使っていたので学習コスト低かった．
- host: localhost, connection: localにするとローカルで動かすスクリプトとして使える.
- lambdaに同梱する 追加パッケージの用意, zipで固めるなどもansibleで記述できる．
- テンプレート使っていろいろ処理・変数を取り回せる
 - vaultで秘匿情報とか埋め込める
- ansibleのモジュールを使うとaws cliを叩くより扱いやすい.

### 辛かったこと
- まだansible付属のmoduleだけじゃ無理なことが多い
 - 足りない部分はaws cliを叩くことに
 - 幸運なことにaws cliのレスポンスがjsonなのでansibleで加工しやすい
- 各モジュールの返り値(registerで受け取れる物) の仕様が一定じゃないので辛い時がある．

とりあえず試しにansibleで毎日12時にawsの料金をslackに通知するlambdaをデプロイしてみます．
(ここではlambdaのコードには触れてないです. githubには入れてあります)

実験環境：ansible==2.2.0.0
コード:https://github.com/kikusu/lambda-aws-billing-to-slack


# zipでコードを固める
自分はLamdaに追加パッケージを入れる場合は.gitignoreし易いように`site-package`を切ってそこにれています．

```
├ site-packages/
│ └ (追加pkg)
├ lambda_function.py
└ requirements.txt
```

pipで必要なpkgを取ってきた後全部zipに圧縮します．

```bash
pip install -r requirements.txt -t site-package
zip -ur lambda.zip *
```

ansibleで書くとこんな感じ

```yaml
- name: install required pkg
  pip:
    requirements: "requirements.txt"
    extra_args: "-t site-packages"
    chdir: "{{ source_dir }}"

- name: create lambda.zip
  shell: zip -ur lambda.zip *
  register: create_lambda_zip
  changed_when: create_lambda_zip.rc != 12
  failed_when: create_lambda_zip.rc not in [0, 12]
  args:
    chdir: "{{ source_dir }}"
```

zipに`-u`つけているのは更新がない時zipが変わらないようにしてlambdaのversionupのデプロイが走らないようにするためです．

# Lambda用にRoleを作る
ansibleの`iam`, `iam_policy`を使ってlambdaを実行できるroleを作ります．

```yaml
- name: create role
  iam:
    iam_type: role
    name: "{{ role_name }}"
    state: present
    trust_policy:
      Version: '2012-10-17'
      Statement:
      - Action: sts:AssumeRole
        Effect: Allow
        Principal:
          Service: lambda.amazonaws.com
      - Action: sts:AssumeRole
        Effect: Allow
        Principal:
          Service: events.amazonaws.com

- name: attatch policy
  iam_policy:
    iam_type: role
    iam_name: "{{ role_name }}"
    state: present
    policy_name: "{{ policy_name }}"
    policy_json:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
          Resource: '*'
        - Effect: Allow
          Action:
            - cloudwatch:GetMetricStatistics
          Resource: '*'
        - Effect: Allow
          Action:
            - s3:GetObject
          Resource: '*'
- name: get role arn
  command: "aws iam get-role --role-name {{ role_name }}"
  changed_when: false
  register: role

- set_fact:
    role_arn: "{{ (role.stdout|from_json).Role.Arn }}"
```
各statementにlambdaを実行するために必要な権限を追加しておいてください．

roleを作ったあとroleのarnをawsコマンドで取得します．レスポンスがjsonなので簡単にエスケープすることができます．
v2.2.0.0現在, role作成時のみiam moduleからARNが取得できる状態になっており2回以降はモジュールからARNが取得できないのでawsコマンドで取得するようにします．

# LambdaをDeployする
lambdaモジュールを使ってデプロイします． 簡単ですね．

```yaml
- name: create lambda function
  lambda:
    name: "{{ lambda_name }}"
    zip_file: "{{ source_dir }}/lambda.zip"
    handler: lambda_function.lambda_handler
    runtime: python2.7
    role: "{{ role_arn }}"
    timeout: 5

- name: get lambda arn
  command: "aws lambda get-function --function-name {{ lambda_name }}"
  changed_when: false
  register: lambda

- set_fact:
      lambda_arn: "{{ (lambda.stdout|from_json).Configuration.FunctionArn }}"
```

lambdaモジュールはlambda関数に変更が無かろうがARNを取得することができます．
ただしv2.2.0.0現在,その取得できるARNの値が変更の有無で違います．

```
# 変更無
"function_arn": "arn:aws:lambda:*******:********:function:AWSBillingToSlack",

# 変更有
"function_arn": "arn:aws:lambda:*******:********:function:AWSBillingToSlack:2",
```

変更があった場合はARNの最後にバージョンの数字がついてきます．orz
changedかどうかでarnの値を整形する方法もあると思いますが 一旦lambdaの方もawsコマンドを叩いてARNを取得するようにしました.

# 定時実行の設定をする

後はcloudwatch eventでcronイベントをセットしてlambda実行権限を付与すれば終わりです．
cronの時間設定はUTCなのでJSTと違うことに注意です．
add-permissionはモジュールがなさそうなのでawsコマンドを使って実行します．

```yml
- name: create cloudwatch event
  cloudwatchevent_rule:
    name: "{{ lambda_name }}"
    schedule_expression: cron(0 3 * * ? *)
    description: "{{ lambda_name }} cron"
    role_arn: "{{ role_arn }}"
    targets:
      - id: "{{ lambda_name }}"
        arn: "{{ lambda_arn }}"
  register: event

- name: aws lambda add-permission
  command: >-
    aws lambda add-permission
    --function-name {{ lambda_name }}
    --statement-id 'cron'
    --action "lambda:InvokeFunction"
    --principal events.amazonaws.com
    --source-arn {{ event.rule.arn }}
  register: add_permission
  changed_when: "{{ 'provided already exists' not in add_permission.stderr }}"
  failed_when: "{{ add_permission.rc != 0 }} and {{ 'provided already exists' not in add_permission.stderr }}"
```

# やってみて
- デプロイ前に事前ビルド(zipに固めるところまで)できるのは良い
- テンプレートで role名やarnを引き回せるのは楽 
- 各モジュールのレスポンスの方針が一定だと嬉しい (変更がある場合でもない場合でもARNを返すとか)
- シェルスクリプトより保守しやすい