これは ドリコム 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だとGoland、Atom、Visual 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パッケージで実現してみます。
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言語はデフォルトパッケージでとてもシンプルなテスト機能をサポートしています。
先程のサンプルコードにバージョン情報出力時のフォーマットをチェックするテストを追加してみましょう。
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
#!/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
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
#!/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ツール開発環境の作り方について記載しました。
こちらに今回の全てのソースコードがありますので是非参考にして頂ければと思います。