Foreverly

メモ帳

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が失敗するようにする運用がいいと思う。

GKE + datadogの監視の仕組みをhelmfileを使っての下準備

GKE + datadogの監視をするのにhelmfileを使ってdatadog-agentを入れるのと、 helmfileをCircle CIで回してパッケージ管理の準備までしてしまおうというもの。

Integrationの有効化

Datadogなので、まずはintegrationの有効化 必要に応じてだけど、代替以下

Docker
Google Cloud Platform
Google Container Engine
Google Compute Engine
Google CloudSQL
Google Cloud Pubsub
Google Cloud Storage
kubernates

こちら参考 GCPのIntegrationを有効化するために サービスアカウント が必要なので作成しておく。

監視項目

監視したい内容は以下とする

種類 監視対象 監視項目 監視方法 (使うメトリクス項目)
Work metrics アプリの Pod (web, api, worker) unavailable な Pod がいないことを確認 kubernetes_state.deployment.replicas_unavailable
Work metrics アプリの Pod (web, api, worker) Pod が restart loop に陥っていないことを確認 "CrashLoopBackOff/n分間にn回以上再起動していないかどうか"
Work metrics kube-system namespace の Pods unavailable な Pod がいないことを確認
Work metrics Kubernetes worker nodes 全ての node status が Ready であることを確認 check: kubernetes_state.node.ready
Work metrics Kubernetes worker nodes 全ての node が schedulable であることを確認 "metrics: kubernetes_state.node.status/tag: status:schedulable == 0"
Resources metrics Kubernetes worker nodes node のリソースのキャパシティに余裕があることを確認 "kube_node_status_capacity_cpu_cores/kube_node_status_capacity_memory_bytes"
種類 監視対象 監視項目 監視方法 (使うメトリクス項目)
Resources metrics Kubernetes worker nodes Keepalive (Connectivity) 監視 従来の VM 運用と同じやり方
Resources metrics Kubernetes worker nodes CPU Usage 従来の VM 運用と同じやり方
Resources metrics Kubernetes worker nodes Memory Usage 従来の VM 運用と同じやり方
Resources metrics Kubernetes worker nodes Disk Usage 従来の VM 運用と同じやり方
  • Datadog Dashboard に出しておきたい項目
項目 メトリクス
CPU core 使用率 kubernetes.cpu.usage.total / kubernetes.cpu.capacity
Memory 使用率 kubernetes.memory.usage / kubernetes.memory.capacity
Disk 使用率 kubernetes.filesystem.usage_pct
CPU reqests/limits kubernetes.cpu.requests / kubernetes.cpu.limits
Mem reqests/limits kubernetes.memory.requests / kubernetes.memory.limits

DatadogのAgentの用意

Monitoring Kubernetes with Datadog How to monitor Google Kubernetes Engine with Datadog

datadogの記事をみるとGKEなどのkubernatesをdatadogでモニタリングするには各ノードにdatadog-agentをdeamonsetで常駐させるのが一般的らしい。 Pod間通信でメトリクスを取ってくるようです。

以下のようなものを用意して、 kubectl create -f datadog-agent.yaml で当てる感じ。 datadogのkubernatesのintegrationのページにも書いてありまする。

TIPS

  • DaemonSetのAPIversionはKubernetesバージョン1.9以降の場合は、apps/v1 を使用
  • apps/v1 ではspec.selectorが必要になります。
  • 他のpodからcustom metricsをDogStatsD経由で送信するため、8125/UDPをHost(ノード)に公開します
  • Traceの機能も同様に他のpodから使いたいため8126/TCPをHost(ノード)に開ける。
  • API_KEYはSecretから取得。作成コマンドは以下参照。
  • kubectl create secret generic datadog-secret --from-literal=api_key=*****
apiVersion: extensions/v1beta1 
kind: DaemonSet
metadata:
  name: datadog-agent
spec:
  selector:
    matchLabels:
      name: datadog-agent
  template:
    metadata:
      labels:
        app: datadog-agent
      name: datadog-agent
    spec:
      serviceAccountName: datadog-agent
      containers:
      - image: datadog/agent:latest
        imagePullPolicy: Always
        name: datadog-agent
        ports:
          - containerPort: 8125
            # Custom metrics via DogStatsD - uncomment this section to enable custom metrics collection
            # hostPort: 8125
            name: dogstatsdport
            protocol: UDP
          - containerPort: 8126
            # Trace Collection (APM) - uncomment this section to enable APM
            # hostPort: 8126
            name: traceport
            protocol: TCP
        env:
          - name: DD_API_KEY
            valueFrom:
              secretKeyRef:
                name: datadog-secret
                key: api-key
          - name: DD_COLLECT_KUBERNETES_EVENTS
            value: "true"
          - name: DD_LEADER_ELECTION
            value: "true"
          - name: KUBERNETES
            value: "true"
          - name: DD_KUBERNETES_KUBELET_HOST
            valueFrom:
              fieldRef:
                fieldPath: status.hostIP
          - name: DD_APM_ENABLED
            value: "true"
        resources:
          requests:
            memory: "256Mi"
            cpu: "200m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        volumeMounts:
          - name: dockersocket
            mountPath: /var/run/docker.sock
          - name: procdir
            mountPath: /host/proc
            readOnly: true
          - name: cgroups
            mountPath: /host/sys/fs/cgroup
            readOnly: true
        livenessProbe:
          exec:
            command:
            - ./probe.sh
          initialDelaySeconds: 15
          periodSeconds: 5
      volumes:
        - hostPath:
            path: /var/run/docker.sock
          name: dockersocket
        - hostPath:
            path: /proc
          name: procdir
        - hostPath:
            path: /sys/fs/cgroup
          name: cgroups

Kubernetesのintegrationをみると manifestを書いて、それにk8sクラスタに当てる必要がある。 manifest管理をどうするかの問題が発生するが、今回はhelmを使ってインストールをする。

helm + helmfileを用いたdatadog-agentインストール

ますhelmとはK8sのパッケージ管理ツール。 リポジトリに登録された構成情報(Chart)を、 installコマンドに適時引数で設定情報を与え、簡単に自分のクラスタに導入(Release)することができる。 そしてhelmfilek8sクラスタのhelm releasesを管理してくれるもの。 helmfile.yaml にデプロイしたいhelm Chartを書いて、 helmfile sync を実行するとインストールやアップグレードを gemfile のように冪等に管理してくれるもの。

datadog-agentをクラスタに配置する場合は以下のYAMLを書くだけ。

releases:
  - name: datadog
    namespace: kube-system
    chart: stable/datadog
    version: 1.27.2
    values:
      - ./datadog/prd.values.yaml

Circle CIでhelmインストールする

GKEなので、kubectl,helm,helmfileをインストールして、 gcloudコマンドでログインして、クラスタのクレデンシャルとって、helmを当てていく流れ。

以下はCircle CI側の環境変数に入れておく。

  • COOGLE_CLOUD_REGION
  • GCLOUD_SERVICE_KEY
  • GOOGLE_PROJECT_ID
version: 2

defaults: &defaults
  working_directory: ~/hogehoge
  docker:
    - image: google/cloud-sdk:242.0.0-slim

global_env: &global_env
  GOOGLE_PROJECT_ID: hogehoge
  COOGLE_CLOUD_REGION: asia-northeast1
  K8S_CLUSTER: hogehoge

common_steps:
  install_base_packages: &install_base_packages
    command: apt-get update && apt-get install wget kubectl
  install_helm: &install_helm
    name: install helm
    command: |
      # install helm
      wget https://storage.googleapis.com/kubernetes-helm/helm-v2.13.1-linux-amd64.tar.gz
      tar zxvf helm-v2.13.1-linux-amd64.tar.gz
      mv ./linux-amd64/helm /usr/local/bin/helm
      helm init --client-only
      helm plugin install https://github.com/databus23/helm-diff

      # install helmfile
      wget https://github.com/roboll/helmfile/releases/download/v0.54.2/helmfile_linux_amd64
      chmod +x ./helmfile_linux_amd64
      mv ./helmfile_linux_amd64 /usr/local/bin/helmfile
    gcloud_login: &gcloud_login
      name: gcloud login
      command: |
        echo ${GCLOUD_SERVICE_KEY} | base64 -d | gcloud auth activate-service-account --key-file=-
        gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
    setup_cluster_and_helm: &setup_cluster_and_helm
      name: setup cluster credential and helm environment
      command: |
        gcloud beta container clusters get-credentials ${K8S_CLUSTER} --region ${COOGLE_CLOUD_REGION} --project ${GOOGLE_PROJECT_ID}
        helm repo update

jobs:
  helmfile-sync-dry-run:
    <<: *defaults
    environment:
      <<: *global_env
    steps:
      - checkout
      - run: *install_base_packages
      - run: *install_helm
      - run: *gcloud_login
      - run: *setup_cluster_and_helm
      - run:
          name: helmfile diff
          command: helmfile --file prd.helmfile.yaml diff
      - run:
          name: helmfile sync dry-run
          command: helmfile --file prd.helmfile.yaml sync --args --dry-run
  helmfile-sync:
    <<: *defaults
    environment:
      <<: *global_env
    steps:
      - checkout
      - run: *install_base_packages
      - run: *install_helm
      - run: *gcloud_login
      - run: *setup_cluster_and_helm
      - run:
          name: helmfile sync
          command: |
            helmfile --file prd.helmfile.yaml sync

workflows:
  version: 2
  dry-run-and-sync:
    jobs:
      - helmfile-sync-dry-run:
          filters:
            branches:
              only: /.*/
      - helmfile-sync:
          requires:
            - helmfile-sync-dry-run
          filters:
            branches:
              only: master
  • サービスアカウントを使用してアクセス承認
    • gcloud auth activate-service-account が正常に完了すると、gcloud initgcloud auth login と同様に、サービスアカウントの認証情報がローカルシステムに保存され、指定したアカウントがCloudSDKの構成のアクティブなアカウントに設定される。
    • サービスアカウントは事前に作成しておく(DatadogのGCPのintegrationするときに必要になる。)
    • --key-file=-- を渡すことで標準入力でキーを渡すことができる
  • gcloud コマンドライン ツールでアクティブなプロジェクトを設定
    • gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
    • gcloud は対話型インターフェースなので、自動化には邪魔なので、プロンプトの無効化--quiet でする。 --quiet フラグは先頭の項目の右側に挿入する。
  • クラスタの認証情報を取得する
  • Helm CLI を初期設定
    • helm init --client-only ちなみにインターネット環境がない場合は --skip-refresh
  • helmクライアント側でキャッシュしているので、最新のリポジトリ内の情報を得るために helm repo update を実行して、再取得することでキャッシュをリフレッシュさせる。
  • helm diff でreleasesのdiffをとる
  • helmfile --file prd.helmfile.yaml sync --args --dry-run
    • Helmfileは実行中にさまざまなイベントをトリガーとする。 イベントが発生すると、argsを指定してコマンドを実行することで、関連するフックが実行される。
  • helmfile --file prd.helmfile.yaml sync
    • helmfile syncサブコマンドは、helmfileの説明に従ってクラスタの状態を同期する。 デフォルトのhelmfileはhelmfile.yaml。任意のyamlファイルを --file path/to/your/yaml/file フラグを指定することで渡すことができる。
    • helmfileはマニフェストで宣言されたreleaseごとに helm upgrade --install を実行。必要に応じて、secretsを復号化してhelmチャートの値として使用する。 また、指定されたチャートリポジトリを更新し、参照されているローカルチャートの依存関係も更新する。
    • helm upgrade --install は releaseが存在するかどうかに応じてインストールまたはアップグレードすることができる。

これで、helmで Datadog agent のインストールができる。また、何かhelmで追加したくなったらhelmfileに書いていくことになる。

気になりンゴ

--dry-run は実はまだ機能追加されてない?helpに特に出てこない。 --args でhookさせているけど、helmfileにhookさせるものをかかないとhookされないのかな

releases:
- name: myapp
  chart: mychart
  # *snip*
  hooks:
  - events: ["prepare", "cleanup"]
    command: "echo"
    args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]