TerraformとDataDogで始めるMonitoring as Code入門

はじめに

この記事は、dwango advent calenderの12日目の記事です!
今年に入ってから、自分の担当しているプロダクトではDataDogを利用してシステムの監視を行なっています。
DataDogを導入したキッカケの一つとして、Terraformで監視設定を構成管理配下に置いてコード化したい!ということがありました。
同じ設定をGUIでぽちぽちするのはなかなかに辛いですし、ドキュメントを書き続けるのも辛いので、すでにAWSのインフラ環境構築で行なっていることと同じようなフローでコード化が行えるのは魅力の一つでした。
ということで、今回は簡単なサンプルコードと共に、TerraformとDataDogで始めるMonitoring as Code入門したいと思います。

事前に必要な作業

  • AWSアカウント、アクセスキー/シークレットキーの準備

    • 1インスタンスぽこっと立ち上げます
  • terraformのインストール
    • 今回は0.11.x系を対象に
    • tfenvが便利
  • DataDogの API Key, Application Keyの払い出し
  • DataDogのslack Integration連携

Terraform DataDog Providerでは何を操作できるのか

2017/12現在、TerraformのDataDog Providerでは以下のリソースの操作を行うことができます。

この記事では、入門ということで、monitorのみ設定します。
コードはこちらにあげてあります。

AWS環境の立ち上げ

  • 1. 上記のリポジトリをgit clone後、下記のようなコマンドでインスタンスに登録するkey_pair用の秘密鍵/公開鍵を作成します
    ※AWS構築時のアクセスキーやプロファイルの設定については割愛します
$ cd aws/
$ ssh-keygen -t rsa -N "" -f batsion
$ mv batsion batsion.pem
  • 2. secret.tfvars.templateをコピーし、作成した公開鍵とagentのインストール時に利用するDataDogのAPI Keysを埋めます
$ cp secret.tfvars.template secret.tfvars
$ vim secret.tfvars
bastion_public_key    = "実際の公開鍵"
datadog_api_key = "実際のAPI Key"
  • 3. terraformを実行し、VPC作成〜インスタンス作成まで行います(apply時にapproveを求められるのでyesを入力
# terraform provider取り込み
$ terraform init
# plan実行
$ terraform plan  -var-file=secret.tfvars
# apply実行
$ terraform apply -var-file=secret.tfvars

以上で監視対象のインスタンスが作成されました。
追加されるとこんな感じにDataDogの方に現れます。
スクリーンショット 2017-12-12 1.49.40.png

DataDogの監視設定追加

さて、続けてmonitor設定の追加を行います。

  • 1. secret.tfvars.templateをコピーし、DataDogのAPI Keys, Application Keysを埋めます
$ cp secret.tfvars.template secret.tfvars
$ vim secret.tfvars
datadog_api_key = "実際のAPI Key"
datadog_app_key = "実際のApplication Key"
  • 2. terraformを実行し、monitor作成まで行います(AWSの時同様にapply時にapproveを求められるのでyesを入力
    bash
    # terraform provider取り込み
    $ terraform init
    # plan実行
    $ terraform plan -var-file=secret.tfvars
    # apply実行
    $ terraform apply -var-file=secret.tfvars

以上でmonitor設定の追加が行われました。
今回はsystem.cpu.user(インスタンスのCPU usertime)の5分平均が50%以上であればwarnnig、60%以上であればcriticalとし、事前に設定したslackチャンネルに通知するようにしています。
これらは、variable.tf にてデフォルト値が設定指定あるので、変更したい場合は適宜変更してみてください。
※例えば下記のように

datadog_monitor_slack_channel = "slack-system-alert"
datadog_monitor_cpu_usertime_thresholds_warning = "60"
datadog_monitor_cpu_usertime_thresholds_critical = "70"

アラートテストを行う

さて、監視がうまくいってるかどうか確認、ということで作成したインスタンスにログインし、インスタンスに負荷を適当にかけてみます
※デフォルトのSecurity Groupでは、サンプルということでどこからでもSSHが可能なようにしているため、batsion_ingress_cidr_blocksの値を適宜変更すると安全かもしれません

# ログイン
$ ssh -i bastion.pem ec2-user@[インスタンス EIP]
# 負荷をかける
$ yes >> /dev/null &

上記を実施後、しばらくすると下記のようにアラートが飛んできます!
スクリーンショット 2017-12-12 1.57.16.png

ということで、yesコマンドを停止し、復旧通知を確認して終了です。おつかれさまでした。
スクリーンショット 2017-12-12 2.11.52.png

なお、作成したインスタンスはterraform destroy -var-file=secret.tfvarsを実行することで削除可能です。

終わりに

簡単でしたが、Monitoring as Code入門でした。
DataDogには、今回のような簡単な監視だけでなく、他にも様々なメトリクスアラートやもっと高度な機械学習型のアラートが存在するので、よりうまい具合に活用しつつ、Monitoring as Codeを推し進めていきたいな、と思います。

続きを読む

中途入社のAWSエンジニアが、稼働中サービスの運用状況をキャッチアップするために意識すること

Classiアドベントカレンダー11日目です。
今回は、AWSエンジニアが稼働中のAWSの管理アカウントを渡されて、ビクビクしながらキャッチアップを行っていったときのメモになります。

1.TL;DR

AWSアカウントのログイン前に準備できることもあるし、AWSアカウントにログインしてわかることもあるし、サーバーにログインしてわかることもある。それぞれのレイヤーでどういったことを確認するかを意識しながらキャッチアップを進めていきましょう。

2.AWSアカウントログイン前の事前準備

pre_aws.png

サービスが稼働しているのであれば、AWSアカウントにログインせずとも、たくさんの情報をキャッチアップすることができます。1日目から何らかの大きなアウトプットを出さないと解雇するような会社は、(おそらく)存在しない筈です。まずは落ち着きましょう^^;

2-1.ドキュメント読み込み

サービスのインフラにAWSが使われることが多くなったからといって、入社前に経験したAWS運用フローがそのまま活かせる訳ではありません。まずは、前任者や運用中のドキュメント管理ツールの中から、今までどのような運用を行っていたかを確認します。
ドキュメントを見たときに意識する観点としては、

  • フロー型:時間による鮮度の劣化があるドキュメント
  • ストック型:システム仕様など、メンテナンスが求められるドキュメント

どちらの情報であるかを意識して読むことが重要です。
フロー型の情報は、障害などで一時的な対応用にメモっていることもあり、運用の中で解決済みのことがあります。そのため、ストック型のドキュメントを中心に見ることが素早いキャッチアップになると思います。
とはいえ、ドキュメントの全てがメンテナンスされている会社というのは稀だと思いますので、各種ドキュメントを見ながら、仮説程度に自分なりのシステム構成図などを書いてみましょう。
要件定義書や各種構成図の変更履歴、課題管理表、リスクコントロール表といったドキュメント類があるのであれば、目を通しておきましょう。

2-2.運用フローを観察する

サービス側のドキュメントについては、まだ文書化されてることが多いですが、運用系ツールに関しては、ドキュメント化されていないことがあります。今の開発スタイルであれば、何らかのチャットツール(Slack,ChatWork,HipChat)上で、

  • デプロイ
  • 各種の通知
  • 運用Bot

の運用といったことが行われていると思います。また、チャットだけでなく、メールでの運用フローも存在しているかもしれません。
こうした運用系ツールの存在は、今後自分がリファクタするときに、「必須要件ではないが、重宝している」ということで、「リファクタ後にも、あの機能を実装して欲しい」といった声が社内から上がると思います。
そのため、このような運用フローがどこで実装されているかを見極めることが重要になってきます。

2-3.インフラ部分のコード読み

「俺はフルスタックエンジニアだ!」という強い意思がある方は、この時点で稼働中のアプリ側のコードまで読み込んでいただければよいですが、まずは入社前に期待されたであろう、インフラまわりのコード化部分を把握しておきましょう。どのみち、いずれはメンテナンスを任されたり、質問されることが増えてきますので、自分のメンテナンスする部分を優先的に確認しておきましょう。
実サーバーの運用はAWSに任せているので、ここで意識しておくことは、

  • Infrastructure Orchestration Tools:Terraform, CloudFormationなど
  • Server Configuration Tools:Chef,Ansible,Itamaeなど

あたりのコードがgithubなどに保存されているからといって、メンテナンスされていない可能性もあります。
コードの設計方針などを確認するのは当然必要なのですが、コードの変更履歴が年単位で放置されていないかどうかも見ておきましょう。特に、AWS関連のコードについては、担当する人がアプリ側よりも少ないので、構築当初のコードのままなのか、運用されているコードなのかはPRなどで確認しておいた方がよいです。

3.AWSのアカウント内を調査する

aws_kansatsu.png

実際にAWSアカウントにログインしたり、APIで各種設定を確認していきます。Web系サービスであれば、TCP/IPモデルやC/Sモデルを意識しながら、下層レイヤー回りから調査していき、ネットワークがどうせ設定されているかを確認していきます。
おそらく、ここで多くの疑問(場合によっては、絶望)が生まれる段階です。「あれ?ドキュメントにこう記述されているけど、設定上は違うような…」という沼にハマることがあるでしょう。負けないでください、一人で抱え込まずに闇を共有できる仲間を見つけましょう。

3-1.外部システム連携の確認

関連するAWSサービス

VPC関連のサービスを中心に、自AWSアカウント以外の連携がないかの確認を行います。

関連しやすいAWSサービス例)

  • DirectConnect
  • NAT Gateway
  • Peering Connection
  • Customer Gateways
  • Virtual Private Gateways
  • VPN Connections
  • NetWorkACL
  • SecurityGroup
  • EIP

などに、何らかのインスタンスが稼働していて、productionやhonbanなどの文言がついたものがあれば、それはドキュメント上には存在しないが、サービス上何らかの理由で稼働しているものである可能性があります。
自社のサービスがAWSアカウント内だけで完結しているのであればよいのですが、誤ってここのインスタンスなどを削除すると、場合によってはシステム復旧できないぐらいの痛手になるので、慎重に確認していきましょう。
特に、SecurityGroupは、最近でこそInboundルールにDescriptionをつけられるようになりましたが、数年運用されているシステムには、何で利用しているIPアドレスなのかがわからないことがあるので、設定確認中に不明なIPアドレスを見つけたら社内で有識者に聞いてみましょう。

3-2.システム導線の確認

関連するAWSサービス

インスタンス障害があるとユーザー影響がありそうな、システム導線を追っていきます。

関連しやすいAWSサービス例)

  • ELB(CLB,ALB,NLB)
  • CloudFront
  • EC2
  • ElasticCache(redis,memcached)
  • RDS(Aurora,MySQL,PostgreSQL,SQLServer)
  • ElasticSearch
  • DynamoDB
  • S3

各種のインスタンスサイズが適切かを確認するのはもちろんですが、DB関連についてはバックアップ関連の設定がちゃんと行われているかどうかも確認しておきましょう。バックアップウィンドウの世代数やメンテナンスウィンドウの時間が営業時間内になっているとかは、結構ありがちな設定漏れケースになります。パラメータストアの設定については、本番で稼働している設定が正義なので、設計と違う場合は、社内で経緯を追ってみましょう。

3-3.運用導線の確認

関連するAWSサービス

直接のユーザー影響はないものの、バッチ系およびログインやログ連携など、システム運用で必要な運用導線を追っていきます。

関連しやすいAWSサービス例)

  • EC2
  • Lambda
  • ElasticSearch(& kibana)
  • IAM
  • CloudTrail
  • AWS Config
  • CloudWatch Logs
  • S3
  • Glacier

24224というポート開放を見れば、そのシステムはfluentd関連のフローがあるのはほぼ確定なので、ログの発生から可視化ツールおよびバックアップのフローまで追っていきましょう。また、バッチ系のEC2に関しては、最近のAWSだと、FargateやECS、Lambdaなどで定期バッチを行うことも可能なので、単一障害点をなくすことができないか、今後の計画のために、バッチ系が整理されているドキュメントなどを探してみましょう。

4.サーバー内の設定を確認する

server_chosa.png

最近だと、Server Configuration Toolsが大分普及していたり、コンテナ系の運用が発達してきているので、このあたりのキャッチアップ期間が少なくなるのかなと思います。とはいえ、SSH接続を頻繁に行うサーバーや起動時間が長いサーバーだと、コードの設定と異なる部分が出てきていることがあるかもしれません。
OSの設定やミドルウェアのバージョンなど、SSH接続すると確認した方がよいところは多々ありますが、Server Configuration Toolsの設定と異なっていたり、運用中のアラート設定などで差異がでやすそうな部分を以下に記載します。

4-1.各種メトリクス確認

メモリやプロセスの状況は、通常CloudWatchだけではわからないので、MackerelやZABBIXなどの監視ツールエージェントを入れているのであれば、各サーバーのメトリクスを確認しておきましょう。

4-2.稼働プロセスの確認

pstreeなどで、稼働しているプロセスを確認します。SSH接続が禁止されているのであれば、AWSのSSMエージェントなりで確認できないかを検討してみましょう。設計上のソフトウェアスタックに存在しないプロセスが常駐している場合は、何のエージェントが動いているかを追っておきましょう。

4-3.不要なファイルが出力されていないかの確認

ログレベルがデバッグのままだったり、ログファイルのローテートがなされていない場合があり、アラートは上がっていないけど、サーバー内のリソースを侵食しているときがあります。また、生成されるログファイルが小さすぎると、ディスクに余裕がありそうに見えても、inodeが先に枯渇するときもあります。lsofdf -iなどを可視化するなどして、サーバー内のディスク状況を確認しておきましょう。

4-4.同期処理と非同期処理のプロセス確認

同期処理のプロセスは意識しやすいので、監視対象に入っている可能性が高いです。ただ、非同期系プロセス(Rubyだとsidekiq,Pythonだとcelery,PHPだとphp-resqueなど)が監視対象に入っていないこともあるので、どのサーバーで非同期処理が行われているかを把握しておきましょう。

5.まとめ

AWSや他のパブリッククラウドが全盛になった今でも、3層アーキテクチャのシステム構成やOSI7階層などのレイヤーを意識しながらキャッチアップを行うと、システムを俯瞰しながらキャッチアップを進めることができるのではないかなと思います。とはいえ、前任者がコード化しきれなかった部分もある筈なので、そこは社内で過去経緯を知っている人に笑顔で質問をしてみましょう。技術が発達しても、人に蓄積されるノウハウはまだまだ多いので…
AWSエンジニアが転職する際などのご参考になれば。

続きを読む

AutoScalingGroupのインスタンス起動・停止をSlackに通知する

AutoScalingGroupの作成からSNSとLabmdaと連携してイベントをSlackに通知するところまで。

LaunchConfigurationの作成

AutoScalingGroupingを作るためにはLaunchTemplate, LaunchConfigurationNameまたはInstanceIdが必要なので今回はLaunchConfigurationを作成します。

$ aws autoscaling create-launch-configuration 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --instance-type t2.micro 
  --image-id ami-bf4193c7
  --key-name xxxxx

AutoScalingGroupの作成

先ほど作成したLaunchConfigurationを指定してAutoScalingGroupを作成します。

$ aws autoscaling create-auto-scaling-group 
  --auto-scaling-group-name sample-auto-scale 
  --min-size 1 --max-size 5 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --availability-zones us-west-2a us-west-2b

インスタンスの起動・停止を通知する

AutoScalingGroupからインスタンスのイベントを受け取る方法は LifeCycleHookでもできそうですが、今回はSNSを使った方法でやってみます。

SNSのトピックを作成する

AutoScalingGroupのイベントを通知するためのTopicを作成します。

$ aws sns create-topic --name sample-auto-scale-notification

AutoScalingGroupのイベントをSNSと紐付ける

NotificationConfigurationを追加します。
今回は以下の4つのイベントをSNSで通知するようにしました。

  • EC2_INSTANCE_LAUNCH
  • EC2_INSTANCE_LAUNCH_ERROR
  • EC2_INSTANCE_TERMINATE
  • EC2_INSTANCE_TERMINATE_ERROR
$ aws autoscaling put-notification-configuration 
  --auto-scaling-group-name sample-auto-scale 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --notification-types "autoscaling:EC2_INSTANCE_LAUNCH" "autoscaling:EC2_INSTANCE_LAUNCH_ERROR" "autoscaling:EC2_INSTANCE_TERMINATE" "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"

SNSからSlackに通知するLambdaを作成する

pythonでSlackへ通知するLambdaを作成していきます。

sns-to-slack-notificatioin-lambda.png

import urllib.request
import json

def lambda_handler(event, context):
    url = 'https://hooks.slack.com/services/AAAAAAAAA/BBBBBBBBB/CCCCCCCCCCCCCCCCCCCCCCCC'
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    obj = {"text":json.dumps(event)}
    json_data = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(url, data=json_data, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")

    return response_body

LambdaにSNSをsubscribeさせる

$ aws sns subscribe 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --protocol lambda 
  --notification-endpoint arn:aws:lambda:us-west-2:999999999999:function:sns-to-slack-notification

SNSがLambdaを起動できるようにpermissionを与えます。

$ aws lambda add-permission 
  --function-name sns-to-slack-notification 
  --statement-id autoscaling-sns-to-lambda 
  --action "lambda:InvokeFunction" 
  --principal sns.amazonaws.com 
  --source-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification

インスタンスを追加して通知を受け取る

AutoScalingGroup -> SNS -> Lambdaの設定ができたので実際にインスタンスを追加して通知を確認します。
desired-capacityを増やし、インスタンスを1つ起動します。

$ aws autoscaling set-desired-capacity 
  --auto-scaling-group-name sample-auto-scale 
  --desired-capacity 2

インスタンスが起動されSlackに通知がきました。

スクリーンショット 2017-12-10 14.50.32.png

続きを読む

CodeBuildの実行結果をslackに通知する

はじめに

Globis Advent Calendar10日目は、弊社のエンジニアチームを支えるインフラ技術をお伝えいたします。
開発スピードをさらに促進するために、Blue Green Deployment, Infrastrucure as Code, ChatOps など新しい運用思想を積極的に取り入れて、手動でのオペレーションを極力なくしています。
今回は、弊社で実運用しているChatOpsの一例として、CodeBuildの実行結果をslackに通知する方法を、可能な範囲で具体的にお伝えいたします。

設定方法

AWS Lambdaの設定

コード

import os
import json
import urllib.request

URL = os.environ['WEBHOOK_URL']


def send_slack(obj):  ## slackに通知
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    js = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(URL, data=js, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")


def check_status(status):
    fail = False
    if status == 'IN_PROGRESS':
        color = '#008000'  ## green
    elif status == 'SUCCEEDED':
        color = '#00BFFF'  ## deepskyblue
    else:
        color = '#FF00FF'  ## magenta
        fail = True
    return color, fail


def parse_event(event):  ## cloudwatch event から渡された event(dict型)をパース、slackに渡すobjectを生成。
    detail = event['detail']
    status = detail['build-status']
    initiator = detail['additional-information']['initiator']
    log = detail.get('additional-information', {}).get('logs', {}).get('deep-link', 'Not Exist')
    color, fail = check_status(status)
    fields = [{'title': 'Initiator', 'value': initiator, 'short': False},
              {'title': 'Status', 'value': status, 'short': False},
              {'title': 'LogLink', 'value': log, 'short': False}]
    obj = {'attachments': [{'color': color, 'fields': fields}]}
#    if fail == True:  ## Fail時にチャンネルメンション飛ばしたい時はコメントを外す。
#        obj['attachments'][0]['text'] = '<!channel>'
    return obj


def lambda_handler(event, context):  ## lambdaから最初にcallされる関数。
    obj = parse_event(event)
    send_slack(obj)
    return

環境変数の設定

CloudWatch Event の設定

slackでの通知結果

  • ワンクリックでビルドログを眺めることができます。

運用事例のご紹介

  • AWS Athenaのテーブルにクエリを実行し、中間テーブルを生成するETL処理のバッチ
  • Ruby on Rails アプリケーションデプロイ時の db:migrate 処理
  • 最近はCodeBuildをVPC内で実行できるようになったので、利用できる幅が広がっています!

おわりに

いかがでしたでしょうか。python初心者(僕)でもChatOpsに貢献できます。
グロービスではSREエンジニアを募集しています!
インフラの運用担当だけど手作業を自動化したいと考えている方、開発者だけどインフラも含めてコードで管理したい方、一緒に理想のインフラを作ってみませんか?

続きを読む

Serverlessで日々のAWSコストを算出する

最近、Cost Explorer APIというものが新しくリリースされ、請求ダッシュボードから見られるコストが取得できるようになりました。
Introducing the AWS Cost Explorer API

これを使用し、Serverless FrameworkでEC2やCloudFrontなど各サービス毎のコストを算出してみます。

serverless.yml

serverless.yml
service: cost-checker

provider:
  name: aws
  runtime: nodejs6.10
  region: us-east-1

  iamRoleStatements:
    - Effect: Allow
      Action:
        - ce:GetCostAndUsage
      Resource: "*"

functions:
  costCheck:
    handler: cost.check
    events:
      - schedule: cron(0 1 * * ? *)
    timeout: 300

Cost Explorer API はバージニア北部でしか使えないため region: us-east-1 を指定しdeployします。
cronはUTC表記のため、今回は日本時間の朝10時にCloudWatchEventsでLambdaが実行されます。

Cost Explorer API

cost.js
const AWS = require('aws-sdk');

const costexplorer = new AWS.CostExplorer();

module.exports.check = () => {
  const params = {
    Granularity: 'DAILY',
    Metrics: [ 'UnblendedCost' ],
    GroupBy: [{
      Type: 'DIMENSION',
      Key: 'SERVICE',
    }],
    TimePeriod: {
      Start: '2017-12-07',
      End: '2017-12-08',
    },
  };

  costexplorer.getCostAndUsage(params, (err, data) => {
    if (err) {
      console.error(err, err.stack);
      return;
    }

    console.log(data);    
  });
};

これだけで2017-12-07の各コストが取得できます。
細かいAPIの使い方はこちら
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CostExplorer.html

動的に日付を取得する

これだけでは、2017-12-07の分しか取得できないため、TimePeriodを動的に指定できるようにします。

const _ = require('lodash');

const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());

const TimePeriod = {
  Start: `${start.getFullYear()}-${_.padStart(start.getMonth() + 1, 2, 0)}-${_.padStart(start.getDate(), 2, 0)}`,
  End: `${end.getFullYear()}-${_.padStart(end.getMonth() + 1, 2, 0)}-${_.padStart(end.getDate(), 2, 0)}`,
};

now.getDate() - 1で実行日の前日を取得、この方法であれば月をまたいだときも日付を自動で生成してくれます。
また、lodashを使い_.padStart()で1桁の場合でも簡単に0で埋めることができます。

Slackに送る

かなり雑ですが、costexplorer.getCostAndUsage()で取得した値を、Slackのmessage apiのattachments.fieldsに詰めて送信しています。

const costs = [];
_.each(data.ResultsByTime, (item) => {
  _.each(item.Groups, (group) => {
    costs.push({
      serviceName: _.head(group.Keys),
      value: _.round(group.Metrics.UnblendedCost.Amount * 120, 2),
    });
  });
});

const message = {
  username: 'コストさん',
  channel: '#cost',
  text: `${TimePeriod.Start}のコストです。:money_with_wings:`,
  attachments: [
    {
      fields: _.map(costs, (item) => {
        return {
          title: item.serviceName,
          value: item.value,
          short: true,
        }
      });,
    }
  ],
};

slack.post(message);

金額はドルで返ってくるので、1ドル120円換算にして、小数点2位まで表示

Slack上では、こんな感じで投稿されて便利です。
sc.png

おわり

実際のところ、デイリーでコストを出しても毎日のアクセス数の上下で変わってくるので、よくわからないw
でも、一気にコストを削ったときにこうして見られるのは楽しい。一時的に使ったサーバーを消し忘れたとか、設定のミスとかもこういったレポートで、すぐに気がつけるんじゃないかなーと思います。
何よりメンバーにコスト感が生まれるし、どのAWSサービス使ってるかわかって良い。

Cost Explorer APIは1コール$0.01と結構お高い額を請求されます。
基本的に乱打するものではないと思いますが注意!

続きを読む

CloudWatchでエラーログの内容を通知させたい

前置き

CloudWatch Logsの収集対象としているログにErrorという文字列が出力されたらSNSで通知したい。
CloudWatch Logsのロググループにメトリクスフィルタを設定し、このメトリクスが所定のしきい値を超えたらSNSへ連携するアクションをCloudWatch Alarmとして設定すれば実現できる。しかしこの方法だとErrorという文字列が出力されたことは認識できるが、ログの内容までは通知されない。

CloudWatch Alarm

まず、CloudWatch AlarmからSNSに渡される情報を見てみよう。

message.json
{
    "AlarmName": "sample-error",
    "AlarmDescription": "sampleでエラーが発生しました。",
    "AWSAccountId": "xxxxxxxxxxxx",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold Crossed: 1 datapoint [2.0 (29/11/17 01:09:00)] was greater than or equal to the threshold (1.0).",
    "StateChangeTime": "2017-11-29T01:10:32.907+0000",
    "Region": "Asia Pacific (Tokyo)",
    "OldStateValue": "OK",
    "Trigger": {
        "MetricName": "sample-metric",
        "Namespace": "LogMetrics",
        "StatisticType": "Statistic",
        "Statistic": "SUM",
        "Unit": null,
        "Dimensions": [],
        "Period": 60,
        "EvaluationPeriods": 1,
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Threshold": 1,
        "TreatMissingData": "- TreatMissingData:                    NonBreaching",
        "EvaluateLowSampleCountPercentile": ""
    }
}

たしかに、ログの内容は全く含まれていない。その代わりTrigger内にこのアラームに紐づくメトリクスの情報が含まれている。これを使って欲しい情報を辿っていく。

メトリクスフィルタ

MetricNameNamespaceを指定すればメトリクスフィルタの情報を取得することができる。

sample.py
        logs = boto3.client('logs')

        metricfilters = logs.describe_metric_filters(
            metricName = message['Trigger']['MetricName'] ,
            metricNamespace = message['Trigger']['Namespace']
        )

取得したメトリクスフィルタはこんなかんじ。

metricsfilters.json
{
  "metricFilters": [
    {
      "filterName": "sample-filter",
      "filterPattern": "Error",
      "metricTransformations": [
        {
          "metricName": "sample-metric",
          "metricNamespace": "LogMetrics",
          "metricValue": "1"
        }
      ],
      "creationTime": 1493029160596,
      "logGroupName": "sample-loggroup"
    }
  ],
  "ResponseMetadata": {
    "RequestId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "x-amzn-requestid": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "content-type": "application/x-amz-json-1.1",
      "content-length": "210",
      "date": "Wed, 29 Nov 2017 01:10:33 GMT"
    },
    "RetryAttempts": 0
  }
}

filterPatternlogGroupNameが取得できる。これがあればCloudWatch Logsからログイベントを抽出できそうだ。

開始時刻と終了時刻

ログデータを抽出するためにはfilterPatternlogGroupNameのほかに、フィルタリング対象の開始時刻と終了時刻を指定したい。この時刻はUNIX Timeである必要がある。こちらのサイトを参考にさせていただく。

sample.py
        #ログストリームの抽出対象時刻をUNIXタイムに変換(取得期間は TIME_FROM_MIN 分前以降)
        #終了時刻はアラーム発生時刻の1分後
        timeto = datetime.datetime.strptime(message['StateChangeTime'][:19] ,'%Y-%m-%dT%H:%M:%S') + datetime.timedelta(minutes=1)
        u_to = calendar.timegm(timeto.utctimetuple()) * 1000
        #開始時刻は終了時刻のTIME_FROM_MIN分前
        timefrom = timeto - datetime.timedelta(minutes=TIME_FROM_MIN)
        u_from = calendar.timegm(timefrom.utctimetuple()) * 1000

ログイベントの取得

これで材料が揃った。CloudWatch Logsからログイベントを取得する。

sample.py
        response = logs.filter_log_events(
            logGroupName = loggroupname ,
            filterPattern = filterpattern,
            startTime = u_from,
            endTime = u_to,
            limit = OUTPUT_LIMIT
        )

responseはこんなかんじ。

response.json
{
  "events": [
    {
      "logStreamName": "sample-stream",
      "timestamp": 1511942974313,
      "message": "Errorが発生しました。sample messageです。",
      "ingestionTime": 1510943004111,
      "eventId": "11111111111111111111111111111111111111111111111111111112"
    },
    {
      "logStreamName": "sample-stream",
      "timestamp": 1511942974443,
      "message": "またまたErrorが発生しました。sample messageです。",
      "ingestionTime": 1510943004111,
      "eventId": "11111111111111111111111111111111111111111111111111111112"
    }
  ],
  "searchedLogStreams": [
    {
      "logStreamName": "sample-stream",
      "searchedCompletely": true
    }
  ],
  "ResponseMetadata": {
    "RequestId": "xxxxxxxxxxxxxxxxxxxxxx",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "x-amzn-requestid": "xxxxxxxxxxxxxxxxxxxxxx",
      "content-type": "application/x-amz-json-1.1",
      "content-length": "1000",
      "date": "Wed, 29 Nov 2017 01:10:33 GMT"
    },
    "RetryAttempts": 0
  }
}

フィルタ条件にマッチする複数件のログイベントを取得できる。

メッセージの整形

仕上げに、通知する文面の整形を行う。またまた先程のサイトを参考にさせていただく。

sample.py
        #メッセージの整形
        log_message = u""
        for e in response['events']:
            #UNIX時刻をUTCへ変換後、日本時間に変更している
            date = datetime.datetime.fromtimestamp(int(str(e['timestamp'])[:10])) + datetime.timedelta(hours=9)
            log_message = log_message + 'n' + str(date) + ' : ' + e['message']

        #SNSのタイトル、本文
        title = message['NewStateValue'] + " : " + message['AlarmName']
        sns_message = message['AlarmDescription'] + 'n' + log_message

最終的な構成

CloudWatch Alarm -> SNS -> Lambda -> SNS
一つ目のSNSはLambdaファンクションを呼び出すためのもの。二つ目のSNSはいい感じに整形した文面を関係者へ通知するためのもの。

スクリプト

サンプルとして拙いスクリプトを挙げておく。例外処理がテキトウなのはご愛嬌。

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import json
import datetime
import calendar

#通知先SNSトピックのARN
TOPIC_ARN = "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:topic-name"
#抽出するログデータの最大件数
OUTPUT_LIMIT=5
#何分前までを抽出対象期間とするか
TIME_FROM_MIN=10

sns = boto3.client('sns')

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])

    #SNSフォーマットの作成
    try:
        #CloudWatchのリージョン情報は message['Region'] に含まれる。
        #もしLambdaのリージョンと異なる場合は考慮が必要だが、ここでは同一リージョンを想定する。
        #(逆に言うと複数リージョンのCloudWatchAlarmを単一リージョンのLambdaが捌くことも可能)
        logs = boto3.client('logs')
        #logs = boto3.client('logs',region_name='xxxxxxxx') #←リージョンを指定する場合

        #MetricNameとNamespaceをキーにメトリクスフィルタの情報を取得する。
        metricfilters = logs.describe_metric_filters(
            metricName = message['Trigger']['MetricName'] ,
            metricNamespace = message['Trigger']['Namespace']
        )

        #ログストリームの抽出対象時刻をUNIXタイムに変換(取得期間は TIME_FROM_MIN 分前以降)
        #終了時刻はアラーム発生時刻の1分後
        timeto = datetime.datetime.strptime(message['StateChangeTime'][:19] ,'%Y-%m-%dT%H:%M:%S') + datetime.timedelta(minutes=1)
        u_to = calendar.timegm(timeto.utctimetuple()) * 1000
        #開始時刻は終了時刻のTIME_FROM_MIN分前
        timefrom = timeto - datetime.timedelta(minutes=TIME_FROM_MIN)
        u_from = calendar.timegm(timefrom.utctimetuple()) * 1000

        #ログストリームからログデータを取得
        response = logs.filter_log_events(
            logGroupName = metricfilters['metricFilters'][0]['logGroupName'] ,
            filterPattern = metricfilters['metricFilters'][0]['filterPattern'],
            startTime = u_from,
            endTime = u_to,
            limit = OUTPUT_LIMIT
        )

        #メッセージの整形
        log_message = u""
        for e in response['events']:
            #UNIX時刻をUTCへ変換後、日本時間に変更している
            date = datetime.datetime.fromtimestamp(int(str(e['timestamp'])[:10])) + datetime.timedelta(hours=9)
            log_message = log_message + 'n' + str(date) + ' : ' + e['message']

        #SNSのタイトル、本文整形
        title = message['NewStateValue'] + " : " + message['AlarmName']
        sns_message = message['AlarmDescription'] + 'n' + log_message

    except Exception as e:
        print(e)
        sns_message = message
        title = "error"

    #SNS Publish
    try:
        response = sns.publish(
            TopicArn = TOPIC_ARN,
            Message = sns_message,
            Subject = title
        )

    except Exception as e:
        print(e)
        raise e

参考文献

続きを読む

FINTECH – アマゾン ウェブ サービス (AWS)

Google Homeで受付システムを作ってみました。 東京の本社にはiPadを使った 受付システム があるのですが、大阪支店は諸事情により導入が遅れています。。。 今回は、スプレッドシートに来訪者情報を記録し、その情報を検知するとSlackに通知が行くようにしました。 現状 大阪支店は紙とペンと呼び鈴の超アナログ仕様 … 続きを読む

サービス稼働中のままSQL Serverの領域を拡張/縮小させる

この記事はSilbird Advent Calendar 2017 8日目の記事となります。

弊社では、稼働中のサービスの永続化データ格納先としてAmazon RDS for SQL Serverを利用しています。
その中で経験したDB領域の拡張と縮小について、大きな2つのトラブル事例とその対応内容をご紹介しようと思います。
DB領域の拡張、縮小はサービスを一時的止める(メンテナンスに入れる)た状態でないとできないと思われがちですが、サービス稼働中のまま実行することができます。

[事例1] DB自動拡張中に応答停止

1つめはDBの自動拡張についてです。
DBの初期容量は、想定ユーザー数やアクセス数をもとにある程度余裕を持って見積もっていると思います。しかし、Webサービスの世界ではその見積もりどおりにユーザーが増えていくとは限りません。サービス運営者としては嬉しい悲鳴ですが、ユーザー数・滞在時間の増加によりデータが見積もり以上に容量が増加してしまうケースがあります。

SQL Serverでは初期割り当て時の容量を超えてしまった場合に、領域を自動拡張する機能がデフォルトで有効になっています。しかし、この自動拡張が動いたときに、サービスが停止するトラブルが発生してしまいました。

DB自動拡張のデフォルト設定

下図は、SQL ServerでDBを新規作成しようとしたケースで、初期サイズとして100GBを割り当てています。そして、「自動拡張/最大サイズ」が「10%単位で無制限」となっているのがわかります。
なので、データが初期サイズの100GBを超えた場合に、自動で10GBの領域が自動で作成され、DBの領域は110GBとなります。
image.png

この「10%単位で無制限」というデフォルト設定が曲者で、自動拡張中にサービスからのクエリ要求が応答しない状態になってしまいました。
MSのサポートサイトにも記載がありますが、自動拡張中はトランザクションが停止するようです。実際、SQL Serverが10GBの領域を拡張している間クエリがタイムアウトしていました。

[INF] SQL Server における自動拡張および自動圧縮の構成に関する注意事項

DB自動拡張設定の変更

データは日々拡張していき初期サイズに収まらなくなると、SQL Serverの自動拡張に頼らざるを得ません。サービスをメンテナンスに入れて一気に拡張する方法もありますが、メンテナンスに入れることなく自動拡張の設定を変更することで対応しました。

自動拡張の設定で「自動拡張/最大サイズ」を「100MBで無制限」 とすることで、自動拡張にかかる時間を1秒未満に抑え、サービスへの影響を最小限とすることができました。
image.png

自動拡張の発生履歴

SQL Server Management Studioから直近のDB自動拡張履歴は確認することができます。
DB名を右クリック – レポート – 標準レポート – ディスク使用量 から、現在のディスクの利用状況とともに自動拡張イベント(開始時刻、実行時間、変更後のフィアルサイズ)を確認することができます。

ディスク使用量の概要レポート

自動拡張についてまとめ

  • SQL Serverのデフォルトの自動拡張は「10%単位で無制限」、自動拡張中はトランザクションが停止する
  • 初期サイズが大きいと自動拡張サイズが大きくなり、サービスのダウンタイムが発生する可能性が高くなる
  • 自動拡張の設定を割合(%)から絶対値(MB)とすることで、拡張にかかる時間・サービスへの影響を最小限とすることができる

[事例2] 自動拡張でディスクを圧迫し、拡張不可に

サービスの展開、自動拡張の結果

サービスを複数のプラットフォームに展開し、それに伴ってDBの数を増やし自動拡張を続けた結果、RDSインスタンス作成時に確保したディスク容量を全て使い果たしてしまいました。その時のAWSコンソールから見たときのRDSインスタンスの状況です。(Storage 1MB…)

image.png

データ・トランザクションログ領域ともにこれ以上自動拡張ができない状態になってしまい、特定のDBのデータ更新クエリが全てエラーになる状態になってしまいました。

Amazon RDS for SQL Server の制約

AWS上のクラウドサービスのため、ディスク容量を拡張することで対応できると思われるかもしれませんが、2017年12月時点 RDS for SQL Serverではインスタンス作成時に割り当てたディスク容量を拡張することができません。
そのため、別のインスタンスを作成して移行するか、削除可能なデータがあれば削除して対応する必要があります。

delete文、truncate文がエラーでデータを削除できず

対応にあたってまずやろうとしたことは、いつか使うだろうと思ってため続けていた履歴データの削除です。不要な(サービス稼働に必須ではない)履歴データを削除しようとクエリを実行しましたが、データの削除にもトランザクションログを作成する必要があり、delete, truncate文ともに実行エラーとなってしまいました。
不要なデータの削除はいったん諦めることにしました。

image.png

DB領域の縮小

自動拡張を続けてしまったDBがある一方、初期に割り当てた容量を使い果たしていないDBもありました。そのため、空き領域のあるDBを縮小することで拡張用の領域の確保を試みました。

DBを縮小するコマンドは以下の通りです。

USE [db_name]
DBCC SHRINKFILE (N'db_file_name' , 99000)

コマンドは、DBを右クリック – タスク – 圧縮 – ファイル より
「未使用領域の解放前にページを再構成する」にチェックを入れ、圧縮後のファイルサイズを指定して
「スクリプト」から出力することもできます

shrink.png

このときの注意点としては

ファイルの圧縮は小さい単位(例: 1GB)で複数回実行することです。
一度に大量の圧縮を行うとDBが応答しなくなり、サービスが停止します。例えば150GBのDBを120GBへ圧縮したい場合は 149→148→147・・・ と細かく実行する必要があります。
自動拡張時の苦い教訓があることと、圧縮するDBのサービスは正常に稼働中であったため、細かく圧縮を行いました。圧縮を行うことで、ディスクに空きが生まれ自動拡張が実行されました。
DBの縮小中はトランザクションは停止しないようです。

DB領域の拡張

同時にDB領域の拡張も行いました。自動拡張を続けているDBはこれを機に割り当て領域を拡張して、そもそも自動拡張が発生しないように対応を行いました。

DBを拡張するコマンドは以下の通りです

USE [db_name]
ALTER DATABASE [db_name] MODIFY FILE ( NAME = N'db_file_name', SIZE = 100000000KB )

コマンドは、DBを右クリック – プロパティ – ファイル より、データファイルの初期サイズを変更して、「スクリプト」から出力することもできます。
サービス稼働中に実行するときの注意点としては、圧縮は小さい単位(例: 1GB)で複数回実行することです、もっと大きい単位でも問題なく拡張できるかもしれませんが、1GB単位で実行しました。

不要なデータの削除

一時的にトラブルは解消しましたが、自動拡張を続ける限り今後もディスクの空き容量枯渇のリスクを抱えています。そもそもDBに保持するデータ量を減らすため、一定期間を経過した履歴データは削除を行うようにしました。これによって約30%データ量を削減し、定期的に削除を行うことで今後1年は自動拡張が発生しないような状態となりました。

容量監視の重要さ

この2つの事例はいずれも DBの容量監視をしていれば事前に気づけていました。
恥ずかしながら、このトラブルが発生するまでRDSの空き容量の監視を行っていませんでした。CloudWatch上で簡単に監視できるため、RDSのディスクが一定容量を下回るとSlackへ通知するよう設定を行いました。

まとめ

  • DB自動拡張中はトランザクションが停止する、そのため自動拡張は割合(%)ではなく絶対値(MB)かつ、一度に拡張されるサイズを小さくしたほうがいい
  • DBの手動縮小・拡張は1GB単位など小刻みに行えば、サービス稼働中でも可能
  • 容量監視は大事

続きを読む

お弁当の注文忘れ防止のため、定刻にslackにメッセージをAWS lambdaを使って送ってみた 

はじめに

お昼にお弁当の注文をWebでいつもしているのですが、よく忘れます。
そこでslackに毎朝通知が来るようにすれば忘れ防止になると思い、試していたらハマった箇所があったのでメモです。

まずは単純にSlackへのpostを成功させる

slack chat.postMessage API がスタンダードなメッセージをPOST するAPIがあると思いますが、帯をつけたメッセージにしたいなと思う時、attachementが使えます。

python のrequests モジュールを使ってattachmentを定義する際、少しハマりました。

import json
import requests

SLACK_TOKEN = YourToken
SLACK_CHANNEL = YourChannelId


res = requests.post(url='https://slack.com/api/chat.postMessage', data={
    'token': SLACK_TOKEN,
    'channel': SLACK_CHANNEL,
    'as_user':'true',
    "attachments": json.dumps([
        {
            "title": "おべんとねっと",
            "title_link": "https://www.obentonet.jp/login.html",
            "text": "今日の注文はお済みですか?まだの方はお忘れなく",
            "image_url": "https://pbs.twimg.com/profile_images/918467156697694209/7Za_rDKG.jpg",
            "color": "#36a64f",
            "footer": "XXXX"
        }
    ])
}).json()
print(res)

attachments はJson.dumps かけないとだめのようです。
ちゃんとドキュメントにもかいてありますね。
そのほかのパラメータについては公式ドキュメント を参照してください。

今回はおべんと注文サイトのおべんとねっとさんへタイトルを押したら遷移できるようにしています。

次に定刻でPythonコードが実行されるようにAWS Lambda を設定する

続いて毎朝10時になったらpostをするというのをAWS Lambdaを使って実装します。
Lambda では5,6行目の TOKEN、ACCESS_CHANNELのようなキー情報は、lambdaのソースコードには直接書かずに環境変数に書き出すことができます。
※ただし、今回はモジュールのインストールが必要になることからやめました。

lambda_handler の中身はほぼそのままで、
以下のようなソースになります。
これをそのままAWS コンソールで使おうと思っても使えません。

requestsモジュールはlambdaでは標準インストールされておらず、
こういった場合は、pip installをlambda上でしなくなるところですができません。
ローカルでファイルを作成して、それをzip圧縮してアップロードをしなければなりません。


def lambda_handler(event, context):
    # TODO implement
    import json
    import requests

    SLACK_TOKEN=YourToken
    SLACK_CHANNEL=YourChannelId

    res = requests.post(url='https://slack.com/api/chat.postMessage', data={
        'token': SLACK_TOKEN,
        'channel': SLACK_CHANNEL,
        'as_user':'true',
        "attachments": json.dumps([
            {
                "title": "おべんとねっと",
                "title_link": "https://www.obentonet.jp/login.html",
                "text": "今日の注文はお済みですか?まだの方はお忘れなくー",
                "image_url": "https://pbs.twimg.com/profile_images/918467156697694209/7Za_rDKG.jpg",
                "color": "#36a64f",
                "footer": "XXXX"
            }
        ])
    }).json()

    return res

関数の名前はlumbda_handlerでないとだめなので注意してください。
上記ファイルをローカルに作った任意のフォルダに格納して、
そのフォルダまで移動した状態で、

$ pip install requests -t .

を行います。カレントディレクトリにライブラリをインストールすることができます。

lambdaのアップロードにはzipファイルにする必要があるため、

$ zip -r upload.zip *

ディレクトリの中身丸ごと圧縮してくれます。

その後、コードエントリタイプからzipファイルをアップロードを選択して、
lambdaupload.png

uploadでlambda関数を作る場合、環境変数の外だし定義は使えないようで、
ローカルで作るファイルに直に必要があります。

まずはこの状態で問題なくlambda が動作するのか確認しておいたほうがよいです。
右上のテストボタンを押して、エラーなく正常終了して、slack に通知が飛べばOK

最後にLambdaの起動トリガーをCloudWacth Events のスケジュールにする

あとは、CloudWatchEvents を使って、定刻に呼び出すようにしていきます。

lambda.png

ルールを新規作成して、
スケジュールで実行を選んで、Cron式を任意の時間に設定します。

Cron式:0 1 ? * MON-FRI *
毎週月曜から金曜の10時の場合だと上記のように設定になります。

最後に

これでEC2など立ち上げることなく、定刻になったら指定したslackチャネルに
通知が来るようになります。

お弁当の注文しわすれがなくなりますね!

参考

続きを読む