17
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ドリコムAdvent Calendar 2017

Day 12

Go言語とDockerでインフラエンジニア向けのシンプルなCLIツール開発環境を作る

Last updated at Posted at 2017-12-12

ドリコム Advent Calendar 2017

これは ドリコム Advent Calendar 2017 の12日目です。
11日目はzprodevさんによる、 モバイルブラウザ 実機自動テスト です。

自己紹介

こんにちは。ドリコムでインフラ周りをやっているひらしーです。
今回はUNIX系OSを扱うITインフラ運用を主な仕事とするエンジニア(以下インフラエンジニアとします)がGo言語とDockerを使ってシンプルなCLI環境から始める方法について書いてみました。

サンプルの動作環境

本内容では以下の環境での動作を確認しています。

  • OS: CentOS 7.2
  • go: 1.9
  • docker: 1.12.6

何故インフラエンジニアがGo言語を使えたほうが良いか

インフラエンジニアがITシステムを自動化や省力化する際には、シェルスクリプトやPerl,Ruby,Pythonといったスクリプト言語が良く使われますが、Go言語をインフラエンジニアが習得することで以下のメリットがあると思います。

ワンバイナリの実行ファイルによるシンプルな運用

Go言語は各プラットフォーム用の実行ファイルをコンパイルして生成するため、実行環境や依存ライブラリの影響を受けづらい側面があります。
これは新旧パッケージやOSのバージョンが入り混じったサーバ、込み入った事情で容易に環境を変更できないサーバを数多く管理するような状況において効果的です。

Go言語製OSS(特にインフラ系)の多さ

最近ではクラウド管理のTerraform,Consulや監視ツールのprometheus,コンテナ型仮想環境のDocker(moby)といったインフラ系のOSSがGo言語を採用しており、これらの挙動調査やプラグインの作成のためGo言語の理解が必要なケースも増えています。

Go言語製サービスの増加

言語仕様自体の優秀さやパフォーマンスから商用サービスでの採用も増えており、特にGo言語の開発元であるGoogle社のクラウドサービスであるGoogle Cloud PlatformでのGAE/Goの大規模トラフィックでの採用事例も増えつつあります。
インフラエンジニアがこのようなサービスの運用を担当する際にGo言語を理解することで、解決できる問題の幅も広がります。

Go言語の始め方

Go言語は公式のドキュメントが充実していますので、以下から初めてみるのが良いと思います。

  • 公式(日本語訳)のチュートリアル
    • Go言語仕様自体シンプルなので一通り見るだけでかなり身に付きます。まずはここを見てGo言語環境のインストールから始めるのがセオリーと思います。
  • A Tour of Go
    • すぐにコードを書いて出力を確認できるので今でもGo言語仕様の理解や詰まった時や自作関数のテストに重宝しています。

Go言語のCLI環境構築

インストール

Go言語のインストールはとても簡単です。CLIツールを作る程度であれば公式サイトの通りの手順で問題ないでしょう。

# 環境変数のexport
export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin
export PATH=$PATH:/usr/local/go/bin

# 使いたいGo言語のバージョンのtar.gzを指定
wget https://redirector.gvt1.com/edgedl/go/go1.9.2.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.9.2.linux-amd64.tar.gz

開発環境

Go言語は商用・非商用を問わず開発環境が豊富です。IDEだとGolandAtomVisual Studio辺りを使う方が多いように思われます。
シンプルなCLIツール作成のみが目的であればvimとvim-goプラグインで十分だと思います。

CLIツールの引数処理

プログラム言語によっては公式でCLIのフレームワークが充実していたり、デファクトスタンダードになっているパッケージ(RubyのThor等)があります。Go言語の場合、以下が多く使われているように思います。

  • flag
    • Go言語デフォルトで用意されているパッケージ。凝ったものを作らないならばこれでOK
  • urfave/cli(旧名: codegangsta/cli)
    • シンプルなCLIフレームワーク。ヘルプ・ショートフラグ等サポート
  • spf13/cobra
    • 数多くのOSSで採用されているCLIフレームワーク。引数の複雑な制約もサポート

小規模のツールであればデフォルトパッケージのflagで十分足りるので、最初から大規模な機能が必要な際以外は引数の解析・バリデーション処理が膨らんだタイミングでCLIフレームワークに差し替えるのがお勧めです。

flag引数処理サンプルコード

CLIコマンド引数でバージョン情報を表示する引数をflagパッケージで実現してみます。

cli.go
package main

import (
        "flag"
        "fmt"
        "os"
)

func main() {
        var showVersion bool

        // --version又は-versionでバージョン情報を表示
        flag.BoolVar(&showVersion, "version", false, "print version")
        flag.Parse()

        if showVersion {
                fmt.Println(getVersion())

                // version引数があった場合はバージョン情報を表示して終了
                os.Exit(0)
        }

        // ツール自体の処理
        fmt.Println("This is CLI tool.")
}

func getVersion() string {
        return "0.0.1"
}
# コンパイル
go build -o cli-tool cli.go
# 実行
./cli-tool
>This is CLI tool
./cli-tool --version
0.0.1

まずはMakefileから

JavaではAnt,Maven、RubyではRakeがそれぞれの言語でのビルドツールの定番ですが、Go言語では現状、昔ながらのMakefileが使われることが多いようです。
先程のソースコードをビルドするMakefileを作ってみます。

Makefile

EXEC_FILE = cli-tool
VERSION = 0.0.1
BUILD_FLAGS = -ldflags "\
                -X main.Version=$(VERSION) \
                "
all: clean deps test build

fmt:
        go fmt ./...

deps:
        go get -d -v ./...

test:
        go test -v ./...

build: fmt
        go build $(BUILD_FLAGS) -o "$(EXEC_FILE)" .

clean:
        go clean

.PHONY: fmt clean deps build

先程と同じように以下のコマンドで実行ファイルを出力できます。

make

ターゲットの説明

  • fmt: make fmtコマンドにてソースコードをフォーマットします
  • deps: make depsコマンドにてソースコード内でimportされているパッケージ依存を解決します
  • test: make testコマンドにてテストを実行します
  • build: make buildコマンドにて実行ファイルを生成します
  • clean: make cleanコマンドにてgo buildコマンドで生成されたオブジェクトファイルを削除します
  • all: 上記clean,deps,buildターゲットの順に実行します

テストコード

Go言語はデフォルトパッケージでとてもシンプルなテスト機能をサポートしています。
先程のサンプルコードにバージョン情報出力時のフォーマットをチェックするテストを追加してみましょう。

cli_test.go
package main

import (
        "regexp"
        "testing"
)

// cli.goのテスト対象の関数の前に"Test"を付ける
func TestVersion(t *testing.T) {

        version := getVersion()

        // 文字列が数字のn.n.nでないとエラー
        matched, err := regexp.MatchString(`^[0-9]+.[0-9]+.[0-9]+`, string(version))
        if err != nil {
                t.Errorf("%v", err)
        } else if !matched {
                t.Errorf("version should n.n.n format: %s", string(version))
        }
}

以下のようにmake時に上記のテストを実行してくれるようになります。

make
# make実行結果
go clean
go get -d -v ./...
go test -v ./...
=== RUN   TestVersion
--- PASS: TestVersion (0.00s)
PASS
ok  	_/home/hiraishi_yosuke/repo/advent	0.002s
go fmt ./...
go build -ldflags " -X main.Version=0.0.1 " -o "cli-tool" .

Dockerの活用

DockerはDocker社が提供しているオープンソースのコンテナ型のアプリケーション実行環境です。
CLIツール程度であれば各実行環境毎のテスト・CI環境を実現できます。
インストールは各OS毎に提供されているパッケージのインストールのみでOKです。

sudo yum install -y docker

テスト

Dockerを使うことで、差分のあるOSや環境における実行テストが簡単に実施できます。
ここではシンプルにDockerfileとシェルスクリプトのみでDockerコンテナの処理を実現してみます。

# テスト用ディレクトリ作成
mkdir -p ./docker/test

Dockerfile

# Dockerhubから最新のCentOS7を利用する
FROM centos:7
COPY cli-tool /tmp/cli-tool
CMD  /tmp/cli-tool
docker_test.sh
#!/bin/sh

PROJECT_NAME="cli-tool"
PLATFORM_NAME="centos7"
BIN_NAME=${PROJECT_NAME}
DOCKER_IMAGE_NAME="${PROJECT_NAME}/${PLATFORM_NAME}"
DOCKER_HOME_DIR="./docker/test"
DOCKER_CACHE_DIR="${HOME}/docker"
DOCKER_CACHE_IMAGE_PATH="${DOCKER_CACHE_DIR}/${PLATFORM_NAME}.tar"

# 既にイメージを起動していたら停止する
if sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" > /dev/null 2>&1; then
  sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" | awk  '{print $1}' | xargs sudo docker rm -f > /dev/null
fi

# 既にイメージをキャッシュしていたらそれを利用する
if file ${DOCKER_CACHE_IMAGE_PATH} | grep empty; then
  sudo docker load --input ${DOCKER_CACHE_IMAGE_PATH}
fi

# 実行ファイルをDockerコンテナ内にコピーして実行
cp "${BIN_NAME}" "${DOCKER_HOME_DIR}/${BIN_NAME}"

sudo docker build -t "${DOCKER_IMAGE_NAME}" "${DOCKER_HOME_DIR}"
mkdir -p ${DOCKER_CACHE_DIR}
sudo docker save "${DOCKER_IMAGE_NAME}" > ${DOCKER_CACHE_IMAGE_PATH}
sudo docker run -d "${DOCKER_IMAGE_NAME}"

cid=`sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" | awk  '{print $1}'`
sudo docker logs ${cid}

rm -f "${DOCKER_HOME_DIR}/${BIN_NAME}"

makeコマンドにてテストを追加するには以下を.PHONY:~の手前に追加します。

Makefile

:
docker_test:
        @./docker_test.sh
:
# makeにてDocker内実行テスト
make docker_test

実行ファイルのパッケージング

ビルドしたCLIツールを配布・設定する際には各OSの採用しているパッケージマネージャを利用するのが便利です。以下のようにパッケージング処理もDockerコンテナ内で行うことにより各OSに依存したパッケージングが容易になります。

# パッケージング用ディレクトリ作成
mkdir -p ./docker/packaging

Dockerfile

# Dockerhubから最新のCentOS7を利用する
FROM centos:7
ARG author="package-user"
ARG project_name="cli-tool"

# RPMパッケージ作成に必要なライブラリのインストール
RUN yum install -y yum install rpm-build yum-utils ncurses-devel glibc-devel gcc
RUN mkdir -p ~/rpm/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

# RPMパッケージの各種パラメータ設定
RUN echo "%_topdir $HOME/rpm" > ~/.rpmmacros
RUN echo "%packager ${author}" >> ~/.rpmmacros

# specと実行ファイルをDockerコンテナ内にコピー
COPY ${project_name}.spec   /tmp/${project_name}.spec
COPY ${project_name}.tar.gz /tmp/${project_name}.tar.gz
RUN  cp /tmp/${project_name}.spec  ~/rpm/SPECS/${project_name}.spec
RUN  cp /tmp/${project_name}.tar.gz ~/rpm/SOURCES/${project_name}.tar.gz

# RPMパッケージ作成
RUN rpmbuild -bb ~/rpm/SPECS/${project_name}.spec
cli-tool.spec
Name:           cli-tool
Version:        0.0.1
Release:        1
License:        Unknown
BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Source0:        %{name}.tar.gz
Summary:        awesome CLI tool
Group:          Applications/System
BuildArch:      x86_64

%define debug_package %{nil}

%description
awesome CLI tool

%prep
%setup -q -n packaging

%build

%install
rm -rf $RPM_BUILD_ROOT/*

%{__mkdir} -p ${RPM_BUILD_ROOT}%{_usr}/local/bin

%{__cp} -p \
${RPM_BUILD_DIR}/packaging/cli-tool \
${RPM_BUILD_ROOT}/usr/local/bin/cli-tool

%clean
rm -rf ${RPM_BUILD_ROOT}/*

%files
%defattr(-, root, root)

%attr(0755, root, root) %{_usr}/local/bin/cli-tool
docker_packaging.sh
#!/bin/sh

PROJECT_NAME="cli-tool"
PLATFORM_NAME="centos7"
BIN_NAME=${PROJECT_NAME}
SPEC_FILE="cli-tool.spec"
DOCKER_IMAGE_NAME="${PROJECT_NAME}/${PLATFORM_NAME}"
DOCKER_HOME_DIR="./docker/packaging"
DOCKER_PACKAGING_DIR="packaging"
DOCKER_CACHE_DIR="${HOME}/docker"
DOCKER_CACHE_IMAGE_PATH="${DOCKER_CACHE_DIR}/${PLATFORM_NAME}.tar"
GENERATED_RPM_PATH="/root/rpm/RPMS/x86_64/cli-tool-0.0.1-1.x86_64.rpm"

# 既にイメージを起動していたら停止する
if sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" > /dev/null 2>&1; then
  sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" | awk  '{print $1}' | xargs sudo docker rm -f > /dev/null
fi

# 実行ファイルのアーカイブ,specファイルをDockerコンテナ内にコピーして実行
mkdir -p "${DOCKER_PACKAGING_DIR}"
cp "${BIN_NAME}" "${DOCKER_PACKAGING_DIR}/${BIN_NAME}"
tar cvzf "${DOCKER_HOME_DIR}/${BIN_NAME}.tar.gz" "${DOCKER_PACKAGING_DIR}/${BIN_NAME}"
cp "${SPEC_FILE}" "${DOCKER_HOME_DIR}/${SPEC_FILE}"

sudo docker build -t "${DOCKER_IMAGE_NAME}" ${DOCKER_HOME_DIR}
mkdir -p ${DOCKER_CACHE_DIR}
sudo docker save "${DOCKER_IMAGE_NAME}" > ${DOCKER_CACHE_IMAGE_PATH}
sudo docker run -d --privileged "${DOCKER_IMAGE_NAME}"

cid=`sudo docker ps -a | grep "${DOCKER_IMAGE_NAME}" | awk  '{print $1}'`
sudo docker logs ${cid}

# Dockerイメージ内でビルドされたRPMパッケージをローカルにコピー
sudo docker cp "${cid}:${GENERATED_RPM_PATH}" cli-tool.rpm

sudo rm -f "${DOCKER_HOME_DIR}/${BIN_NAME}.tar.gz"
sudo rm -f "${DOCKER_HOME_DIR}/${SPEC_FILE}"

makeコマンドにてパッケージングを追加するには以下を.PHONY:~の手前に追加します。

Makefile

:
docker_packaging:
        @./docker_packaging.sh
:
# makeにてDocker内実行テスト
make docker_packaging

最後に

本記事ではインフラエンジニア向けのシンプルなCLIツール開発環境の作り方について記載しました。
こちらに今回の全てのソースコードがありますので是非参考にして頂ければと思います。

17
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?