【負荷試験】ブラウザ上で JMeter を操作できるツールを作ってみた話【Flanker】

こんにちは。
突然ですが、皆様は負荷試験を実施したことはございますでしょうか?
私は仕事柄、時折、負荷試験案件に携わることがございまして。
あくまで私のケースですが、おおよそ下記のようなフェーズを辿ることが多いように感じています。

1. 試験設計 ( 方針設計 / 目標決め / シナリオ作成 etc )
2. JMeter 環境構築
3. 負荷試験環境調整 ( 申請 / モニタリング環境 etc )
4. JMeter 実行 ( シナリオ調整 / JMeter 調整 含む )
5. 調査 / 解析
6. 「4」と「5」のエンドレス…

「1」に関してはお客様と詳細なやり取りを行い、いわば要件定義に近い折衝を行う必要がございまして、シナリオ作成に関しては API 仕様書を元に、JMeter から発行するリクエスト内容を定義していかなくてはなりません。

「2」や「3」に関しては、 Ansible 等のツールで省力化に取り組んでいるのですが、その他のフェーズでは依然として雑多なタスクが重なり、大変ストレスがマッハでございました。

経緯

さて、つきましては、雑多なタスク減を目的に、この度「4」の JMeter 操作周りを WEB ベースで完結できるツールを作ってみようと思った次第でございます。
始めに必要な機能を洗い出し、実装する機能を下記 5 ポイントに絞りました。

・シナリオ ( .jmx ) のアップロード
・シナリオ内 Thread group コンポーネントの調整 ( 負荷強度の調整等 etc )
・クラスタリングした JMeter に紐づく Worker ノードの操作
・JMeter の実行、および非同期での進捗確認
・実行結果生成

前提として JMeter を扱うのですから、 最終的には JMeter の WEB クライアントを作る形となります。

既に JMeter 環境のデプロイ用途で Ansible を使っているのもあり、ツールの実行環境含めて Playbook に内包しました。

できたもの

・Flanker

https://github.com/snkk1210/flanker

ツール、および JMeter 環境のデプロイは、下記 Ansible の Playbook を流していただければ完了いたします。

※ 詳細は README を参照ください。

https://github.com/snkk1210/jmeter-MS

実際にツールを使って案件を進めてみたのですが、かなり仕事が楽になったように思います。
※ 作った人間ばいあす

これからも、これ系のツールを作って業務の効率化を進めていきたいと思います。
わぁい。

【JMeter】BeanShell Pre/PostProcessor での JSON 操作例【シナリオ作成】

こんにちは。
負荷試験や性能テストでよく使われる JMeter ですが、コンポーネントの 1つに、BeanShell PreProcessor / PostProcessor という機能が存在します。
こちらは JMeter からのリクエストのレスポンス内容をパースして値を取得したり、発行するリクエストのパラメータを動的に変化させたりと、割となんでもできてしまう便利なコンポーネントになっています。
※ 処理自体は Java でスクリプトを記述していく必要があり、少し癖があるかもですが、それほど難易度が高いわけでもないと思います。

今回は実際にリクエストを投げる API を用意して、シナリオを作成しつつ良く使うパターンを備忘録として残しておこうと思います。
用意する API は Zabbix 辺りが都合よさそうだったので、こちらに対しての負荷試験シナリオ、という前提で作成していきます。
※ Zabbix API の仕様に関しては こちら を参照ください。

JMeter (始めに)

使用する JMeter は現時点で最新版である JMeter 5.5 を使用します。
また、JSON の操作が必要になってくるため、JSONObject のライブラリファイルを JMeter の lib/ ディレクトリ配下に配置しておきます。
※ ファイルは こちら から ZIP 形式でダウンロードできます。

また、シナリオ作成前に JMeter の下記辺りのコンポーネントを用意しておくのが良いかもしれないです。

・View Results Tree : シナリオデバッグ用途
・HTTP Request Defaults : リクエスト共通の設定を定義
※ 全ての Zabbix API は /zabbix/api_jsonrpc.php に対して POST メソッドを用いて API を叩きます。
・HTTP Header Manager : リクエスト共通のリクエストヘッダを定義
※ 全ての Zabbix API へのリクエストには下記のリクエストヘッダが必要です。

Content-Type: application/json-rpc

Zabbix API へは JSON 形式で POST リクエストを発行するため、作成する HTTP Request コンポーネントの Body Data には ${request} という変数を設定しておきます。
※ PreProcessor でリクエストの JSON データを生成します。

作成するシナリオは

user.login → hostgroup.get → host.create → host.get → host.delete → user.logout

の順に偏移する想定です。

シナリオ

シナリオ全体で扱う値用に PreProcessor を用意しておきます。
※ Zabbix ログインユーザとログ出力モード切替の定義用です。
今回はデフォルトの Zabbix 特権ユーザを使用します。

/** 
 * 各変数の初期化
 * 定数値等を設定
 * 
 * initialize Script
 * Bean Shell PreProcessor
 */
// Execute mode
 vars.put("MODE", "debug");
//vars.put("MODE", "release");

/** 
 * ログイン ユーザ
 */
// login User
vars.put("username", "Admin");

// Login Password
vars.put("password", "zabbix");

1. user.login

始めに Zabbix の認証キーを取得します。

・PreProcessor
Zabbix API の仕様に沿った通りに JSON のリクエストを発行します。
ユーザ情報は先の PreProcessor にて設定した値を呼び出しています。

import org.json.JSONObject;
import org.json.JSONArray;

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("user", vars.get("username"));
paramObject.put("password", vars.get("password"));

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成  
JSONObject objRequest = new JSONObject();

objRequest.put("method", "user.login");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データをセット
vars.put("request", objRequest.toString());

・PostProcessor
レスポンスから認証キーを取得します。
※ 認証キーは auth_key に格納しておき、以降のリクエストで利用します。

import org.json.JSONObject;

if (vars.get("MODE") == "debug") { log.info("-- Requested Sample(Pre): " + prev.getSampleLabel()); }

// レスポンスを取得
JSONObject json = new JSONObject(prev.getResponseDataAsString());

// レスポンスから 認証キー を取得
var result = json.getString("result");

if (vars.get("MODE") == "debug") { 
	log.info(result.toString());
}

// 変数にセット
vars.put("auth_key", result.toString());

2. hostgroup.get

ホストグループ Discovered hosts の詳細情報をリクエストします。

・PreProcessor
深い入れ子の JSON を生成する必要があるため、少しだけ工夫します。
また、先のリクエストにて取得した auth_key を呼び出して使用します。

import org.json.JSONObject;
import org.json.JSONArray;

// filter JSONObject 生成
JSONObject filterObject = new JSONObject();

filterObject.put("name", "Discovered hosts");

JSONArray filters = new JSONArray();
filters.put(filterObject);

JSONObject filter = filters.getJSONObject(0);

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("filter", filter);

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "hostgroup.get");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

3. host.create

Zabbix のホストを作成します。

・PreProcessor
作成するホストの名称は複数スレッドでリクエストを流した際の重複を避けるため、Random クラスの nextInt() を用いて、ランダムな値を付与します。
IP アドレスとポートは重複しても問題ないため、固定値に設定しています。

import org.json.JSONObject;
import org.json.JSONArray;
import java.util.Random;

// 0 ~ 999999999 の数値を取得
Random rand = new Random();
long user_id = rand.nextInt(999999999);

// groups JSONObject 生成
JSONObject groupsObject = new JSONObject();

groupsObject.put("groupid", vars.get("groupid"));

JSONArray groups = new JSONArray();
groups.put(groupsObject);

JSONObject group = groups.getJSONObject(0);

// interfaces JSONObject 生成
JSONObject interfacesObject = new JSONObject();

interfacesObject.put("type", 1);
interfacesObject.put("main", 1);
interfacesObject.put("useip", 1);
interfacesObject.put("ip", "192.168.10.100");
interfacesObject.put("dns", "");
interfacesObject.put("port", "10050");

JSONArray interfaces = new JSONArray();
interfaces.put(interfacesObject);

JSONObject interfac = interfaces.getJSONObject(0);


// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("host", "Test Server" + user_id.toString());
paramObject.put("groups", group);
paramObject.put("interfaces", interfac);

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.create");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

4. host.get

Zabbix のホスト一覧を取得し、ランダムにホスト ID を取得します。

・PreProcessor

import org.json.JSONObject;
import org.json.JSONArray;

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("output", "extend");

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.get");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

・PostProcessor
こちらも Random クラスのメソッドを用いて、レスポンスからランダムな hostid を取得し変数に格納しておきます。
※ 後述のホストの削除にて利用します。

import org.json.JSONObject;
import java.util.Random;

if (vars.get("MODE") == "debug") { log.info("-- Requested Sample(Pre): " + prev.getSampleLabel()); }

// レスポンス取得
JSONObject json = new JSONObject(prev.getResponseDataAsString());

var result = json.getJSONArray("result");

// JsonObject の要素数を数える
int length = result.length();

// ランダムな要素数を選定
Random rand = new Random();
int num = rand.nextInt(length);

// ランダムな要素を取得 
JSONObject jObj1 = result.getJSONObject(num);

var hostid = jObj1.getString("hostid");

if (vars.get("MODE") == "debug") { 
	log.info(hostid.toString());
}

// 変数セット
vars.put("hostid", hostid.toString());

5. host.delete

Zabbix のホストを 1つ削除します。

・PreProcessor
先ほど取得した hostid を利用してリクエストを発行します。

import org.json.JSONObject;
import org.json.JSONArray;

String[] hostidArr;
hostidArr = new String[1];
hostidArr[0] = vars.get("hostid");

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.delete");
objRequest.put("id", 1);
objRequest.put("params", hostidArr);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

6. user.logout

最後にログアウトして認証キーを無効化しておきます。

・PreProcessor

import org.json.JSONObject;
import org.json.JSONArray;

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "user.logout");
objRequest.put("id", 4);
objRequest.put("params", null);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

終わりに

恐らく BeanShell PreProcessor / PostProcessor を使えば、書き方次第で、おおよその要件は叶えられるんじゃないでしょうか?
※ 今回のシナリオファイルのサンプルは こちら に配置しております。

話は変わりますが、こちら に JMeter クラスタ環境デプロイ用の Ansible Playbook を配置しておりますのでご利用頂ければ嬉しいです。

SAM + dynamodb-local でローカルにサーバレス開発環境を作って遊んでみる

こんにちは。
最近、DynamoDB について触る機会があったので、少し勉強してみようかな、と。
調べてみると ローカルに DynamoDB を立てることができる dynamodb-local というものがあるそうです。
SAM と dynamodb-local を使ってローカルにサーバレス開発環境を作り、簡単な掲示板を作成してみようと思います。

構成

JavaScript:クライアント
SAM ( ApiGateWay / Lambda ) : バックエンド API
dynamodb-local:データストア
サーバ IP:192.168.33.55

よくあるタイプのやつですね。
SAM ( ApiGateWay / Lambda ) でデータ取得 / データ登録 の REST API を作って、 JavaScript で操作、データは dynamodb-local に保存します。

環境構築

dynamodb-local は Docker のイメージが公開されています。
CentOS7 に SAM と dynamodb-local をセットアップする Ansible の role を こちら に用意しました。

環境構築後は、事前に dynamodb-local にテーブルを作成しておきます。

# テーブル作成
aws dynamodb create-table --table-name 'bbs' \
--attribute-definitions '[{"AttributeName":"postId","AttributeType": "S"}]' \
--key-schema '[{"AttributeName":"postId","KeyType": "HASH"}]' \
--provisioned-throughput '{"ReadCapacityUnits": 5,"WriteCapacityUnits": 5}' \
--endpoint-url http://localhost:8000

バックエンド API

データ取得 / データ登録 の API を Python で作成します。
ディレクトリ構成は下記の通りです。

.
├── bbs
│ ├── create
│ │ └── app.py
│ └── read
│   └── app.py
├── template.yaml
└── vars.json

・データ取得 API ( ./bbs/read/app.py )

import json
import boto3
import time
import logging
import os
from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    logger.info("Event: " + str(event))

    responseHeaders = {
      "Access-Control-Allow-Methods": "OPTIONS,GET",
      "Access-Control-Allow-Headers" : "*",
      "Access-Control-Allow-Origin": "*"
    }

    try:
        table = _get_database().Table('bbs')
        res = table.scan()
    except ClientError as e:
        logger.error("Error: %s", e)

    return {
        "headers": responseHeaders,
        "statusCode": 200,
        "body":  json.dumps(res['Items'], default=decimal_default_proc),
    }

def decimal_default_proc(obj):
    from decimal import Decimal
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError

def _get_database():
    if (os.environ["DYNAMO_ENDPOINT"] == ""):
        endpoint = boto3.resource('dynamodb')
    else:
        endpoint = boto3.resource('dynamodb', endpoint_url=os.environ["DYNAMO_ENDPOINT"])
    return endpoint

・データ登録 API ( ./bbs/create/app.py )

import json
import boto3
import time
import datetime
import logging
import uuid
import os
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    logger.info("Event: " + str(event))

    responseHeaders = {
      "Access-Control-Allow-Methods": "OPTIONS,GET,POST",
      "Access-Control-Allow-Headers" : "*",
      "Access-Control-Allow-Origin": "*"
    }

    req = json.loads(event['body'])

    item = {
        'postId': str(uuid.uuid4()),
        'time': str(datetime.datetime.now()),
        'owner': req['owner'],
        'note': req['note'],
        'enable': bool('True')
    }

    logger.info("Item: " + str(item))

    try:
        table = _get_database().Table('bbs')
        res = table.put_item(Item=item)
        logger.info("Respons: " + str(res))
    except ClientError as e:
        logger.error("Error: %s", e)

    return {
        "headers": responseHeaders,
        "statusCode": 200,
        "body": item,
    }

def _get_database():
    if (os.environ["DYNAMO_ENDPOINT"] == ""):
        endpoint = boto3.resource('dynamodb')
    else:
        endpoint = boto3.resource('dynamodb', endpoint_url=os.environ["DYNAMO_ENDPOINT"])
    return endpoint

・環境変数定義ファイル : DynamoDB への接続先エンドポイント ( ./vars.json )

{
    "Parameters": {
      "DYNAMO_ENDPOINT": "http://192.168.33.55:8000"
    }
}

・SAM の template ファイル : API の仕様やその他諸々 ( ./template.yaml )

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: BBS

Globals:
  Function:
    Timeout: 3
    Environment:
      Variables:
        DYNAMO_ENDPOINT: DYNAMO_ENDPOINT
  Api:
    Cors:
      AllowMethods: "'OPTIONS,GET,POST'"
      AllowHeaders: "'*'"
      AllowOrigin: "'*'"

Resources:
  ReadBBSFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: bbs/read/
      Handler: app.lambda_handler
      Runtime: python3.9
      Events:
        ReadBBS:
          Type: Api
          Properties:
            Path: /bbs/
            Method: get

  CreateBBSFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: bbs/create/
      Handler: app.lambda_handler
      Runtime: python3.9
      Events:
        CreateBBS:
          Type: Api
          Properties:
            Path: /bbs/
            Method: post

API の起動

コードをビルドして API を起動します。
※ デフォルトは 3000 ポートで立ち上がります。

sam build
sam local start-api --env-vars vars.json --host 0.0.0.0

curl で API を叩いてエラー無くレスポンスが返ってくれば OK です。
※ 初回リクエスト時にイメージがビルドされるようで、その際レスポンスが少々遅延するようです。
$ curl http://192.168.33.55:3000/bbs
[]

クライアント

Ajax でデータ取得 API を叩いて投稿情報を取得し、一覧を表示します。
併せてデータ登録 API に投稿を POST して登録を行います。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>bbs</title>
    </head>
    <body>
        <dl id="wrap">
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
            <script type="text/javascript">
            const url = "http://192.168.33.55:3000/bbs";
              $(function(){
                $.getJSON(url, function(note_list){
                  note_list.sort(function(a, b){
                    if (a.time < b.time){
                      return -1;
                    }else{
                      return 1;
                    }
                  });
                  for(var i in note_list){
                    var h = '<dt>'
                          + note_list[i].time
                          + " "
                          + note_list[i].owner
                          + '</dt>'
                          + '<dd>'
                          + note_list[i].note
                          + '</dd>';
                    $("dl#wrap").append(h);
                  }
                });
              });
            </script>
          </dl>
          <p>投稿者: <input type="text" id="owner" size="30"></p>
          <p>NOTE: <input type="text" id="note" size="30"></p>
          <p><button id="button" type="button">投稿</button></p>
          <script type="text/javascript">
            $(function(){
                $("#response").html("Response Values");
        
                $("#button").click( function(){
                    var url = "http://192.168.33.55:3000/bbs";
                        var JSONdata = {
                            "owner": String($("#owner").val()) ,
                            "note": String($("#note").val())     
                        };
        
                    $.ajax({
                        type : 'post',
                        url : url,
                        data : JSON.stringify(JSONdata),
                        contentType: 'application/json',
                        dataType : 'json',
                        scriptCharset: 'utf-8',
                        success : function(time){
                          window.location.reload();
                        },
                        error : function(time){
                          window.location.reload();
                        }
                    });
                })
            })
        </script>
    </body>
</html>

【EASE】Laravel で Chatwork 通知機能を実装してみる【Ansible】

こんにちは。
先日に Laravel で作った Ansible の管理ツールですが、プロビジョニング時に通知を行う機能が欲しいなー、と。
Slack でもよいのですが、会社で主に使っている Chatwork に通知してみます。

【Ansible】playbookをWEB上で管理/実行できるツールを作ってみた【Laravel】

事前準備

Chatwork の API トークンが必要です。
https://developer.chatwork.com/ja/

環境変数(.env)から API トークンと、通知先エンドポイントを読めるようにしておきます。

config/chatwork.php 
==========================================
<?php

# Chatworkのtokenを環境変数から取得
return [
    'cw-token' => env('CW_TOKEN'),
    'cw-endpoint' => env('CW_ENDPOINT'),
];

メソッド

Laravel では Guzzle HTTP クライアントで簡単に http リクエストを発行できるようです。
ヘッダに API トークン、ボディに通知内容のメッセージを指定するんですが、URL エンコードする必要があるので、リクエストを作成する前に asForm メソッドを呼び出します。
※プロビジョニング実行時、完了時で合計 2 つのメソッドを作成します。

use Illuminate\Support\Facades\Http;

    /**
     * 実行時 Chatwork 通知メソッド
     */
    public static function notify2ChatworkStart($user){
        $token = config('chatwork.cw-token');
        $endpoint = config('chatwork.cw-endpoint');
        $date = date("Y/m/d H:i:s");

        $message = "[info][title]EASE Ansible MG started provisioning at " . $date . "[/title]\n" . " by " . $user->name . "[/info]";
        $response = Http::asForm()->withHeaders([
            'X-ChatWorkToken' => $token,
        ])->post($endpoint, [
            'body' => $message
        ]);
        return $response;
    }

    /**
     * 終了時 Chatwork 通知メソッド
     */
    public static function notify2ChatworkEnd($ansible_output){
        $token = config('chatwork.cw-token');
        $endpoint = config('chatwork.cw-endpoint');
        $date = date("Y/m/d H:i:s");

        $message = "[info][title]EASE Ansible MG ended provisioning at " . $date . "[/title]\n" .  $ansible_output . "[/info]";
        $response = Http::asForm()->withHeaders([
            'X-ChatWorkToken' => $token,
        ])->post($endpoint, [
            'body' => $message
        ]);
        return $response;
    }

使い方

環境変数が設定されている時にメソッドを呼び出すだけです。
とてもお手軽。

# Chatworkへの通知(実行時)
if (config('chatwork.cw-token')){ Playbook::notify2ChatworkStart($user); };

# Chatworkへの通知(完了時)
if (config('chatwork.cw-token')){ Playbook::notify2ChatworkEnd($ansible_output[count($ansible_output)-2]); };  

【Ansible】Playbook を WEB で管理できるツールを作ってみた【Laravel】

こんにちは。
Ansible の Playbook 管理ツールといえば、 Ansible Tower / AWX / Ansible Semaphore 等々がございます。
これらを業務フローに組み込めないかな、と試してみたのですが、いずれもあまりピンとこず。

ないなら自分で作ってしまえと思い、年末の勢いでツールを作ってみました。
https://github.com/snkk1210/ease

実現したかったこと

ツールを作成するに辺り、実現したかったポイントは下記 7 点です。

・WEB ブラウザ上で Playbook が管理できる。
・WEB ブラウザ上で Playbook に定義された処理を実行できる。
・処理実行 ( プロビジョニング ) 履歴を保持できる。
・既に存在する Playbook ( リポジトリ ) の資産を流用できる。
・ユーザ毎に細かな権限設定が可能であり、組織的に管理できる。
・プロビジョニング時に公開鍵認証/パスワード認証の両方が使用できる。
・外部ファイルをアップロードし、処理実行時に利用することができる。

社内でのユースケースとしては、下記のようなことを実現したく実装しました。

だいたいの使い方

1. プロビジョニング対象への接続認証を登録

サイドメニューから「Make Auth」を選択し、認証名、パスワード、秘密鍵を登録

2. Playbook を作成

サイドメニューから「Make Playbook」を選択し、下記項目をそれぞれ登録

・Playbook
→ Playbook の名前

・repository
→ Playbookに使うリポジトリ名

・認証
→ 作成した接続認証

・private_key
→ 認証に使う秘密鍵
※ここで設定した秘密鍵情報が優先される(空にすると接続認証の情報を使用)

・inventory
→ インベントリファイル

・vars
→ 変数ファイル

・main
→ レポジトリに存在する role を include する Playbook

3. Playbook を選択

サイドメニューから「Playbooks」を選択し、一覧の中から使用する Playbook の「Run」を選択

4. Playbookを実行

セレクトボックスから「鍵認証」「パスワード認証」を選択して、「ドライラン」または「実行」を選択

権限周り

実装する権限は下記 3 ロールに絞りました。
※ もっと細かく調整できるように改修したいですね。

admin → 全てのリソースの操作が可能/ユーザの作成が可能
read-only → 全てのリソースの「閲覧」のみ可能
他ユーザ → 自分で作成したリソースのみ操作が可能

終わりに

余力があれば、アカウント権限操作周りの機能と、WEB ベースでのリポジトリ追加機能を実装する予定です。
始めて Laravel 使いましたが、こんな感じに実装できるんですね。
色々と勉強になりました。

【Raspberry Pi】semaphore を導入してみる【Ansible】

ラズパイのリソースが余っていたので、semaphore を導入して遊んでみようと思います。
※ 事前に下記を導入する必要ありです。
・MySQL >= 5.6.4/MariaDB >= 5.3
・git >= 2.x

“【Raspberry Pi】semaphore を導入してみる【Ansible】” の続きを読む

【Ansible】Ansible で AWS の3層ネットワークを構築する【IaC】

こんにちは。
表題の通り AWS の 3層ネットワークを構築する playbook を用意してみます。
※ 詳細は README を参照ください。
https://github.com/keisukesanuki/aws-vpc-3layer

作るもの

・VPC
・SUBNET
・INTERNETGATEWAY
・NATGATEWAY
・ROUTETABLE

ディレクトリ構造

.
├── README.md
├── ansible.cfg
├── hosts
├── roles
│   └── aws_vpc
│       ├── tasks
│       │   └── main.yml
│       └── vars
│           └── main.yml
└── vpc_create.yml

playbook

---
# tasks file for aws_vpc
- name: create_vpc
  ec2_vpc_net:
    name: "{{ vpc_name }}"
    cidr_block: "{{ vpc_cidr }}"
    region: "{{ region }}"
    profile: "{{ profile }}"
    dns_hostnames: yes
    dns_support: yes
  register: vpc_info

# PUBLIC_SUBNET 作成
- name: create_public_subnet
  ec2_vpc_subnet:
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.pub_subnet_cidr }}"
    az: "{{ item.subnet_az }}"
    region: "{{ region }}"
    resource_tags: { "Name":"{{ item.pub_subnet_name }}" }
    profile: "{{ profile }}"
  register: pubsub_info
  with_items:
    - "{{ pub_subnet }}"

# DMZ_SUBNET 作成
- name: create_dmz_subnet
  ec2_vpc_subnet:
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.dmz_subnet_cidr }}"
    az: "{{ item.subnet_az }}"
    region: "{{ region }}"
    resource_tags: { "Name":"{{ item.dmz_subnet_name }}" }
    profile: "{{ profile }}"
  register: pubsub_info
  with_items:
    - "{{ dmz_subnet }}"

# PRIVATE_SUBNET 作成
- name: create_private_subnet
  ec2_vpc_subnet:
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.pri_subnet_cidr }}"
    az: "{{ item.subnet_az }}"
    region: "{{ region }}"
    resource_tags: { "Name":"{{ item.pri_subnet_name }}" }
    profile: "{{ profile }}"
  register: prisub_info
  with_items:
    - "{{ pri_subnet }}"

# IGW 作成
- name: create_igw
  ec2_vpc_igw:
    vpc_id: "{{ vpc_info.vpc.id }}"
    region: "{{ region }}"
    tags: { "Name":"{{ igw_name }}" }
    profile: "{{ profile }}"
  register: igw_info

# ROUTETABLE 作成(IGW)
- name: create_route_table
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_info.vpc.id }}"
    subnets: "{{ atache_igw_subnet }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ igw_info.gateway_id }}"
    region: "{{ region }}"
    profile: "{{ profile }}"
    resource_tags: { "Name":"{{ rttable_pub_name }}" }

# NGW の ID を取得
- name: get_subnet_id
  shell: aws ec2 describe-subnets --region {{ region }} --profile {{ profile }} --output text | grep -B 1 {{ ngw_subnet_name }} | awk 'NR==1 {print $12}'
  register: ngw_subnet_id

#- name: show
#  debug:
#    msg: "{{ ngw_subnet_id.stdout }}"

# NGW 作成
- name: create_ngw
  ec2_vpc_nat_gateway:
    subnet_id: "{{ ngw_subnet_id.stdout }}"
    region: "{{ region }}"
    profile: "{{ profile }}"
  register: ngw_info

#- name: show
#  debug:
#    msg: "{{ ngw_info.nat_gateway_id }}"

# NGW 作成まで待つ
- name: wait_for_ngw
  pause:
    minutes: 5

# ROUTETABLEの作成(NGW)
- name: create_route_table2
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_info.vpc.id }}"
    subnets: "{{ atache_ngw_subnet }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ ngw_info.nat_gateway_id }}"
    region: "{{ region }}"
    profile: "{{ profile }}"
    resource_tags: { "Name":"{{ rttable_dmz_name }}" }

NATGATEWAY の ID が上手く取得できなかったので awscli の結果をパースして ngw_subnet_id に渡しています。

変数定義

---
# vars file for aws_vpc

# REGION
  region: "ap-northeast-1"

# PROFILE
  profile: "default"

# VPC
  vpc_name: "sanuki-wd-vpc2"
  vpc_cidr: "10.10.0.0/16"

# IGW
  igw_name: "sanuki-igw2"

# NGW
  ngw_name: "sanuki-ngw2"


# NGWを作成するサブネット名
  ngw_subnet_name: "sanuki-wd-public-subnet2-a"

# ROUTETABLE(PUBLIC)
  rttable_pub_name: "sanuki-pub-rt2"

# ROUTETABLE(DMZ)
  rttable_dmz_name: "sanuki-dmz-rt2"

# PUBLIC_SUBNET
  pub_subnet:
    - { pub_subnet_cidr: "10.10.10.0/24" ,subnet_az: "ap-northeast-1a" ,pub_subnet_name: "sanuki-wd-public-subnet2-a" }
    - { pub_subnet_cidr: "10.10.20.0/24" ,subnet_az: "ap-northeast-1c" ,pub_subnet_name: "sanuki-wd-public-subnet2-c" }


# DMZ_SUBNET
  dmz_subnet:
    - { dmz_subnet_cidr: "10.10.30.0/24" ,subnet_az: "ap-northeast-1a" ,dmz_subnet_name: "sanuki-wd-dmz-subnet2-a" }
    - { dmz_subnet_cidr: "10.10.40.0/24" ,subnet_az: "ap-northeast-1c" ,dmz_subnet_name: "sanuki-wd-dmz-subnet2-c" }


# PRIVATE_SUBNET
  pri_subnet:
    - { pri_subnet_cidr: "10.10.50.0/24" ,subnet_az: "ap-northeast-1a" ,pri_subnet_name: "sanuki-wd-private-subnet2-a" }
    - { pri_subnet_cidr: "10.10.60.0/24" ,subnet_az: "ap-northeast-1c" ,pri_subnet_name: "sanuki-wd-private-subnet2-c" }

# IGWに紐付けるサブネット
  atache_igw_subnet:
    - "10.10.10.0/24"
    - "10.10.20.0/24"

# NGWに紐付けるサブネット
  atache_ngw_subnet:
    - "10.10.30.0/24"
    - "10.10.40.0/24"

NatGateway が片方の AZ にしかないため、冗長性の観点からは ? となりますが、まぁいいでしょう。

↓ 2層の playbook はこちら

【Ansible】AnsibleでAWSのネットワークを構築する【IaC】

【Python】Excel から Ansible のコードを自動生成する①【構築自動化】

Excel から必要な情報を抜き出して、Ansible の ini ファイルを置換する Python スクリプトを書いてみました。

#!/usr/bin/python3
# coding: UTF-8

import openpyxl
import sys

# Ansible の ini ファイルを定義(置換前)
org_file_name = "all.yml.org"
# Ansible の ini ファイルを定義(置換後)
file_name = "all.yml"

# コマンドライン引数の数を確認
if len(sys.argv) != 2:
    print("input error")
    sys.exit(1)

# Excel ファイル名を変数に代入
args = sys.argv
target = args[1]

# Excel データを取得
wb = openpyxl.load_workbook(target)
sheet = wb.get_sheet_by_name('Sheet1')

# セルデータの取得関数
def get_cell(x, y):
    param = sheet.cell(row=x,column=y).value
    return param

# 必要なセルの情報を取得
domain = get_cell(2, 2)
docroot = get_cell(3, 2)

# 置換前の ini ファイルを開く
with open(org_file_name, encoding="cp932") as f:
    data_lines = f.read()

# 置換
data_lines = data_lines.replace("xxx", domain)
data_lines = data_lines.replace("yyy", docroot)

# 置換後のiniファイルを作成
with open(file_name, 'w', encoding="cp932") as f:
    f.write(data_lines)

スクリプトのコマンドライン引数としてExcelファイル(.xlsx)を指定して実行してください。

【Ansible】踏み台経由で Ansible を実行する インベントリファイルの書き方

[stg_web]
test-stg-web01 ansible_host=192.168.33.10 ansible_ssh_common_args='-o ProxyCommand="ssh -i ~/.ssh/test.pem -W %h:%p -q bastion-user@192.168.33.30"'

[prod_web]
test-prod-web01 ansible_host=192.168.33.20 ansible_ssh_common_args='-o ProxyCommand="ssh -i ~/.ssh/test.pem -W %h:%p -q bastion-user@192.168.33.30"'

【Ansible】Master/Slave構成の JMeter をデプロイする【IaC】

Ansible で Master/Slave 構成の JMeter をデプロイする Playbook を作ってみました。
https://github.com/keisukesanuki/jmeter-MS.git
※ 詳細は GitHub の README を参照くださいませ。

ディレクトリ構成

.
├── README.md
├── ansible.cfg
├── group_vars
│   ├── all.yml
│   └── all.yml.example
├── hosts.example
├── roles
│   ├── common
│   │   ├── README.md
│   │   └── tasks
│   │       ├── etckeeper_commit.yml
│   │       ├── host_change.yml
│   │       └── main.yml
│   ├── dummy
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── dummy.txt
│   ├── jmeter51
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter53
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter54
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter55
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── minimum
│   │   └── httpd
│   │       ├── README.md
│   │       ├── handlers
│   │       │   └── main.yml
│   │       ├── tasks
│   │       │   ├── main.yml
│   │       │   └── security.yml
│   │       └── templates
│   │           ├── mpm.conf
│   │           └── security.conf
│   ├── python-scripts
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       ├── csv2gspread.py
│   │       ├── sacred-drive.json
│   │       └── start-controller_cui.sh
│   ├── reboot
│   │   └── tasks
│   │       └── main.yml
│   ├── slave-jmeter
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter-node.service
│   │   │   └── jmeter-server.sh
│   │   └── tasks
│   │       └── main.yml
│   └── tigervnc
│       ├── README.md
│       ├── files
│       │   ├── vncpasswd.sh
│       │   └── vncserver@.service_root
│       └── tasks
│           └── main.yml
├── scenario
│   └── example
│       └── zabbix
│           └── zabbix_load_scenario.jmx
├── target.yml
└── target.yml.example

38 directories, 51 files