Foreverly

メモ帳

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

Terraformの基本

terraformの基本事項についてaws providerを使用して確認していく。 terraformコマンドで構築する前にクレデンシャル情報を環境変数に渡しておくのを忘れずに。

export AWS_ACCESS_KEY_ID=hogehoge
export AWS_SECRET_ACCESS_KEY=hogehoge
export AWS_DEFAULT_REGION=hogehoge

コマンド

terraform init はプロバイダ用のバイナリをダウンロードする。 terraform plan はdry-run terraform apply でplanの内容を実行する

要注意メッセージ

リソースの再作成

# aws_instance.example must be replaced 例えば、awsのproviderでinstanceを作っていたとして、 何かtfファイルを編集したらインスタンスが再作成されてしまったなんてことが起きるので注意。 destroyも要確認。

Terraform の構成要素

  1. 変数

variableで変数の定義ができる。

variable "example_instance_type" {
  default = "t3.micro"
}

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = var.example_instance_type
}

locals でローカル変数が定義できる。 variableはコマンド実行時に変数を上書きできるがlocalsは上書きできない違いがある。

  1. output

output で値を出力することができる apply時にターミナル上で値が確認できるようになる。 他にはmoduleから値を取得する時に使う。

variable "example_instance_type" {
  default = "t3.micro"
}

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = var.example_instance_type
}

output "example_instance_id" {
  value = aws_instance.example.id
}

applyすると、実行結果に、作成されたインスタンスの ID が出力される。

Outputs:

example_instance_id = i-090d4a8d3ec3fac74
  1. データソース

データソースを使うと外部データを参照できる。

最新のAmazonLinux2のAMIを以下のように定義して参照してみる、 filter などを使って検索条件を指定し、most_recent で最新のAMIを取得している。

data "aws_ami" "recent_amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.????????-x86_64-gp2"]
  }

  filter {
    name      = "state"
    variables = ["abailable"]
  }
}

resource "aws_instance" "example" {
  ami           = data.aws_ami.recent_amazon_linux_2.image_id
  instance_type = "t3.micro"
}
  1. provider

AWSGCP、Azure、Openstackなど構築する際、そのAPIの違いを吸収するものがプロバイダ。

リージョンの定義をaws mojuleを使ってするとこうなる。

provider "aws" {
  region = "ap-northeast-1"
}

Interpolation Syntax

  1. 参照

EC2 向けセキュリティグループの定義

80 番ポートを許可すると以下 ※接続元のIPアドレスを制限していないです

resource "aws_security_group" "example_ec2" {
  name = "example-ec2"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

vpc_security_group_ids からセキュリティグループへの参照を追加し、EC2インスタンスと紐づけます。 vpc_security_group_ids はリスト形式で渡すため、値を [] で囲んでいます。 TYPE.NAME.ATTRIBUTE の形式で他のリソースの値を参照できます。

variable "example_instance_type" {
  default = "t3.micro"
}

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = var.example_instance_type
  vpc_security_group_ids = [aws_security_group.example_ec2.id]

  user_data = <<EOF
#!/bin/bash
  yum install -y httpd
  systemctl start httpd.service
EOF
}

output "example_public_dns" {
  value = aws_instance.example.public_dns
}
  1. 条件分岐

Terraform では、三項演算子が使える。 本番環境と開発環境でインスタンスタイプを切り替えたい時。

variable "env" {}

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = var.env == "prod" ? "m5.large" : "t3.micro"
}

env変数をTerrafor 実行時に切り替えると、plan 結果が変わる。

$ terraform plan -var 'env=prod'
$ terraform plan -var 'env=dev'

ただ環境を分けたい時はディレクトリで分けたりしてもよいかな。

  1. 組み込み関数

Terraform には、文字列操作やリスト操作、よくある処理が組み込み関数として提供されている。 外部ファイルを読み込む file 関数を使ってみる。

ユーザデータを user_data.sh としてスクリプトで外に出す。 main.tf ファイルと同じディレクトリに置く。

#!/bin/bash
yum install -y httpd
systemctl start httpd.service

以下でapply すると、user_data.sh ファイルを読み込んでくれる。

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = "t3.micro"
  user_data     = file("./user_data.sh")
}
  1. テンプレート

Terraform には、実行時に値を埋め込むテンプレート機能がある。 user_data.shuser_data.sh.tpl とテンプレートファイル化してみる。 インストールパッケージを差し替えられるように、「package」変数を定義する。

#!/bin/sh
yum install -y ${package}
systemctl start ${package}.service

これを利用するために template_file データソースを定義する template に、テンプレートファイルのパスを指定する vars 句を記述すると、テンプレートの変数に値を代入できる

data "template_file" "httpd_user_data" {
  template = file("./user_data.sh.tpl")

  vars = {
    package = "httpd"
  } 
}

template_file データソースを参照する data.template_file.httpd_user_data.renderedのように記述することで、テンプレートに変数を埋め込んだ結果を取得できる

resource "aws_instance" "example" {
  ami           = "ami-0f9ae750e8274075b"
  instance_type = "t3.micro"
  user_data     = data.template_file.httpd_user_data.rendered
}

tfstateファイル

Terraform が変更した差分を検出して、必要な部分だけ変更できることが確認できた。 この判断をtfstateファイルで行っている。 tfstate ファイルは Terraform が生成するファイルで、現在の状態が記録されている。 Terraform は tfstate ファイルと、HCL で記述されたコードの内容に差分があれば、その差分のみを変更するよう振る舞う。 terraform.tfstate ファイルは terraform apply を実行していれば作成される。 中身を見るとJSON文字列に、現在の状態が記述されているのがわかる。

tfstateファイルは terraform apply を実行したローカルに保存されるが、 これだとチーム開発したときに、他の人にtfstateファイルがないことになってしまい、 全リソース再作成が起きるおそれがあるので、リモートのストレージを、バックエンドとして利用しましょう。AWSならS3などを利用しましょう。

tfstateの格納先が記載されたファイル init.tf を作り、事前作成したバケットを指定します。

terraform {
  backend "s3" {
    bucket = "tfstate-pragmatic-terraform-on-aws"
    key    = "example/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

リモートのバックエンドとして使用するS3バケットには、バージョニング設定をすることが強く推奨されます。 S3に保存すれば、tfstateファイルから、いつでも以前の状態に戻せるようにもなります。 また、DynamoDB と組み合わせると、ロックも可能

リソースの削除

terraform destroy でリソース削除できます。実行には注意しましょう。

モジュール

Terraform にもモジュール化の仕組みがあります。 モジュールは別ディレクトリにする必要があるので、まずは modules ディレクトリを 作成します。 そして、モジュールを定義する main.tf ファイルを作成します。 利用する側を resources ディレクトリなどにして、そこに環境ごとにpathを切ってmain.tfを置けば modules 配下のmoduleを環境毎に再利用することも可能になります。

  1. モジュールの定義

http_server モジュールを実装します。 Apache をインストール した EC2 インスタンスと、80 番ポートを許可したセキュリティグループを定義してみます。 http_server モジュールのインタフェースは次のとおりです。 - 入力パラメータ instance_type - EC2 のインスタンスタイプ - 出力パラメータ public_dns - EC2 のパブリック DNS

variable "instance_type" {}

resource "aws_instance" "default" {
  ami                    = "ami-0f9ae750e8274075b"
  vpc_security_group_ids = [aws_security_group.default.id]
  instance_type          = var.instance_type
  
  user_data = <<EOF
    #!/bin/bash
    yum install -y httpd
    systemctl start httpd.service
EOF
}

resource "aws_security_group" "default" {
  name = "ec2"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

output "public_dns" {
  value = aws_instance.default.public_dns
}  
  1. モジュールの利用

モジュール利用側の main.tf ファイルを以下のように実装します。 利用するモジュールは source に指定します。

module "dev_server" {
  source        = "./http_server"
  instance_type = "t3.micro"
}

output "public_dns" {
  value = module.dev_server.public_dns
}

apply はモジュール利用側のディレクトリで実行します。 ただし、モジュールを使用する場合、もうひと手間必要です。 terraform get コマンドか terraform init コマンドを実行して、モジュールを事前に取得しておく必要があります。 terraform init を実行するスクリプトを作成して、circle ciなどを利用してinitで失敗したらbuildが失敗するようにする運用がいいと思う。