1
1

More than 3 years have passed since last update.

AWS SAM + Step Functions でアクセスチェックをする。(ruby runtime)

Last updated at Posted at 2020-05-02

概要

AWS SAM(Lambda) + Step Functions を利用してウェブサイトの外形監視を行います。

やること

  1. サイトの外形監視をする。(lambdaで実行)
  2. アクセスした際にステータスコードが200 or 302の場合、メールで通知(lambda + SES)
  3. アクセスアクセスした際に上記のステータスコード以外の場合は、時間をおいて再アクセスをする。
    StepFunctionsでループの処理を実装し、最大5回実行する。
  4. 正常なステータスコードが取得できなかった場合は実行を失敗として終了する。
    失敗時の通知はCloudWatch Eventで通知予定ですが、今回は実装しません。

注意)
通常は成功時は通知をせず、失敗時に通知を行うと思いますが、今回は成功した場合に通知をする設定にしています。(学習目的のため)

実行環境

  1. AWS CLIを設定済であること(バージョン1)
  2. AWS SAMをインストール済であること
  3. SESを設定済であること

環境設定の参考サイト

AWS CLI インストール
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-install.html

AWS CLI の設定
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html

AWS SAM CLI インストール
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

Amazon SES とは
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/Welcome.html

Amazon SESによるメール送信環境の構築と実践
https://dev.classmethod.jp/articles/amazon-ses-build-and-practice/

初期設定

AWS SAM の初期化

$ sam init --runtime ruby2.5
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Project name [sam-app]: ruby-stepfunc

Quick start templates may have been updated. Do you want to re-download the latest [Y/n]: Y

-----------------------
Generating application:
-----------------------
Name: ruby-stepfunc
Runtime: ruby2.5
Dependency Manager: bundler
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./ruby-stepfunc/README.md

ディレクトリを移動する

$ cd ruby-stepfunc

Lambda関数作成

今回はWEBサイトのレスポンスチェックと結果をメールで通知するスクリプトを作成します。

アクセスチェックのスクリプト

$ mkdir -p function/check_response/
$ vi function/check_response/main.rb
main.rb
require 'aws-sdk'

############################################
# アクセスのレスポンスチェック
############################################
def lambda_handler(event:, context:)
  count = event['checkresponse']['result']['count'].to_i rescue 0
  url   = event['url']
  uri   = URI.parse(url)

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme === "https"
  headers = { "Content-Type" => "text/html" }

  begin
    response  = http.get(uri.path, headers)
    available = response.code == ('200' || '302') ? true : false
    count += 1
    result = {
      available: available,
      response_code: response.code,
      uri: uri.to_s,
      msg: response.msg,
      header: response.header,
      count: count
    }
  rescue => e
    count += 1
    result = {
      available: false,
      uri: uri.to_s,
      msg: e.message,
      count: count
    }
  end

  {
    statusCode: 200,
    body: {
      message: result[:msg],
    }.to_json,
    result:  result
  }
end

メール送信機能

SESの設定は以下のサイトを参考に行ってください。
実際に外部へ送信される際は、サンドボックスの解除申請も忘れずに行ってください。

$ mkdir -p function/send-email/
$ vi function/send-email/main.rb
main.rb
require 'aws-sdk'
require "net/http"

############################################
# メール送信
############################################
def lambda_handler(event:, context:)
  sender     = ENV['SENDER']
  ses_region = ENV['SES_REAGION'] || "us-west-1"
  encoding   = ENV['ENCODING']    || "UTF-8" 
  recipient  = event['email']

  subject  = "アクセス監視の通知"
  textbody = event['checkresponse']['result']['msg'] rescue "no text"
  ses = Aws::SES::Client.new(region: ses_region)
  begin
    # Provide the contents of the email.
    resp = ses.send_email({
      destination: {
        to_addresses:  [ recipient ],
      },
      message: {
        body:    { text: { charset: encoding, data: textbody } },
        subject: { charset: encoding, data: subject }
      },
      source: sender,
    })
    result = {
      msg: "Email sent!"
    }

  # If something goes wrong, display an error message.
  rescue Aws::SES::Errors::ServiceError => error
    result = {
      msg: "Email not sent. Error message: #{error}"
    }
  end

  {
    statusCode: 200,
    body: {
      message: result[:msg],
    }.to_json,
    result:  result
  }
end

Step Functionの設定

以下、AWS SAMのyamlになります。(StepFunctionsの設定を含む)

$ vi template.yaml

your-email, your-ses-region にはそれぞれSESで設定した適した値を入れてください。
(sesでdomain認証をした場合は@前の文字列は任意に決定できます。)

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: |
  ruby-stepfunc
  SAM Template for ruby-stepfunc

Mappings:
  prd:
    Ses:
      sender: [your-email]
      region: [your-ses-region]
  stg:
    Ses:
      sender: [your-email]
      region: [your-ses-region]

Parameters:
  Environment:
    Description: Type of this environment.
    Type: String
    Default: prd
    AllowedValues:
      - prd
      - stg

Globals:
  Function:
    Timeout: 120
    Handler: main.lambda_handler
    Runtime: ruby2.5
    MemorySize: 128

Resources:

  LambdaRole:
      Type: 'AWS::IAM::Role'
      Properties:
        RoleName: !Sub '${AWS::StackName}-check-response-role'
        Path: /
        AssumeRolePolicyDocument:
          Statement:
            - Effect: "Allow"
              Principal:
                Service:
                  - "lambda.amazonaws.com"
              Action:
                - "sts:AssumeRole"
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
          - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
          - arn:aws:iam::aws:policy/AmazonEC2ContainerServiceFullAccess
          - arn:aws:iam::aws:policy/AmazonSESFullAccess

  StatesExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - !Sub states.${AWS::Region}.amazonaws.com
            Action: "sts:AssumeRole"
      Path: "/"
      Policies:
        - PolicyName: StatesExecutionPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "lambda:InvokeFunction"
                Resource: "*"

  CheckResponse:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub 'CheckResponse-${AWS::StackName}'
      CodeUri: function/check-response/
      Role: !GetAtt LambdaRole.Arn

  SendEmail:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub 'SendEmail-${AWS::StackName}'
      CodeUri: function/send-email/
      Role: !GetAtt LambdaRole.Arn
      Environment:
        Variables:
          SENDER: !FindInMap [ !Ref Environment, Ses, sender ]
          SES_REAGION: !FindInMap [ !Ref Environment, Ses, region ]
          ENCODING: UTF-8

  ServiceCheck:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      DefinitionString: !Sub |-
        {
          "Comment": "Check WEBSITE resoponse",
          "StartAt": "CheckResponse",
          "States": {
            "CheckResponse": {
              "Type": "Task",
              "Resource": "${CheckResponse.Arn}",
              "ResultPath": "$.checkresponse",
              "Next": "ChoiceState"
            },
            "ChoiceState": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.checkresponse.result.available",
                  "BooleanEquals": true,
                  "Next": "SendEmail"
                },
                {
                  "And": [
                    {
                      "Variable": "$.checkresponse.result.available",
                      "BooleanEquals": false
                    },
                    {
                      "Variable": "$.checkresponse.result.count",
                      "NumericGreaterThanEquals": 5
                    }
                  ],
                  "Next": "FunctionEnd"
                }
              ],
              "Default": "wait_seconds1"
            },
            "wait_seconds1": {
              "Type": "Wait",
              "Seconds": 10,
              "Next": "CheckResponse"
            },
            "SendEmail": {
              "Type": "Task",
              "Resource": "${SendEmail.Arn}",
              "End": true
            },
            "FunctionEnd": {
              "Type": "Fail",
              "Cause": "Email not sent.",
              "Error": "Error"
            }
          }
        }
      RoleArn: !GetAtt [ StatesExecutionRole, Arn ]

Outputs:
  ServiceCheck:
    Description: Check Function ARN
    Value: !Ref: ServiceCheck

注意
ロールの権限はモリモリになっています。
本番運用の際にはしっかり権限を絞ってください。

StepFunctionsの補足説明

構成図

構成図.png

Step 説明
CheckResponse 指定したURLのレスポンスを確認します。
ChoiseState CheckResponseのレスポンスによって、SendEmail,wait_second1に分岐します。
SendEmail 指定したURLへアクセス結果を送信します。
wait_seconds1 10秒時間をあけてからCheckResponseへ際アクセスします。
FunctionEnd 指定したループの上限を超えた場合、処理は失敗として終了します。
End 終了

フローサンプル1(成功)

構成図success.png

  1. CheckResponseで指定したURLのレスポンスを確認します。
  2. ChoiseStateでCheckResponseのレスポンスが200 or 302 であることを確認できましたのでSendEmailに分岐します。
  3. SendEmailで指定したURLへアクセス結果を送信します。

フローサンプル2(失敗)

failure.png

  1. CheckResponseで指定したURLのレスポンスを確認します。
  2. ChoiseStateでCheckResponseのレスポンスが200 or 302 以外であることが確認できましたのでwait_seconds1に分岐します。
  3. wait_seconds1で10秒時間をあけてからCheckResponseへ際アクセスします。
  4. 2,3の処理を繰り返して、200 or 302のステータスコードを取得できなかったので、FunctionEndへ分岐します。
  5. FunctionEndで意図的に処理を失敗にして終了します。

条件分岐・ループ処理処理

デプロイ

cloudformationで使用する、template.yamlの生成とlambdaのファイルをS3へ固めてアップロードする。

bucket_name は事前に準備をしたバケット名を指定してください。

$ sam package --template-file template.yaml \
    --output-template-file packaged.yaml \
    --s3-bucket [bucket_name]

template.ymlを実行する(CloudFormation)

stack_name はCloudFormationのスタック名になります。任意の名前を決めて実行してください。

$ sam deploy --template-file packaged.yaml \
    --parameter-overrides Environment=stg \
    --stack-name [stack_name] \
    --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM 

{
"email": "yoshioka@farend.jp",
"url": "https://mica1111.g2staging.farend.ne.jp/"
}

実行

コマンドラインから実行する

ステートマシンの実行時に入力する値のjsonファイルを作成します。

$ vi input_value.json
input_value.json
{
  "email": "[your_notification_emal]",
  "url": "https://[check_url]/"
}

[statemachine_arn] は先ほど作成した、Step FunctionsのステートマシンのARNを入力します。
ARNはCloudFormationの出力から確認もできます。

$ aws stepfunctions start-execution --state-machine-arn [statemachine_arn]  --name `uuid` --input file://input_value.json

AWSコンソールから実行する

先ほど作成したStep Functionsのステートマシンを選択し、「実行の開始」をクリックし、以下の値を入力して「実行の開始」をしてください。

{
  "email": "[your_notification_emal]",
  "url": "https://[check_url]/"
}

まとめ(所感)

今回実行した処理程度でしたらLambda一つで実行しても良いかと思います。
ただ、Lambdaは実行時間で課金されるので、Sleepを使用する際にはStepFunctionsでループして間にwaitを挟んで実行した方が良さそうです。または今回は試していませんが、並列実行が必要な処理の場合などに威力を発揮しそうです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1