忙しい人向けのserverspec入門 ~HelloWorld編~

目的

serverspecを使ってみたいけど、勉強する時間がなかなか取れず、使い始めるのを躊躇している人のために、 とりあえずserverspecの便利さを体験してもらいたい。

動機

serverspecを使ってみたいけど、導入するための準備がなかなか進まない、という話を最近よく耳にするのがきっかけ。

serverspecは普段ansibleで作ったLinux環境をテストするのに頻繁に使っているので、個人的には使い手が 増えてほしいツールの一つなのだが、確かに、準備をするには面倒な作業が必要になる。 そのため、敷居を高く感じてしまう人もいると思う。

全く何もないWindows環境でserverspecを動かせる環境を作るには、少なくとも下記のステップを踏まなければならない。

  1. Linuxサーバーを少なくとも2台(serverspecを実行するサーバーと、テスト対象のサーバー)用意する

  2. serverspec を実行するサーバーに、ruby,gem, bundler をインストールする

  3. servespec を実行するサーバーから、テスト対象のサーバーにSSHで接続できるようにする

  4. ruby(正確にはrspec)でテストコードを書く

  5. rake コマンドでserverspecを実行

といった感じである。 Linuxrubyを使い慣れている人にとってはなじみの作業が多いが、慣れていない人には、hello world するだけでも かなりの時間と労力を要する作業になってしまう。

ただ、ここで挫折して、serverspecを使わずに手作業で、リリースのたびに細かい環境テストを実行するのは非常に時間がもったいないし、 管理するサーバーの台数が増えればミスも増えてくるだろうから、お客さんにとっても、開発者にとっても、不幸な結果を 生みかねない。 単純な作業はserverspecに肩代わりしてもらって、余った時間を別の作業に充てたほうが、双方ハッピーになれるはずである。

なので、上記の作業に慣れていない人でも30分ぐらいで HelloWorldにこぎつけられるようにして、気軽にserverspecの導入を検討してもらえるようになってほしい。

動作環境 バージョン情報

Windows10

Vagrant2.0

VirtualBox 5.1.26

前提

Vagrant,VirtualBoxはインストール済みである前提です。(インストール方法は公式サイトを見てください。ごめんなさい)

やること

1. Linux サーバーを2台用意する & 2. serverspec を実行するサーバーに、ruby,gem, bundler をインストールする

いつも通り、下記のVagrantfileを作成して、VirtualBox仮想マシンを2台作ります。

Vagrant.configure(2) do |config|
  SERVERS.map do |hostname, ipaddress|
    config.vm.define hostname.to_s do |conf|
      conf.vm.provider "virtualbox" do |vb|
        vb.name = hostname.to_s
        vb.memory = "2048"
      end
      conf.vm.box = "bento/centos-7.2"
      conf.vm.hostname = hostname.to_s
      conf.vm.network "private_network", ip: ipaddress

      conf.vm.provision :shell, inline: <<-SHELL 
      for interface in $(grep 'NM_CONTROLLED=no' /etc/sysconfig/network-scripts/ifcfg-* -l)
      do
        sudo sed -i -e 's/NM_CONTROLLED=no/NM_CONTROLLED=yes/' $interface
        sudo ifdown $interface
        sudo ifup $interface
      done
      SHELL

      conf.vm.provision :shell, inline: <<-SHELL if hostname.to_s == 'serverspec'
        sudo yum install -y git
        sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv
        sudo git clone https://github.com/sstephenson/ruby-build.git /usr/local/rbenv/plugins/ruby-build
        sudo touch /etc/profile.d/rbenv.sh
        sudo chmod 777 /etc/profile.d/rbenv.sh
        echo 'export RBENV_ROOT="/usr/local/rbenv"' >> /etc/profile.d/rbenv.sh
        echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"'  >> /etc/profile.d/rbenv.sh
        echo 'eval "$(rbenv init -)"'  >> /etc/profile.d/rbenv.sh
        sudo chmod -R 755 /usr/local/rbenv
        sudo yum install -y gcc openssl-devel readline-devel zlib-devel bundler
        sudo -i rbenv install 2.4.2
        sudo -i rbenv global 2.4.2
        sudo -i -u vagrant mkdir /home/vagrant/serverspec
        sudo -i -u vagrant ssh-keygen < <(echo; echo)
      SHELL

      conf.vm.provision :shell, inline: <<-SHELL if hostname.to_s == 'webserver'
        sudo yum install -y httpd
        sudo systemctl start httpd
        sudo systemctl enable httpd
      SHELL
    end
  end
end

SERVERS = {
  serverspec: '192.168.56.200',
  webserver: '192.168.56.201',
}

上記のVagrantfile を適当なディレクトリに保存したあと、コマンドプロンプト(PowershellWindow)でそのディレクトリにcdして、vagrant upコマンドを実行すれば、VirtualBox上にLinuxサーバーが2台出来上がります。

出来上がったサーバーの簡単な情報を下表に示します。

# サーバー名 概要 IPアドレス ログインユーザー パスワード
1 serverspec サーバースペックを実行するサーバーです。ruby, gem, bundler はインストール済みです。 192.168.56.200 vagrant vagrant
2 webserver テスト対象のサーバーです。apache httpdがインストール済みで、サービスが立ち上がっています。 192.168.56.201 vagrant vagrant

出来上がったサーバーには、TeraTerm等のSSHクライアントを使ってアクセスしてください。

3. servespec を実行するサーバーから、テスト対象のサーバーにSSHで接続できるようにする

テスト対象のサーバーにSSHで接続するには、テスト対象のサーバーの~/.ssh/authorized_keys に、serverspecサーバーの公開鍵情報を 書き込んであげる必要があります。 コピペでやってもいいですが、せっかくなので、 ssh-copy-id を使います。 serverspec サーバーにSSH でログインしたら、下記のコマンドを実行しましょう。

 ssh-copy-id -i ~/.ssh/id_rsa vagrant@192.168.56.201

上記のコマンドを実行すると、2回英語で質問されるので、1回目はyes 2回目は、vagrantユーザーのパスワードをそれぞれ答えてください。

終了すると、webserver の /home/vagrant/.ssh/authorized_keys に、serverspecサーバーの公開鍵情報がめでたく書き込まれます。 ssh-copy-idコマンドは、ディレクトリの作成やパーミッションの設定などを適宜やってくれるので、コピペに比べ、ミスも少なく安心です。

SSHで接続できることを確認するには、serverspecサーバー上で下記のコマンドを実行してください。

ssh vagrant@192.168.56.201 : ;echo $?

このコマンドを実行して、標準出力に0と表示されれば設定は成功しています。 パスワードを聞かれたり、0以外の数字が出力され場合は、ssh-copy-id コマンドを再度実行してみるか、webserverの/home/vagrant/.ssh/authorized_keysに serverspecサーバーの公開鍵情報をコピペしてください。

4. テストコードを書く

これでやっとserverspecを実行するための前提条件が整いました。

ここからは、『webserverにapacheがインストール済みであることを確認するテスト』を実行するまでの手順を説明します。

まず、serverspecサーバーで、下記コマンドを実行してください。

cd ~/serverspec
sudo -i gem install serverspec rake

コマンドが成功したら、下記コマンドを実行して、必要なファイルを生成します。

serverspec-init

いくつか質問されるので、下記の通り答えましょう。

Select OS type:

1) UN*X
2) Windows

Select number: 1

Select a backend type:

1) SSH
2) Exec (local)

Select number: 1

Vagrant instance y/n: n

Input target host name: 192.168.56.201

ここまで出来たら準備完了。

下記のようなファイル、ディレクトリが出来上がるはずです。

/home/vagrant/serverspec/  
   |  
   |- Rakefile  
   |- spec_helper.rb  
   |- spec/  
         |- 192.168.56.201/  
                 |- sample_spec.rb  

ここから、sample_spec.rbを編集して、テストをすることになるのですが、serverspec-init コマンドで作成したsample_spec.rbには、 幸いにも、「webserverにapacheがインストール済みで、80番ポートで待ち構えていることを確認する」という内容のテストが すでに記述されています。

ので、ひとまず実行します。

rake spec

コマンドを実行すると、オールグリーンでテストが終了します。

※webserverのapacheを止めたり、アンインストールしたりすると、それぞれテストが失敗するので、やってみてください。

やっていること。。。の説明は次回

前置きが長くなってしまったので、serverspecが実行している処理の中身や、制御方法については次回解説します。

awspecを使って、AWSリソース(セキュリティグループ)のテストを自動化してみる

動機

前回に引き続き、AWS環境周りのテスト。 LocalStackで動作確認、motoでmockテストができるようになったところで、本番環境向けにテストしてみたいなと思ってた矢先、 プロが awspec

github.com

という、AWSリソースのテスト(assertion機能付き)フレームワークがあると教えてくれたので、早速使ってみた。

バージョン情報

Windows10

Vagrant2.0

VirtualBox 5.1.26

CentOS 7.2

ruby 2.4.2

ゴール

こんな感じのセキュリティグループを以下の観点でテストしてみたい。

1.Name タグが sampleであること

2.22番ポートはIPアドレス 10.10.10.10 からのみアクセス可能(のみ、が重要)

3.3389番ポートはどこからでもアクセス可能(!)

f:id:yamatatsu-blog:20180205225333p:plain

事前準備

公式手順に従って、ruby gem から awspecをインストールするだけなので省略。。。したいところだが、ただ実行するだけだと、公式ドキュメントの丸パクリ(というか劣化版)になってしまう。 ので、インフラエンジニアの端くれとして多少の差別化をはかり、(申し訳程度に)Vagrantfileを添付することにする。

Vagrant.configure(2) do |config|
      config.vm.provider "virtualbox" do |vb|
        vb.name = "awspec"
        vb.memory = "2048"
      end
      config.vm.box = "bento/centos-7.2"
      config.vm.hostname = 'awspec'
      config.vm.network "private_network", ip: "192.168.100.201"

      config.vm.provision :shell, inline: <<-SHELL 
      for interface in $(grep 'NM_CONTROLLED=no' /etc/sysconfig/network-scripts/ifcfg-* -l)
      do
        sudo sed -i -e 's/NM_CONTROLLED=no/NM_CONTROLLED=yes/' $interface
        sudo ifdown $interface
        sudo ifup $interface
      done
      SHELL

      config.vm.provision :shell, inline: <<-SHELL
        sudo yum install -y git
        sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv
        sudo git clone https://github.com/sstephenson/ruby-build.git /usr/local/rbenv/plugins/ruby-build
        sudo touch /etc/profile.d/rbenv.sh
        sudo chmod 777 /etc/profile.d/rbenv.sh
        echo 'export RBENV_ROOT="/usr/local/rbenv"' >> /etc/profile.d/rbenv.sh
        echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"'  >> /etc/profile.d/rbenv.sh
        echo 'eval "$(rbenv init -)"'  >> /etc/profile.d/rbenv.sh
        sudo chmod -R 755 /usr/local/rbenv
        sudo yum install -y gcc openssl-devel readline-devel zlib-devel
        sudo -i rbenv install 2.4.2
        sudo -i rbenv global 2.4.2
        sudo -i gem install awspec
        sudo yum install -y epel-release
        sudo yum install -y python-pip
        sudo pip install pip --upgrade
        sudo pip install awscli
      SHELL
end

、、、上記のVagrantfileで作った環境にSSHでログインしたら、aws configure を実行して、credentialを登録。 そのあと、 awspec init を実行し、spec/seacrets.yml を下記のように記述すれば準備完了。

region: ap-northeast-1
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

実行するコード

spec/security_group_spec.rb を下記のように記述する。

require 'spec_helper'
describe security_group('セキュリティグループID') do
  it { should exist }
  it { should have_tag('Name').value('sample') }
  its(:inbound) { should be_opened_only(22).for('10.10.10.10/32')}
  its(:inbound) { should be_opened(3389).protocol('tcp').for('0.0.0.0/0')}
end

結果

上記のコードを保存したら、Rakefileのあるディレクトリで、rake spec を実行すればテスト完了。 下記のようにオールグリーンのコンソールが出ればO.K.

f:id:yamatatsu-blog:20180205231808p:plain

やってること

記述や実行方式は、serverspecやrspecと同じ。

1. have_tag を用いて、タグのチェックを行っている。

2. be_opened_only を用いて、22番ポートは、10.10.10.10/32から「のみ」アクセスを許可していることをチェックしている。

3. be_opened を用いて、0.0.0.0/0 に対してアクセスを許可していることをチェックしている。

感想

serverspecに慣れ親しんでいる身としては、この方式(環境も)でテストが書けるのはうれしい。

作成した環境の内部の設定はserverspecで今までチェックできたが、セキュリティグループが「許可されている」ことは確認できても、 「余計なものが許可されていない」ことを確認するのは難しかった。 セキュリティグループに限らず、テストの範囲をEC2インスタンスの内部だけでなく、リソース全体に広げることができるので、テストの質が上がりそう。

また、前回、前々回の記事と併せて考えると、下記のような感じで環境構築作業、確認作業が自動化できそう。

(実装フェーズ)

1.LocalStackでCloudFormation や リソース作成の動作確認

2.motoでboto3スクリプトの自動テスト

(テストフェーズ)

3.awspecでリソース作成確認

4.serverspecでサービスの動作確認

どのツールがどの程度まで品質を担保できるものなのかは、もうちょっと使ってみてからまたブログに書きます。

蛇足 ~ be_opened_only のちょっと意地悪な例 ~

be_opened_onlyの揚げ足を取るわけではないのだが、下記のような設定のセキュリティグループ(22番ポートにたいして、10.10.10.9/32と10.10.10.10/32 のIPアドレスを2つ列挙)に対して、10.10.10.8/30 からのみ許可しているかどうかテストすると、感覚的にはテスト成功しそうだが、実際はテスト失敗になる。(ブロードキャストアドレスと、ネットワークアドレスを入れても同様)

f:id:yamatatsu-blog:20180205234850p:plain

require 'spec_helper'
describe security_group('sg-eb68a092') do
  it { should exist }
  it { should have_tag('Name').value('sample') }
  its(:inbound) { should be_opened_only(22).for('10.10.10.8/30')}
  its(:inbound) { should be_opened(3389).protocol('tcp').for('0.0.0.0/0')}
end

。。。しかし、こんな「ポリシーがあるのかないのかよくわからないセキュリティポリシー(笑)」をテストするようなこともないだろうし、あったとしても、本来NGになるべきものがOKになっているわけではないので、無視していいでしょう。

motoをつかってboto3(AWS PythonSDK)スクリプトのテストを自動化する

動機

前回、LocalStackを使ってboto3のテストを実行してみて、リソース操作系スクリプトの動作確認を気軽に行えるようになった。

しかし、LocalStackは、EC2などのサービスには(2018年2月現在)未対応であったり、異常系のテストがエミュレートできないものが あったりと、現場で使うにはまだ改善点が多いことが分かったので、別の方法も考えてみることに。

で、情報を探してみたら、moto github.com

、、、というboto3のエミュレートを行えるライブラリを発見したので、使ってみた。

バージョン情報

Python 3.6.3

boto3 1.4.7

moto 1.2.0

対象のスクリプト

こんな感じの、「S3バケットを作成し、コマンドの実行結果を標準出力に出力する」ような スクリプトのテストを考えてみる。

import boto3
from botocore.exceptions import ClientError

"""
  S3 にバケットを作成します。
"""

bucket_name = 'kudarizakawonobore'

SUCCESS = 0
ERROR = 1

def main():
    client = boto3.client('s3')
    try:
        client.create_bucket(Bucket=bucket_name ,
                             CreateBucketConfiguration={
                                 'LocationConstraint': 'ap-northeast-1'
                                 }
                             )
        print('SUCCESS: バケットを作成しました')
        return SUCCESS
    except (ClientError) as e:
        print(e.response['Error']['Message'])
        error_code = e.response['Error']['Code']
        if error_code == 'BucketAlreadyExists':
            print('ERROR: 他の人が使っている名前です')
            return ERROR
        else:
            print('予期せぬエラー')
            raise e

if __name__ == '__main__':
    response = main()
    exit(response)

テスト側

上記スクリプトを、正常系、異常系含めてテストするには、下記のようなテストを書く。

import boto3
import src.create_bucket as target
import unittest
from io import StringIO
from unittest.mock import patch
from moto import mock_s3

class TestCreateS3(unittest.TestCase):

    @mock_s3
    def test_main(self):
        """正常系"""
        with patch('sys.stdout', new_callable=StringIO) as out:
            self.assertIs(target.main(), target.SUCCESS)
            print(out.getvalue())
            self.assertTrue(out.getvalue().find('SUCCESS:'))

    @mock_s3
    def test_main_error_bucket_already_exists(self):
        """すでに存在する場合はメッセージを表示して、ERRORコードを返す"""
        client = boto3.client('s3')
        client.create_bucket(Bucket=target.bucket_name,
                             CreateBucketConfiguration={
                                 'LocationConstraint': 'ap-northeast-1'
                             }
                             )
        with patch('sys.stdout', new_callable=StringIO) as out:
            self.assertIs(target.main(), target.ERROR)
            self.assertTrue(out.getvalue().find('ERROR: '))

if __name__ == '__main__':
    unittest.main()

やってること

motoを使うために必要な操作は二つで、テストケースでfrom moto import mock_s3motoをインポートするのと、 テスト用のメソッドに、 @mock_s3 とデコレータを付与すること。これだけ。

@mock_s3 のデコレータを付与されたメソッド内では、S3 関連の操作がすべてモック化される。 正常系のテストtest_mainでは、S3バケットが一つも存在しない空間にS3のバケットを作成しにいくので、正常終了する。

一方、異常系のテストtest_main_error_bucket_already_existsでは、一度同名のバケットを作成してからスクリプトを 実行しているので、ClientErrorが発生して異常終了する。

ちょっとした問題点

異常系のパターンをすべて網羅しているわけではない。

例えば、S3のCreateBucketでは、BucketAlreadyOwendByYou のエラーが発生するパターンがエミュレートできない。

具体的にいうと、本来 motoを使わずスクリプトを2回実行する(すでに存在するバケットを再度作成する)と、BucketAlreadyExists(他の人が使っている)ではなく、BucketAlreadyOwnedByYou(自分がすでに所有している)エラーが発生するべきである。しかし、test_main__error_bucket_already_exists で記述している通り、同一のコンテキストでバケットを2回作成したとしても、発生するエラーはBucketAlreadyExists となってしまう。 (motoのソースコードを少し見てみたが、 BucketAlreadyOwendByYouのエラーを発生させることはできなさそう。。。)

そのため、create_bucketを行う前に、同名のバケットが存在しないか事前チェックを行うなど、実装側の方でカバーしたり、テストの書き方を工夫する必要がある。

LocalStackとの住み分け

このぐらい単純なスクリプトのテストだと、motoの方が簡単にテストできるので優れていそうだが、大量のリソースを操作するスクリプトや、CloudFormationの動作確認などでは、ダッシュボード機能でリソースの状態を確認できるLocalStackの方が活躍する場面が増えてくる(と思う)

どちらもまだ使いこなせていないので、引き続き学習していきます。

LocalStackを使ってPythonSDK(AWS)の動作確認をしてみた

概要

AWS環境構築用にboto3 や AWS CLIを使っているのだが、動作確認するだけで自分の環境が汚れてしまうのがどうも気にくわない。

また、スクリプト自体はunittest や shunit2 で単体テストを回して品質を担保したいが、一番重要なAWSコマンド実行の部分を動作させられないといまいち。

環境を汚さず品質担保するうまい方法はないかプロに聞いてみたら、LocalStackというのがあるからやってみれ、とのことだった。 ので、ひとまず使ってみた。

ゴール

WindowsVirtualBox上に、LocalStackのサーバーを立てて、LocalStackに向けてS3のバケット作成コマンド実行する。

バージョン情報

Windows10

VirtualBox 5.1.30

Vagrant 2.0

Python3.6

結論

LocalStack仮想サーバーを構築する

下記のVagrantfileをローカルの適当なディレクトリに作成して、vagrant up を実行する。

(IPアドレス、ポート、メモリサイズ等は環境に合わせて変更してください)

Vagrant.configure(2) do |config|
      config.vm.provider "virtualbox" do |vb|
        vb.name = "local_stack"
        vb.memory = "2048"
      end
      config.vm.box = "bento/centos-7.2"
      config.vm.hostname = 'localstack'
      config.vm.network "private_network", ip: "192.168.100.201"

      config.vm.provision :shell, inline: <<-SHELL 
      for interface in $(grep 'NM_CONTROLLED=no' /etc/sysconfig/network-scripts/ifcfg-* -l)
      do
        sudo sed -i -e 's/NM_CONTROLLED=no/NM_CONTROLLED=yes/' $interface
        sudo ifdown $interface
        sudo ifup $interface
      done
      SHELL

      config.vm.provision :shell, inline: <<-SHELL
        sudo yum install -y docker
        sudo systemctl start docker
        sudo systemctl enable docker
        sudo -i docker run -itd -p 4567-4583:4567-4583 -p 8080:8080 localstack/localstack
      SHELL
end

ブラウザで http://192.168.100.201:8080 にアクセスして、下記のような画面(ダッシュボード)が表示されれば、LocalStackの構築は完了。

f:id:yamatatsu-blog:20180124224202j:plain

LocalStackに向けて create bucketを実行する

以下のようなPythonコードを実行する。 boto3のインストール方法については割愛。

import boto3 as bt3

client = bt3.client('s3', endpoint_url='http://192.168.100.201:4572')
client.create_bucket(Bucket='kudarizakawonobore-bucket')

実行成功したら、 LocalStackのダッシュボードを確認すると、S3のバケット作成が行われたことが確認できる。

f:id:yamatatsu-blog:20180124225121j:plain

やってること

vagrantのプロビジョニングでdocker のインストールとコンテナの構築を実施。
コンテナ作成時にポート4567~4583 までを仮想マシンのポートとつなげておけば、仮想マシンのポートをエンドポイントに指定するだけで応答を返してくれる。
(ダッシュボードは8080)

vagrantが使える環境であれば割と簡単に実行できる。

課題

今回はとりあえず動かしてみただけだが、単体テスト実行時だけは LocalStackを向いて、本番実行するときは所望の環境に向いてくれるように設定するのが好ましい。 やり方についてはこれから検討予定。

プロビジョニングツールのスレーブノードに公開鍵をいっせいに配布する

動機

サーバープロビジョニングにansibleやchefを使う前準備として、hostサーバーの公開鍵を複数台のスレーブノードに配布する必要がある。
ノードの数が2~3台程度なら、手順書を作って、各サーバーにsshログインして、エディタでコピペ、という手順でも大した苦にならないが、 サーバーの数が増えてくると、手順書を作ったり、実行したりするだけでもかなりの時間を食ってしまう。

目的

ansibleやchefのスレーブノードに、hostサーバーの公開鍵をコマンド1発で配布できるようにする。 (あと、手順書を簡略化する。)

バージョンとか

クライアントPC : Windows10
ansibleホスト・スレーブ : CentOS7.2

前提

クライアントから各スレーブノードにsshのパスワード認証ログインができる。
クライアントPCでbash,ssh,sshpassが使える。(sshpassはwindows Bashのapt-getでインストールできる。)

結論

sshリモートコマンドでばらまく。
Windows10からはコマンドプロンプトからbashが使えるので、下記のようなBashスクリプトを書いて実行する。 (ネットワーク設定が適切にされていれば、VirtualBox上のLinux仮想マシンでも同様のことができるはず。)

#!/bin/bash
read key < id-rsa.pub
while read ip user pass;do
  sshpass -p $pass ssh ${user}@${ip} "echo $key >> ~/.ssh/authorized_keys" < /dev/null
done <<SERVER
192.168.XXX.XXX user1 pass1
192.168.YYY.YYY user2 pass2
SERVER

※ id-rsa.pub はプロビジョニングツールホストで作成した公開鍵。

ログインするユーザーと、スレーブノードのユーザーが異なる場合は、こんな感じ。 (ログインユーザーがnopassでsudo実行できる前提)

#!/bin/bash
read key < id-rsa.pub
while read ip loginuser loginpass user ;do
  sshpass -p $loginpass ssh ${loginuser}@${ip} "sudo -u $user bash -l" <<<"echo $key >> ~/.ssh/authorized_keys"
done <<SERVER
192.168.XXX.XXX loginuser1 password1 user
192.168.YYY.YYY loginuser2 password2 user
SERVER

やってること

key 変数に公開鍵情報を格納し、sshリモートコマンドで、authorized_keysに追記リダイレクトしている。 ログインユーザーとスレーブノードユーザーが異なる場合は、チルダ展開されるタイミングを、 sudo bash -l環境変数が初期化された後に調整する必要があるため、ヒアストリングでコマンドを渡すようにしている。

補足

踏み台サーバーを経由するような構成の場合は、少し変更が必要。

踏み台サーバーからスレーブノードへssh公開鍵認証が設定済みの場合

sshの公開鍵認証が通っているような構成であれば、sshpassを使わず、同様のコマンドで鍵をばらまくことができる。

#!/bin/bash
read key < id-rsa.pub
while read ip loginuser user ;do
  ssh ${loginuser}@${ip} "sudo -u $user bash" <<<"echo $key >> ~/.ssh/authorized_keys"
done <<SERVER
192.168.56.101 loginuser1 user
192.168.56.102 loginuser2 user
SERVER

踏み台サーバーからスレーブノードへパスワード認証でログインする構成で、かつ、rbashでコマンド制限されている場合。

この場合は、ターミナルのマクロを使って実行する必要がある。(後日githubで公開予定)

注意

authorized_keysへの追記リダイレクト>>を、間違えて上書きリダイレクト>にしてしまうと、 踏み台サーバーからログインできなくなって大変なことになるので、扱いは慎重に。

蛇足

書き込み完了ごとに、確認を入れたり、継続/終了を判断したい場合は、こちらの記事を参考にして、 プログラムを拡張してください。

bashのselect文を使ってwhile readの自動処理を半自動化する

目的

bash の while read 文を実行しているとき、決まった場所で処理を中断して、 続行か否かを判断する処理を挟めるようにする

動機

while read文で処理を回している最中、特定の場所で処理を中断して、目視による 確認手順を差し込みたい場合がある。
そういうときに、ループを回しながら、確認を求めてくる、「半自動化」みたいな処理が できると便利だったりする。

結論

bash の select文を使って、こんな感じのコードにする

#!/bin/bash
exec 254<&0
while read PARAM;do
  echo $PARAM # ループしたい処理
  select answer in "continue" "exit";do
    case "$answer" in
        continue) break ;;
        exit)     exit  ;;
    esac
  done <&254
done <<PARAM
param1
param2
param3
PARAM

上のコードは、echo文でparam1,param2,param3を順次標準出力に出力していくループの 中で、次のecho文を実行するか、中断するか、確認を求めてくれる。

解説(やってること)

while read 文でヒアドキュメントを読み込んで順次処理を実行している中で、select文を読み込んであげているだけなのだが、一つだけ注意しなければいけない点がある。

ファイルディスクリプタの複製を使う

大事なのが、2行目の exec 254<&0 と、11行目の done <&254
このふたつをやっておかないと、while read文の中でselect文を実行しても、キーボードからの入力を求めてくれないばかりか、ループ処理もparam1を処理しただけで終わってしまう。

これは、while read文のdoからdoneまでの間に実行されるプロセスの、ファイルディスクリプタの0番(標準入力)が、 12行目のdone <<PARAMによって、ヒアドキュメントに切り替えられているために起きる現象である。(select文が、ヒアドキュメントの方を読み込みに行ってしまう)

なので、事前にキーボード入力のファイルディスクリプタを、別の番号(ここでは254番)に複製しておいてあげて、 select文を実行するときに、あらかじめ複製しておいたファイルディスクリプタを指定してあげれば、select文の実行時にキーボードから入力を求めてくれるようになる。

応用編

(いまどき使う場面があるか不明だが)たとえば↓みたいなコードを使えば、postgreでSQLを順次実行しながら、 コミットするかロールバックするか確認を取ってくれるようになる。

#!/bin/bash

# 事前処理(オートコミットをOFFにする)
prepare(){ echo "\set AUTOCOMIIT off";}

# トランザクション開始
begin(){ echo "BEGIN;"; }

# コミットして続行か、ロールバックか、任意のコマンドを実行するか選ばせる
menu(){
  select cmd in "continue" "rollback" "exit";do
    if [ -z "$cmd" ];then
      echo "$REPLY"
      continue
    fi
    case "$cmd" in
      continue)
        echo "COMMIT;"
        return 0
        ;;
      rollback)
        echo "ROLLBACK;"
        echo "\q"
        return 1
        ;;
      exit)
        echo "\q"
        return 1
        ;;
    esac
  done <&254
}

# update_tbl1.sql, update_tbl2.sql(同一トランザクション)を実行したのち、
# 確認を求めてからupdate_tbl3.sqlを実行する
output_sql(){
  prepare
  while read sqlfiles;do
    cat <(begin) $sqlfiles
    echo "done $sqlfiles !" >&2
    menu || return 1
  done <<FILES
  update_tbl1.sql update_tbl2.sql
  update_tbl3.sql
FILES
}

# ファイルディスクリプタを複製する
exec 254<&0

# 標準出力にSQLを出力する
output_sql

上で作ったsample.shを、下記のように、psqlコマンドに渡してあげれば、psqlで順次SQLを実行してくれる。

$ bash sample.sh | psql -U USER -d DBNAME

下記のような選択肢が出てくるので、コミットして続行する場合は1を、ロールバックして終了する場合は2を選択する。 番号ではなく、例えばselect * from tbl1;みたいなコマンドを入力すると、その通り実行してくれる。

1) continue
2) rollback
3) exit

psqlの標準出力と、select文のプロンプトが混ざって見づらい場合は、
多少めんどくさいが、psql側の標準出力をファイルにリダイレクトしてあげて、
別ターミナルからtail -f コマンドなどでみるようにするといい

$ bash sample.sh | psql -U USER -d DBNAME > dbconsole 2>&1
$ tail -f dbconsole

蛇足

実行したSQLの記録を取りたい場合は、間にteeコマンドを挟んであげる

$ bash sample.sh | tee backup.sql | psql ...

CSVファイル中のカラム数が異なるレコードをLinux上で手軽に検出する

目的

CSVファイルの中にある、不正なレコード(カラム数違い)を、できるだけ手軽に検出、修正する

動機

データの初期投入とかで、CSVからデータをインポートしたいが、ファイルの編集ミスやら、
エスケープ漏れなどで、うまくインポートができなくなってしまうことがある。
こういうとき、プログラムの改修やらログレベルの変更やらに頼らずに、簡単なコマンドだけで
不正なレコードを検出して、vimとかでちゃちゃっと直してしまいたい。
せっかくサーバーにあげたのに、エクセルで編集して、SCPで再転送とか、マジで勘弁してほしい

アプローチ

行ごとのカラム数を数えて、カラム数が異なる行を探す

結論

CSVのレコードがすべて正しい(カラム数が同一)ときは、下記のコマンドで「正しいこと」は確認できる

$ awk -F, '{print NF}' < sample.csv | uniq | wc -l
-> 1

このコマンドの実行結果が1であればすべてのカラム数が同じ。2以上なら、何かしら不正なレコードがある ことがわかる。

例えば、下記のように、3行目に不正なレコード(カラム数が5)を含んでいるCSVファイルだと、 結果は1にならない。

1,aaa,bbb,ccc
2,ddd,eee,fff
3,ggg,hhh,iii,jjj
4,kkk,lll,mmm
$ awk -F, '{print NF}' < sample.csv | uniq | wc -l
-> 3

この時、何行目が不正なレコード、つまり、カラム数が4でないレコードであるかを知るには、
下記のコマンドを実行すればいい。

$ awk -F, '{print NF"::"NR}' < sample.csv | grep -v '^4::'
-> 5::3

⇒ 出力が、 ‘5::3’ と出たので、3行目に、カラム数5のレコードが存在していることがわかる。

レコードも一緒に見たい場合は、$0 も出力してやる

$ awk -F, '{print NF"::"NR, $0}' < sample.csv | grep -v '^4::'
-> 5::3 3,ggg,hhh,iii,jjj

解説(やってること)

awk の F オプションにカンマを指定することで、awk に渡す文字列のデリミタをカンマに変えることができる。
そのため、awk -F, '{print NF}'としてあげると、 「カンマ区切りで行を分割し、1行に含まれるフィールドの数を表示する」という動きになるので、
1行毎のカラム数を標準出力に出力してくれる。( NF は、フィールド数)

カラム数が正しいレコード(カラム数4)であれば、 “4:: ” から始まるはずなので、これをgrep -v で
除外してあげれば、不正なレコードの行数だけを取得することができる。

応用編

ダブルクォートでカンマをエスケープしているCSVの場合

こんなCSVの場合

"aaa","bbb","ccc"
"ddd","eee","fff"
"ggg","hhh","iii"
"jjj","kkk","lll,mmm"

デリミタを , ではなく、"," にしてやる。(空レコードもダブルクォートでくくらていることを想定)

$ awk -F'","' '{print NF}' < sample.csv | uniq | wc -l
-> 1

タブ区切り、空白区切りの場合

タブ区切り(tsv)の場合は、ちょっとだけ工夫してあげる必要がある。 タブやスペース区切りの場合、空のカラムが存在しているとき、連続スペースを一つのデリミタとして 認識してしまうため、正しく数えることができない。

そういう場合は、sedコマンドで一度別の文字に置き換えてからawkに渡してやると、うまくいく。 ただし、デリミタにする文字がレコード中にあると、今度は誤検出されてしまうため、
出現する確率が限りなく低い文字(バッククオートとか)にしておきましょう。

$ sed -r 's/\t/``/g' < sample.csv | awk -F'``' {print NF"::"NR} | uniq | wc -l
-> 1