ようへいの日々精進XP

よかろうもん

最近ギョームでやったこと (1) 〜 EC2 のメモリ使用率とディスク使用率を監視するツールを作って, CircleCI で RPM パッケージを生成出来るようにした 〜

tl;dr

現在, 働いている会社 (YAMAP) では, EC2 インスタンス (OS は Amazon Linux がメイン) がまだ数台動いています. これらのインスタンスのメモリとディスクの使用率を CloudWatch のカスタムメトリクスに送りつけて監視を行いたかったのでツールを作りました. そして, そのツールを CircleCI でビルドして, さらに RPM パッケージ化出来るようにしたので, その概要と苦労した点をメモります.

尚, Datadog 等の SaaS ツールの方がええやんという思いはありましたが, これらの EC2 は退役を控えているシステムであり, あまりコストはかけず, お手軽に監視を追加する方法を考えた結果自作ツールとなりました. 技術的な負債としては小さいものだと考えています.

まだまだベータ版

まだまだベータ版なので, 以下のような課題を抱えています.

  • RPM バッケージをどこにアップロードするか検討中... 多分, S3 に置くけど, 安全に RPM リポジトリとして利用する方法を調査中
  • RPM パッケージのインストール手順の作成 (itamae とか Ansbile とか)

監視ツールについて

メモリ使用率, ディスク使用率を監視するツールは勉強を兼ねて, 先人達の知恵を借りつつ Go で実装してみました. コードについては, 近日中に追加したいと思いますが, 以下のような機能を提供しています.

  • メモリ監視は /proc/meminfo から情報を取得する
  • ディスクに関しては, syscall パッケージ を利用して情報を取得する
  • 取得した情報を計算して CloudWatch のカスタムメトリクスとして登録する

尚, 実装にあたり, 以下のリポジトリのコードを参考にさせて頂き, Go ルーチンを利用した実装にしてみました. とても参考になりましたありがとうございました.

github.com

メモ

メモリ使用率監視

イチからメモリ監視の実装を書くのは辛い (というか, 無理です) ので, 以下のパッケージを利用しました.

/proc/meminfo 以下の情報をパースして構造体として返してくれるので, 以下のように直感的にメモリの使用量等を取得出来ます.

       memory, err := memory.Get()
        if err != nil {
            fmt.Fprintf(os.Stderr, "%s\n", err)
            return
        }
        if *argDebug {
            fmt.Println("---------- debug print ----------")
            fmt.Printf("memory total: %d bytes\n", memory.Total)
            fmt.Printf("memory used: %d bytes\n", memory.Used)
            fmt.Printf("memory cached: %d bytes\n", memory.Cached)
            fmt.Printf("memory free: %d bytes\n", memory.Free)
        }

最高です.

ディスク使用率監視

先述の通り, syscall パッケージを利用しています. 以下のような短いコードで指定したパスの使用量等を取得しています.

func DiskUsage(path string) (disk DiskStatus) {
    fs := syscall.Statfs_t{}
    err := syscall.Statfs(path, &fs)
    if err != nil {
        return
    }
    disk.Total = fs.Blocks * uint64(fs.Bsize)
    disk.Free = fs.Bfree * uint64(fs.Bsize)
    disk.Used = disk.Total - disk.Free
    return
}

また, メモリ使用率と同じような感じで取得出来るようにしました.

       disk := DiskUsage(*argPath)
        if *argDebug {
            fmt.Println("---------- debug print ----------")
            fmt.Printf("disk total: %d bytes\n", disk.Total)
            fmt.Printf("disk used: %d bytes\n", disk.Used)
        }

RPM パッケージ

RPM パッケージを生成するにあたり, 以下のパッケージが必要になります.

yum install rpmdevtools yum-utils

また, 肝になるのは SPEC ディレクトリ以下に設置する以下のような spec ファイルです.

%define _binaries_in_noarch_packages_terminate_build 0

Summary: EC2 Monitoring Toolset for disk Usage
Name:    ec2-monitoring-toolset-disk
Version: 0.0.3
Release: 1
License: MIT
Group:   Applications/System
URL:     https://github.com/xxxxxxxx/ec2-monitor-tools

Source0:   %{name}
Source1:   %{name}.initd
Source2:   %{name}.logrotate
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root

Requires: logrotate

%description
%{summary}

%prep

%build

%install
%{__rm} -rf %{buildroot}
%{__install} -Dp -m0755 %{SOURCE0} %{buildroot}/usr/local/bin/%{name}
%{__install} -Dp -m0755 %{SOURCE1} %{buildroot}/%{_initrddir}/%{name}
%{__install} -Dp -m0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/logrotate.d/%{name}

%clean
%{__rm} -rf %{buildroot}

%post
/sbin/chkconfig --add %{name}
/sbin/chkconfig --level 3 %{name} on

%files
%defattr(-,root,root)
/usr/local/bin/%{name}
%{_initrddir}/%{name}
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}

個々の設定内容については言及しませんが, init スクリプト等の関連するスクリプトの設置までこのファイルでやれるということを知りました. 上記のファイルでは,

Source0:   %{name}
Source1:   %{name}.initd
Source2:   %{name}.logrotate
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root

... 略

%install
%{__rm} -rf %{buildroot}
%{__install} -Dp -m0755 %{SOURCE0} %{buildroot}/usr/local/bin/%{name}
%{__install} -Dp -m0755 %{SOURCE1} %{buildroot}/%{_initrddir}/%{name}
%{__install} -Dp -m0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/logrotate.d/%{name}

このあたりです. また, 配布したいツール自身 (今回だと, Go でビルドしたバイナリファイル) は SOURCES ディレクトリに放り込んでおけば良いことも知りました. ですので, Go でビルドしたバイナリではなくても, シェルスクリプト等も同じように RPM パッケージとして配布することが出来ます. 社内等の組織の中でバッチ処理等のスクリプトを配布する場合, 配布の手法を統一するという意味では RPM パッケージ化するというのもありかもしれません.

CircleCI

Workflows を使っています. 以下のようなシンプルなワークフローです.

f:id:inokara:20190602235724p:plain

  • build ジョブでは go test (取って付けたようなテスト) が行われた後, RPM パッケージを生成します
  • 生成された RPM パッケージを実際に Amazon Linux コンテナにインストールして, 意図した通りにインストールされているか等をテストするのが test ジョブです
  • deploy ジョブは RPM パッケージを S3 バケットにアップロードするステップを想定しています

今回の作業で得た知見は, 最初の build ジョブで生成された RPM パッケージを次の test ジョブや deploy ジョブに共有したい場合どうするか. .circleci/config.yml の以下の部分で指定しています.

jobs:
  build:
    docker:
      - image: amazonlinux:1
    working_directory: /root/go/src/toolset
    steps:
... 略 ...
      - persist_to_workspace:
          root: /root/rpmbuild/RPMS/x86_64
          paths:
            - ./*

  test:
    docker:
      - image: amazonlinux:1
    steps:
      - attach_workspace:
          at: /root/rpmbuild/RPMS/x86_64
      - checkout
... 略 ...

  deploy:
    docker:
      - image: amazonlinux:1
    steps:
      - attach_workspace:
          at: /root/rpmbuild/RPMS/x86_64
... 略 ...
 
workflows:
  build-test-deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test

build ジョブの stepspersist_to_workspace キーで共有したいパスを指定して, 次のジョブの stepsattach_workspace.at で共有されたパスを指定すれば良いようです. 詳細についは, 以下のドキュメントに記載されています.

以上

メモでした. ちゃんと RPM パッケージを作ったことが無かったのでとても勉強になりましたし, Go をちゃんと勉強しなければと思った次第です.