ヘヴィメタル・エンジニアリング

AWS特化型エンジニアのほのぼのヘヴィメタルブログ

ヘヴィメタル・エンジニアリング

クラウド特化型ヘヴィメタルエンジニアのほのぼのブログ

文章添削を簡単にできるシステムをDockerで作ってみた ~textlint~

はじめに

私は弊社のTech Blogを運営させていただいていて、その際に大きな課題となっているのが添削です。

複数人で1つの記事を添削するのですが、誤字脱字や表現間違い、typoなど多くミスがあり、人間の目だけでチェックするのには限界があり、見落としてしまうこともあります。

そんなときに自動でチェックしてくれるツールがないかと調べたときに見つけたのが、textlintでした。

今回はそのtextlintを使った文章チェック環境をDockerで作成するところまで書いていこうと思います。

textlint とは

npmで提供されているOSSライブラリで、textlintコマンドを使うと簡単に文章内のミスを検知してくれます。

github.com

textlintは予め用意されているルールをnpm installすることで使えます。

github.com

そして、自分でルールを作成してローカルや、publicに公開して使うこともできます。

textlint 使い方

使い方は簡単です。

まずtextlintをinstallします。

$ npm install textlint

次に適当なルールをインストールします。試しに文中で助詞が複数回連続で出てくるミスを検知してくれるルールを選択。

$ npm install textlint-rule-no-doubled-joshi

あとはファイルをチェックするだけ

$ echo "AWSでCloudWatchでログを収集します。" > text.md
$ textlint --rule no-doubled-joshi text.md
/Users/xxx/text.md
  1:15  error  一文に二回以上利用されている助詞 "で" がみつかりました。  no-doubled-joshi

✖ 1 problem (1 error, 0 warnings)

このように連続で助詞を使うような間違った使い方を指摘してくれます。

.textlintrc

.textlintにルールを予め書いておくことでtextlintコマンドを用いるときにルールの指定をする必要がありません。

jsonyaml形式で記述できます。

{
    "rules": {
        "no-doubled-joshi": true
    }
}
$ textlint text.md
/Users/xxx/text.md
  1:15  error  一文に二回以上利用されている助詞 "で" がみつかりました。  no-doubled-joshi

✖ 1 problem (1 error, 0 warnings)

大まかな使い方の説明は以上です。

Docker環境作成

今回作ったものはGitHub上に公開しています。

github.com

text-checkerというシステムを作成しました。

これをgit cloneしてDocker環境を立ち上げれば誰でも簡単にセットアップされたtextlintが使えるようにしていて、例としてカスタムルールも作成しています。

詳しく作る過程を説明していきます。

カスタムルールの作成

今回作成する環境では自分でカスタマイズしたルールも使えるようにします。

先ほど説明したように、textlintには便利なルールが多数用意されています。

ですがほしいルールがなかった場合は、自分で作成することをおすすめします。

作成したルールはnpmパッケージとしてパブリックに公開することもできますし、ローカルのみで使うこともできます。

まずは、create−textlint−ruleをインストールします。

$ npm install create-textlint-rule

次にルールを作成します。

$ create-textlint-rule test-rule

できたルールを見ると以下のような構成です。

.
├── README.md
├── lib
│   ├── index.js
│   └── index.js.map
├── package.json
├── package-lock.json
├── node_modules
├── src
│   └── index.js
└── test
    └── index-test.js

index.jsにルール、index−test.jsにtestのテンプレートが用意されています。

今回は特定の文字を見つけたらアラートを出すルールを作成します。

以下のようにiosIOSといった誤った表記(正しくはiOS)のものをcheckします。

index.ts

import { TextlintRuleModule } from '@textlint/types';
import { tokenize } from "kuromojin";

// https://github.com/MosasoM/inappropriate-words-ja
const inappropriateWords = [
    'ios',
    'IOS'
]

const module: TextlintRuleModule = (context) => {
    const { getSource, report, RuleError, Syntax } = context;

    return {
      async [Syntax.Str](node) {
        const text = getSource(node);
        const tokens = await tokenize(text);

        tokens.forEach(({ surface_form, word_position }) => {
          if (!inappropriateWords.includes(surface_form)) {
            return;
          }

          const index = word_position - 1;

          const ruleError = new RuleError(
            `不適切表現「${surface_form}」が含まれています。`,
            { index }
          );

          report(node, ruleError);
        });
      }
    }
  };
export default module;

typescriptも利用できます。

1からカスタムルールを作成することもできますが、既存で作成されているカスタムルールを参考にすると楽です。textlintはそのやり方を推奨しています。

私はこちらのカスタムルールを参考にさせていただきました。

github.com

次にpackage.jsonとtsconfig.jsonです。

package.json

{
    "version": "1.0.0",
    "keywords": [
      "textlintrule"
    ],
    "main": "lib/index.js",
    "files": [
      "lib/",
      "src/"
    ],
    "scripts": {
      "build": "textlint-scripts build",
      "test": "textlint-scripts test",
      "watch": "textlint-scripts build --watch",
      "tsc": "tsc"
    },
    "dependencies": {
      "kuromojin": "^2.0.0"
    },
    "devDependencies": {
      "@textlint/types": "^1.3.1",
      "@types/node": "^12.12.39",
      "textlint-scripts": "^3.0.0",
      "textlint-tester": "^5.1.15",
      "ts-node": "^8.10.1",
      "typescript": "^3.6.4"
    },
    "name": "textlint-rule-mistaken-ward-check",
    "directories": {
      "test": "test"
    },
    "author": "",
    "license": "ISC",
    "description": ""
  }

tsconfig.json

{
    "compilerOptions": {
      /* Basic Options */
      "module": "commonjs",
      "moduleResolution": "node",
      "esModuleInterop": true,
      "noEmit": true,
      "target": "es2015",
      /* Strict Type-Checking Options */
      "strict": true,
      /* Additional Checks */
      /* Report errors on unused locals. */
      "noUnusedLocals": true,
      /* Report errors on unused parameters. */
      "noUnusedParameters": true,
      /* Report error when not all code paths in function return a value. */
      "noImplicitReturns": true,
      /* Report errors for fallthrough cases in switch statement. */
      "noFallthroughCasesInSwitch": true
    }
  }

これで環境は整いました。

以下のコマンドでセットアップすれば使えるようになります。

$ npm install
$ npm run build

Dockerfile

実際にカスタムルールを作成できたので、それとマネージドルールを使って文章チェックの環境を作成してみましょう。

以下が作成したDockerfileです。

FROM node:14-slim

# setting
RUN apt-get update
RUN apt-get -y install vim
RUN { \
        echo 'set encoding=utf-8'; \
        echo 'set fileencodings=utf-8'; \
        echo 'set fileformats=unix,dos,mac'; \
    } > ~/.vimrc
RUN . ~/.vimrc

# copy
COPY package*.json /text-checker/
COPY .textlintrc /text-checker/
COPY textlint-rule-mistaken-ward-check /text-checker/textlint-rule-mistaken-ward-check

WORKDIR /text-checker
RUN npm install

# custom rule setting
WORKDIR /text-checker/textlint-rule-mistaken-ward-check
RUN npm install
RUN npm run build

WORKDIR /text-checker
RUN npm install ./textlint-rule-mistaken-ward-check

# path textlint
WORKDIR /usr/bin
RUN touch textlint
RUN ln -s --force /text-checker/node_modules/.bin/textlint textlint


WORKDIR /

いくつかポイントがあります。

vimを使う際のエンコード

環境内でvimエディターを使うことにします。

今回は日本語の添削をするので、vimを使う際に.vimrcutf-8エンコードするような処理を入れる必要があります。

これをしないと正しく日本語チェックをtextlintでできません。

RUN apt-get -y install vim
RUN { \
        echo 'set encoding=utf-8'; \
        echo 'set fileencodings=utf-8'; \
        echo 'set fileformats=unix,dos,mac'; \
    } > ~/.vimrc
RUN . ~/.vimrc

textlintのパスを通す

実際にnpm installでtextlintをインストールしただけではtextlintコマンドを使うことはできないのでPATHを通しましょう。

以下のようにusr/binにコマンドの設定が配置されているので、そこにtextlintファイルを作成して、機能へのシンボリックリンクを貼ります。

実際は/text-checker/node_modules/.bin/textlintを打つことで機能は使えますが、usr/binにコマンド設定を追加したことでtextlintコマンドとして使うことができます。

WORKDIR /usr/bin
RUN touch textlint
RUN ln -s --force /text-checker/node_modules/.bin/textlint textlint

これでDockerfileは作成完了です。

カスタムルールのサブモジュール化

実際にこの環境をローカルに落として使うときに、カスタムルールはサブモジュール化することをおすすめします。

そうでないと環境本体の更新カスタムルールの更新が煩雑になり、機能開発維持に支障が出ます。

サブモジュール化することでカスタムルールは別のgitリポジトリとして捉えられるので別個のシステムとして組み込み、機能開発のプロセスを分けることができます。

サブモジュール化するのは簡単で、以下のようなコマンドを打ちます。

$ git submodule add https://github.com/KenFujimoto12/textlint-rule-mistaken-ward-check.git textlint-rule-mistaken-ward-check

これによってルートディレクトリのように以下のような設定が作成されて、カスタムルールのサブモジュール化は完了です。

.gitmodules

[submodule "textlint-rule-mistaken-ward-check"]
    path = textlint-rule-mistaken-ward-check
    url = https://github.com/KenFujimoto12/textlint-rule-mistaken-ward-check.git

動作

実際に使ってみましょう。

今回の環境は以下のように取得、立ち上げをします。

$ git clone git@github.com:KenFujimoto12/text-checker.git
$ cd text-checker
$ git clone --recursive git@github.com:KenFujimoto12/textlint-rule-mistaken-ward-check.git
$ docker build -t text-checker .
$ docker run -it --name text-checker text-checker:latest bin/bash

docker runで環境に入ることができたら、実際にtextファイルを作成してチェックしてみましょう。

> cd text-checker
> vim test.txt
## copy and paste blog text into xxx.txt
> textlint --rulesdir node_modules/textlint-rule-mistaken-ward-check/lib/ test.txt -f pretty-error

index: 不適切表現「ios」が含まれています。
/text-checker/test.txt:3:6
                 v
    2.
    3. こんかいはiosについての話をしたいと思います。
    4.
                 ^

ja-no-weak-phrase: 弱い表現: "思います" が使われています。
/text-checker/test.txt:3:20
                                           v
    2.
    3. こんかいはiosについての話をしたいと思います。
    4.
                                           ^

index: 不適切表現「ios」が含まれています。
/text-checker/test.txt:5:1
       v
    4.
    5. iosアプリは実際はそんなに難しいものでもなく、誰でも簡単に開発できちゃいます。
    6.
       ^

✖ 3 problems (3 errors, 0 warnings)

ちゃんと意図したようなチェックをしてくれています。

終わりに

実際にtextlintを使ったweb添削サービスはいくつかあるみたいですが、自分でオリジナルの添削環境が作成できるので、今回のようにDockerを使った環境構築がおすすめです。

他にもいろんなルールを作成していきましょう。

ECS内のインスタンスを自動ドレイニングするシステム構築 ~ ECS を使う上で外せないステータス 、ドレイニング~

はじめに

よくAWSに触れるものです。

AWSにはElastic Container Service(以下ECS)と呼ばれるフルマネージド型のコンテナオーケストレーションサービスがあります。

ECSはEC2インスタンス or Fargate上にコンテナをスケーラブルに配置、管理、監視できるサービスで、比較的扱いやすいサービスです。

そんなECSですがコンテナのライフサイクルやEC2を使う場合は配置、サービス(ECS特有の用語)によるタスクスケーリングなどはそれなりの知識が必要なのも事実です。

今回はそんなコンテナのライフサイクルに関連して、ドレイニングについて解説 & そのドレイニングを用いた安全なインスタンスの終了のさせ方を解説していきたいと思います。

ドレイニングについて

EC2のライフサイクルは複数あります。

こちらの公式ドキュメントに図が載っています。

docs.aws.amazon.com

これ以外にもECS側でのEC2のステータスがあります。それがDraining(ドレイニング)です。

Auto ScalingでScale Inすると、ドレイニング状態のインスタンスは、クラスター内でそのインスタンスが持つタスクを他のキャパシティに余裕があるインスタンス内に移動させます。

キャパシティはCPUとMemoryどちらとも収まる範囲で算出されます。

以下に図を示します。

f:id:xkenshirou:20210108010151p:plain

ステータスがStoppedにならずにタスクが移動するので、コネクションは途絶えずにダウンタイムは生じません。

一方、ドレイニング状態にないままAuto ScalingでScale Inすると、以下の図ではランダムに選ばれたインスタンスがStopped状態になり、内部のタスクは削除されます。

f:id:xkenshirou:20210108010555p:plain

ドレイニング状態とは違い、タスクはそのまま削除されたのでコネクションは消えダウンタイムが生じる可能性があります。

ちなみに、Scale Inの際に削除を希望しないインスタンスを残す場合はAuto Scaliningの設定からインスタンスの削除保護を選択することで可能です。

f:id:xkenshirou:20210108011439p:plain

Auto Scalingの設定画面からインスタンス管理を選択、削除を希望しないインスタンスを指定してスケールイン保護の設定を選択します。

これで指定したインスタンスはScale Inの対象から外れ削除されません。

以下からドレイニングの手段について解説します。

ドレイニング手段

手動

インスタンスを手動でドレイニングする方法はとても簡単です。

ECSコンソールからClusterを選択し、ECSインスタンスを選択してアクションからドレイニングを指定するだけです。

f:id:xkenshirou:20210109080502p:plain

指定のインスタンスがドレイニング状態になったことが確認できたらインスタンスを削除してみましょう。

f:id:xkenshirou:20210125192037p:plain

するとインスタンス内のタスクが他のキャパシティに余裕のあるインスタンスに移ることがわかります。

ただ手動のやり方だと、スケールインする際に毎回手動のオペレーションが増え、よきせぬスケールインには対応できません。

そのため今回は自動でドレイニングを走らせるシステムを構築したのでご紹介します。

自動

自動でドレイニングするシステムを構築します。

今回作成するリソースは以下です。

  • SNS Topic
  • SNS Subscription
  • Lambda Function
  • LambdaExecutionRole
  • Lambda Permission
  • Auto Scaling Lifecycle Hook
  • Auto Scaling Lifecycle HookのIAM

流れとしては、Auto ScalingによってScale In →アクティビティ通知 -> SNS -> Lambda です。

ここでポイントとなるのはLifecycle Hookです。

Life cycle HookはAuto Scalingによってインスタンスが起動、削除される際にその実行を待ってカスタムアクションを追加することができます。

今回でいうと、Scale Inの際にインスタンスが削除されますが、それがHookとなり、指定のパラメータ通りの動きをします。

後ほど詳しく説明します。

以下が実際のテンプレートです。

Parameters:
  EcsClusterName:
    Type: String
    Description:
      Enter the target ECS cluster name.
  EcsInstanceAsg:
    Type: String
    Description:
      Enter the AutoScalingGroup name.

Resources:
  SNSLambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "autoscaling.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AutoScalingNotificationAccessRole
      Path: "/"
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      Policies:
        -
          PolicyName: "lambda-inline"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                - autoscaling:CompleteLifecycleAction
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                - ecs:ListContainerInstances
                - ecs:DescribeContainerInstances
                - ecs:UpdateContainerInstancesState
                - sns:Publish
                Resource: "*"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AutoScalingNotificationAccessRole
      Path: "/"
  ASGSNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      Subscription:
        -
          Endpoint:
             Fn::GetAtt:
                - "LambdaFunctionForASG"
                - "Arn"
          Protocol: "lambda"
    DependsOn: "LambdaFunctionForASG"
  LambdaFunctionForASG:
    Type: "AWS::Lambda::Function"
    Properties:
      Description: Gracefully drain ECS tasks from EC2 instances before the instances are
                   terminated by autoscaling.
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: python3.6
      MemorySize: 128
      Timeout: 60
      Code:
        ZipFile: !Sub |
            import xxx
            import xxx
            <ここにpythonの処理を書いていきます>
  LambdaInvokePermission:
    Type: "AWS::Lambda::Permission"
    Properties:
       FunctionName: !Ref LambdaFunctionForASG
       Action: lambda:InvokeFunction
       Principal: "sns.amazonaws.com"
       SourceArn: !Ref ASGSNSTopic
  LambdaSubscriptionToSNSTopic:
    Type: AWS::SNS::Subscription
    Properties:
       Endpoint:
          Fn::GetAtt:
             - "LambdaFunctionForASG"
             - "Arn"
       Protocol: 'lambda'
       TopicArn: !Ref ASGSNSTopic
  ASGTerminateHook:
    Type: "AWS::AutoScaling::LifecycleHook"
    Properties:
      AutoScalingGroupName: !Ref EcsInstanceAsg
      DefaultResult: "ABANDON"
      HeartbeatTimeout: "900"
      LifecycleTransition: "autoscaling:EC2_INSTANCE_TERMINATING"
      NotificationTargetARN: !Ref ASGSNSTopic
      RoleARN:
         Fn::GetAtt:
         - "SNSLambdaRole"
         - "Arn"
    DependsOn: "ASGSNSTopic"

上記でlifecycle HookではHeartbeatTimeoutDefaultResultの設定が肝です。

HeartbeatTimeoutはLifecycle Hookが動いてから経過した時間を指し、その後DefaultResultを実行します。

なので上記ではAuto ScalingでScale Inが走り、インスタンス削除処理がHookになり、そのHookが起こってから900秒立ってもLifecycle Hookが完了しないならABANDON、つまりインスタンスは強制的に削除されます。

このHookがないと、例えば、他のインスタンスにタスクが移るときにポート被りやリソース不足で移動できないとずっとインスタンスがドレイニング状態のままインスタンスが残り続けることになります。

なので、一定期間立ったら強制的に削除するようにして、強制終了したことをslackなどに通知するといいでしょう。

次にLambdaにおける処理を見てみましょう。

import json
import time
import boto3
CLUSTER = '${EcsClusterName}'
REGION = '${AWS::Region}'
ECS = boto3.client('ecs', region_name=REGION)
ASG = boto3.client('autoscaling', region_name=REGION)
SNS = boto3.client('sns', region_name=REGION)
def find_ecs_instance_info(instance_id):
    paginator = ECS.get_paginator('list_container_instances')
    for list_resp in paginator.paginate(cluster=CLUSTER):
        arns = list_resp['containerInstanceArns']
        desc_resp = ECS.describe_container_instances(cluster=CLUSTER,containerInstances=arns)
        for container_instance in desc_resp['containerInstances']:
            if container_instance['ec2InstanceId'] != instance_id:
                continue
            print('Found instance: id=%s, arn=%s, status=%s, runningTasksCount=%s' %
                  (instance_id, container_instance['containerInstanceArn'],
                    container_instance['status'], container_instance['runningTasksCount']))
            return (container_instance['containerInstanceArn'],
                    container_instance['status'], container_instance['runningTasksCount'])
    return None, None, 0
def instance_has_running_tasks(instance_id):
    (instance_arn, container_status, running_tasks) = find_ecs_instance_info(instance_id)
    if instance_arn is None:
        print('Could not find instance ID %s. Letting autoscaling kill the instance.' %
              (instance_id))
        return False
    if container_status != 'DRAINING':
        print('Setting container instance %s (%s) to DRAINING' %
              (instance_id, instance_arn))
        ECS.update_container_instances_state(cluster=CLUSTER,
                                              containerInstances=[instance_arn],
                                              status='DRAINING')
    return running_tasks > 0
def lambda_handler(event, context):
    msg = json.loads(event['Records'][0]['Sns']['Message'])
    if 'LifecycleTransition' not in msg.keys() or \
        msg['LifecycleTransition'].find('autoscaling:EC2_INSTANCE_TERMINATING') == -1:
        print('Exiting since the lifecycle transition is not EC2_INSTANCE_TERMINATING.')
        return
    if instance_has_running_tasks(msg['EC2InstanceId']):
        print('Tasks are still running on instance %s; posting msg to SNS topic %s' %
              (msg['EC2InstanceId'], event['Records'][0]['Sns']['TopicArn']))
        time.sleep(5)
        sns_resp = SNS.publish(TopicArn=event['Records'][0]['Sns']['TopicArn'],
                                Message=json.dumps(msg),
                                Subject='Publishing SNS msg to invoke Lambda again.')
        print('Posted msg %s to SNS topic.' % (sns_resp['MessageId']))
    else:
        print('No tasks are running on instance %s; setting lifecycle to complete' %
              (msg['EC2InstanceId']))
        ASG.complete_lifecycle_action(LifecycleHookName=msg['LifecycleHookName'],
                                      AutoScalingGroupName=msg['AutoScalingGroupName'],
                                      LifecycleActionResult='CONTINUE',
                                      InstanceId=msg['EC2InstanceId'])

find_ecs_instance_infoはECS上でのインスタンスの状態を検知してくれます。

ECS上のインスタンスすべての情報を取り、そのjson値からstatusを返します。

instance_has_running_tasksインスタンス内にコンテナが走っている場合はインスタンスのステータスをドレイニング状態にします。

最後にlambda_handlerの中でmainの処理を実行します。

インスタンス内のSNS経由のメッセージで情報を受け取りインスタンス内のコンテナが0になったときに待機状態にあったlifecycle Hookを完了します。

以上のテンプレートをCloudformationで流すことで自動ドレイニングシステムは作成されます。

1つ手作業があり、Scale Inが走った際にSNSを通してLambda実行するためのアクティビティ通知を設定しないといけません。

f:id:xkenshirou:20210110133312p:plain

AWSコンソールでAuto Scaling Groupを選択して、アクティビティ通知の作成を選択、「終了」アクションが起こったときに指定のSNSに通知するように設定します。

以上でシステムの構築は完了です。

実際にScale Inをしてみて、任意のインスタンスがドレイニング状態になり安全にインスタンスが削除されるか確認しましょう。

終わりに

今回はECSのインスタンスのライフサイクルに関することを書きました。

ECSのインスタンスリソース調整やタスク配置は奥が深いので、今度はそちらについても書こうかなと思います。

PrometheusとGrafanaをGKEに簡単構築 ~チュートリアル編~

はじめに

よくGCPに触れるものです。

Kubernetesのメトリクス監視によく用いられることで頻繁に耳にするPrometheusGrafana

Prometheusはコンテナからデータを取得して簡易的にグラフ化する機能があります。

ですがあくまで簡易的なものなので、監視用コンソールを作成できるGrafanaをあわせて用います。

現時点であまり触れたことがないので、GKE環境にPrometheusとGrafana環境を構築する方法を備忘録として書きたいと思います。

GKE環境構築

ここを1から始めるとなると大変なのでGCPチュートリアルに用意されているものを使いましょう。

cloud.google.com

このチュートリアルExposing the sample app to the internetの項目まで進めます。

ここで一つ注意点として、Deploymentを作成する際に--namespace test-appとNamespaceを指定しておきましょう。

Namespaceは以下のようなコマンドで作成します。

$ kubectl create namespace test-app

ここで出てくる用語やKubernetes周りの用語は以下にまとめました。

用語 意味
Node クラスタで実行するコンテナを配置するサーバー
Pod コンテナの集合体。コンテナ実行方式を定義。
Namespace クラスタ内で作る仮想的な空間
ReplicaSet 同じ仕様のPodを複数生成・管理する
Deployment ReplicaSetの世代管理する
Service Podへのアクセス定義
Ingress Serviceをクラスタ外に公開する

チュートリアル通りにクラスタが作成でき、Podが正常に起動したらPrometheusとGrafanaの環境を構築しましょう。

Prometheus

まず監視リソースを作成するNamespaceを作成しましょう。

$ kubectl create namespace monitor

次に今回使うChartリポジトリをとってきましょう。

helm v3を使っています。

$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

このリポジトリの中には様々なChartが含まれています。

試しにChart一覧を見てみましょう。

$ helm search repo prometheus
NAME                                                CHART VERSION   APP VERSION DESCRIPTION
prometheus-community/kube-prometheus-stack          12.12.0         0.44.0      kube-prometheus-stack collects Kubernetes manif...
prometheus-community/prometheus                     13.2.0          2.24.0      Prometheus is a monitoring system and time seri...
prometheus-community/prometheus-adapter             2.10.1          v0.8.2      A Helm chart for k8s prometheus adapter
prometheus-community/prometheus-blackbox-exporter   4.10.1          0.18.0      Prometheus Blackbox Exporter
prometheus-community/prometheus-cloudwatch-expo...  0.12.1          0.8.0       A Helm chart for prometheus cloudwatch-exporter
...

今回使うChartはprometheus-community/prometheusです。

このChartから編集可能なパラメーターを取得します。

$ helm inspect values prometheus-community/prometheus > prometheus-value.yaml

ChartはService、Deployment、Ingressなどを構築するテンプレート郡をパッケージとしてまとめたものです。

その各パラメーターを編集するためにファイルに落とし込むコマンドがhelm inspect valuesです。

今回は特にパラメーターを編集せずにPrometheusを立ち上げますが、以下のようにファイルも一緒に読み込むコマンドを実行します。

$ helm install --name-template prometheus --namespacemonitor -f prometheus-values.yaml prometheus-community/prometheus
NAME: prometheus
LAST DEPLOYED: Fri Jan 15 20:23:08 2021
NAMESPACE: monitor
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The Prometheus server can be accessed via port 80 on the following DNS name from within your cluster:
prometheus-server.monitor.svc.cluster.local


Get the Prometheus server URL by running these commands in the same shell:
  export POD_NAME=$(kubectl get pods --namespace monitor -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")
  kubectl --namespace monitor port-forward $POD_NAME 9090


The Prometheus alertmanager can be accessed via port 80 on the following DNS name from within your cluster:
prometheus-alertmanager.monitor.svc.cluster.local


Get the Alertmanager URL by running these commands in the same shell:
  export POD_NAME=$(kubectl get pods --namespace monitor -l "app=prometheus,component=alertmanager" -o jsonpath="{.items[0].metadata.name}")
  kubectl --namespace monitor port-forward $POD_NAME 9093
#################################################################################
######   WARNING: Pod Security Policy has been moved to a global property.  #####
######            use .Values.podSecurityPolicy.enabled with pod-based      #####
######            annotations                                               #####
######            (e.g. .Values.nodeExporter.podSecurityPolicy.annotations) #####
#################################################################################


The Prometheus PushGateway can be accessed via port 9091 on the following DNS name from within your cluster:
prometheus-pushgateway.monitor.svc.cluster.local


Get the PushGateway URL by running these commands in the same shell:
  export POD_NAME=$(kubectl get pods --namespace monitor -l "app=prometheus,component=pushgateway" -o jsonpath="{.items[0].metadata.name}")
  kubectl --namespace monitor port-forward $POD_NAME 9091

For more information on running Prometheus, visit:
https://prometheus.io/

実際にPrometheusのPodが作成されたか確認をしましょう。

$ kubectl get pods --namespace monitor
NAME                                            READY   STATUS    RESTARTS   AGE
prometheus-alertmanager-69bbbcc5fd-mhkgd        2/2     Running   0          6m16s
prometheus-kube-state-metrics-f9db4cbdf-6m85h   1/1     Running   0          6m16s
prometheus-node-exporter-7bhnw                  1/1     Running   0          6m17s
prometheus-node-exporter-8sh6s                  1/1     Running   0          6m17s
prometheus-node-exporter-dfn7q                  1/1     Running   0          6m17s
prometheus-pushgateway-86d5954b97-csmm2         1/1     Running   0          6m16s
prometheus-server-9d5694867-cf9jr

作成されたPrometheusをweb上で見てみましょう。

出力結果にもありますが、該当のPodを見つけ出し、ローカルからポートフォワーディングします。

$ kubectl get pods --namespace monitor -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}"
$ kubectl --namespace monitor port-forward <pod name> 9090

ローカルホストに9090ポートでアクセスすると、

f:id:xkenshirou:20210115203343p:plain

このような画面が見れればPrometheusは構築できました。

Grafana

Grafanaの構築方法もPrometheusと変わりません。

同じmonitorというNamespaceにリソースを作成します。

Grafanaのチャート一覧も見てみましょう。

$ helm repo add grafana https://grafana.github.io/helm-charts
$ helm search repo grafana
NAME                                            CHART VERSION   APP VERSION DESCRIPTION
grafana/grafana                                 6.1.17          7.3.5       The leading tool for querying and visualizing t...
stable/grafana                                  5.5.7           7.1.1       DEPRECATED - The leading tool for querying and ...
grafana/fluent-bit                              2.2.0           v2.1.0      Uses fluent-bit Loki go plugin for gathering lo...
grafana/loki                                    2.3.0           v2.1.0      Loki: like Prometheus, but for logs.
grafana/loki-canary                             0.2.0           2.1.0       Helm chart for Grafana Loki Canary
...
$ helm inspect values grafana/grafana >grafana-value.yaml
$ helm install --name-template grafana --namespace monitor -f grafana-values.yaml grafana/grafana
NAME: grafana
LAST DEPLOYED: Fri Jan 15 20:40:51 2021
NAMESPACE: monitor
STATUS: deployed
REVISION: 1
NOTES:
1. Get your 'admin' user password by running:

   kubectl get secret --namespace monitor grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

2. The Grafana server can be accessed via port 80 on the following DNS name from within your cluster:

   grafana.monitor.svc.cluster.local

   Get the Grafana URL to visit by running these commands in the same shell:

     export POD_NAME=$(kubectl get pods --namespace monitor -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana" -o jsonpath="{.items[0].metadata.name}")
     kubectl --namespace monitor port-forward $POD_NAME 3000

3. Login with the password from step 1 and the username: admin
#################################################################################
######   WARNING: Persistence is disabled!!! You will lose your data when   #####
######            the Grafana pod is terminated.                            #####
#################################################################################

Podが作成されたか確認をしましょう。

$ kubectl get pods --namespace monitor
NAME                                            READY   STATUS    RESTARTS   AGE
grafana-648fc9d4d8-xqjpb                        1/1     Running   0          51s

次にPrometheusと同じようにGrafanaもwebから見てみましょう。

先程の出力結果を参考に

$ kubectl get pods --namespace monitor -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana" -o jsonpath="{.items[0].metadata.name}"
$ kubectl --namespace monitor port-forward <pod name> 3000

ローカルホストに3000ポートでアクセスすると、

f:id:xkenshirou:20210115204823p:plain

ここでIDとパスワードを要求されます。

IDはadmin で、パスワードは先程Grafanaをhelm installした際に取得方法が書かれています。

$ kubectl get secret --namespace monitor grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
xxxxxxxxxxxx

そのIDとパスワードを入力してログインします。

f:id:xkenshirou:20210115205409p:plain

これでGrafanaの構築は完了しました。

Grafanaでメトリクスを見る

GrafanaはPrometheusで監視しているリソースの様々なメトリクスを作成できます。

そのメトリクスを組み合わせてオリジナルのダッシュボードを作成できます。

まずはData Sourceに作成したPrometheusを指定します。

f:id:xkenshirou:20210115205710p:plain

f:id:xkenshirou:20210115205755p:plain

f:id:xkenshirou:20210115210119p:plain

HTTP URLに作成したPrometheusのサーバーを指定します。

次にDashboardを作成しましょう。

f:id:xkenshirou:20210115211335p:plain

f:id:xkenshirou:20210115211422p:plain

ダッシュボードに乗せるグラフを作成します。

Data Sourceを指定します。

f:id:xkenshirou:20210115211707p:plain

その下のMetricsのクエリを記述する箇所があるので記述します。

今回はサンプルとしてPrometheusからクエリを参考にします。

まずPrometheusの画面からStatusを指定して、Targetを選択し、クラスター内のPodをターゲットにできているかを確認します。

f:id:xkenshirou:20210115211944p:plain

f:id:xkenshirou:20210115213546p:plain

確認ができたらGraphを指定してクエリを見ます。

f:id:xkenshirou:20210115212332p:plain

出てきたクエリの中から適当なものをコピーします。

ちなみにPrometheusでもグラフは見れます。

f:id:xkenshirou:20210115212449p:plain

Grafanaの画面に戻ってクエリをペーストします。

f:id:xkenshirou:20210115212608p:plain

表示されたグラフが先程Prometheusで見れたものと同じであれば完了です。

おわりに

今回は簡単なPrometheusとGrafanaを構築してみました。

Chartのカスタマイズや、Prometheus、Grafanaの設定はやりこみ要素が多いので今度そちらについても触れればと思います。