CloudFormationを使って、CloudWatchLogsフィルタ作成しアラート送信

参考にしたテンプレート

Using an AWS CloudFormation Template to Create CloudWatch Alarms

カスタマイズしたテンプレート

個人的には以下のアラームで十分でした。

  • AWSマネジメントコンソールへのログイン失敗アラーム
  • IAM権限によるアクセス拒否アラーム
  • セキュリティグループ作成・変更・削除アラーム
  • EC2インスタンス作成・起動・削除・停止・再起動アラーム
CloudWatch_Alarms_for_CloudTrail_API_Activity_customize.json
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "AWS CloudTrail API Activity Alarm Template for CloudWatch Logs",
  "Parameters" : {
      "LogGroupName" : {
          "Type" : "String",
          "Default" : "CloudTrail/DefaultLogGroup",
          "Description" : "Enter CloudWatch Logs log group name. Default is CloudTrail/DefaultLogGroup"
      },
      "Email" : {
          "Type" : "String",
          "Description" : "Email address to notify when an API activity has triggered an alarm"
      }
  },
  "Resources" : {
      "SecurityGroupChangesMetricFilter": {
          "Type": "AWS::Logs::MetricFilter",
          "Properties": {
              "LogGroupName": { "Ref" : "LogGroupName" },
              "FilterPattern": "{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }",
              "MetricTransformations": [
                  {
                      "MetricNamespace": "CloudTrailMetrics",
                      "MetricName": "SecurityGroupEventCount",
                      "MetricValue": "1"
                  }
              ]
          }
      },
      "SecurityGroupChangesAlarm": {
          "Type": "AWS::CloudWatch::Alarm",
          "Properties": {
              "AlarmName" : "SecurityGroupChanges",
              "AlarmDescription" : "セキュリティグループ作成・変更・削除アラーム",
              "AlarmActions" : [{ "Ref" : "AlarmNotificationTopic" }],
              "MetricName" : "SecurityGroupEventCount",
              "Namespace" : "CloudTrailMetrics",
              "ComparisonOperator" : "GreaterThanOrEqualToThreshold",
              "EvaluationPeriods" : "1",
              "Period" : "300",
              "Statistic" : "Sum",
              "Threshold" : "1"
          }
      },
      "EC2InstanceChangesMetricFilter": {
          "Type": "AWS::Logs::MetricFilter",
          "Properties": {
              "LogGroupName": { "Ref" : "LogGroupName" },
              "FilterPattern": "{ ($.eventName = RunInstances) || ($.eventName = RebootInstances) || ($.eventName = StartInstances) || ($.eventName = StopInstances) || ($.eventName = TerminateInstances) }",
              "MetricTransformations": [
                  {
                      "MetricNamespace": "CloudTrailMetrics",
                      "MetricName": "EC2InstanceEventCount",
                      "MetricValue": "1"
                  }
              ]
          }
      },
      "EC2InstanceChangesAlarm": {
          "Type": "AWS::CloudWatch::Alarm",
          "Properties": {
              "AlarmName" : "EC2InstanceChanges",
              "AlarmDescription" : "EC2インスタンス作成・起動・削除・停止・再起動アラーム",
              "AlarmActions" : [{ "Ref" : "AlarmNotificationTopic" }],
              "MetricName" : "EC2InstanceEventCount",
              "Namespace" : "CloudTrailMetrics",
              "ComparisonOperator" : "GreaterThanOrEqualToThreshold",
              "EvaluationPeriods" : "1",
              "Period" : "60",
              "Statistic" : "Sum",
              "Threshold" : "1"
          }
      },
      "ConsoleSignInFailuresMetricFilter": {
          "Type": "AWS::Logs::MetricFilter",
          "Properties": {
              "LogGroupName": { "Ref" : "LogGroupName" },
              "FilterPattern": "{ ($.eventName = ConsoleLogin) && ($.errorMessage = \"Failed authentication\") }",
              "MetricTransformations": [
                  {
                      "MetricNamespace": "CloudTrailMetrics",
                      "MetricName": "ConsoleSignInFailureCount",
                      "MetricValue": "1"
                  }
              ]
          }
      },
      "ConsoleSignInFailuresAlarm": {
          "Type": "AWS::CloudWatch::Alarm",
          "Properties": {
              "AlarmName" : "ConsoleSignInFailures",
              "AlarmDescription" : "AWSマネジメントコンソールへのログイン失敗アラーム",
              "AlarmActions" : [{ "Ref" : "AlarmNotificationTopic" }],
              "MetricName" : "ConsoleSignInFailureCount",
              "Namespace" : "CloudTrailMetrics",
              "ComparisonOperator" : "GreaterThanOrEqualToThreshold",
              "EvaluationPeriods" : "1",
              "Period" : "300",
              "Statistic" : "Sum",
              "Threshold" : "3"
          }
      },

      "AuthorizationFailuresMetricFilter": {
          "Type": "AWS::Logs::MetricFilter",
          "Properties": {
              "LogGroupName": { "Ref" : "LogGroupName" },
              "FilterPattern": "{ ($.errorCode = \"*UnauthorizedOperation\") || ($.errorCode = \"AccessDenied*\") }",
              "MetricTransformations": [
                  {
                      "MetricNamespace": "CloudTrailMetrics",
                      "MetricName": "AuthorizationFailureCount",
                      "MetricValue": "1"
                  }
              ]
          }
      },
      "AuthorizationFailuresAlarm": {
          "Type": "AWS::CloudWatch::Alarm",
          "Properties": {
              "AlarmName" : "IAMAuthorizationFailures",
              "AlarmDescription" : "IAM権限によるアクセス拒否アラーム",
              "AlarmActions" : [{ "Ref" : "AlarmNotificationTopic" }],
              "MetricName" : "AuthorizationFailureCount",
              "Namespace" : "CloudTrailMetrics",
              "ComparisonOperator" : "GreaterThanOrEqualToThreshold",
              "EvaluationPeriods" : "1",
              "Period" : "300",
              "Statistic" : "Sum",
              "Threshold" : "1"

          }
      },
        "AlarmNotificationTopic": {
          "Type": "AWS::SNS::Topic",
          "Properties": {
              "TopicName" : "CloudTrail-Alarm-AlarmNotification",
              "DisplayName" : "CloudTrailAlarm",
              "Subscription": [
                  {
                      "Endpoint": { "Ref": "Email" },
                      "Protocol": "email"
                  }
              ]
          }
      }
  }
}

続きを読む

AWS Batchを使ってバッチ処理を実装してみた。

今回やりたかったこと

  • 社内で適用しているセキュリティのSaaSサービスのアップデートの自動化をしたい
  • 処理内容は、基本的にapiをコールしてこねこねしていくだけ

Lambdaでいいのでは?

  • SaaSが提供しているapiサーバーが、アクセス集中時だと5分経ってもレスポンスを返してくれない

そこでAWS BATCH

AWS BATCHとは

  • Job queueを受けた段階で、予め指定しておいたスペック(スペックが足りなかったら自動で最適なものを立ててくれるらしい?)のEC2を立ち上げてECRからコンテナイメージを持ってきてタスク実行してくれる
  • スケジュールを設定して実行してくれる!とかは無さそう。

アーキテクチャ

スクリーンショット 2017-03-19 22.05.34.png

Dockerfileはこんな感じ

FROM centos:latest
RUN curl -kl https://bootstrap.pypa.io/get-pip.py | python
RUN pip install awscli
RUN yum install -y git
ADD init.sh /opt/
CMD ["sh","/opt/init.sh","test"]

事前に処理実行に必要なコードだったりシェルスクリプトなんかは、ECRに含めておく。
シェルには、コードコミットから実行ソースをcloneするのに必要なssh key(事前にKMSとかで暗号化しておいたもの)、復号化処理、処理実行とかを書いておく。

あとは、Job queueを送ってあげれば、自動でEC2を立ち上げ、コンテナをマウントして最新のソースコードをcloneしてきて、処理を実行してくれます。
自動で立ち上がったEC2はタスク処理終了後(CMDのとこのやつ)、1時間単位の課金が切れるタイミング?で自動で削除されます。(立ち上がりっぱなしとかの心配なし!)

job queueに送信
aws --profile <profile> --region <region> batch submit-job --job-name <好きな名前> --job-queue <job queueの名前> --job-definition <定義したjob>

結果については、 Jobsの画面で確認することができます。
スクリーンショット 2017-03-19 22.17.59.png

まとめ

先日参加した「JAWS-UG KOBE」のコンテナまつりでHiganWorks sawanobolyさんがElasticDockerRunって呼んでるって言ってました。
DockerImageを基にいい感じに実行してくれるが、逆にBatch?なのかって印象でした。
もし、スケジュール実行で定期的にbatch処理をしたい!とかでれば、lambdaなんかを使ってqueueに送信してやるといいかなぁと思いました。

追記

シェル内にkmsで暗号化したssh keyを含めているのですが、出来れば外出ししたい。。
パラメーターストアとかに出せるかと思ったのですが、文字数制限の関係で無理でした。

続きを読む

AWSの各サービスを雑に紹介する

えー、投稿しておいて何ですが、本稿本当に雑ですので、ご利用にあたってはあくまで自己責任ということで、よろしくお願いします。

コンピューティング

  • Elastic Compute Cloud (EC2)
    仮想専用サーバ、従量課金制 ≫公式

  • EC2 Container Registry (ECR)
    DockerHubみたいなやつ ≫英語公式 / Google翻訳 ≫Developers.IO

  • EC2 Container Service (ECS)
    Dockerオーケストレーション(デプロイ、起動停止制御) ≫公式 ≫@IT

  • Lightsail
    仮想専用サーバ、定額制 ≫公式

  • AWS Batch
    ECS対応バッチジョブスケジューラ ≫公式 ≫公式 ≫Developers.IO

  • Elastic Beanstalk
    プログラム実行環境 (Java, PHP, .NET, Node.js, Python, Ruby)、EC2を使用 ≫公式 ≫YouTube

  • AWS Lambda
    プログラム実行環境 (Node.js, Java, C#, Python)、サーバレス ≫公式

  • Auto Scaling
    EC2対応オートスケール制御 ≫公式

  • Elastic Load Balancing
    負荷分散、BIG-IPとかその手のヤツのクラウド版 ≫公式 ≫@IT

ストレージ

  • Amazon Simple Storage Service (S3)
    オブジェクトストレージ。ファイルサーバとしても一応使える ≫公式

  • Amazon Elastic Block Store (EBS)
    ブロックデバイス ≫CodeZine

  • Elastic File System (EFS)
    ファイルサーバ ≫公式

  • Glacier
    バックアップストレージ ≫公式

  • Snowball
    HDDをFedExで送るオフラインデータ転送

  • Storage Gateway
    バックアップデバイスはお客様各自のオンプレミスにてご用意下さい、AWSは対向するインターフェースを提供します、というもの ≫CodeZine ≫Developers.IO

データベース

ネットワーキング & コンテンツ配信

移行

  • Application Discovery Service
    オンプレミスサーバの構成管理情報を収集する ≫公式

  • Database Migration Service (DMS)
    RDBをオンプレミスからAWSへ乗り換えるときに使う支援ツール

  • Server Migration Service (SMS)
    サーバをオンプレミスからAWSへ乗り換えるときに使う支援ツール

開発者用ツール

  • CodeCommit
    GitHubみたいなやつ

  • CodeBuild
    従量課金制ビルド

  • CodeDeploy
    コードデプロイ

  • CodePipeline
    Continuous Integration (CI) オーケストレーション。ビルド→デプロイの自動実行フロー定義。

  • AWS X-Ray
    分散アプリケーションのトレース ≫Serverworks

管理ツール

セキュリティ、アイデンティティ、コンプライアンス

  • AWS Identity and Access Management (IAM)
    AWSの認証、権限管理単位 ≫Developers.IO

  • Inspector
    脆弱性検出 ≫公式

  • Certificate Manager
    X.509証明書の管理 ≫公式

  • AWS Cloud Hardware Security Module (HSM)
    秘密鍵の保管(暗号、署名) ≫公式

  • AWS Directory Service
    Active Directory ≫Developers.IO

  • AWS Web Application Firewall (WAF)
    ファイアーウォール ≫公式

  • AWS Shield
    DDoS対策 ≫公式

分析

人工知能

IoT

ゲーム開発

モバイルサービス

  • Mobile Hub
    AWSのいろんなmBaaS系サービスを統合的に使えるコンソール画面 ≫Qiita

  • Cognito
    ソーシャル認証+データ同期。FacebookログインとかTwitterログインとか ≫Cookpad

  • AWS Device Farm
    テスト環境。Android, iOSの実機にリモートアクセスしてテストができる ≫公式

  • Mobile Analytics
    アプリの使用データの測定、追跡、分析 ≫公式 ≫Developers.IO

  • Pinpoint
    プッシュ ≫Qiita

アプリケーションサービス

  • Step Functions
    フローチャートみたいなビジュアルワークフローを画面上に描いて分散アプリケーションを構築する、というもの ≫公式

  • Amazon Simple Workflow (SWF)
    旧世代サービス。現在はStep Functionsを推奨 ≫公式

  • API Gateway
    HTTP API化 ≫公式

  • Elastic Transcoder
    動画、音声のフォーマット変換。つんでれんこaaSみたいなヤツ ≫Serverworks

メッセージング

  • Amazon Simple Queue Service (SQS)
    メッセージキュー ≫公式

  • Amazon Simple Notification Service (SNS)
    プッシュ ≫公式

  • Amazon Simple Email Service (SES)
    E-mail配信。メルマガとか ≫公式

ビジネスの生産性

デスクトップとアプリケーションのストリーミング

  • Amazon WorkSpaces
    仮想デスクトップ ≫impress

  • Amazon WorkSpaces Application Manager (WAM)
    Amazon WorkSpaces端末にアプリを配信するツール ≫serverworks

  • AppStream 2.0
    Citrix XenAppみたいなやつ ≫Developers.IO

参考文献

AWS ドキュメント
https://aws.amazon.com/jp/documentation/

AWS re:Invent 2016 発表サービスを三行でまとめる
http://qiita.com/szk3/items/a642c62ef56eadd4a12c

続きを読む

Terraformを使って GCP Pub/Sub から AWS Lambda へのPush通知の仕組みを作成した

先日作ったのでその覚書的なものです。
簡単に言うとこんな感じのものを作りました。

GKE上のDockerアプリ -> Stackdriver logging -> Pub/Sub -> Lambda -> Slack

作成の動機

概ね以下の背景です。

  • GCPをメインに、AWSも多少織り交ぜつつな感じのアプリケーションを作っている。
  • Stackdriverでアプリの監視を行っているがエラーログを検知したらSlackに通知する仕組みが欲しかった。
  • Stackdriver単体だとログ内容をSlackに通知できなさそうだった。
  • StackdriverはフィルタリングしたログをPub/Subへ流すことができるらしい。
  • Pub/Subはキューにデータが流れてきたときに指定したエンドポイントにPush通知できるらしい。

と、こんなかんじの前提があり Pub/Sub から lambda に流して、そこからさらにSlackに流せば実現できると目論んだ次第です。

LambdaにもPub/Subにも今回始めて触ったということもあってか結構迷走してしまいました。。

以降から本題となります。

Terraformに読み込ませるHCL

これがほとんど全てな感じですが、実際に使ったHCLをコメントを交えつつ添付しておきます。
varになっているところは適宜置換が必要かと思われますのでご注意下さい。

基本的にはTerraformサイト上のサンプルを組み合わせているだけです。
Terraformマジ便利ですね、最高です。
もっといろいろな知見がネット上に溜まってくれるとうれしいです。

data "aws_caller_identity" "self" {}

#### ここから API Gateway 周り####
resource "aws_api_gateway_rest_api" "alert_api" {
  name = "alerter"
}

resource "aws_api_gateway_method" "root_post_method" {
  rest_api_id   = "${aws_api_gateway_rest_api.alert_api.id}"
  resource_id   = "${aws_api_gateway_rest_api.alert_api.root_resource_id}"
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "alert_integration" {
  rest_api_id             = "${aws_api_gateway_rest_api.alert_api.id}"
  resource_id             = "${aws_api_gateway_rest_api.alert_api.root_resource_id}"
  http_method             = "${aws_api_gateway_method.root_post_method.http_method}"
  integration_http_method = "POST"
  type                    = "AWS"
  uri                     = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${aws_lambda_function.alerter.arn}/invocations"
}

resource "aws_api_gateway_method_response" "alert_200_response" {
  rest_api_id = "${aws_api_gateway_rest_api.alert_api.id}"
  resource_id = "${aws_api_gateway_rest_api.alert_api.root_resource_id}"
  http_method = "${aws_api_gateway_method.root_post_method.http_method}"
  status_code = "200"
}

resource "aws_api_gateway_integration_response" "alert_response" {
  rest_api_id = "${aws_api_gateway_rest_api.alert_api.id}"
  resource_id = "${aws_api_gateway_rest_api.alert_api.root_resource_id}"
  http_method = "${aws_api_gateway_method.root_post_method.http_method}"
  status_code = "${aws_api_gateway_method_response.alert_200_response.status_code}"
}

## stage名に使う名前をランダム文字列として生成する
resource "random_id" "secret_path" {
  keepers = {
    ami_id = "${aws_lambda_function.alerter.id}"
  }

  byte_length = 40
}

resource "aws_api_gateway_deployment" "alert_deployment" {
  depends_on = ["aws_api_gateway_method.root_post_method"]

  rest_api_id = "${aws_api_gateway_rest_api.alert_api.id}"
  stage_name  = "${random_id.secret_path.hex}"
}

resource "aws_api_gateway_domain_name" "alert" {
  domain_name = "${var.alerter_domain}"

  certificate_name        = "alert-ssl"
  certificate_body        = "${file("cert_path")}"
  certificate_chain       = "${file("chain_path")}"
  certificate_private_key = "${file("key_path")}"
}

resource "aws_api_gateway_base_path_mapping" "alert" {
  api_id      = "${aws_api_gateway_rest_api.alert_api.id}"
  domain_name = "${aws_api_gateway_domain_name.alert.domain_name}"
}

#### ここから IAM 周り####
resource "aws_iam_role" "lambda_alert_role" {
  name               = "lambda_alert_role"
  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      }
    }
  ]
}
POLICY
}

#### ここから Lambda 周り####
resource "aws_lambda_permission" "apigw_alerter" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.alerter.arn}"
  principal     = "apigateway.amazonaws.com"

  source_arn    = "arn:aws:execute-api:${var.aws_region}:${data.aws_caller_identity.self.account_id}:${aws_api_gateway_rest_api.alert_api.id}/*/${aws_api_gateway_method.root_post_method.http_method}/"
}

resource "aws_lambda_function" "alerter" {
  filename         = "${var.path_to_lambda_source}"
  function_name    = "alerter"
  role             = "${aws_iam_role.lambda_alert_role.arn}"
  handler          = "index.handler"
  runtime          = "nodejs4.3"
  source_code_hash = "${base64sha256(file(${var.path_to_lambda_source}))}"
  environment {
    variables = {
      webhook_url = "${var.slack_webhook_url}"
      channel = "${var.slack_channel}"
    }
  }
}

#### ここから Route53 周り ####
resource "aws_route53_record" "alerter" {
  zone_id = "${aws_route53_zone.primary.zone_id}"

  name = "${aws_api_gateway_domain_name.alert.domain_name}"
  type = "A"

  alias {
    name    = "${aws_api_gateway_domain_name.alert.cloudfront_domain_name}"
    zone_id = "${aws_api_gateway_domain_name.alert.cloudfront_zone_id}"
    evaluate_target_health = true
  }
}

####
#### ここから GCP
####

#### ここから Pub/Sub 周り ####
resource "google_pubsub_topic" "log_alert_topic" {
  name = "log-alert-topic"
}

## CAUTION: 以下のリンクの「他のエンドポイントの登録」の手順を行っていないドメインをendpointに入れているとapplyに失敗します
## https://cloud.google.com/pubsub/advanced#push_endpoints
resource "google_pubsub_subscription" "aws_lambda_subscription" {
  name  = "default-subscription"
  topic = "log-alert-topic"

  ack_deadline_seconds = 10

  push_config {
    push_endpoint = "https://${aws_api_gateway_domain_name.alert.domain_name}/${aws_api_gateway_deployment.alert_deployment.stage_name}"

    attributes {
      x-goog-version = "v1"
    }
  }
}

ハマったところとか困ったところを何点か

これだけだと味気ないので、いくつかハマってしまった点を記録しておきます

Pub/Sub へ Push Subscriber への外部ドメインの登録

(おそらく)セキュリティ上の理由から Pub/Sub でPush通知するためにはそのエンドポイントの所有者であることを証明しないといけません。

HCLの方にもコメントをしていますが、AppEngine等のPub/Subと同じGCPプロジェクト内で管理されている以外のドメインを利用したい場合には事前に手作業でドメイン認証が必要となります。
このドメイン認証を行うために、 API Gateway をカスタムドメインで公開してやる必要がありました。
若干面倒ではありますが、認証できないエンドポイントにリクエストを投げられるようにしてしまうとDOS攻撃みたいなことにつかえてしまうのでこれは仕方がないのだと思います。

ともかく事前この作業をやっておかないと terraform apply に失敗してしまいます。
作業内容は別に難しいものではなく、大きく分けて以下の2ステップで完了します。

  • Google Search Console をつかってRoute53をイジイジしてドメイン認証

    • DNSのTXTレコードを使ったよくある感じの認証です。
  • GCP Console の API Manager から API Gateway に設定したカスタムドメインを登録

Pub/Sub と Lambda 間の認証

こちらは、単に自分の Pub/Sub と Lambda の知識不足な可能性があります。
HTTPヘッダーの中身をカスタムする等の方法による認証方法が見つけられずかなり悩んでしました。

結局、当座の対応としてHCL内で生成したランダム文字列を公開URLに含ませておき、これを知らないとAPIを叩けないという仕様にしています。
正直これについてはもっと上手な解決策があるような気がしてなりません。。

続きを読む

Serverless FrameworkとS3で超簡単な投票システムを作った話

はじめに

社内でいつも通り仕事をしていると、約1週間で簡単な投票アプリを作ってくれないか?というオファーを受け、構成を考えているときに「Serverless Frameworkを使えばいいじゃん」と神のお告げが降ってきましたので、触ってみることにしました。

最終目標

スマホから投票できて、最終的にCSVでまとまった投票データを吐き出す

構成はこんな感じ

青枠の部分がServerlessFramework

serverless-img.PNG

大まかな流れ

  1. S3に投票フォームをアップロードしてバケットごとWeb公開する
  2. フォームの投票内容をAPIGateWayに向かってPOSTする
  3. APIGateWayがLambdaをキックする
  4. Lambdaが受け取ったJSONをDynamoDBに書き込む
  5. 完了!

さあ、はじめてみようか

書いていく前に・・・今回ServerlessFrameworkを触るにあたって@hiroshik1985様のとことんサーバーレス①:Serverless Framework入門編を参考にさせていただきました。

ServerlessFrameworkのインストール

npmで入れちゃいます

$ npm install -g serverless

AWS Credentialsを設定する

ServerlessFramework用のIAMを作成して、AWSConfigureに登録します

$ aws configure
AWS Access Key ID [None]: 先ほど作成したIAMのアクセスキー
AWS Secret Access Key [None]: 先ほど作成したIAMのシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: ENTER

プロジェクト作成

serverless用のディレクトリを作成し、そのディレクトリの中で下記のコマンド実行
今回はnode.jsで
※serverlessコマンドはインストール時に用意されるslsエイリアスを使うと便利です

$ sls create --template aws-nodejs --name vote
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.8.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

こんなのが表示されたら成功。なんかかっこいい・・・
これでディレクトリ内にhandler.jsやserverless.ymlが作成される

はじめてのデプロイ

デプロイのコマンドはこちら

$ sls deploy -v

特に怒られれなければデプロイ成功です。AWSのコンソール画面ですでにLambda等が立ち上がっていると思います

Lambdaファンクションを書くよ

Lambdaファンクションは基本的にhandler.jsに書きます。
たとえばこんな風に

handler.js
import AWS from 'aws-sdk'

AWS.config.update({region: 'ap-northeast-1'})

const db = new AWS.DynamoDB.DocumentClient()

export const register = (event, context, callback) => {
    const body = JSON.parse(event.body)
    const params = {
        TableName: "names",
        Key: {
            id: body.employeeNumber
        },
        UpdateExpression: "set #Group1 = :vote1, #Group2 = :vote2, #Group3 = :vote3",
        ExpressionAttributeNames: {
            "#Group1": "voteGroup1",
            "#Group2": "voteGroup2",
            "#Group3": "voteGroup3"
        },
        ExpressionAttributeValues: {
            ":vote1": body.voteGroup1,
            ":vote2": body.voteGroup2,
            ":vote3": body.voteGroup3
        },
        ReturnValues: "UPDATED_NEW"
    }



    try {
        db.update(params, (error, data) => {
            if (error) {
                callback(null, {
                    statusCode: 400,
                    headers:{ "Access-Control-Allow-Origin" : "*" },
                    body: JSON.stringify({message: 'Failed.', error: error, params: params})
                })
            }
            callback(null, {
                statusCode: 200,
                headers:{ "Access-Control-Allow-Origin" : "*" },
                body: JSON.stringify({message: 'Succeeded!', params: params})
            })
        })
    } catch (error) {
        callback(null, {
            statusCode: 400,
            headers:{ "Access-Control-Allow-Origin" : "*" },
            body: JSON.stringify({message: 'Failed.', error: error, params: params})
        })
    }
}

フォームからPOSTメソッドでJSON形式のデータをDynamoDBに格納します
送られてくるJSONデータは{“employeeNumber”: “001”, “voteGroup1”: “1”, “voteGroup2”: “2”, “voteGroup3”: “3”}のような形で送られてくることを想定しています

そのほか設定を書くよ

serverless.ymlにDynamoDBやiamRoleなどの設定を書きます
たとえばこんな風に

serverless.yml

service: vote

provider:
  name: aws
  runtime: nodejs4.3
  stage: dev
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/*"
      Action:
        - "dynamodb:*"

plugins:
  - serverless-webpack

functions:
  register:
    handler: handler.register
    events:
      - http:
         path: names
         method: post
         cors: true

resources:
  Resources:
    hello:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: names
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

これで再度デプロイするとLambdaファンクションが設定され、DynamoDBにテーブルが作成されます

試しに実行だ

デプロイ後に表示されるAPIGateWayのエンドポイントに対してcurlでリクエストを送信します
※「 https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names 」はAPIGateWayのエンドポイントです


curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"employeeNumber": "001", "voteGroup1": "1", "voteGroup2": "2", "voteGroup3": "3"}'  https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names

問題なくリクエストが送信されれば、DynamoDBにテータが格納されていると思います

フォームの作成

あまり手の込んだフォームをコーディングしている時間がなかったので、シンプルにまとめました
CSSは「Material Design Lite」というCSSフレームワークを使用し、AjaxでAPIGatewayのエンドポイントに対してHTTPリクエストを投げています

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>投票アプリ</title>
    <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="vote.js"></script>
    <script type="text/javascript" src="mdl/material.min.js"></script>
    <link rel="stylesheet" href="mdl/material.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="common.css">
</head>
<body>

<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
    <header class="mdl-layout__header">
        <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">投票アプリ</span>
            <!-- Add spacer, to align navigation to the right -->
            <div class="mdl-layout-spacer"></div>
        </div>
    </header>
    <main class="mdl-layout__content">
        <div id="page-content" class="mdl-grid">
            <div class="mdl-cell mdl-cell--12-col">
                社員番号:<br>
                <select name="employee_number" class="employee-number">
                    <option value="">選択してください</option>
                    <option value="001">001</option>
                    <option value="002">002</option>
                    <option value="003">003</option>
                    <option value="004">004</option>
                    <option value="005">005</option>
                    <option value="006">006</option>
                </select>
                <span class="font-red">※必須</span>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                投票するグループを<span class="font-red">3つ</span>選択してください<span class="font-red">※必須</span>
                <br>
                1位:
                <select name="vote_group1" class="vote-group1">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                2位:
                <select name="vote_group2" class="vote-group2">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                3位:
                <select name="vote_group3" class="vote-group3">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                <button id="vote-button"
                        class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">投票
                </button>
            </div>

            <div class="mdl-cell mdl-cell--12-col">
                <textarea id="response" disabled></textarea>
            </div>

        </div>
    </main>
</div>
</body>
</html>
common.css

.font-red {
    color: red;
    font-weight: bold;
    font-size: 16px;
}
vote.js

$(function () {
    $("#response").html("Response Values");

    /**
     * 投票ボタンを押したときの処理
     */
    $("#vote-button").click(function () {
        var url = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names';

        var employeeNumber = $(".employee-number").val();
        if (employeeNumber === "") {
            alert("社員番号は必須です");
            return;
        }

        /**
         * 選択肢の登録数のバリデーション
         */
        var voteGroup = {
            group1: $(".vote-group1").val(),
            group2: $(".vote-group2").val(),
            group3: $(".vote-group3").val()
        };

        var count = 0;
        $.each(voteGroup, function (key, val) {
            if (val !== "") {
                count++;
            }
        });
        if (count < 3) {
            alert("グループを3つ選択してください");
            return
        }

        /**
         * 重複チェック
         */
        if(voteGroup.group1 === voteGroup.group2){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group1 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group2 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }

        var JsonData = {
            employeeNumber: employeeNumber,
            voteGroup1: voteGroup.group1,
            voteGroup2: voteGroup.group2,
            voteGroup3: voteGroup.group3
        };

        alert(JSON.stringify(JsonData));

        $.ajax({
            type: 'post',
            url: url,
            data: JSON.stringify(JsonData),
            contentType: 'application/JSON',
            dataType: 'JSON',
            scriptCharset: 'utf-8',
            success: function (data) {
                window.location.href = "thankYou.html";
            },
            error: function (data) {

                // Error
                alert("error");
                alert(JSON.stringify(data));
                $("#response").html(JSON.stringify(data));
            }
        });
    });

});

これらのファイルをWeb公開したS3バケットにアップロードし、S3のエンドポイントにアクセスし画面を確認します
あとはフォームの項目を入力し、「投票ボタン」をクリックすると・・・
無事成功すればDynamoDBにPOSTしたJSONが追加されています
また要望により、同じ社員番号で投票すると内容を更新できるようにしています

さいごに

いかんせん1週間とはいえ通常業務の合間を縫ってやっていたので、かなり雑な処理になっていると思います(汗)
しかも最後のCSV吐き出しはコンソールから生成するというなんともいけてない感じ・・・
次回いつ使うかわからないけど、改善に励んでいこうとおもっております!

ちなみにServerlessFrameworkで作成した環境を消去するときは下記を実行すればよいみたいです


sls remove

もっといい感じになったらまた記事書き直すんだ・・・

続きを読む

RedashをDockerで起動する(2017年3月版)

やったこと

  • Redashをローカル環境のDockerで起動する
  • ローカル環境(Dockerホスト)でリッスンしているMySQLへ接続する
  • Amazon Athenaへ接続する

TL;DR

とにかくRedashをDockerで起動してみたいんだけど、どうしたらいいの? (これだけだとAmazon Athenaは使えません)

  1. 作業ディレクトリを作って、そこへ移動する
  2. 下にある docker-compose.yml ファイルをコピーして、そのディレクトリへ配置する
  3. docker-compose run --rm server create_db する
  4. docker-compose up -d する
  5. http://localhost:28080 へアクセスする

起動

redashディレクトリを作成して、その下にリポジトリをcloneする。Amazon Athenaを使いたいのでそのためのリポジトリもクローンしている。こんな感じの構成にする。

redash/
    `-- https://github.com/getredash/redash.git (v1.0.0-rc.2)
    `-- https://github.com/getredash/redash-amazon-athena-proxy.git (95a9850)
    `-- docker-compose.yml (作成)

redashリポジトリの docker-compose.production.yml をコピーして docker-compose.yml を作り、変更する。(今回redashリポジトリで必要なのはこのymlファイルだけ)

主な変更点:

  • redashのイメージをbuildするようになっていたが、Docker Hub:redash/redashにイメージが上がっているのでimageを指定
  • ホスト側のポートは重複があるようなら変更
  • スキーマキャッシュの時間を5分に
  • 日付フォーマットを指定
  • Amazon Athenaを使うためにproxyサービスの追加・環境変数の追加
  • Postgresのデータを保存するvolumeは名前付きボリュームに
  • ゲートウェイのアドレスを固定するためにネットワーク設定をする(ゲートウェイへ接続するとホストに接続できる)

結果的にこんな感じになった。

docker-compose.yml
version: '2'
services:
  server:
    image: redash/redash:1.0.0.b2804
    command: server
    depends_on:
      - postgres
      - redis
    ports:
      - "5000:5000"
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: "INFO"
      REDASH_REDIS_URL: "redis://redis:6379/0"
      REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
      REDASH_COOKIE_SECRET: veryverysecret
      REDASH_SCHEMAS_REFRESH_SCHEDULE: 5
      REDASH_DATE_FORMAT: YYYY/MM/DD
      # for Amazon Athena
      # REDASH_ADDITIONAL_QUERY_RUNNERS: redash.query_runner.athena
      # ATHENA_PROXY_URL: http://redash-amazon-athena-proxy:4567/query
  worker:
    image: redash/redash:1.0.0.b2804
    command: scheduler
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: "INFO"
      REDASH_REDIS_URL: "redis://redis:6379/0"
      REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
      QUEUES: "queries,scheduled_queries,celery"
      WORKERS_COUNT: 2
      REDASH_SCHEMAS_REFRESH_SCHEDULE: 5
      REDASH_DATE_FORMAT: YYYY/MM/DD
      # for Amazon Athena
      # REDASH_ADDITIONAL_QUERY_RUNNERS: redash.query_runner.athena
      # ATHENA_PROXY_URL: http://redash-amazon-athena-proxy:4567/query
  redis:
    image: redis:2.8
  postgres:
    image: postgres:9.3
    volumes:
      - postgres-data:/var/lib/postgresql/data
  nginx:
    image: redash/nginx:latest
    ports:
      - "28080:80"
    depends_on:
      - server
    links:
      - server:redash
  # for Amazon Athena
  # redash-amazon-athena-proxy:
  #   build: redash-amazon-athena-proxy
volumes:
  postgres-data: {}
networks:
  default:
    ipam:
      config:
        - subnet: 172.31.0.0/16
          gateway: 172.31.0.1

こうしておいて下記のコマンドを順に実行すると、Redashが起動する。
docker-compose run --rm server create_db
docker-compose up -d

ホスト側のポート28080をwebサーバに割り当てているので、下記URLへアクセスするとRedashの初期画面が開く。
http://localhost:28080

Amazon Athenaへ接続できるようにする

Javaプロキシを立てて接続する方式になっている。上記ymlファイルで # for Amazon Athena とコメントアウトしているところをコメント解除するとAmazon Athenaが使えるようになる。(コメント解除後に再度 docker-compose up -d が必要)

参考にした情報

クエリランナーの有効化の方法とプロキシのURLがわからなくて苦労したので、辿ったファイルをメモしておく。

公式ブログ:

redashリポジトリ:

redash-amazon-athena-proxyリポジトリ:

接続設定(データソースの作成)

次の迷いポイントは接続設定だと思う。

MySQL

ホスト側でリッスンしているMySQLに接続するには、dockerネットワークのゲートウェイを指定する。
Host: 172.31.0.1
他はMySQLの設定に準ずる。

Amazon Athena

Amazon AthenaのテーブルはAWSコンソールなりで別途作成する必要がある。(事前に作成してなくても、接続自体は可能)

Redash側の設定に必要なもの:

  • AWSのアクセスキーID
  • AWSのシークレットアクセスキー
  • 利用するリージョン
  • Amazon Athena用stagingバケット

破棄

起動したRedashを停止するには、
docker-compose down

Postgresに保存したデータを含めて削除するには、
docker-compose down --volumes

環境

試した環境は下記の通り

  • macOS Sierra 10.12.3
  • Docker version 1.13.1
  • docker-compose version 1.11.1

続きを読む