Foreverly

メモ帳

sendgridのeventdataをS3に送るやつです

構成

SendGridのエラーをS3に格納するfunctionを実行する構成です。

f:id:oza__shu:20200717200041p:plain

各サービスの役割とポイントとなる設定についてみていきましょう。 S3→Lambda→APIGateway→SendGridの順にみていきましょう〜〜〜

S3

  • eventdataの保存先にS3を使用します。bucketを用意してlifecycleで保存期間を設定して終わりです。

AWS Lambda

  • API Gatewayで受けたeventdataをS3に保存するために、AWS Lambdaを使います。公式ドキュメントをみるとイベントデータはJSON配列で送信されるようです。今回はPython3でfunctionを書きました。forで回して1件ずつ処理してS3に格納していきます。内容をざっと見ると、timestampのunixstampをJSTに変換しログ名にもいれて、dict型からstr型bytes型に変換しioモジュールでファイルに書き出し、それをboto3でアップロードさせました。Lambda関数のコード抜粋を載せます。
# configure with env vars
BUCKET_NAME = os.environ['LOG_S3_BUCKET']

def put_to_s3(data: dict, bucket: str, key: str):
    xray_recorder.begin_subsegment('s3 upload')
    strdata = json.dumps(data)
    bindata = strdata.encode()
    try:
        with io.BytesIO(bindata) as data_fileobj:
            s3_results = s3.upload_fileobj(data_fileobj, bucket, key)

        logger.info(f"S3 upload errors: {s3_results}")

    except S3UploadFailedError as e:
        logger.error("Upload failed. Error:")
        logger.error(e)
        import traceback
        traceback.print_stack()
        raise
    xray_recorder.end_subsegment()

def handler(event, context):
    logger.info(event)
    data_list = event['body']
    data_dicts = json.loads(data_list)
    logger.info(data_dicts)
    for _, data in enumerate(data_dicts):
        unix_timestamp = data['timestamp']
        jst_time = datetime.fromtimestamp(unix_timestamp)
        key = data['event'] + "/" + jst_time.strftime("%Y-%m/%d/%H/%Y-%m-%d-%H:%M:%S-") + "-" + data['sg_event_id'] + ".log"
        put_to_s3(data, BUCKET_NAME, key)

eventに何が受け取るのか最初わからずハマりました。文字列がきていたのでjson.loadsでdictに変更して値がとれるようになりました。 Lambda関数には、S3へのUpload権限を与えるのを忘れないようにしてください。

公式ドキュメントのここらへんが参考になりそう。

functionのzip化はarchive_fileを使うでもいいと思います。

Pythonのコードはとりあえず、ここらへんから参考に育てていきました。あとここらへんとかも。

API Gateway

  • Event WebhookのHTTPリクエストをLambda Proxyを経由してAPI Gatewayで受けます。API Gatewayはリソースを作成後、ANYメソッドを作成します。プロキシリソースとの Lambda プロキシ統合を参考に設定します。設定内容としては、greedy パス変数 {proxy+} を使用してプロキシリソースを作成します。そしてプロキシリソースに ANY メソッドを設定します。API Gateway REST APIAWS_PROXYで指定されるLambda プロキシ統合は、バックエンドの Lambda 関数と統合するために使用します。Lambda プロキシ統合を使うと何がよいかなのですが、API Gatewayがリクエストとレスポンスのマッピング設定をよしなに設定してくれるので、マッピングテンプレートを書かなくなることです。以上でAPI Gatewayの設定は完了です。最後にAPIをterraformでapplyしてデプロイしてAPI Gatewayの設定は完了です。こっちのドキュメントも参照した方がいいと思います。 この記事も見やすかったです。

SendGrid

  • Event WebhookはSendGridダッシュボードの「Settings > Mail Settings > Event Notification」で設定します。HTTP POST URLAPI GatewayのエンドポイントのURLを設定します。SELECT ACTIONSで受け取りたいイベントのチェックボックスをONにして設定を保存します。今回はエラーを受け取りたいのでDropped, Deffered, Bouncedに✅を入れます。Event Notification設定画面で「Test Your Integration」ボタンを選択して、S3にテストデータが保存されていることが確認できれば設定は完了です。ここらへんに書いてあります。

まとめ

いかがでしたか?SendgridのeventdataをAWSを利用して保存する方法をみてきました。S3は耐障害性が高く、Lifecycleでログ保存期間についても簡単に設定することができます。今回の構成をeventdataの保存を考える際に参考にしてみてください。

datadogのアラート通知先を環境毎に変えたい

環境毎にアラート項目を作り分けてて面倒くさかったので、一つの監視で複数環境を監視できるようにした。 環境毎に閾値を変えたいという要望は捨て置く。

datadogでタグを使って1モニターから複数の通知先を出し分け設定する こちらを参考にした。

ここでの想定は

  • envタグとかenviromentタグにprd,dev,stg.sbx,shd,lrd...などなど付いている
  • slackチャンネルが hoge_alert 部屋と hoge_dev_alert 部屋があり、slackのwebhookのurlをdatadogのslackのintegrationで設定し、通知可能であることです。

slackが落ちた時に大丈夫なように、prdのcriticalだけMail通知もすると安全そう

以下のように書けば、一つの監視で通知先を分けれるし、ステータス毎にメンションも分けられるのでおすすめです。

{{#is_alert}}  
Criticalだよ!
{{#is_match "env" "prd"}}  <!channel>  @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}}  <!here>  @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_alert}}


{{#is_warning}}
WARNINGだよ。 
{{#is_match "env" "prd"}} <!here> @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_warning}} 

{{#is_no_data}}
NoDataだよ!
{{#is_match "env" "prd"}} <!here>  @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_no_data}} 

{{#is_recovery}}
復旧したよ!
{{#is_match "env" "prd"}} @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_recovery}}

EKS環境に対してlocust podをskaffoldでデプロイして負荷試験準備した

EKSは別に関係ないです。 負荷試験環境を用意した時、skaffoldでlocust環境を用意したので、そのメモ。

事前準備

  1. docker daemonの起動とskaffoldのインストール
  2. ecrにimageを置くためのリポジトリを用意

Description

Projectのlocustを動かすmanifestとシナリオ群を用意する。

パッケージ構成はこんな感じになった

├── Dockerfile                    //locustのimageの元
├── README.md                     //説明書
├── src                           //シナリオ置き場
│   └── {scenario_name}.py        // シナリオ
├── scripts                       //locust起動script置き場
│   └── docker-entrypoint.sh
└── deployment                    // シナリオmanifest置き場
     ├── helm                     // シナリオ毎にpathを切ります
     │  ├── Chart.yaml            // locustで実行するスクリプト
     │  ├── templates             // 
     │  │  ├─ _helpers.tpl        // helmのhelper
     │  │  ├─ master-deploy.yaml  // locustのmasterのmanifest
     │  │  ├─ master-svc.yaml     // locustのmasterのserviceのmanifest
     │  │  ├─ worker-deploy.yaml  // locustのworkerのmanifest
     │  │  └- worker-hpa.yaml    // hpaのmanifest
     │  └── values.yaml           // locustのチャートの値
     ├── helm_vars                // シナリオ毎にpathを切ります
     │  └─ values.yaml            // locustのチャートの値のoverride用
     └── skaffold.yaml            // skaffoldのmanifest

locust

locust自体はこの方がやっていることをそのまま参考にしています

docker-entrypoint.shはこんな感じにした。

#!/usr/bin/env sh

if [ -z "${TARGET_URL}" ]; then
    echo "ERROR: TARGET_URL not configured" >&2
    exit 1
fi

LOCUST_MODE="${LOCUST_MODE:=standalone}"
_LOCUST_OPTS="-f ${LOCUSTFILE_PATH:-/locustfile.py} -H ${TARGET_URL}"

if [ "${LOCUST_MODE}" = "master" ]; then
    _LOCUST_OPTS="${_LOCUST_OPTS} --master"
elif [ "${LOCUST_MODE}" = "slave" ]; then
    if [ -z "${LOCUST_MASTER_HOST}" ]; then
        echo "ERROR: MASTER_HOST is empty. Slave mode requires a master" >&2
        exit 1
    fi

    _LOCUST_OPTS="${_LOCUST_OPTS} --slave --master-host=${LOCUST_MASTER_HOST} --master-port=${LOCUST_MASTER_PORT:-5557}"
fi

echo "Starting Locust in ${LOCUST_MODE} mode..."
echo "$ locust ${LOCUST_OPTS} ${_LOCUST_OPTS}"

exec locust ${LOCUST_OPTS} ${_LOCUST_OPTS}

Dockerfile はこんな感じ

ENTRYPOINT ["./docker-entrypoint.sh"] にしたらpermission errorで実行できなかった。

FROM python:3.8-alpine
RUN apk add --no-cache -U zeromq-dev ca-certificates
COPY requirements.txt /tmp
RUN apk add --no-cache -U --virtual build-deps libffi-dev g++ gcc linux-headers &&\
    pip install --upgrade pip &&\
    pip install -r /tmp/requirements.txt && \
    apk del build-deps
EXPOSE 443 5557 5558
WORKDIR /locust
COPY ./src src
COPY ./scripts/docker-entrypoint.sh .
ENTRYPOINT ["sh", "./docker-entrypoint.sh"]

locustのmasterとworkerのマニフェストはhelmで書いて、 サービス毎にシナリオを追加できるようにしました。

master-deploy.yaml の中身

skaffold.yaml

こんな感じにマニフェスト作成 profilesを使って、対象を分けた。

apiVersion: skaffold/v1
kind: Config

build:
  local: {}
  artifacts:
    - image: {{AWSアカウントID}}.dkr.ecr.ap-northeast-1.amazonaws.com/project/locust
      context: .
  tagPolicy:
    sha256: {}
portForward:

profiles:
  - name: project-locust1
    deploy:
      kubeContext: arn:aws:eks:ap-northeast-1:{{AWSアカウントID}}:cluster/{{クラスタ名}}
      helm:
        releases:
          - name: project-locust1
            namespace: locust-test
            chartPath: ./deployment/helm
            valuesFiles:
              - ./deployment/helm_vars/locust1-values.yaml
            values:
              image: {{AWSアカウントID}}.dkr.ecr.ap-northeast-1.amazonaws.com/project/locust
            imageStrategy:
              helm: {}
  - name: project-locust2
    deploy:
      kubeContext: arn:aws:eks:ap-northeast-1:{{AWSアカウントID}}:cluster/{{クラスタ名}}
      helm:
        releases:
          - name: project-locust2
            namespace: locust-test
            chartPath: ./deployment/helm
            valuesFiles:
              - ./deployment/helm_vars/locust2-values.yaml
            values:
              image: {{AWSアカウントID}}.dkr.ecr.ap-northeast-1.amazonaws.com/project/locust
            imageStrategy:
              helm: {}
    portForward:
      - resourceType: service
        resourceName: locust2-master-svc
        namespace: locust-test
        port: 8089

Usage

deployとdeleteはこんな感じ。 -p でprofileを指定して、locustのターゲットを指定した。

## deploy
$ skaffold run -f deployment/skaffold.yaml -p project-locust1

## delete
$ skaffold delete -f deployment/skaffold.yaml -p project-locust1

新規にシナリオを追加したい時

以下の手順で増やしていけばいいだけ。

  1. src 配下にシナリオを追加
  2. helm_vars 配下にシナリオごとに変更したい値をoverrideさせるyamlを書く
  3. skaffold.yamlprofiles に追加したいシナリオの情報を追記する

番外編

aws2コマンドにしたらaws ecrへのloginコマンドが変わっていた。 以下でイケました。

 aws ecr get-login-password \
              --region {{リージョン名}} \
          | docker login \
              --username AWS \
              --password-stdin {{AWSアカウントID}}.dkr.ecr.ap-northeast-1.amazonaws.com

参考URL

【AWS】初めてのECR

今度はあんまりゴツくない!?「わりとゴツいKubernetesハンズオン」そのあとに

[アップデート]AWS CLI v2 で $ aws ecr get-login を使うときの注意点

Locustを触ってみた

Google発のコンテナアプリケーション開発支援ツール「Skaffold」や「Kaniko」を使ってみる

skaffold run

Kubernetesのアプリケーション開発で楽をしたい。そうだ、Skaffoldを使ってみよう!

/docker-entrypoint.sh": permission denied

Leetcodeやっていき(無料Easy編)

とりあえず無料のEasyをすべて解くのを目指す。 ちゃんとテストも書いていく。

771. Jewels and Stones

与えられた文字列の中から、対象の文字列が何個あるか数える問題。 forで一文字ずつcount()を使用して確認していく。 for文も通常の書き方より、リスト内包表記を使用した方が早いし、短い。 最後はsum()で合計を出せばよい。

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        return sum(S.count(j) for j in J)

ここで覚えるやつ

1342. numberOfSteps

偶数なら2で割って、それ以外なら1で引いた数の合計を求める問題。

そのまま解いた。

class Solution:
    def numberOfSteps(self, num: int) -> int:
        i = 0
        while num > 0:
            if num % 2 == 0:
                num /= 2
                i += 1
            else:
                num -= 1
                i += 1
        return i

1108 Defanging an IP Address

該当の文字列を置き換えればよい問題。

class Solution:
    def defangIPaddr(self, address: str) -> str:
        return '[.]'.join(address.split('.'))

412 Fizz Buzz

fizzbuzzの結果をlistに追加していく問題。

class Solution:
    def fizzBuzz(self, n: int) -> str:
        result = []
        for i in range(1, n + 1):
            if (i % 3) == 0 and (i % 5) == 0:
                result.append("FizzBuzz")
            elif (i % 3) == 0:
                result.append("Fizz")
            elif (i % 5) == 0:
                result.append("Buzz")
            else:
                result.append(str(i))
        return result

1365 How Many Numbers Are Smaller Than the Current Number

sorted()でiterableの要素を並べ替えた新たなリストを返し、index()で要素番号を出力する。

class Solution:
    def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]:
        return [sorted(nums).index(i) for i in nums]

Envoy Meetup Tokyo #1でLTしてきた

Envoy Meetup Tokyo #1で、 「本番環境でEnvoyを導入するためにやったこと」というタイトルでLTしてきました。

発表資料はこちら

自分はCotrolPlaneを特に用意していないので、Envoyを気軽に導入したい人には参考になるかと思います。

Decksetで発表しようとしたら全表示されなかったので、急遽旧verのpdf化した資料で発表することになったのが、反省point。

会場のEnvoy利用者は1割程度な感じでした。 Istio利用者は数%程度だったのではないでしょうか。

また昨日の全体的な発表内容的にはAWS App MeshやIstioが多い印象だったので、 DataPlaneとしてのEnvoyについてというより、CotrolPlaneについての方が関心が多いのかなという印象をうけました。

Istioも1.5からIstiodが入ったりして、覚えるコンポーネントが少しは減ると思うので、そろそろ触ってみようかな。

外部の勉強会で発表とか2年ぶりくらいなので今年はもっと発表していきたい 💪

terraformerを使ってコード化対応

手動で管理していたCloudDNSをコード管理したかったので、terraformerを使ってコード化しました。

Installation

brew install terraformer

事前準備

コマンド実行したら以下エラーが出たので、ディレクトリにpluginをinstall

Copy your Terraform provider's plugin(s) to folder ~/.terraform.d/plugins/{darwin,linux}_amd64/, as appropriate.

mkdir -p ~/.terraform.d/plugins/darwin_amd64
cd ~/.terraform.d/plugins/darwin_amd64
wget https://releases.hashicorp.com/terraform-provider-google/2.17.0/terraform-provider-google_2.17.0_darwin_amd64.zip
unzip terraform-provider-google_2.17.0_darwin_amd64.zip

Command実行

CloudDNSの以下リソースのコード化を実行

terraformer import google --resources=dns --projects=hogehoge --regions=asia-northeast1
2019/10/23 13:47:53 google importing project hogehoge region asia-northeast1
2019/10/23 13:47:54 google importing... dns
...

2019/10/23 13:48:00 google Connecting....
2019/10/23 13:48:00 google save dns
2019/10/23 13:48:00 google save tfstate for dns

tfstateファイルとtfファイルが作成されていることを確認

ll generated/google/hogehoge/dns/asia-northeast1/
 default.tfstate*
 dns_managed_zone.tf*
 dns_record_set.tf*
 outputs.tf*
 provider.tf*

GoでTravis-CIを使ってのテストとデプロイ

Goで何か書いた時にCIの設定をまとめてみた。 もっといい方法がありそうなので、ブラッシュアップしていきたい。 golangci-lint でsyntaxチェックして goreleaser でdeployの流れ。

  • .travis.yml

travisの設定は以下の用にする。

branches: ciの設定(ブランチがマスターブランチ以外にプッシュされた場合、CIは実行しないように) notifications: slackの設定 jobs: lintチェックさせる。 ./bin/golangci-lint pathに注意 deploy: deployはgoreleaserを使う

language: go
branches:
  only:
    - master 
go:
  - 1.11.x
env:
  - GO111MODULE=on
git:
  depth: 1
before_script:
  - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.13.2
install: true
notifications:
  slack:
    secure: hogehoge
jobs:
  include:
    - stage: lint
      script:
        - go get -v -t -d ./...
        - ./bin/golangci-lint run --config .golangci.yml
    - stage: test
      script: go test -v -race -cover ./...
    - stage: build
      script: go build
deploy:
  - provider: script
    skip_cleanup: true
    script: curl -sL https://git.io/goreleaser | bash
    on:
      tags: true
  • .golangci.yml

golangci configは以下の内容で大体十分。

run:
  deadline: 5m
linters:
  disable-all: true
  enable:
    - gofmt
    - golint
    - govet
    - staticcheck
    - unused
    - ineffassign
    - misspell
    - gocyclo
    - deadcode
    - typecheck
  • .goreleaser.yml

builds.binary: バイナリ名 release: githubリポジトリのownerとnameを入れておきます。

before:
  hooks:
  - go mod download
builds:
  - binary: hogehoge
    goos:
      - darwin
      - linux
    goarch:
      - amd64
changelog:
  filters:
    exclude:
      - .github
      - Merge pull request
      - Merge branch
archive:
  format: tar.gz
  name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
  files:
    - README.md
  replacements:
    darwin: Darwin
    linux: Linux
    amd64: x86_64
release:
  github:
    owner: hogehoge
    name: hogehoge