PythonでAWS EC2を起動・停止する
はじめに
ブラウザを開いてAWSコンソール画面にログインして、EC2を起動・停止するのが面倒くさい。
なので、プログラム(Python)で起動するようにしてみた。
環境
- Python 3.6
- boto3
ソースコード
boto3のドキュメントを参考にしつつ作成
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)
参考にしたサイト
例外の定義はコチラ(ファイル名: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やらロードバランサーやら)を起動・停止するのに使った一部を抜粋
CentOS7で開発環境用のDNSサーバを構築した話
はじめに
とあるサービスを作っている最中に負荷について調べていたところ、将来的にDBの負荷分散が必要になる可能性があった。
サービスはAWS上に構築するため、DBの負荷分散にはリードレプリカを利用し、DBのアクセスを振り分ける感じになる。
DBの負荷分散を想定してアプリを制作していなかったので、アプリの改造か必要かどうかの確認や改造後の動作確認が必要になり、DBアクセスをロードバランスする(またはそれっぽい)仕組みを開発環境に構築することにした。
マスターDBとリードレプリカなどのDBへのアクセスの振り分けについては、以下の方法があると思う。
- TCPのロードバランサ
- DNSを使ったなんちゃってロードバランサ(DNSラウンドロビン)
- Pgpool-Ⅱを使う(DBがPostgreSQLのとき)
Pgpool-Ⅱを使う場合、Pgpool-Ⅱの置き場所をドコにしたら良いのか?EC2を1台増やさないといけないのでは?っと思ったので、あまり調べもせずに除外
最初にNICを2つ持ったCentOS7仮想マシンを用意して、ロードバランサを構築しようとしたけれども、上手く行かなかったので断念
ということもあって、本番環境ではRoute53にマスターDBとリードレプリカのレコードセットを用意することにして、開発環境にはRoute53を同じ動作をしてもらうDNSサーバを構築することにした。
ちなみにDNSラウンドロビン方式だと、Webアプリケーション(SpringBoot)も接続先を切り替える改造が必要で、それについては別の記事を参照
目次
想定している本番環境
想定している本番環境は以下の感じ。
この中のRoute53の役割と同じような動作をするDNSサーバを構築するのが目的
バージョン・IPアドレス
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サーバの設定を変更する必要あり。
WindowsのDNSサーバの変更
MacのDNSサーバの変更
Kotlin + Koinでテストコードを書いてみた
はじめに
ユニットテストの自動化はやった方が良いぞと分かっていても、Androidのコーディングに全然慣れていなかったので手がつかなかった。
プログラミング言語のメージャーバージョンの更新(JavaやらSwift)、ライブラリの更新などで、自分の将来の手間を減らすためにも、ユニットテストの自動化を必要性を感じたので、Androidでのテストの方法を調べてみた。
参考サイト
環境
環境はKotlinと、DIコンテナとしてKoinを使用
SpringBootの影響か、DIコンテナを使わないとテストが書けない身体に・・・(そもそも、DIコンテナ使う前はテスト書いてない)
自分の能力では、DIコンテナを利用しないとモックが作りづらい・・・
Kotlinで使用できるDIコンテナは幾つかあるけど、個人的にKoinが学習コストが低そうだった
(JavaではDagger2を使ってたけど、試しにKotlin + Daggar2のコードを書こうとして、面倒臭くなって諦めた)
- Android Studio 3.4
- Kotlin 1.3.30
- Koin 1.0.2
ライブラリの準備
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の方のテストの自動化についても調べようと思う