EAFP

~ Easier to Ask for Forgiveness than Permission ~

FaaSのIacに関して「Zappa」で立ち向かってみる

この記事はRecruit Engineers Advent Calendar 2019 の19日目の記事です。

adventar.org

こんにちは @kazu0716 です

サーバサイド開発、インフラ/ミドルの構築・運用、データ分析/基盤構築・運用、クラウドセキュリティに関して考えることをしています

最近は、オンプレ環境からのPublicクラウドへの移行をもっぱらやってます

今回も、ノリでアドベントカレンダーなんて参加しました

昨年同様ギリギリになって書き出してます。

そして、前回のアドベントカレンダーから、何も書いてないのが今年最大の反省点ですね・・・

ネタはいっぱいあるので、来年こそは!!

Issues of FaaS in our worksplace

lambdaでpythonでなくとも、javascriptrubyでさくっと何かの処理を自動化したりしたいなーってことよくありますよね

個人で何かやろうってときもそうだし、職場でも無数のlambdaが動いていたりします

lambdacloud functionFaaS(Function as a Service) に分類されるそうですが

さくっと作ってそのまま放置とかされると、結構厄介かなというのがあり、きちんと管理したいなと思うことがよくありました

最近だと、lamdbaのランタイムのEOL等で、既存の動いているlambdaを見たときに「なんやねんこれ!どこにソースあんねん!」ってなりましたww

  • こういうのとか

f:id:kazu_0716:20191219144055p:plain

  • こういうのとか

f:id:kazu_0716:20191219144117p:plain

私はPythonが好きで、何やるにもだいたいpythonでやるので、pythonでlambda作るとき/管理するのに便利なツールないかなと探していました

What's Zappa

Zappaとはpythonをlambdaにdeplyする便利ツールです

github.com

API-GatewayやCloudWatchも一緒にdeployしてくれたりと便利なのですが、個人的には lambdaのIac(Infrastructure as a code) ができるのが良いなと思ってます

Iac(Infrastructure as a code)の詳細に関しては、リンク先のwikiを読んでいただくのが良いかと思いますが、私がやりたきことは

  • lambdaやAPI-Gateway、CludWatchの設定情報をgithubソースコード管理したい(AWS key等のCredentialな情報は除く)
    • Credentialな情報は環境変数とかで設定するとかして、githubとかには置かないようにします。漏洩すると困るので
  • 設定変更をし、deployやupdateをコマンド1行とかで手軽にしたい
    • s3にソースをuplodaして、AWSコンパネからボタンをぽちぽちしたくない。。。

を満たしてくれる、この便利ツールを早速使ってみようと思います

How to use Zappa

ZappaでFlaskとAPI-Gatewayとかってのは、既に記事があるので今回は、CloudWatchでCron的にPython Scriptを実行してみようと思います

dev.classmethod.jp

What's you make

監視のSciprtでcronで定期的にクローリングして、その結果をDatadogに飛ばすものを作ろうと思います

FaceBookやGooglePlayが落ちてないか、もしくは担当サービスのStaticページが落ちてないかの監視をするために作りました

Deploy lambda by using Zappa

とっても簡単です。Zappaサイコー!!

  • AWS Commandをインストールし、アクセスキー等の認証情報を設定します
$ aws configure
AWS Access Key ID [****************4HN5]:
AWS Secret Access Key [****************c78Q]:
Default region name [ap-northeast-1]: 
Default output format [None]: 
  • zappaコマンドでs3経由でlambdaにdeployする(参考)
    • dev は環境名。名前を変えると同じものができる
      • 今回は external-monitoring-devというものができる
      • prod にすると external-monitoring-prod になる
      • API-Gatewayも同じ命名規則で作成される
# 初回時
$ zappa deploy dev

 # 既に起動している場合はupdateする
$ zappa update dev
  • cloudwatchのログも確認できる
$ zappa tail dev

Settings Zappa

Deployは簡単なのですが、Zappaではlambdaと何かの組み合わせでを細かい設定含め、1コマンドdeplyすることができます

詳細はREADMEを読んでいただくのが良いと思うのですが、今回はこんな感じの設定をしました

$ cat zappa_settings.json
{
    "dev": {
        "lambda_description": "外部監視用スクリプト",
        "app_function": "app.lambda_handler",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "external-monitoring",
        "runtime": "python3.7",
        "s3_bucket": "$hoge$",
        "events": [
            {
                "function": "app.lambda_handler",
                "expression": "rate(5 minutes)"
            }
        ],
        "apigateway_enabled": false,
        "keep_warm": false,
        "log_level": "INFO",
        "memory_size": 512,
        "timeout_seconds": 300,
        "manage_roles": true
    }
}

ポイントはeventsの設定で特定関数が5分ごとに実行する設定をしている ということになるかと思います

今回はrole等は設定してないですが、この場合問題なく動くのですが、deplyするプロジェクト名を使って自動的に作成してしまうので

zappaでdeployするたびにroleが量産される ということが起こるので、少し注意が必要かと思います

# Advanced settingsから抜粋
"role_name": "MyLambdaRole", // Name of Zappa execution role. Default <project_name>-<env>-ZappaExecutionRole. To use a different, pre-existing policy, you must also set manage_roles to false.
"role_arn": "arn:aws:iam::12345:role/app-ZappaLambdaExecutionRole", // ARN of Zappa execution role. Default to None. To use a different, pre-existing policy, you must also set manage_roles to false. This overrides role_name. Use with temporary credentials via GetFederationToken.

Python Script

特に、面白いところはないと思いますwww

可読性を最大にするため、極力謎記法は使ってないです

Credentialな情報は環境変数に設定しているので、その設定は初回のみAWSのコンパネでぽちぽちしています

アクセス先のURL等の情報は config.ini を作って外出ししています

$ cat app.py
# -*- coding: utf-8 -*-
import json
import logging
import os
from configparser import ConfigParser
from threading import Thread

import requests
from bs4 import BeautifulSoup
from datadog import api, initialize

config = ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), './config.ini'), 'UTF-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# NOTE: Datadog SDKの初期設定
options = {'api_key': os.environ['APIKEY'],
           'app_key': os.environ['APPKEY']}
initialize(**options)


def create_dd_event(title, text, tags):
    if api.Event.create(title=title, text=text, tags=tags, alert_type="error")['status'] != "ok":
        raise Exception("Datadogへのイベント送信が失敗しました。")
    else:
        logger.info("datadogにイベント通知しました。")


def monitor_enmusubi():
    """
    サイトの外部監視
    """
    try:
        for url in config["Enmusubi"]:
            code = requests.get(config["Enmusubi"][url]).status_code
            if code != 200:
                create_dd_event(
                    "サイト({0}, ステータスコード: {1})".format(url, code),
                    config["Enmusubi"][url],
                    ["env:external", "service:enm", "target:enmusubi"]
                )
    except Exception as e:
        raise e


def monitor_facebook():
    """
    Facebookの障害監視
    """
    try:
        if requests.get(config['Facebook']['Url']).json()['current']['health'] != 1:
            create_dd_event(
                "Facebook",
                config['Facebook']['DashboardUrl'],
                ["env:external", "service:enm", "target:facebook"]
            )
    except Exception as e:
        raise e


def monitor_app_store():
    """
    AppleStoreの障害検知
    """
    # TODO: 動的ページなのでSeleniumが必要になるので後回し
    pass


def monitor_datadog():
    """
    Datadogサービスの障害検知
    """
    try:
        html = BeautifulSoup(requests.get(config['Datadog']['Url']).text)
        if "error" in ["error" for status in html.find_all("span", class_="component-status") if "Operational" not in status.text]:
            create_dd_event(
                "Datadog",
                config['Datadog']['Url'],
                ["env:external", "service:enm", "target:datadog"]
            )
    except Exception as e:
        raise e


def lambda_handler(event, context):
    """
    CloudWatchに定期実行される関数
    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format
        # api-gateway-simple-proxy-for-lambda-input-format
        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    context: object, required
        Lambda Context runtime methods and attributes
        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
    """

    try:
        results = []
        # NOTE: 監視用関数をマルチスレッドで実行
        for task in [monitor_datadog, monitor_facebook, monitor_app_store, monitor_enmusubi]:
            t = Thread(target=task)
            t.start()
            results.append(t)

        # NOTE: 全てのスレッドが完了したことを確認
        for result in results:
            result.join()

    except Exception as e:
        logging.error("エラーが発生しました。" + str(e.args))

終わりに

lambdaとかcloud functionは便利ですが、雑に作らずこういうツール使って、githubにコード残してくれると離任の際に引き継ぎも楽なので、小さなところもIacしていきましょう!