2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

APIGateway+Lambdaで、コンソール画面を作成し、EC2の起動・停止・スペック変更を操作する①

Last updated at Posted at 2021-08-13

はじめに

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ロールを作成します。

  1. ポリシーの作成
    ・下記のポリシーをコピペ
    ・名前を入力し、ポリシー作成

  2. ロールの作成
    ・信頼されたエンティティの種類を選択は、AWSサービス
    ・ユースケースの選択はLambda
    ・先程作成したポリシーを付与し、ロールを作成

IaRole
{
    "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つ作成

  1. 関数の作成
    ・一から作成
    ・関数名を入力(StartEC2,StopEC2,SpecChangeEC2,CheckEC2)
    ・ランタイムはpython3.8
    ・アクセス権限は、既存のロールで、先程作成したロールを選択し、lambda作成

  2. コード編集
    下記のコードをコピペして、デプロイをします

StartEC2
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']
    }
StopEC2
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']
    }
SpecChangeEC2
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を使用しています。

CheckEC2
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を以下のように作成します
aaaaaaxxxxxxは、自身のものを埋め込むこと。
aaaaaaは、先程の呼び出しURLの独自のもの、xxxxxxはインスタンスIDになります。
今回は、EC2を2つ操作します。

index.html
<!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ページです↓

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通りの方法があります

追加作業

以下のの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を忘れていない?

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?