もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

PythonでAWS EC2を起動・停止する

はじめに

ブラウザを開いてAWSコンソール画面にログインして、EC2を起動・停止するのが面倒くさい。

なので、プログラム(Python)で起動するようにしてみた。

環境

ソースコード

boto3のドキュメントを参考にしつつ作成

boto3.amazonaws.com

EC2を起動・停止するメイン処理はこんな感じになった

ファイル名は「aws.py」で作成

# -*- coding: utf-8 -*-

import boto3
import json
import logger
from exception import NotSupportError, NotFoundResource
from botocore.exceptions import ClientError

with open('credentials.json', 'r') as stream:
    credentials = json.load(stream)

class EC2:
    """
    EC2を起動・停止する機能を提供する
    """

    def __init__(self, instance_id, region_name):
        """
        コンストラクタ
        :param instance_id: EC2のインスタンスID
        :param region_name: リージョン名
        """
        self.__log = logger.Logger('EC2')
        self.__instance_id = instance_id
        self.__local_session = boto3.session.Session(
            aws_access_key_id=credentials['AccessKey'],
            aws_secret_access_key=credentials['SecretAccessKey'],
            region_name=region_name
        )

    def start(self):
        """
        起動する
        """
        ec2 = self.__create_ec2()
        try:
            state_name = ec2.state['Name']
        except AttributeError as e:
            # AttributeErrorは、昔あったインスタンスIDを使うと、(Pythonの)インスタンスは生成されるが、
            # 属性stateにアクセスできず例外が発生する
            self.__log.error(e)
            raise NotFoundResource(e)

        self.__log.debug('state is %s' % (state_name,))
        if state_name == 'running':
            self.__log.info('ec2(%s) is already started.' % (self.__instance_id,))
            return
        elif state_name in {'pending', 'shutting-down', 'terminated', 'stopping'}:
            raise NotSupportError('ec2(%s) is %s.' % (self.__instance_id, state_name))
        elif state_name == 'stopped':
            self.__log.info('try start ec2(%s).' % (self.__instance_id,))
            ec2.start()
            # runningになるまで待機
            ec2.wait_until_running()
            self.__log.info('ec2(%s) is started.' % (self.__instance_id,))
            pass
        else:
            raise NotSupportError('Unknown status: %s' % (state_name,))

    def stop(self):
        """
        停止する
        """
        ec2 = self.__create_ec2()
        try:
            state_name = ec2.state['Name']
        except AttributeError as e:
            # AttributeErrorは、昔あったインスタンスIDを使うと、(Pythonの)インスタンスは生成されるが、
            # 属性stateにアクセスできず例外が発生する
            self.__log.error(e)
            raise NotFoundResource(e)

        self.__log.debug('state is %s' % (state_name,))
        if state_name == 'stopped':
            self.__log.info('ec2(%s) is already stopped.' % (self.__instance_id,))
            return
        elif state_name in {'pending', 'shutting-down', 'terminated', 'stopping'}:
            raise NotSupportError('ec2(%s) is %s.' % (self.__instance_id, state_name))
        elif state_name == 'running':
            self.__log.info('try stop ec2(%s).' % (self.__instance_id,))
            ec2.stop()
            self.__log.info('ec2(%s) is stopping.' % (self.__instance_id,))
            pass
        else:
            raise NotSupportError('Unknown status: %s' % (state_name,))

    def __create_ec2(self):
        """
        EC2を操作するオブジェクトを生成する
        :return: EC2を操作するオブジェクト
        """
        self.__log.debug('ec2: %s' % (self.__instance_id,))
        ec2_resource = self.__local_session.resource('ec2')
        try:
            return ec2_resource.Instance(self.__instance_id)
        except ClientError as e:
            self.__log.error(e)
            raise NotFoundResource(e)

credentials.jsonは、AWS IAMで作成した認証情報を保存している。

ちなみに、ログを出力しているクラスの定義はコチラ(ファイル名:logger.py)

# -*- coding: utf-8 -*-

from logging import Formatter, handlers, StreamHandler, getLogger, INFO


class Logger:
    def __init__(self, name=__name__):
        self.logger = getLogger(name)
        self.logger.setLevel(INFO)
        formatter = Formatter("[%(asctime)s] [%(process)d] [%(name)s] [%(levelname)s] %(message)s")

        # stdout
        handler = StreamHandler()
        handler.setLevel(INFO)
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

    def debug(self, msg):
        self.logger.debug(msg)

    def info(self, msg):
        self.logger.info(msg)

    def warn(self, msg):
        self.logger.warning(msg)

    def error(self, msg):
        self.logger.error(msg)

    def critical(self, msg):
        self.logger.critical(msg)

参考にしたサイト

qiita.com

例外の定義はコチラ(ファイル名:exception.py)

# -*- coding: utf-8 -*-


class NotSupportError(Exception):
    """
    サポートしていない動作が行われようとしたときに発生
    """
    pass


class NotFoundResource(Exception):
    """
    リソースが見つからないときに発生
    """
    pass

使い方

例えば、以下のようなJsonファイルがあった場合

{
    "EC2": [
      {
        "InstanceId": "i-xxxxxxxxxxxxxxx",
        "RegionName": "ap-northeast-1"
      }
    ]
}

以下のようにするとEC2が起動出来る

# -*- coding: utf-8 -*-
import aws

# jsonの読み込み処理(省略)


def start_ec2(ec2_machines):
    if ec2_machines is None or len(ec2_machines) == 0:
        return
    for machine in ec2_machines:
        ec2 = aws.EC2(
            instance_id=machine['InstanceId'],
            region_name=machine['RegionName']
        )
        ec2.start()

if __name__ == '__main__':
    start_ec2(json['EC2'])

停止は、EC2クラスのstopメソッドを呼ぶようにする

おわりに

今回のプログラムはサービス全体(EC2やらロードバランサーやら)を起動・停止するのに使った一部を抜粋

Pythonロードバランサーの生成・削除、Route53の登録も出来たので、それらについてはまたいつか・・・

CentOS7で開発環境用のDNSサーバを構築した話

はじめに

とあるサービスを作っている最中に負荷について調べていたところ、将来的にDBの負荷分散が必要になる可能性があった。

サービスはAWS上に構築するため、DBの負荷分散にはリードレプリカを利用し、DBのアクセスを振り分ける感じになる。

DBの負荷分散を想定してアプリを制作していなかったので、アプリの改造か必要かどうかの確認や改造後の動作確認が必要になり、DBアクセスをロードバランスする(またはそれっぽい)仕組みを開発環境に構築することにした。

マスターDBとリードレプリカなどのDBへのアクセスの振り分けについては、以下の方法があると思う。

  1. TCPのロードバランサ
  2. DNSを使ったなんちゃってロードバランサ(DNSラウンドロビン
  3. Pgpool-Ⅱを使う(DBがPostgreSQLのとき)

Pgpool-Ⅱを使う場合、Pgpool-Ⅱの置き場所をドコにしたら良いのか?EC2を1台増やさないといけないのでは?っと思ったので、あまり調べもせずに除外

最初にNICを2つ持ったCentOS7仮想マシンを用意して、ロードバランサを構築しようとしたけれども、上手く行かなかったので断念

ということもあって、本番環境ではRoute53にマスターDBとリードレプリカのレコードセットを用意することにして、開発環境にはRoute53を同じ動作をしてもらうDNSサーバを構築することにした。

ちなみにDNSラウンドロビン方式だと、Webアプリケーション(SpringBoot)も接続先を切り替える改造が必要で、それについては別の記事を参照

monakaice88.hatenablog.com

目次

想定している本番環境

想定している本番環境は以下の感じ。

f:id:monakaice88:20180728164600p:plain

この中のRoute53の役割と同じような動作をするDNSサーバを構築するのが目的

バージョン・IPアドレス

  • Virtual Box 5.2.28
  • Vagrant 2.2.4
  • CentOS 7
  • bind 9.9.4

  • IPアドレス・名前

    • マスターDB
      • 192.168.35.10
      • DNS名:updatable-db.sample.internal
    • スレーブDB
      • 192.168.35.11
      • DNS名:read-only-db.sample.internal
    • DNSサーバ
      • 192.168.35.20

bindのインストール

まずCentOS7にbindをインストールする。

sudo yum update -y
sudo yum install -y bind bind-utils

bindの設定ファイルの修正

設定ファイル「/etc/named.conf」を修正する。

差分のみ記載

options {
    // ListenするIPを修正
    listen-on port 53 { 127.0.0.1; 192.168.35.0/24; };

    // 問い合わせを許可するIP・ホストを修正
    allow-query     { localhost; 192.168.35.0/24; };

    // dnssecの機能を無効。(なんで無効にしたか覚えていない・・・)
    // 参考にしたサイトの設定をそのまま持ってきたかも
    // 有効のままでも問題ないかも
    dnssec-enable no;
    dnssec-validation no;

    // 追加。構築したDNSで名前解決できない場合は、本来使用しているDNSに転送
    forwarders {
        192.168.xxx.xxx;
        xxx.xxx.xxx.xxx;
    };
    forward only;
};

// 正引き
zone "sample.internal" IN {
    type master;
    file "sample.internal.zone";
};

// 逆引き
zone "35.168.192.in-addr.arpa" {
    type master;
    file "35.168.192.in-addr.arpa.rev";
};

正引き・逆引き設定ファイルの作成

正引き設定設定を「/var/named/sample.internal.zone」に記載する。

$TTL    3600

@       IN      SOA     sample.internal. root.sample.internal.    (
        2018061001
        3600
        900
        604800
        86400
)

                        IN      NS      ns.sample.internal.
ns                      IN      A       192.168.35.20
read-only-db            IN      A       192.168.35.11
updatable-db            IN      A       192.168.35.10

逆引き設定を「/var/named/35.168.192.in-addr.arpa.rev」に記載する。

$TTL 3600

@                IN      SOA sample.internal. root.sample.internal. (
                         2018061001 ; serial
                         3600       ; refresh 1hr
                         900        ; retry 15min
                         604800     ; expire 1w
                         86400      ; min 24hr
)

                 IN      NS     ns.sample.internal.
20               IN      PTR    ns.sample.internal.
11               IN      PTR    read-only-db.sample.internal.
10               IN      PTR    updatable-db.sample.internal.

作成した設定ファイルの所有者を変更する。

sudo chown named:named /var/named/sample.internal.zone
sudo chown named:named /var/named/35.168.192.in-addr.arpa.rev

NetworkManagerの設定の変更

リソルバを固定するため、「/etc/NetworkManager/NetworkManager.conf」の26行目あたりにあるDNSの設定を変更する。

[main]
#plugins=ifcfg-rh,ibft
dns=none

変更後は、NetworkManagerを再起動する。

sudo systemctl restart NetworkManager

「/etc/resolv.conf」の設定を変更する。

nameserver 192.168.35.20

おわりに

本番環境のRoute53と同じ動作をするための開発環境用のDNSサーバを構築できた。

初めてのDNSサーバの構築で、DNSの設定だけではなく、NetworkManagerやらresolv.confを修正しないといけなかったので意外と時間が掛かった気がする。

構築したDNSサーバを使用するときは、PCのDNSサーバの設定を変更する必要あり。

WindowsDNSサーバの変更

support.t-com.ne.jp

MacDNSサーバの変更

pc-karuma.net

Kotlin + Koinでテストコードを書いてみた

はじめに

ユニットテストの自動化はやった方が良いぞと分かっていても、Androidのコーディングに全然慣れていなかったので手がつかなかった。

プログラミング言語のメージャーバージョンの更新(JavaやらSwift)、ライブラリの更新などで、自分の将来の手間を減らすためにも、ユニットテストの自動化を必要性を感じたので、Androidでのテストの方法を調べてみた。

参考サイト

qiita.com

insert-koin.io

developer.android.com

環境

環境はKotlinと、DIコンテナとしてKoinを使用

SpringBootの影響か、DIコンテナを使わないとテストが書けない身体に・・・(そもそも、DIコンテナ使う前はテスト書いてない)

自分の能力では、DIコンテナを利用しないとモックが作りづらい・・・

Kotlinで使用できるDIコンテナは幾つかあるけど、個人的にKoinが学習コストが低そうだった

JavaではDagger2を使ってたけど、試しにKotlin + Daggar2のコードを書こうとして、面倒臭くなって諦めた)

ライブラリの準備

Koinのライブラリを追加するために、Gradleを修正する

dependencies {
    
    ...(中略)

    // Koin for Android
    implementation 'org.koin:koin-android:1.0.2'
    implementation 'org.koin:koin-test:1.0.2'
}

実装部分の作成

まず、現在時刻を取得するための処理を作る

まずはインターフェースを定義

interface ISystemClock {

    fun currentDateTime(): Calendar

}

「ISystemClock」を実装するクラスを定義する

class SystemClock: ISystemClock {

    override fun currentDateTime(): Calendar {
        return Calendar.getInstance()
    }

}

次に、適当な文字列+時刻の文字列を返す処理を作る

インターフェースを定義

interface IMessenger {

    fun getMessage(): String

}

実装を定義

class Messenger(
    private val iSystemClock: ISystemClock
) : IMessenger {

    override fun getMessage(): String {
        val formatter = SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒", Locale.US)
        val dateString = formatter.format(this.iSystemClock.currentDateTime().time)
        return "Hello world!!! ${dateString}"
    }

}

実装は「Hello world!!!」に現在時刻を付け足した文字列を返すだけ

現在時刻の取得は、先に定義したISystemClockの実装から取得するようにしている

テストコードの作成

作成したMessengerの実装が正しく動作するか確認するためのテストを作成する

とりあえず単純なテストを作成してみるが、getMessageメソッドで現在時刻を取得するので失敗する

class MessengerTest {

    private val messenger = Messenger(SystemClock())

    @Test
    fun getMessage() {
        Assert.assertEquals("Hello world!!! 2019年04月01日 10時18分52秒", messenger.getMessage())
    }

}

なので、ISystemClockを実装して決まった時間を返すモッククラスを作成する

class MockSystemClock: ISystemClock {

    override fun currentDateTime(): Calendar {
        val calendar = Calendar.getInstance()
        calendar.set(2019, Calendar.APRIL, 1, 10, 18, 52)
        return calendar
    }

}

そしてテスト時に、モッククラスのインスタンスを使用するように、koinで依存性を定義する

依存性の定義は、適当な場所に「DIModules.kt」というファイルを作成して以下のように記述した

val mockModule: Module = module {
    single { MockSystemClock() as ISystemClock }
    single { Messenger(get()) as IMessenger }
}

依存性を定義したので、テストも定義したものを使用できるように修正する

class MessengerTest: AutoCloseKoinTest() {

    private val iMessenger by inject<IMessenger>()

    @Before
    fun before() {
        startKoin(listOf(mockModule))
    }

    @Test
    fun getMessage() {
        Assert.assertEquals("Hello world!!! 2019年04月01日 10時18分52秒", iMessenger.getMessage())
    }

}

おわりに

Androidアプリに限らず、DIの考え方を利用すると色々な挙動のモックを定義できるので、ユニットテストが作りやすい

iOSの方のテストの自動化についても調べようと思う