5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ガバクラ共同利用方式のように直接操作できないアカウントの EC2 を閉域 API Geteway 経由でオンプレミスから起動停止できるようにする

Last updated at Posted at 2025-11-03

先日、デジタル庁が 継続的運用経費削減 (FinOps) ガイド を公開しました。この中で「運用保守作業の削減アプローチ例」として、地方自治体向けに作業の内製化が挙げられており、サービスの起動停止処理を自治体職員が行う、といった例示がされていました。

地方自治体のガバメントクラウド移行の現状として、AWS だと EC2 や ECS を使ったアプリケーションが多いので、ここでいうサービスの起動停止処理とは EC2 のインスタンスや ECS のタスクを起動停止することを指していると思われます。

しかし、ガバメントクラウドでは、地方自治体が自身でパブリッククラウドのアカウントを運用管理せず、事業者が運用管理する「共同利用方式」が多いため、自治体職員がサービスの起動停止の処理を行うことは困難です。

ガバメントクラウドには GCAS という基盤が提供する認証認可の機能があり、AWS だと IAM Identity Center のシングルサインオンでアカウントの許可セットにアクセスできますが、共同利用方式の場合は事業者の GCAS アカウントのみがこれを許可されています。

そのため、自治体職員の GCAS アカウントからマネジメントコンソールや AWS CLI などで共同利用方式の AWS アカウントのリソースを操作することができません。また、自治体のガバメントクラウド環境は基本的に閉域となるため、インターネットを経由して Systems Manager などから EC2 インスタンスを操作することもできません。

そこで、AWS の場合で、共同利用方式のアカウントの閉域 VPC 内にある EC2 インスタンスを自治体職員が自分で起動や停止の操作を行うためにはどうしたらよいか、アーキテクチャを考え、実際に個人の環境で検証してみました。

EC2 起動停止アーキテクチャの概要

具体的には、共同利用方式のアカウントに、EC2 インスタンスの起動停止の操作をできる激ヤバ Lambda 関数を作成して API Gateway 経由で実行できるようにし、この激ヤバ API Gateway には VPC エンドポイント経由で閉域の地方自治体オンプレミス環境からアクセスさせる構成とします。

PowerShell などから API にアクセスできるようにスクリプトを作り、自治体職員からスクリプトで起動停止の操作を実行する、という発想です。

構成図は以下のとおりです(全て東京リージョンとします)。

なお、既に地方自治体のオンプレミス環境の DNS サーバーと VPC の Route 53 インバウンドエンドポイントは連携しており、VPC に API Gateway をデプロイすると地方自治体オンプレミス環境から API Gateway のエンドポイントを名前解決できることとします。

AWSNetwork7.drawio.png

API Gateway のアクセス制御については後で詳しく説明します。それでは早速構築してみます。

EC2 インスタンスを起動・停止する Lambda 関数の作成

Lambda 関数として、Python の boto3 クライアントから EC2 の API にアクセスし、対象の EC2 インスタンスを起動・停止できるようにします。

EC2 の API を操作するだけなので、Lambda 関数を VPC に関連付けする必要はありません。

Lamda 関数の実行ロールの作成

Lambda 関数から EC2 の起動・停止操作が許可されるよう、以下のような IAM ポリシーを作成し、Lambda の実行ロールとする IAM ロールへ割り当てました。

インスタンスの起動・停止の状態を取得したいため、ec2:DescribeInstanceStatus も許可しています。

信頼されたエンティティ

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "lambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

許可ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ec2:StartInstances",
                "ec2:StopInstances",
                "ec2:DescribeInstanceStatus"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*",
            "Effect": "Allow"
        }
    ]
}

基本設定から Lambda 関数の実行タイムアウト時間の設定

デフォルトのタイムアウト時間だと処理が間に合わないため、適当に 30 秒程度に変更しました。

EC2 を起動・停止する Lambda 関数のコード

実際のコードは以下のとおりです。イベントで以下の JSON を渡してください。Action キーに Start をセットするとインスタンスの起動を、Stop をセットすると停止を行います。

{"Action": "Start | Stop"}

EC2 API を操作して起動または停止した後、インスタンスの状態を返すようにしています。

一応リトライの処理も入れていますが、例えば InsufficientInstanceCapacity Error が発生する状況でどの程度効果があるのかは分かりません。

import time

import boto3
import botocore
from botocore.config import Config

# エラー時のリトライ数を設定
config = Config(retries={"total_max_attempts": 10, "mode": "standard"})
# EC2 のリージョンを指定
region = "ap-northeast-1"
# 対象のインスタンス ID をリストに列挙する
instances = [
    "対象のインスタンス ID",
]


def lambda_handler(event, context):
    ec2 = boto3.client("ec2", config=config, region_name=region)
    action = event["Action"]
    instance_states = dict()
    try:
        if action == "Start":
            ec2.start_instances(InstanceIds=instances)
        elif action == "Stop":
            ec2.stop_instances(InstanceIds=instances)
        else:
            return {
                "statusCode": 200,
                "body": "Action パラメーターが不正です。",
            }

        time.sleep(15)
        statuses = ec2.describe_instance_status(InstanceIds=instances, IncludeAllInstances=True)
        for status in statuses["InstanceStatuses"]:
            instance_states[status["InstanceId"]] = status["InstanceState"]["Name"]

        return {
            "statusCode": 200,
            "body": {
                "Action": action,
                "States": instance_states,
            },
        }
    except botocore.exceptions.ClientError as error:
        return {
            "statusCode": 503,
            "body": error.response["Error"]["Message"],
        }

Lambda 関数のテストに成功したら、API Gateway 経由で Lambda 関数を実行できるようにしていきます。

Lambda 関数を閉域から実行させる API Gateway の作成

インターフェース型 VPC エンドポイントの作成

API Gateway のエンドポイントタイプをプライベート(閉域)でデプロイするためには、事前に以下のインターフェース型 VPC エンドポイントを作成する必要があります。

  • com.amazonaws.ap-northeast-1.execute-api

なお、API Gateway をプライベートでデプロイすると、別途 Route 53 で設定しない限り、デプロイした VPC からしか名前解決できません。そのため、オンプレミスのクライアントから名前解決させるためには、amazonaws.com の名前解決クエリを Route 53 インバウンドエンドポイントへ条件付きフォワード(転送)する必要があることに注意してください。

REST API の作成

API Gateway のマネジメントコンソールから REST API の作成を行います。API エンドポイントタイプを「プライベート」にし、先ほど作成した VPC エンドポイントを指定します。

スクリーンショット 2025-11-02 21.14.32.png

メソッドの作成

メソッドタイプは「POST」を、統合タイプは「Lambda 関数」を、Lambda 関数には先ほど作成したものをそれぞれ指定してメソッドを作成します。

スクリーンショット 2025-11-02 21.18.03.png

リソースポリシーでアクセス制御の作成

プライベートな API Gateway は、リソースポリシーを適用しないとデプロイできません。

そこで、デプロイする VPC から且つ特定のオンプレミスの送信元 IP アドレスからのみ API Gateway にアクセスできないよう、以下のリソースポリシーを作成しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*", # 自動で ARN に展開されます
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpc": "デプロイする VPC の ID"
        }
      }
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*", # 自動で ARN に展開されます
      "Condition": {
        "NotIpAddress": {
          "aws:VPCSourceIp": "オンプレミスのクライアントの IP アドレス"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*", # 自動で ARN に展開されます
    }
  ]
}

Lambda 実行用 API のデプロイとテスト

API をデプロイします。ステージ名は任意のものを設定します(ここでは「v1」としました)。

スクリーンショット 2025-11-02 21.53.53.png

デプロイできたら、マネジメントコンソールの「ステージ」の「ステージの詳細」にエンドポイントの URL が表示されているので、これを任意のクライアントからエンドポイントへ POST でアクセスしてみます。

# 起動処理
$ curl -w'\n' -X POST "https://hoge.execute-api.ap-northeast-1.amazonaws.com/v1" -H "Content-Type: application/json" -d '{"Action": "Start"}'
# 停止処理
$ curl -w'\n' -X POST "https://hoge.execute-api.ap-northeast-1.amazonaws.com/v1" -H "Content-Type: application/json" -d '{"Action": "Stop"}'

スクリーンショット 2025-11-02 23.37.01.png

Lambda 関数で設定した期待値が返ってきていれば成功です。

リソースポリシーで許可していない IP アドレスのクライアントで実行したら、エラーが返ってきます。

スクリーンショット 2025-11-03 0.19.32.png

これを Bash なり PowerShell なりでスクリプトにしておき、オンプレミスの指定した IP アドレスの端末から実行すれば、EC2 の API を直接操作することなく、閉域網のネットワーク越しに EC2 インスタンスの起動・停止ができるようになります。

一応、ここまでの手順でも最低限の機能が出来たと思います。

Lambda 実行用 API のアクセス制御のため Cognito でユーザープールを作成

ここまでの手順で作成した API は、アクセス制御が送信元 IP アドレスのみであるため、少々心許ないです。

そこで、Cognito のユーザープールを使い、API アクセスにユーザー認証を設定することにします。

現在 Cognito はインターフェース型 VPC エンドポイントに対応していないため、単独で閉域の VPC から利用することができませんが、API Gateway と統合することで閉域の VPC から Cognito を利用することが可能です。

ただし、Cognito の認証情報は AWS のグローバルネットワークを経由してしまうことに注意してください。また、実際のガバメントクラウド環境で検証したわけではないので、使えない可能性があることにも留意願います。

AWS の閉域網でのユーザー認証については、AWS ブログを参考にしました。

ユーザープールの作成

Cognito のマネジメントコンソールからユーザープールを作成します。

アプリケーションタイプは「シングルページアプリケーション (SPA)」とします。今回は検証用途のため、自己登録は有効化せずにユーザーディレクトリを作成します。

スクリーンショット 2025-11-03 6.04.33.png

ユーザープールが出来たら「アプリケーションクライアント」の情報を編集し、ユーザー名とパスワードによる認証 (USER_PASSWORD_AUTH) を有効にします。

スクリーンショット 2025-11-03 6.09.13.png

ユーザーの作成

任意のメールアドレスをユーザー名としてユーザーを作成します。パスワードは仮なので適当(ポリシーに沿う必要はある)に設定します。

スクリーンショット 2025-11-03 6.17.06.png

ユーザーを作成した直後はパスワードのステータスが未確認となっています。

スクリーンショット 2025-11-03 6.19.39.png

そのため、一旦 AWS CLI からコマンドでユーザーのパスワード(例として「Passw0rd!」としています)を設定してしまいます。

$ aws cognito-idp admin-set-user-password --region ap-northeast-1 --user-pool-id "ユーザープール ID" --username "ユーザー名に設定したメールアドレス" --password Passw0rd! --permanent --profile "プロファイル名"

ユーザーのパスワードのステータスが「確認済み」となったので認証できるようになりました。

スクリーンショット 2025-11-03 6.27.58.png

Cognito に閉域の VPC からアクセスするための API Gateway の作成

API Gateway から Cognito を操作するための IAM ロールの作成

API Gateway から Cognito を操作できるようにするため、以下の IAM ロールを作成します。

信頼エンティティ

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "apigateway.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

許可ポリシー

許可ポリシーには以下の ARN の AWS 管理ポリシーをセットしました。

  • arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
  • arn:aws:iam::aws:policy/AmazonCognitoPowerUser

REST API の作成

API Gateway のインターフェース型 VPC エンドポイントは作成済みのため、閉域 VPC から Cognito にアクセスするための REST API を作成して大丈夫です。

リソースの作成

作成した REST API に、リソース名を「initiate-auth」としてリソースを作成します。

スクリーンショット 2025-11-03 7.17.37.png

メソッドの作成

作成したリソースにメソッドを作成します。前回作成したメソッドと異なる部分として、統合タイプは「AWS のサービス」とし、AWS のリージョンとサービス (Cognito IDP) を選択します。アクションタイプのアクション名は「InitiateAuth」とし、実行ロールには上記で作成した IAM ロールを選択します。

スクリーンショット 2025-11-03 7.19.15.png

リソースポリシーの作成と API のデプロイ

リソースポリシーは Lambda の API と同じように設定し、API をデプロイします。

Cognito API のテスト

リソースポリシーに設定した送信元 IP アドレスのクライアントから API にリクエストを投げます。

$ curl -w'\n' -X POST "https://fuga.execute-api.ap-northeast-1.amazonaws.com/v1/initiate-auth" -H "Content-Type: application/json" -d '{"AuthFlow": "USER_PASSWORD_AUTH", "AuthParameters": {"USERNAME": "Cognito で作成したユーザー名", "PASSWORD": "Passw0rd!"}, "ClientId": "Cognito のアプリケーションクライアント ID"}'

出力が長いので割愛しますが、ID Token が返ってきていればテストが成功です。

それでは最初に作成した Lambda を実行する API に Cognito でトークン認証を設定していきます。

Lambda 実行用 API にトークン認証を設定する

Cognito オーソライザーの作成

マネジメントコンソールから最初に作成した Lambda 実行用 API を選択し、オーソライザーを作成します。

オーソライザーのタイプは「Cognito」を、Cognito ユーザープールに先ほど作成したものを、トークンのソースは「Authorization」とします。

スクリーンショット 2025-11-03 7.29.07.png

作成していたメソッドを編集し、メソッドクエストの設定の「認可」を上記のオーソライザーに設定します。

スクリーンショット 2025-11-03 7.30.30.png

設定が終わったら API をデプロイします。これにより、Lambda 実行用 API は、リクエストの HTTP ヘッダーに Cognito で認証して得る ID Token の値をセットしないと実行できないようになります。

オーソライザーのテスト

Cognito の API をテストして返ってきたレスポインスから Token ID を抽出し、「トークンの値」に貼り付けてテストします。

スクリーンショット 2025-11-03 7.36.44.png

クレームが返ってきたらテスト成功です。

Cognito 認証用 API からトークンを取得して Lambda 実行用 API をクライアントから実行してみる

実際にクライアントから Curl で API にアクセスしてみます。

最初に Cognito 認証用 API にアクセスして ID Token を取得したら、一旦これを環境変数にセットし、Lambda 実行用 API へのリクエストを組み立てる時にセットしています。

$ curl -w'\n' -X POST "https://fuga.execute-api.ap-northeast-1.amazonaws.com/v1/initiate-auth" -H "Content-Type: application/json" -d '{"AuthFlow": "USER_PASSWORD_AUTH", "AuthParameters": {"USERNAME": "Cognito で作成したユーザー名", "PASSWORD": "Passw0rd!"}, "ClientId": "Cognito のアプリケーションクライアント ID"}'
$ TOKEN="ID Token の値を貼り付ける"
$ curl -w'\n' -X POST "https://hoge.execute-api.ap-northeast-1.amazonaws.com/v2" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"Action": "Start"}'

スクリーンショット 2025-11-03 7.40.32.png

HTTP ヘッダーにトークンをセットしなかった場合は認証エラーとなり、セットした場合は期待した戻り値が返ってきたことが確認できました。

これで全ての API が完成しました。後は自治体職員が運用で使うことを想定してスクリプトに組み込んでいきます。

PowerShell から EC2 インスタンスを起動・停止できるようにする

ここまで作成した API を使って、PowerShell から EC2 インスタンスを起動・停止するスクリプトのサンプルを示します。

test.ps1
$ErrorActionPreference = "Stop"

# Cognito と統合した API Gateway のエンドポイント
$authApiUrl = "https://fuga.execute-api.ap-northeast-1.amazonaws.com/v1/initiate-auth"
# Cognito ユーザープールに作成したユーザー名・パスワード
$userName = "admin@localhost"
$password = "Passw0rd!"
# Cognito アプリケーションクライアント ID
$clientId = "xyz123"
# Lambda と統合した API Gateway のエンドポイント
$execApiUrl = "https://hoge.execute-api.ap-northeast-1.amazonaws.com/v2"
# 起動の場合は Start を、停止の場合は Stop を指定する
$Action = "Start"

# Cognito から Bearer Token を取得する処理

# Request Body に渡す JSON 文字列を組み立てる
$authRequestBody = '{"AuthFlow": "USER_PASSWORD_AUTH", "AuthParameters": {"USERNAME": "' + $userName + '", "PASSWORD": "' + $password + '"}, "ClientId": "' + $clientId + '"}'

# Cognito と統合した API Gateway のエンドポイントにリクエスト
$authResponse = Invoke-WebRequest $AuthApiUrl -Method "Post" -Headers @{"Content-Type" = "application/json" } -Body $authRequestBody | ConvertFrom-Json

# 戻り値から ID Token の文字列を抽出
$idToken = $authResponse.AuthenticationResult.IdToken

# Lambda から EC2 を起動または停止する処理

# Request Body に渡す JSON 文字列を組み立てる
$execRequestBody = '{"Action": "' + $Action + '"}'
# Lambda と統合した API Gateway のエンドポイントにリクエスト
Invoke-WebRequest $execApiUrl -Method "Post" -Headers @{"Content-Type" = "application/json"; "Authorization" = "Bearer ${idToken}" } -Body $execRequestBody

スクリプトを実行すると以下のような出力が還ります。

スクリーンショット 2025-11-03 10.51.10.png

これで PowerShell スクリプトから共同利用方式のアカウントにある EC2 インスタンスを起動・停止できると思います。

もう一つの方法(踏み台 EC2 経由で EC2 API を操作する)

今回、API Gateway 経由で Lambda から EC2 API を実行するアーキテクチャを検証してみました。

実は、閉域でもネットワーク接続がある前提であれば、もう一つ実現できる方法があります。

共同利用方式のアカウントの VPC に地方自治体のオンプレミスからリモート接続できる踏み台 EC2 を用意し、この EC2 に今回 Lambda 関数へセットしたような権限の IAM ロールをアタッチすれば、EC2 の AWS CLI などから EC2 の API にアクセスし、インスタンスの起動や停止ができると思います。

ただし、安易にサーバーへ自治体職員からリモート接続することは、セキュリティ上の重大な懸念があり、やめておいた方がよいです(自分が運用管理補助者なら許可しないと思います)。

そもそも EC2 インスタンスの起動・停止を自治体職員がやるべきか?

今回の検証は、デジタル庁の FinOps ガイドの記載を実装へ落とした時にどういったアーキテクチャがあるかを確認したかったことが最大の目的です。

個人的には自治体職員が EC2 インスタンスの起動や停止を日常的に行うことは反対です。

なぜなら、サーバーの起動や停止時には、事前にサービスを停止したり、バックアップを取ったり、考慮しなければならない手順があるでしょうし、また、パブリッククラウドの特性として、リソース不足など予期せぬ原因で API が正常に実行できない可能性を考える必要があるからです。

では自治体職員はどこまでやれる可能性があるのか?

一方で、自治体職員が一部の運用を内製することでコスト削減ができる可能性はあるのかもしれません。

これはとても難しい話で、自治体職員がどこまでクラウドの運用を内製できるかと言われても、地方自治体によって状況は違うし、一律な答えなんかないんだろうなと思います。

じゃあ何ができて何ができないのか、これから考えていこうとする時に、自分のような現場にいる人間が PoC して気付いたことをフィードバックすることはもしかしたら何か意義があるかもしれないと思っていて、これからも色んなことを学んでいきたいと思いました。

第 4 回 Gov-JAWS の参加レポートに代えて、FinOps ガイドを元に自分なりに考えたりしたことを記事にしてみました。がんばるぞー。

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?