やったこと
iPadからEC2インスタンスをワンタップで起動・停止できるようにした。
(gif対応のため、インスタンス立ち上げ時間を大幅にトリミングしています。実際には40秒ほどかかります。)
全体のコードはこちら
動機
EC2に開発環境を置いており、iPadからssh接続して開発を行っている。
が、毎回ログイン面倒くさい。
認証から入らないとだし、何よりawsのwebページはiPadからだとスクロールするのに癖がある。
awsのアプリでもEC2を起動したいだけなのに、
services->ec2->Instances->instance->Actions->start
と、かなり深い。
あと、立ち上げて実際にrunningとなり、ssh接続できるようになるのを毎回確認しないといけないのが面倒くさかったので、そこら辺を自動化したかった。
環境・使用アプリ
- iPad pro 2世代
- Pythonista3
- blink
- ショートカットapp
前提
Blinkでec2に接続できるよう、公開鍵の設定は済ませていること
手順
開発手順は下記の通り。
- Blinkの設定から、x Callback Urlを有効にして、キーを取得
- awsで、ec2を操作できるユーザを作成し、キーを取得
- pythonista3でec2を操作するプログラムを書く
- ショートカットアプリで組み立てる
Blink設定
Blinkを開いた状態でcmd
+,
押下でSettings
を開く。
X Callback Url
をOn
にして、URL key
をひかえる。
aws設定
操作用のポリシーを作成
EC2を操作するユーザに付与するポリシーを作成する。
IAM -> ポリシー -> ポリシーの作成
EC2の情報/状態取得と、開始/停止操作の権限が必要なので、ポリシーは下記の通り。
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
}
]
}
適当な名前(ここではEC2_start_stop_policy
)を付けて保存する。
操作用のIAMユーザ作成
pythonista3用のユーザを作成し、先ほど作成したポリシーを付与する。
IAM -> ユーザー -> ユーザーを追加
作成したら、アクセスキーと秘密キーを取得し、ひかえる。
作成したユーザー -> 認証情報 -> アクセスキーの作成
pythonista3でコーディング
StaShにてパッケージインストール
pythonでawsを操作するパッケージであるboto3を利用する。
デフォルトのままではインストールされていないので、StaSh経由でインストールする。
pip install boto3 botocore jmespath
boto3だけでは依存パッケージがインストールされなかったので、必要なものも一緒に入れる。
もし過不足あれば都度pip install
参考: StaShの入れ方
boto3
下記記事を参考にした。
Blinkの起動について、ショートカットappにBlinkがなかったが、URLスキーマ対応されていたので、pythonから直接起動する。
ただし、後述する癖あり(不具合?仕様?URLスキーマまわり明るくないので分からない)。
import sys
import console
import time
import urllib.parse
import webbrowser
import boto3
import config
class AwsController():
instance_id = config.instance_id
aws_access_key_id = config.aws_access_key_id
aws_secret_access_key = config.aws_secret_access_key
region_name = config.region_name
def __init__(self) -> None:
self.instances = [self.instance_id]
self.ec2 = boto3.client(
'ec2',
aws_access_key_id=self.aws_access_key_id,
aws_secret_access_key=self.aws_secret_access_key,
region_name=self.region_name
)
def start(self) -> None:
self.ec2.start_instances(InstanceIds=self.instances)
def stop(self) -> None:
self.ec2.stop_instances(InstanceIds=self.instances)
def fetch_instance_status(self) -> str:
return \
self.ec2.describe_instances(InstanceIds=self.instances)\
['Reservations'][0]\
['Instances'][0]\
['State']['Name']
def fetch_instance_domain(self) -> str:
return \
self.ec2.describe_instances(InstanceIds=self.instances)\
['Reservations'][0]\
['Instances'][0]\
['NetworkInterfaces'][0]\
['Association']['PublicDnsName']
def wait_until_running(self) -> bool:
while self.fetch_instance_status() != 'running':
print('waiting...')
time.sleep(10)
time.sleep(10)
print('run!')
return True
class BlinkController (object):
BLINK_KEY = config.BLINK_KEY
def connect_to(self, user_name, domain, ssh_key_name) -> None:
print(
'blinkshell://run?key=' + BlinkController.BLINK_KEY +
'&cmd=' +
urllib.parse.quote(
'ssh -v -i "' + ssh_key_name + '" ' +
user_name + '@' + domain
)
)
webbrowser.open(
'blinkshell://run?key=' + BlinkController.BLINK_KEY +
'&cmd=' +
urllib.parse.quote(
'ssh -v -i "' + ssh_key_name + '" ' +
user_name + '@' + domain
)
)
def main() -> None:
aws_controller = AwsController()
instance_status = aws_controller.fetch_instance_status()
if instance_status == 'running':
console.alert('now RUNNING','STOP?', 'yes', hide_cancel_button=False)
aws_controller.stop()
elif instance_status == 'stopped':
console.alert('now STOPPED','RUN?', 'yes', hide_cancel_button=False)
aws_controller.start()
aws_controller.wait_until_running()
blink_controller = BlinkController()
blink_controller.connect_to(
user_name=config.user_name,
domain=aws_controller.fetch_instance_domain(),
ssh_key_name=config.ssh_key_name
)
else:
print('now stopping or pending. please exec later.')
return
if __name__ == '__main__':
main()
コード概要
1つのスクリプトで、EC2をトグルしたかったのでこの書き方になっている(が、分けた方が良かったと思う)。
停止操作は見たままなので省略。
開始操作の手順は下記の通り。
- EC2の情報を取得、停止済みなら開始するか尋ねる
- EC2に開始指令を出す
- 該当インスタンスのステータスをポーリングし、'running'が返ってくるまで待つ
- 'running'が返ってきたら、少し待ってから再度インスタンスの情報を取得し、パブリックドメイン名など、ssh接続に必要な情報を取得
- runningとなった直後はdnsが確立していないのか(?)ssh接続できないことが多々あったので、余裕を持たせる
- Blinkをsshコマンド付きでurlスキーマ経由で起動する
書いたコードはpythonista3のurlスキーマ経由でショートカットappから起動するため、
設定 -> shortcuts -> Pythonista URL -> Copy URL
で控えておく。
ショートカット
ショートカットappを初めて使ったので、変なことをしてるかも。
Blinkについて、先にも少し述べたがurlスキーマからコマンド付きで実行する場合の留意点。
あらかじめBlinkを立ち上げておかないと、urlスキーマのcmd
以降が無視されることがあるようで、sshコマンドがうまく実行されなかった。
そのため、ショートカットappで、pythonista3のスクリプトを起動した直後にBlinkを起動するようにした。
ただしその場合、フォーカスがBlinkに飛んでしまい、pythonスクリプトのアラートが確認できなくなるので、さらにPythonistaを画面に表示するようにして対応した。
ちなみに、
- Blinkを開く
- pythonista3のurlスキーマを開く
でも良さそうに見えるが、1.
後手動でショートカットappに戻らないと2.
が実施されなかったので断念。
終わりに
出来上がったのがこちら。
dockに入れておいて、
- 開発したくなったらショートカットポチっ
- お供のコーヒーを淹れる
- 戻ってくるとssh接続済み
- 開発やめるときはdockの同ショートカットぽち
なので、とても楽チンになった
ちなみに
本当はアイコンを見るだけでインスタンスが起動中かどうかわかるようにしたかったが、ショートカットappには動的にアイコンを切り替える機能はないよう。
macを持っていないので、凝ったアプリは作れない。断念。
iPadOS15で、swift playgroundでアプリを作成できるようになるとのことなので、もしそっちでアイコンを切り替えられるなら嬉しい。
ちなみに2
下記の記事のように、EIPを利用せず、インスタンス起動時に毎回route53のホストゾーンにサブドメインを登録するlambdaを使っているが、
インスタンス起動 -> lambda実行 -> dns反映
と、接続までさらに時間がかかってしまうので、今回はパブリックドメインで接続することにした。
もしEIPを利用しない独自ドメインでsshしたい場合は、ポリシーにroute53の状態チェックも追加し、route53にドメインが追加されたされたことを確認する処理が必要となる。