はじめに
Ec2の操作は、AWSコンソールから操作しますが、ログインすることが面倒だったりやチームで開発する場合、EC2の起動・停止のためだけにIAMユーザーを作成するのは手間だったりするため、S3に配置したhtmlでEC2を操作できる仕組みを作成しました。
ただし、メンバー以外に操作されないよう、S3にはCloudfront OAI、CloudFrontとApiGatewayにはBasic認証をかけてます。
構成図
S3にhtmlを配置して、CloudFrontで配信し、S3からアクセスできないよう制限します。
そして、html上でEC2を操作できるようLambdaを使用します。
完成ページ
EC2の現在のスペックと起動有無が非同期で表示され、ワンクリックでEC2を起動・停止・スペック変更できます。
流れ
①Lambda用のIAMロールを作成
②lambdaを4つ作成
③ApiGatewayを設定
④htmlを作成
⑤htmlをS3にアップロード
⑥Cloudfrontからページを読み込むように設定
⑦WebページにBasic認証を有効化する
⑧htmlから起動などのLambda実行後、htmlページにリダイレクトする
*⑧は次回に行います。
Lambda用のIAMロールを作成
インスタンスの起動・停止・スペック変更をLambdaに許可するため、IAMロールを作成します。
-
ポリシーの作成
・下記のポリシーをコピペ
・名前を入力し、ポリシー作成 -
ロールの作成
・信頼されたエンティティの種類を選択は、AWSサービス
・ユースケースの選択はLambda
・先程作成したポリシーを付与し、ロールを作成
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:*:*:instance/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"ec2:ModifyInstanceAttribute",
"ec2:DescribeInstances",
"ec2:ReportInstanceStatus"
],
"Resource": "*"
}
]
}
Action一覧
起動:ec2:StartInstances
停止:ec2:StopInstances
スペック変更:ec2:ModifyInstanceAttribute
EC2情報を取得:ec2:DescribeInstances
EC2ステータス取得:ec2:ReportInstanceStatus
lambdaを4つ作成
-
関数の作成
・一から作成
・関数名を入力(StartEC2,StopEC2,SpecChangeEC2,CheckEC2)
・ランタイムはpython3.8
・アクセス権限は、既存のロールで、先程作成したロールを選択し、lambda作成 -
コード編集
下記のコードをコピペして、デプロイをします
import boto3
import os
def lambda_handler(event, context):
ec2 = boto3.client('ec2', region_name=os.environ['region'])
ec2.start_instances(InstanceIds=[event['instance_id']])
return {
"Instance Started": event['instance_id']
}
import boto3
import os
def lambda_handler(event, context):
ec2 = boto3.client('ec2', region_name=os.environ['region'])
ec2.stop_instances(InstanceIds=[event['instance_id']])
return {
"Instance Stopped": event['instance_id']
}
import boto3
import os
def lambda_handler(event, context):
NEW_INSTANCE_SIZE = event['instace_spec']
ec2 = boto3.client('ec2', region_name=os.environ['region'])
# stop
ec2.stop_instances(InstanceIds=[event['instance_id']])
waiter=ec2.get_waiter('instance_stopped')
waiter.wait(InstanceIds=[event['instance_id']])
#change
ec2.modify_instance_attribute(InstanceId=event['instance_id'],
Attribute='instanceType',
Value=NEW_INSTANCE_SIZE)
#start
ec2.start_instances(InstanceIds=[event['instance_id']])
return {
"Instance Change": event['instace_spec']
}
SpecChangeEC2は、変更前にインスタンスを停止させる必要があり、停止するまでに数秒かかるため、waitを使用しています。
import boto3
import os
def lambda_handler(event, context):
list = []
ec2 = boto3.client('ec2', region_name=os.environ['region'])
ec2_data = ec2.describe_instances()
for reservation in ec2_data['Reservations']:
for instance in reservation['Instances']:
ec2_tags = dict([(tag['Key'], tag['Value']) for tag in instance['Tags']])
ec2_name = ec2_tags.get('Name', 'unknown')
if ec2_name == "":
ec2_name = "unknown"
data = {"id": instance['InstanceId'], "spec": instance['InstanceType'], "status": instance['State']['Name'], "name": ec2_name}
list.append(data)
return list
list.append(data)
で、リージョン内のすべてのEC2をjson形式で取得できます。
3. 設定から環境変数を変更
環境変数は、対象のインスタンスのリージョンを設定する。今回は、東京リージョンを設定します。
4. テスト
一般設定のタイムアウトをデフォルト値3秒であり、タイムアウトでエラーになるのを防ぐため、6秒に変更します。
SpecChangeEC2は、時間がかかるため、60秒に設定します。
instance_idの値は、操作したいEC2のうち一つです。
以下入力し、テストし、成功することを確認します。
{
"instace_spec": "t2.micro",
"instance_id": "i-XXXXXXXX"
}
また、CheckEC2は、json形式で取得できていることがわかります。
ApiGatewayを設定
API Gatewayは,Web APIを提供するサービスであり、APIの入り口といえます。APIがリクエストを受けた後の処理は、先程作成したlambda関数を実行します。
例えば、~/prod/start
というURLをリクエストすると、StartEC2のLambda関数が実行されます。
ステージは prod とします (後述)
Lambda関数 | API リソース | API メソッド | URL |
---|---|---|---|
StartEC2 | start | GET | ~/prod/start |
StopEC2 | stop | GET | ~/prod/stop |
SpecChangeEC2 | specchange | GET | ~/prod/specchange |
CheckEC2 | check | GET | ~/prod/check |
①REST APIの構築をクリックします
②以下の設定で作成します
③リソースを作成します
④リソース名を入力し、API Gateway CORS を有効にする
にチェックし、リソースを作成をクリックします
⑤GETメソッドを作成します
先程作成したLambda関数を紐付けて作成します。optionメソッドは削除してください
⑥GETを選択し、統合リクエストをクリックします
⑦マッピングテンプレートで、application/jsonという名前で追加し、以下の通りにテンプレートを入力し、保存します。
パススルー動作の変更のポップアップが表示されるので、統合を保護をクリックします
{
"instance_id" : "$input.params('instance_id')",
"instace_spec" : "$input.params('instace_spec')"
}
⑨start、stop
メソッドの場合、{start}のクエリ文字にinstance_id=i-XXXXXXXXXX
を入力し、テスト実行すると、対象のEC2が起動・停止します。
check
メソッドの場合は、nullを入力します。
specchange
メソッドの場合は、instance_id=i-xxxxxx&instace_spec=t2.micro
を入力します
specchangeメソッドのテストは、{"message": "Endpoint request timed out"}
と表示され、タイムオーバーになる可能性があります。
理由は、ApiGatewayのタイムアウト仕様は最大29秒(デフォルト)までの制限があるため、停止→スペック変更→起動までに29秒を超えるとエラーが出るというわけです。
ただし、タイムオーバーと表示されてもスペック変更はされているため、このままにしておきます。
エラーを表示したくない場合、EC2を停止してから、スペック変更テストを行うとタイムアウトになりません。
⑩テストに問題がなければ、リソースを③〜⑨手順に従いstart
,stop
,specchange
,check
の4つ作成します
check
リソースのみCORS有効化をクリックし、CORSを有効にして既存のCORSヘッダーを置換
をクリックします
⑪APIをデプロイすると(ステージ名はprod
)、URLの呼び出しが表示されます。
呼び出しURLのprod
の後ろに、start?instance_id=インスタンスID
を加え、URLに遷移すると対象のEC2が起動します
例↓
https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/start?instance_id=i-xxxxxx
aaaaaa
は次のhtml作成のときに使用します。
htmlを作成
htmlを以下のように作成します
aaaaaa
とxxxxxx
は、自身のものを埋め込むこと。
aaaaaaは、先程の呼び出しURLの独自のもの、xxxxxxはインスタンスIDになります。
今回は、EC2を2つ操作します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EC2Control</title>
</head>
<body>
<h1 id="ec2-name-1">読込中(name)</h1>
<h2 id="ec2-id-1">読込中(id)</h2>
<p>ステータス</p>
<ul>
<li id="status1-EC2-1">起動・停止・スペック変更中</li>
<li id="status2-EC2-1">読込中(スペック)</li>
</ul>
<p>アクション</p>
<ul>
<li id="action-start-EC2-1" onclick="return clickEventStart()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/start?instance_id=i-xxxxxx">起動する</a></li>
<li id="action-stop-EC2-1" onclick="return clickEventStop()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/stop?instance_id=i-xxxxxx">停止する</a></li>
<li id="action-spec-large-EC2-1" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.large&instance_id=i-xxxxxx">t2.largeに変更して再起動</a></li>
<li id="action-spec-medium-EC2-1" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.medium&instance_id=i-xxxxxx">t2.mediumに変更して再起動</a></li>
<li id="action-spec-small-EC2-1" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.small&instance_id=i-xxxxxx">t2.smallに変更して再起動</a></li>
<li id="action-spec-micro-EC2-1" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.micro&instance_id=i-xxxxxx">t2.microに変更して再起動</a></li>
<li id="action-spec-nano-EC2-1" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.nano&instance_id=i-xxxxxx">t2.nanoに変更して再起動</a></li>
</ul>
<h1>EC2-2</h1>
<h1 id="ec2-name-2">読込中(name)</h1>
<h2 id="ec2-id-2">読込中(id)</h2>
<p>ステータス</p>
<ul>
<li id="status1-EC2-2">起動・停止・スペック変更中</li>
<li id="status2-EC2-2">読込中(スペック)</li>
</ul>
<p>アクション</p>
<ul>
<li id="action-start-EC2-2" onclick="return clickEventStart()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/start?instance_id=i-xxxxxx">起動する</a></li>
<li id="action-stop-EC2-2" onclick="return clickEventStop()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/stop?instance_id=i-xxxxxx">停止する</a></li>
<li id="action-spec-large-EC2-2" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.large&instance_id=i-xxxxxx">t2.largeに変更して再起動</a></li>
<li id="action-spec-medium-EC2-2" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.medium&instance_id=i-xxxxxx">t2.mediumに変更して再起動</a></li>
<li id="action-spec-small-EC2-2" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.small&instance_id=i-xxxxxx">t2.smallに変更して再起動</a></li>
<li id="action-spec-micro-EC2-2" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.micro&instance_id=i-xxxxxx">t2.microに変更して再起動</a></li>
<li id="action-spec-nano-EC2-2" onclick="return clickEventChange()"><a href="https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/specchange?instace_spec=t2.nano&instance_id=i-xxxxxx">t2.nanoに変更して再起動</a></li>
</ul>
<script>
fetch('https://aaaaaa.execute-api.ap-northeast-1.amazonaws.com/prod/check') // (1) リクエスト送信
.then(response => response.json()) // (2) レスポンスデータを取得
.then(json => { // (3)レスポンスデータを処理
//id
document.getElementById('ec2-name-1').innerHTML = json[0].name;
document.getElementById('ec2-id-1').innerHTML = json[0].id;
//status
if (json[0].status == "running"){
document.getElementById('status1-EC2-1').innerHTML = "起動中";
document.getElementById("action-start-EC2-1").style.display ="none";
}else if(json[0].status == "stopped"){
document.getElementById('status1-EC2-1').innerHTML = "停止";
document.getElementById("action-stop-EC2-1").style.display ="none";
}else{
document.getElementById('status2-EC2-1').innerHTML = "エラーです"
};
//spec
if (json[0].spec == "t2.large"){
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
document.getElementById("action-spec-large-EC2-1").style.display ="none";
}else if(json[0].spec == "t2.medium"){
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
document.getElementById("action-spec-medium-EC2-1").style.display ="none";
}else if(json[0].spec == "t2.small"){
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
document.getElementById("action-spec-small-EC2-1").style.display ="none";
}else if(json[0].spec == "t2.micro"){
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
document.getElementById("action-spec-micro-EC2-1").style.display ="none";
}else if(json[0].spec == "t2.nano"){
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
document.getElementById("action-spec-nano-EC2-1").style.display ="none";
}else{
document.getElementById('status2-EC2-1').innerHTML = json[0].spec + " (現在のスペック)";
};
//20秒ごとにリロード
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("ec2-name-1").addEventListener("click", function(){
window.location.reload();
})
});
setTimeout("location.reload()",20000);
//id
document.getElementById('ec2-name-2').innerHTML = json[1].name;
document.getElementById('ec2-id-2').innerHTML = json[1].id;
//status
if (json[1].status == "running"){
document.getElementById('status1-EC2-2').innerHTML = "起動中";
document.getElementById("action-start-EC2-2").style.display ="none";
}else if(json[1].status == "stopped"){
document.getElementById('status1-EC2-2').innerHTML = "停止";
document.getElementById("action-stop-EC2-2").style.display ="none";
}else{
document.getElementById('status2-EC2-2').innerHTML = "エラーです"
};
//spec
if (json[1].spec == "t2.large"){
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
document.getElementById("action-spec-large-EC2-2").style.display ="none";
}else if(json[1].spec == "t2.medium"){
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
document.getElementById("action-spec-medium-EC2-2").style.display ="none";
}else if(json[1].spec == "t2.small"){
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
document.getElementById("action-spec-small-EC2-2").style.display ="none";
}else if(json[1].spec == "t2.micro"){
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
document.getElementById("action-spec-micro-EC2-2").style.display ="none";
}else if(json[1].spec == "t2.nano"){
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
document.getElementById("action-spec-nano-EC2-2").style.display ="none";
}else{
document.getElementById('status2-EC2-2').innerHTML = json[1].spec + " (現在のスペック)";
};
});
function clickEventStart() {
var checked = confirm("起動します");
if (checked == true) {
return true;
} else {
return false;
}
}
function clickEventStop() {
var checked = confirm("停止します");
if (checked == true) {
return true;
} else {
return false;
}
}
function clickEventChange() {
var checked = confirm("スペック変更します");
if (checked == true) {
return true;
} else {
return false;
}
}
</script>
</body>
</html>
javascriptのfetchを使用することで、非同期でインスタンスIDと現在のスペックと起動の有無を確認できます。
インスタンスごとに同じようなコードが繰り返していますので、もっとスマートに書けると思いますが、とりあえずこれでいきます。
onclick="return clickEventStart()"
としていますが、onClickにreturnをつけないと、キャンセル時も起動してしまいます。
JSのconfirmでダイアログのボタン押下時の処理の分岐
htmlをS3にアップロード
S3のバケット作成
・バケット名入力
・パブリックアクセスをすべてブロックのチェックを外します
・バケット作成をクリック
・先程作成したhtmlをアップロード(パブリック読み取りアクセス権の付与を選択すること)
・プロパティ→静的ウェブサイトホスティングを編集し、静的ウェブサイトホスティングを有効、インデックスドキュメントを入力し保存します
・アクション→公開をクリックし,バケットウェブサイトエンドポイントからhtmlページに遷移できます
詳しくはこちら参照
https://qiita.com/yuichi1992_west/items/ab3b7e91c34f822cb12b
Cloudfrontからページを読み込むように設定
CloudFront OAIを作成して、ユーザーは S3 バケットへ直接アクセスを不可とし、CloudFront 経由でのみアクセスできるようにします。
S3の静的ウェブサイトホスティングは無効
に設定します。
有効だと、CloudFront OAIによるアクセス制限はできません。
s3の設定
・静的ウェブサイトホスティング:無効
Cloudfrontの設定
・Origin domain:ec2contol.s3.ap-northeast-1.amazonaws.com
・Name:ec2contol.s3.ap-northeast-1.amazonaws.com
・S3 bucket access:YES
→Create new OAI
・Bucket policy:YES
Origin domainに必ずリージョン名も含めてください。ec2contol.s3.ap-northeast-1.amazonaws.com
ではなくec2contol.s3.amazonaws.com
とすると、CloudFrontのURLにアクセスしてもS3のURLにリダイレクトされることがあるようです。
こちら参考になります。CloudFrontのURLにアクセスしてもS3のURLにリダイレクトされる
CloudFrontは、ディストリビューションのURLへアクセスする際に、"index.html"の指定を省略できる設定があります。
ルート URLのアクセス時に"index.html" をオブジェクトとして返す設定を行います。
settingsのEdit
から
・Default root object:index.html
と入力
ディストリビューションのURLへアクセスすると、ページが表示されました!
また、S3からのアクセスもできないことを確認しておきましょう!
WebページにBasic認証を有効化する
ApiGatewayとCloudFrontから直接のアクセスされる可能性がありますので、それぞれにBasic認証をかけます。
ApiGatewayは、こちらの手順通りに行いました。
「API GatewayにLambda(Node.js)でBASIC認証かける」
ただし、checkのGETメソッドのみBasic認証は外しています。非同期でfetchが働かなかったためです。checkは操作されることはないので、このような対応にしました。
API Gatewayの方は設定後、デプロイを忘れずにしましょう!
CloudFrontは、こちらの手順通りに行いました。
以下の2通りの方法があります
- [Lambda@EdgeでBasic認証設定]
(https://qiita.com/r-wakamatsu/items/43fa0f3c4b2e7c9528cf) - CloudFront FunctionsでBasic認証
追加作業
以下ののEdit behaviorコンソールの記載しておきます。
CloudfrontのEdit behavior
Origin and origin groups
S3bucketのURLを指定します
Cache key and origin requests
・Legacy cache settings:Headers
→Include the following headers
→Access-Control-Request-Headers」Access-Control-Request-Method、Origin3つを加える
・Legacy cache settings:Object caching
→Object caching
→Customize
→TTL3つすべて0にする
簡単にBasic認証をかけることができました。
⑧htmlから起動などのLambda実行後、htmlページにリダイレクトする
htmlから起動をクリックするとページが遷移され、戻るボタンを押す必要があります。
煩わしいので、遷移せずhtmlにリダイレクトするよう設定していきます。
次回に繰り越します。
API Gateway でLambda実行後、特定のページにリダイレクトする ②
参考文献
boto3でEC2インスタンスサイズ変更
AWS Lambda 関数で EC2 インスタンスを起動・終了する
CloudFront + S3 構築のポイント
API Gateway + LambdaでREST API開発を体験しよう [10分で完成編]
javascriptでAPI Gatwayにリクエスト送ったらエラーが出た! > それ、CORSを忘れていない?