#初めに
著者はGo言語、インフラ構築自動化技術に関する知識はあまりないので、知識のある上級者は適切なアドバイスなり、間違いの指摘をして頂けると幸です。
このようなシチュエーションを想定しています。
ある日、上司からこんな無茶ぶりが・・・
「君、優秀なんだね。Go言語が良いみたいだからGo言語でセキュアなAPIを軽く作っておいてよ。ついでにインフラ構築自動化も流行ってるから、導入して実行環境の構築も自動化しといて」
という無茶ぶりされたときにあなたはこのようなまなざしを上司に向けるでしょう。
安心して下さい。そんなあなたのためにこの記事を書きました。笑
冗談はさておき、本題に入ります。
#構成
今回の構成を図に表してみます。
図の参照元
Go Lang, GitLab, Vagrant, Docker, CircleCi, Ansible, Terraform, AWS
工夫した点
- 開発環境と実行テスト環境の明示的な切り分け(commitしないと動作確認できない仕組み)
- DockerのマルチコンテナでDBコンテナとwebサーバーコンテナを用意して、AWS上で実現しようとする環境の模倣
- terraformでスケールしても対応可能なauto scale 構成のEC2とDatabaseにはRDSを構築
- AnsibleでAWSのEC2の環境を自動構築
本来はEC2の環境もDockerで構成した方がベターな気もしますが、EC2上に複数のサービスを立てる必要がなかったので今回のようにしました。
#各主要技術の採用理由
上記の環境は他の言語、また手動でも実現可能です。では学習コストを払ってでも各技術における採用すべき正当な理由を上げてみます。
##GO言語
- コンパイル時にすべてのパッケージは静的にリンクされるため、実行ファイルは1つのバイナリファイルになるため、依存関係に悩まされず、ファイルも軽いのでデプロイの心配が少なく運用が楽
- インストールは比較的簡単
- チュートリアルも充実しており、その場で試せるプレイグラウンドもWeb上に用意されており、気軽に学習を始める事が可能
参考
(採用事例で学ぶGoLangの使いドコロ )
##Json Web Token
- 認証用のトークンの期限を決められる。またRSA方式を利用すれば秘密鍵と公開鍵を用いた認証が可能
- HTTPヘッダーに載せられるため、非常に軽く、必要な情報を全て載せることが可能
- 認証のためにData Baseを必要としない。
参考
(Introduction to JSON Web Tokens)
##Docker
メリット、デメリットの所感を記述した記事があったのこちらをご参照下さい。
##Terraform
- AWS, Google Cloud PlatForm, Herokuなどの主要なwebサービスの構築の自動化
- 構築した環境をgitで管理することで変更履歴を残せる
- 明示的に環境の再現が可能
- 複製が容易
- コードとシステムが一致(設計書のように実際の環境とのズレが生じない)
- 他のサービス(Jenkinsなど)と連携することで自動テストも可能
Step by stepで学ぶTerraformによる監視付きAWS構築
Ansible
- ymlでかけるため簡単
- エージェントレス(構成される側の準備は不要!!)
- 冪等性(ある操作を何回行っても同じ結果)
インフラ自動構築エンジン "Ansible"の勘所を1日でつかむ ~基礎入門編~
#Go言語
全体のシステムの中で注目して欲しい部分を赤線で囲っています。
APIサーバーはGo言語で記述しました。
記述の際は有名なrevelというフレームワークを使用しました。
revelを採用した理由は下記です。
- testのための機能が揃っている
- deployも簡単
- 必要なサンプルがある
私の中では必要なサンプルがあるが重要で自分で一から全て書くのはハマりどころが多くモチベーションが下がります。
ですがサンプルがあることによって、コードリーディングを自然にするようになり、王道の記述の仕方の仕方が分るようになるので良いのではないでしょうか。
これは人によるので、あくまで私の意見です。
今回の参考にしたサンプルは下記です。
##DataBase
参照元
http://linuxserver.jp/wp-content/uploads/2015/06/mysql_hosting.png
こちらを参考にした理由はDataBaseを使用したコードが載っていたためです。
このサンプルではgorpを使用してORMラッパーを実現して実装されています。
変更した部分はDBアクセス部分です。
変更前
"github.com/go-gorp/gorp"
_ "github.com/mattn/go-sqlite3"
:
:
db.Init()
Dbm = &gorp.DbMap{Db: db.Db, Dialect: gorp.SqliteDialect{}}
変更後
"github.com/go-gorp/gorp"
_ "github.com/go-sql-driver/mysql"
:
:
uri := read_conf("db.uri")
db_access_uri := "ユーザー名:パスワード@tcp(" + uri +")/データベース名"
db, err := sql.Open("mysql", db_access_uri)
if err != nil {
panic(err)
}
Dbm = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
主な変更点
- mysqlのdriverをインストールすることによってmysqlを扱えるようにしました。
- DataBaseサーバーのアドレスを
app.conf
から取得できるようにしました。 - mysql用のアクセスのコードに変えています。
使用したいSQLを変更したい場合は下記のSQLドライバーから選択し、下記の部分をそのドライバー用に書き換える必要があります。
_ "github.com/go-sql-driver/mysql"
Dbm = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
csvファイルを読み込んで、Databaseに反映させたい場合は下記のようなコードになります。
- csvファイルの読み込みます
- 読み込んだファイルをモデルファイルに反映します
- スライスに保存します
- スライスからデータを取得してData Baseに書き込み処理します
prev, err := filepath.Abs(".")
if err != nil {
defer fmt.Println("Error")
}
file, err := os.Open(prev + "/csv_diary_data/登録したいcsvファイル")
failOnError(err)
defer file.Close()
f_count := 0
reader := csv.NewReader(file)
var model []*models.定義したモデル名
for {
recoad, err := reader.Read()
if err == io.EOF {
break
} else {
failOnError(err)
}
//recordの数字はcsvファイルの列数に一致
tmp_list = append(tmp_list, &models.定義したモデル名{recoad[0], record[1],
recoad[2], recoad[3]})
f_count++
}
for _, model := range tmp_list {
if err := Dbm.Insert(model); err != nil {
panic(err)
}
}
##Json Web Token
Json Web Tokenを使用して認証処理をGo言語で行います。
まず認証用の鍵を作成します。
openssl genrsa -out demo.rsa 1024
openssl rsa -in demo.rsa -pubout > demo.rsa.pub
これで認証用の鍵を作成できました。
この鍵を認証を行なうコードと同一のフォルダにおきます。
認証処理は下記のコードになります。
2つの関数で構成されています。
- JWTトークンを取得して、値の整合性を判定する部分
- 公開鍵を読み込む部分
func CheckJWTHandler(api_token string) (bool){
tokenString := api_token
if tokenString == "" {
return false
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return LookupPublicKey()
})
if err != nil || !token.Valid {
fmt.Println("Failed")
return false
}
return true
}
func LookupPublicKey() (*rsa.PublicKey, error) {
prev, err := filepath.Abs(".")
if err != nil {
defer fmt.Println("Error")
}
key, _ := ioutil.ReadFile(prev + "/demo.rsa.pub")
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
return parsedKey, err
}
1の部分がややこしいので説明すると
下記のように”#_#”に置き換えます
jwt.Parse(tokenString, #_#)
そうすると取得したトークンと*の部分をParseして比較していることが分ります。
*を展開していきます。
jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {#_#})
ある関数から値を取得していることが分ります。
メソッドレシーバーとしてtoken *jwt.Token
を使用しています。
戻り値としてinterface
とerror
を取得することが目的です。
jwt.Parse(tokenString,
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return LookupPublicKey()
}
)
メソッドレシーバーを使用してtoken.Method.(*jwt.SigningMethodRSA)
の部分でトークン認証用のメソッドとして正しいかをチェックしています。
正しければreturn LookupPublicKey()
公開鍵を読み込んで、interface{}, error
に返しています。
実際に認証のトークンを作成する場合は下記のサイトのDebuggerを選択し、RS256を選択すると作成されるトークンの例が確認できるので、そこから先ほど生成した公開鍵と秘密鍵のデータを登録すればJWTトークンが作成できます。
Json Web Tokenの具体的な使用方法は下記をご覧下さい
時刻情報を付与したい場合は下記を使用すると簡単にunix timeが取得できます。
参考
RS256認証の鍵作成コマンド
ssh-keygen -t rsa -b 4096 -f jwtRS256.key
# Don't add passphrase
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
cat jwtRS256.key
cat jwtRS256.key.pub
#DeveLop
ここからはVagrant、Docker、CircleCiを使用した開発環境構築について記述していきます。
Go言語ならクロスコンパイラだし、バイナリ一つだからそれはいらなくないという指摘はもっともなのですが、下記の理由でDockerを使用しました。
- 仕組み上、commitしないとコードの確認できないため、commit単位が細かくなる
- 作成した環境が明示的
- マルチコンテナによりwebサーバーとDBサーバーを仮想的に分離してテスト可能
今回の構成の参考は下記です。
figを使用しているため、そこはDocker-composeに置き換えてもらう方が良いです。
##Vagrant
Vagrantfileの中身です。
今回は参考にしたリンクで動作させようとするとエラーが出たので私の場合は下記のVagrantfileにしました。
特別な設定は特になく、
- ipアドレスを指定して、ウェブサーバーの起動をローカルでも確認できるようにしている
- メモリを明示的にいくつ確保するか指定している点(メモリが足りなくてエラーが起きる場合もあるので)
程度です。
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Every Vagrant virtual environment requires a box to build off of.
config.vm.box = "お好きなlinuxディストリビューションを指定"
# Create a private network, which allows host-only access to the machine
# using a specific IP.
config.vm.network "private_network", ip: "指定したいIPアドレス"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "virtualbox" do |vb|
# Use VBoxManage to customize the VM. For example to change memory:
vb.customize ["modifyvm", :id, "--memory", "必要そうなメモリ数"]
end
end
このvagrantfileを使用して
vagrant up
vagrant ssh
を行なえばvagrantの環境に入れます。
vagrantの環境に入ったら下記コマンドを実行して下さい。
git clone https://github.com/ahawkins/docker-project-template.git
##Docker
私の場合はVagrant環境をUbuntuで構成したので下記リンクからVagrant上にDocker環境を構築しました。
通常のインストールではスーパーユーザーでしかDockerが使用できないので下記のコマンドで権限を与えた方がベターです。
sudo usermod -aG docker ユーザー名
今回はマルチコンテナを使用するのでDocker composeも必要になります。
下記コマンドを入力するとすぐにインストールできます。
バージョンは目的に応じて、指定して下さい。個人的には最新のバージョンで良いかと思います。
curl -L https://github.com/docker/compose/releases/download/バージョンを指定/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
参考
https://docs.docker.com/compose/install/
###Dockerfile
Dockerfileでgo言語の環境設定を行います。
先ほどgit clone
したdocker-project-template
の中に/dockerfiles/test
ファイルがあります。
これがDockerfileになっています。
FROM ubuntu:14.04
RUN apt-get update && \
apt-get install -y build-essential mercurial git subversion wget curl vim iptables sqlite3 libsqlite3-dev mysql-server libmysqld-dev rsync
# go tarball
RUN wget -qO- http://golang.org/dl/goのバージョン指定.linux-amd64.tar.gz | tar -C /usr/local -xzf -
# GOPATH
RUN mkdir -p /goprojects && \
mkdir -p /goprojects/bin && \
mkdir -p /goprojects/pkg && \
mkdir -p /goprojects/src && \
mkdir -p /goprojects/src/github.com
RUN mkdir /root/.ssh && chmod 700 /root/.ssh
# If you use the git lab, you have to need the below command
COPY id_rsa /root/.ssh/
COPY id_rsa.pub /root/.ssh/
RUN ssh-keyscan -t rsa gitlab.com >> /root/.ssh/known_hosts
RUN chmod 600 /root/.ssh/id_rsa
RUN cd /goprojects/src/github.com && \
git clone 自分のgithubもしくはgitlabのリンクを指定
RUN cd /goprojects/src/github.com/クローンしたフォルダ && \
git checkout -b ローカルブランチ リモートブランチ
# env vars
ENV GOPATH GOPATHを指定したいフォルダを指定
ENV PATH クローンしたフォルダ内に含まれるbinフォルダを追記:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
RUN go get github.com/revel/revel && \
go get github.com/revel/cmd/revel && \
go get github.com/dgrijalva/jwt-go && \
go get github.com/go-gorp/gorp && \
go get github.com/mattn/go-sqlite3 && \
go get golang.org/x/crypto/bcrypt && \
go get github.com/go-sql-driver/mysql
RUN git clone https://github.com/revel/samples.git $GOPATH/src/github.com/revel/samples
# 毎回pull処理を実行するために必要
ADD date.txt /data/
RUN cd /goprojects/src/github.com/クローンしたフォルダ && \
git pull
# COPY key
COPY demo.rsa.pub クローンしたフォルダのcontrallerにコピー
WORKDIR クローンしたフォルダをデフォルトのワーキングディレクトリに設定
RUN cd クローンしたcontrallerのフォルダに移動 && \
revel test 作成したrevel名 dev
適宜、必要な部分は読者の環境に合わせて記述し直して下さい。
私がハマった所を記述しておくと
下記のようにhostを登録しておかないとgitlabではhost認証時にエラーが出ます。
RUN ssh-keyscan -t rsa gitlab.com >> /root/.ssh/known_hosts
下記のコマンドは一見不要ですが、dockerはcacheの機能があるため、同じ操作はしてくれません。
これは高速化のために非常に有用なのですが、git pull
を使用する際は困ってしまいます。
そこで小手先の技術ですが、date
コマンドで毎回中身が変わるようなファイルを作成して、それをADDすることで、それ以降の処理のcache機能を無効化して処理を行なっています。
ADD date.txt /data/
ServerSpec
ServerSpecによってDocker環境が適切に設定されているか確認しましょう。
ServerSpecを動作させるためにローカルホストには下記の設定が必要です。
rubyの設定を下記のように行ないます。
sudo apt-get install -y rake
curl -sSL https://rvm.io/mpapis.asc | gpg --import -
curl -L get.rvm.io | bash -s stable
source ~/.profile
source ~/.rvm/scripts/rvm
#上記でrvmが動作しない場合
source /etc/profile.d/rvm.sh
rvm install ruby-2.2.2
gem install rake
gem install docker-api
gem install serverspec
spec_helper.rbの設定を行なってDocker環境をチェック出来るように設定します。
# coding : utf-8
require 'serverspec'
require 'docker'
#backend docker
set :backend, :docker
# docker_url setting DOCKER_HOST
set :docker_url, ENV["DOCKER_HOST"]
# SSL
Excon.defaults[:ssl_verify_peer] = false
後は通常のServerSpecと同様の処理なので下記を参照して環境チェックようのrubyを記述して頂けると環境チェックが行なえます。
【Docker】Serverspecを用いたDockerのテスト
少し工夫した点だけ抜粋しておきます。
下記でDockerの設定を行なうのですが、Dockerのキャッシュが設定されていない場合に時間が遅く、タイムアウトエラーで処理できないことがあります。
それを防ぐためにdefaultのタイムアウトの時間を遅く設定することでキャッシュがなくて動作が遅くても動作させています。
before(:all) do
# Seting Current Directory Dockerfile
Excon.defaults[:write_timeout] = 1000
Excon.defaults[:read_timeout] = 1000
image = Docker::Image.build_from_dir('.')
# Setting OS
set :os, family: :ubuntu
# docker image
set :docker_image, image.id
end
def os_version
command('cat /etc/issue').stdout
end
次に同様のチェック処理が入るときは配列を用意してチェックしています。
array = Array['git', 'wget', 'curl', 'vim', 'sqlite3', 'mysql', 'rsync']
for var in array do
it 'install check ' + var do
expect(command('ls -la /usr/bin').stdout).to match(var)
expect(file('/usr/bin/' + var)).to be_executable
expect(command(var + ' --version').stdout).to_not be nil
end
end
権限のチェック部分です。公式リファレンスだけだと私は分りにくかったので一応、載せておきます。
it 'mode check id_rsa' do
expect(file('/root/.ssh/id_rsa')).to be_mode 600
end
Docker compose
今回は開発環境をマルチコンテナでDBサーバーコンテナとWEbサーバーコンテナを用意する必要があります。
そのため下記のようなDocker compose用のymlを用意します。
mysql:
image: mysql
ports:
- "3306:3306"
volumes:
- myconf.d:/etc/mysql/conf.d
environment:
MYSQL_DATABASE: "データベース名"
MYSQL_ROOT_PASSWORD: "パスワード"
MYSQL_USER: "ユーザー名"
MYSQL_PASSWORD: "ユーザーパスワード"
web:
build: .
restart: always
links:
- mysql
ports:
- '80:9000'
このymlを用意して実行することでmysqlのコンテナが立ち上がり、mysqlのコンテナにひも付けらた形でdockerのコンテナも立ち上がります。
Go言語のrevelの設定
ここで一旦Go言語のrevelに戻って頂く必要があります。
DBを繋ぐための設定をrevel側でする必要があります。
app.confというファイルにその内容を記述します。
[dev]と[prod]で開発環境とdeploy後の環境を使い分けるように記述します。
[dev]
mode.dev=true
watch=true
module.testrunner=github.com/revel/modules/testrunner
db.uri = "vagrant上でifconfigで確認できるeth0のipアドレス:3306"
db.name = "データベース名"
[prod]
watch=false
module.testrunner=
mode.dev = false
db.uri = "rdsのエンドポイント:3306"
db.name = "データベース名"
##CircleCi
ここまでで環境構築のための準備が終わりました。
CircleCiによって継続的CIができるようにしましょう。
今回、使用するレポジトリにはすでにcircleciのための準備がされているので、ほとんどする作業はありません。
circleci.ymlの中身を確認します。
machine:
services:
- docker
dependencies:
pre:
- curl -L https://github.com/docker/fig/releases/download/1.0.0/fig-`uname -s`-`uname -m` > ~/bin/fig
- chmod +x ~/bin/fig
- make pull -j 4
- make environment
- make build
test:
override:
- make test-ci
今回はfig
を使用しないのでfig
の部分を無視すると実質的にmake test-ci
の処理を行なっているだけになります。
Makefileにtest-ci
の処理が記述されているので、それを今回用の処理に書き換えます。
今回はdocker composeを使用してマルチコンテナを実現するので下記を書き換えます。
変更前
DOCKER_RUN:=docker run -it $(foreach link,$(LINKS),--link $(REPO_NAME)_$(link)_1:$(link))
:
:
test-ci: images/tests
$(DOCKER_RUN) $(REPO_NAME)/tests
変更後
DOCKER_RUN:=docker run -i -t $(REPO_NAME)/tests:$(LINKS) bash
:
:
test-ci: images/tests
$(DOCKER_RUN)
単純に実行するだけに書き換えました。
今回はgit pull
を毎回実行するためにdate
コマンドでファイルの書き換えを行なう必要があるため実行する時は下記のようなコマンドになります。
date > date.txt && make test-ci
Docker Hub
コンテナのテストとアプリのテストが出来たコンテナのイメージをDocker hubに移しておきます。
Docker Hubの使用方法は下記をご覧下さい
Docker hubを使用することで環境設定とアプリケーションがテストされたコンテナを共有することができます。
#AWS
ここからはAWS環境における継続的インテグレーションの実現のための内容になります。
もう一度、図を見てここから行なう作業部分を確認してみましょう。
TerraformでAWSのインスタンスを設定して立ち上げて、そこからAnsibleで環境設定を行なう流れです。
##Terraform
まずterraformで環境構築を行ないます。
terraform初心者の方は下記をご覧下さい。
Step by stepで学ぶTerraformによる監視付きAWS構築
今回もsampleを参考に実装します。
このサンプルはほとんど変える必要はなく、
AWS-Auto Scale Groupのvariables.tfのみ変更したら良いです。
環境によって変わる可能性がありますが、私のケースではus-east-1d
を削除したら動作しました。
変更前
variable "availability_zones" {
default = "us-east-1b,us-east-1c,us-east-1d,us-east-1e"
description = "List of availability zones, use AWS CLI to find your "
}
変更後
variable "availability_zones" {
default = "us-east-1b,us-east-1c,us-east-1e"
description = "List of availability zones, use AWS CLI to find your "
}
あとは基本的なキーの登録を行なっておけば動作します。
キーの登録などが分らない方はStep by stepで学ぶTerraformによる監視付きAWS構築はご覧下さい。
キーの作成で注意する点はリージョンです。
us-eastのリージョンで作成しないと動作しないので注意して下さい。
キーについては一つ罠があります。
例えば"hoge.pem"の鍵を登録する場合は下記のように"pem"を外して登録する必要があります。
variable "key_name" {
description = "Name of the SSH keypair to use in AWS."
default = "hoge"
}
##Ansible
Ansibleの初心者は公式ドキュメントと下記をご覧になってから読まれた方が良いです。
インフラ自動構築エンジン "Ansible"の勘所を1日でつかむ ~基礎入門編~
Ansibleについてですが、Dockerの設定のみを行い、Docker hubから必要なコンテナを取得するようにします。
こうすることで環境差が生まれず環境設定とアプリケーションがテストされた環境をAWS上に実現できます。
ansibleで最初にはまる部分は再起動の部分になるので、その部分は記述しておくので良かったら参考にしてください。
task用
- name: Restart the server
shell: reboot now
sudo: yes
- name: Wait until the virtual machine stop ssh port stop responding
local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped
sudo: false
- name: Wait for server to come up
local_action: wait_for host={{ inventory_hostname }} port=22 delay=30
sudo: false
inventory_hostname変数を設定する用
vars設定用
inventory_hostname: 対象のサーバーのipアドレス
参考
http://d.hatena.ne.jp/incarose86/20150215/1424017177
#総括
現状、流行っている技術を一通り抑えれたので良かったと思いますが、本来あるべき姿はなぜこの技術を使用するか学習コストは見合っているのかを考えてやるべきです。
今回の枠組みで最も良いと思ったのがcommitメッセージを細かくする環境にしたことです。それによりcommitの単位が細かくなり、commitメッセージの書き方にそって書くようにすると考える癖が自然につき、良い習慣になると思いました。
何か仕事をしたり興味あるものを見つけたとき、
簡単にあきらめないで「もう少し考えよう」
もう一工夫してみよう」と意識的に
自分を駆り立てることが重要である。
簡単なことではないが。磯貝芳郎
この言葉を旨に工夫できる点を見つけては取り入れていこうと思います。