最小限の手順でサクッと構成したい
【2023/2/9更新】※API Gatewayを使わない方法を赤で追記しました。コードの見直しもしています。
「EC2の起動とか停止とかをURLアクセスでできないかね。URLパラメーターでサーバー名を指定して(Nameタグで指定して)起動する、みたいな。」
という要望は、EC2の起動・停止を手動で運用している環境で生じていると思います。
実現する投稿は多くあるけれども、セキュリティや拡張性も考えて実装している例が多く(まあそうするのが当然なので)、その結果として難しくなってしまい、やっぱコンソールで手動操作でいいやとあきらめてしまうことも考えられます。
ちょっともったいないですね。
そこで、API Gateway + Lambda を使用して API Gatewayを使わずLambdaのみで できるだけ少ない手順で 実装してみます。
入力値や変更する選択肢は全て書き起こしてみます。
・EC2は全て ap-northeast-1(東京リージョン)にある前提です。
※AWSでEC2の”起動”はローンチのことを指しますが、ここでは最初のセリフのように”起動”を一般的な使われ方(AWSでは”開始”に当たる)で使っています。
では進めましょう。
1.IAMポリシーの作成
IAM > ポリシー > ポリシーを作成 ボタン
ビジュアルエディタは使いません。"ビジュアルエディタ" タブの右にある "JSON" タブを押します。
以下をコピーしてペーストします。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "*"
}
]
}
- ポリシー名:ec2startstop (適当な名前を付けます)
2.IAMロールの作成
IAM > IAMロール > ロールを作成 ボタン
- ユースケースの選択:Lambda
- アタッチするポリシー:ec2startstop (さっき作ったもの)、AWSLambdaBasicExecutionRole
- ロール名は ec2startstop とします。(ポリシー名と同じ名前でもエラーになりません)
3.Lambdaの作成
Lambda > 関数 > 関数を作成 ボタン
- オプション:一から作成
- Lambda関数名:ec2startstop (他のリソースと同じ名前でもエラーになりません)
- ランタイム:Python 3.x
- デフォルトの実行ロールの変更-既存のロールを使用する: ec2startstop
関数の作成ボタンを押す
コード(元のコードを消して、以下のコードをコピペして上書きします。)
import boto3
import re
ec2 = boto3.client('ec2')
def lambda_handler(event, context):
ret_data = {
'isBase64Encoded': False,
'statusCode': 200,
'headers': { 'Content-Type': 'text/html' }
}
access_ok = False
if isinstance(event.get('requestContext', {}).get('http', {}).get('sourceIp'), str):
for ip_prefix in ['1', '2', '3', '4', '5', '6', '7', '8', '9' ]:
if event['requestContext']['http']['sourceIp'].startswith(ip_prefix):
access_ok = True
break
if not access_ok:
# アクセス元のIPアドレスがプレフィックスのリストのいずれかと一致しなければ中断
return dict( { 'body': 'Source IP permission error' }, **ret_data )
instance_ids = []
param = event.get('queryStringParameters')
if param == None or not re.match(r'^(start|stop):[^:]+:[^:]+$', param.get('command', '')):
# URLパラメーターの最小限のチェックでエラーがあれば中断
return dict( { 'body': 'URL parameter error' }, **ret_data )
# URLパラメータ(例:command=start:Name:ServerA01)からコロン区切りで切り出す
start_stop, tag_key, tag_value = param['command'].split(':')[0:3]
try:
ret_describe = ec2.describe_instances(
Filters=[ { 'Name': 'tag:'+tag_key, 'Values': [tag_value] } ]
)
except Exception as e:
return dict( { 'body': f'API error (Exception: {str(e)})' }, **ret_data )
# 指定したタグキーと値の組合せに合致するEC2のインスタンスIDをリスト化する
for rsv in ret_describe['Reservations']:
for inst in rsv['Instances']:
instance_ids.append( inst['InstanceId'] )
ret_data['body'] = f"{param['command']} is valid. "
if instance_ids == []:
# 該当EC2が見つからなかったので中断
ret_data['body'] += 'instance not found error'
return ret_data
elif len(instance_ids) >= 7:
# 該当EC2が多すぎるので中断(セーフティネット)
ret_data['body'] += 'too many instances error'
return ret_data
try:
if start_stop == 'start':
ec2.start_instances( InstanceIds = instance_ids )
else:
ec2.stop_instances( InstanceIds = instance_ids )
ret_data['body'] += f'{start_stop}_instances called {str(instance_ids)}'
except Exception as e:
ret_data['body'] += f'API error (Exception: {str(e)})'
return ret_data
[2023/2/9] 上記のコードは、主に4点を更新しました。
・正規表現を更新(絞り込みが粗かったため) r'(start|stop):.*:.*'
→ r'^(start|stop):[^:]+:[^:]+$'
・アクセス元IPのプレフィックスで許可するようにコードを追加
(上記コード内では ['1', '2', '3', ・・・, '9' ]
として全てのIPを許可にしています。適宜修正ください)
・API実行時のExeptionを取得して中断する
・セーフティネットとしてインスタンスが7個以上の場合失敗させる
(上記コード内では >= 7
としています。適宜修正ください)
- 一般設定 - タイムアウト:3秒 → 10秒 に延ばしておきます
-
関数URL - 「関数URLを作成」ボタンを押す
- 認証タイプ:None
関数URL「 https:~xxxxx.lambda-url.xxxxx.on.aws/ 」のリンクが生成されたことを確認します。
4.Lambdaのテスト
関数URL「 https:~xxxxx.lambda-url.xxxxx.on.aws/ 」のURLにアクセスします。
Webブラウザに URL parameter error
が返ってくればOKです。
以下も合わせて実施してもよいです。
コードが正しく動作するか一応テストします。
テストタブで、テストを作成します。
{
"requestContext": { "http": { "sourceIp": "12.34.56.78" } },
"queryStringParameters": { "command":"start:Name:dummy" }
}
コードの修正に合わせてテストデータに $.requestContext.http.sourceIp
とダミーのIPを追加しています。
上記をインプットのテストデータとして、変更を保存します。
テストボタンを押します。結果に instance not found
があればOKです。
dummy
を存在するEC2のNameタグに変更して保存します。(ServerA01 が存在するなら dummy を ServerA01 に)
再度テストボタンを押します。結果に start_instances called [i-xxxxxxxxxxxxxxx]
があればOKです。
5.API Gatewayの作成
関数URLを使う場合は、この API Gatewayの作成は不要です。一応、引用の形式で残します。
API Gateway > APIを作成 > REST API
プロトコル: REST
新しい API の作成: 新しい API
API名: ec2startstop
APIの作成ボタンを押します。アクション-メソッドの作成 を押すとブランクのボックスが現れるので GET を選択
GET をクリックすると GET - セットアップ 画面になります。
統合タイプ: Lambda 関数
Lambda プロキシ統合の使用: チェックを入れる
Lambda 関数: ec2startstop
保存ボタンを押します。
Lambda 関数に権限を追加する のダイアログでOKボタンを押します。アクション-APIのデプロイ を押すとダイアログが表示されます。
デプロイされるステージ: [新しいステージ]
ステージ名: s ※何でもいいのですがURLの一部に入ってきますのでそのつもりで
デプロイ ボタンを押します。ステージエディタの画面で
URL の呼び出し: https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s
が表示されます。
xxxxxxxx はランダムで割り当てられます。/s の後にパラメータを付与しないままURLをWebブラウザーで呼び出してみましょう。
URL parameter error
と表示されたらOK。API Gatewayを経由してLambdaが実行されたことになります。
6.URLの組み立て
URLの後ろに
?command=<start または stop>:<タグのキー名>:<タグの値>
を付与してURLを組み立てます。
例:
Nameタグの値が ServerA01 のEC2を起動・停止する場合
https:~xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=start:Name:ServerA01
https:~xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=stop:Name:ServerA01
https:~xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=start:Name:ServerA01
https:~xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=stop:Name:ServerA01
Envタグの値が Development のEC2をまとめて起動・停止する場合
https:~xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=start:Env:Development
https:~xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=stop:Env:Development
https:~xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=start:Env:Development
https:~xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=stop:Env:Development
7.実行
組み立てたURLでアクセスします。
EC2の起動・停止ができたでしょうか。
この方法で実装されたURLは、学習のためと割り切ってください。
セキュリティも何もありません。 送信元IPによるアクセス制御が可能です。
URLが意図せずに第3者に漏れてしまったら、、、勝手にOSを停止されたりして大変ですので。