- 公開日
- 最終更新日
【Amazon EC2】自動停止システム構築ハンズオン
この記事を共有する
目次
パーソル&サーバーワークスの印鑰です。
AWSを中心としたクラウドインフラの設計・構築・運用を担当しています。
エージェント開発にも注力しているエンジニアです。
このハンズオンで学べること
このハンズオンでは、以下一連の流れを体験することで、AWSのコスト最適化とインフラ自動化のスキルが身につきます。
- Infrastructure as Code(IaC)によるインフラ構築
- Bashスクリプトとsystemdによる定期監視の仕組み
- Amazon CloudWatch LogsとAWS Lambdaを連携させたイベント駆動型の自動化処理
どんな人向け?
- AWS マネジメントコンソールの基本操作ができて、Linuxコマンドライン(cd、ls、cat、viエディタ)の基礎がわかる方なら大丈夫です。
- AWS CloudFormationやBashスクリプトの知識があるとさらに楽しめますが、なくてもコピー&ペーストで進められます。
1.概要
このシステムには以下のような特徴があります。
| 特徴 | 説明 |
|---|---|
| コスト削減 | 使っていないAmazon EC2を自動で止めて、無駄な出費をカット |
| 柔軟な設定 | アイドル時間やデバウンス間隔を自由に調整 |
| タグで簡単制御 | auto_stopタグをつけるだけで停止対象を管理 |
| Session Manager対応 | SSHだけじゃなく、Session Managerの接続もバッチリ検知 |
2.システムアーキテクチャ
全体構成図

3.前提条件
ハンズオンをはじめる前に、以下をお読みください。
所要時間
- 約1時間
- 構築作業:約10~20分
- 動作確認:約20~30分
- 環境削除:約10分
必要な権限
以下の権限が必要です。
- AWS マネジメントコンソールにアクセスできること
- AWS CloudFormationスタックをデプロイできること
- Amazon EC2、AWS IAM、Amazon CloudWatch Logs、AWS Lambdaを操作できること
リージョン
東京リージョン(ap-northeast-1) にリソースを作成してください。
開発環境
ローカルPCに以下がインストール・設定済みであることをご確認してください。
- Windows 11
- Visual Studio Code
付録:Session Managerポートフォワーディング経由のSSH接続 を行う場合は、以下が必要です。
- AWS CLI(認証設定済み)
- Session Manager Plugin
費用について
このハンズオンの費用の目安は、東京リージョンで1時間あたり約$0.10~$0.20程度です。
大事なポイント: 作業が終わったら、必ず環境削除の手順を実行してください。そのままにしておくと課金が続きます。また、為替やログ量、リージョン価格差で変動します。
AWSの設定
このハンズオンでは、AWSの設定が以下となっていることを前提としています。
- AWS Systems Manager Session Manager
- アイドルセッションタイムアウト:20分
4.構築手順
さあ、ここからが本番です。一緒に作っていきましょう。
ステップ一覧
- 基盤スタックのデプロイ
- EC2インスタンススタックのデプロイ
- Session Managerでインスタンスに接続
- 監視スクリプトの作成
- スクリプトの動作テスト
- systemdサービスとタイマーの設定
- CloudWatch Agentの設定
- 動作確認用の設定変更
ステップ1: 基盤スタックのデプロイ
まずは、VPC、AWS IAMロール、Amazon CloudWatch Logs、AWS Lambda関数といった基盤となるリソースを作っていきます。

1-1. AWS CloudFormationテンプレートファイルの作成
ローカルPCでec2-auto-stop-base-stack.yamlという名前のファイルを作って、以下の内容をコピー&ペーストしてください。
ここを押すと展開します
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Base stack: VPC, IAM, Logs, Lambda, KeyPair'
Parameters:
Environment:
Type: String
Default: dev
ProjectId:
Type: String
Default: '001'
VpcCidr:
Type: String
Default: '10.0.0.0/16'
PublicSubnetCidr:
Type: String
Default: '10.0.1.0/24'
LogRetentionInDays:
Type: Number
Default: 7
AllowedValues: [1,3,5,7,14,30,60,90,120,150,180,365,400,545,731,1827,3653]
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Stack Configuration
Parameters:
- Environment
- ProjectId
-
Label:
default: Network Configuration
Parameters:
- VpcCidr
- PublicSubnetCidr
-
Label:
default: Log Configuration
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${Environment}-${ProjectId}-vpc'
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnetCidr
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${Environment}-${ProjectId}-public-subnet'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Security Group for SSM-only'
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: '0.0.0.0/0'
EC2Role:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${Environment}-${ProjectId}-ec2-ssm-rl'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
EC2LogPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub '${Environment}-${ProjectId}-ec2-log-group-write-log-pl'
Roles:
- !Ref EC2Role
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:DescribeLogStreams
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${Environment}-${ProjectId}-ec2-log-group-write-logs'
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${Environment}-${ProjectId}-ec2-log-group-write-logs:*'
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-instance-profile'
Roles:
- !Ref EC2Role
EC2KeyPair:
Type: AWS::EC2::KeyPair
Properties:
KeyName: !Sub '${Environment}-${ProjectId}-ec2-keypair'
KeyType: rsa
KeyFormat: pem
EC2WriteLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '${Environment}-${ProjectId}-ec2-log-group-write-logs'
RetentionInDays: !Ref LogRetentionInDays
LambdaWriteLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '${Environment}-${ProjectId}-lambda-log-group-write-logs'
RetentionInDays: !Ref LogRetentionInDays
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${Environment}-${ProjectId}-lambda-ec2-stop-rl'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: lambda.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: !Sub '${Environment}-${ProjectId}-lambda-ec2-stop-pl'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: [ ec2:DescribeInstances ]
Resource: '*'
- Effect: Allow
Action: [ ec2:StopInstances ]
Resource: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'
Condition:
StringNotEquals: { aws:ResourceTag/auto_stop: 'false' }
- Effect: Allow
Action: [ logs:CreateLogStream, logs:PutLogEvents ]
Resource: [ !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${Environment}-${ProjectId}-lambda-log-group-write-logs:*' ]
LambdaFunction:
Type: AWS::Lambda::Function
DependsOn: [ LambdaWriteLogGroup ]
Properties:
FunctionName: !Sub '${Environment}-${ProjectId}-lambda-ec2-stop'
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt LambdaRole.Arn
Timeout: 30
LoggingConfig:
LogFormat: Text
LogGroup: !Sub '${Environment}-${ProjectId}-lambda-log-group-write-logs'
Code:
ZipFile: |
import boto3
import gzip
import json
import base64
import os
import logging
logger = logging.getLogger(__name__)
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO").upper())
handler_stream = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
handler_stream.setFormatter(formatter)
logger.handlers = [handler_stream]
logger.propagate = False
ec2 = boto3.client("ec2")
def lambda_handler(event, context):
environment = os.environ.get("ENVIRONMENT", "dev")
project_id = os.environ.get("PROJECT_ID", "001")
try:
payload = base64.b64decode(event["awslogs"]["data"])
data = json.loads(gzip.decompress(payload))
except Exception as e:
logger.error(json.dumps({"msg":"CloudWatch Logs ペイロードのデコードに失敗","error":str(e)}))
return {"statusCode": 400, "body": "Bad payload"}
for log_event in data.get("logEvents", []):
msg = log_event.get("message", "")
try:
obj = json.loads(msg)
except json.JSONDecodeError:
logger.warning(json.dumps({"msg":"JSONメッセージのパースに失敗","raw":msg}))
continue
if obj.get("event") not in ("no-active-sessions"):
logger.debug(json.dumps({"msg":"対象外イベントのため無視","event":obj.get("event")}))
continue
instance_id = obj.get("instanceId")
if not instance_id:
logger.warning(json.dumps({"msg":"イベントに instanceId が含まれていません","event":obj}))
continue
try:
r = ec2.describe_instances(InstanceIds=[instance_id])
except Exception as e:
logger.error(json.dumps({"msg":"DescribeInstances の呼び出しに失敗","instanceId":instance_id,"error":str(e)}))
continue
reservations = r.get("Reservations", [])
if not reservations or not reservations[0].get("Instances"):
logger.warning(json.dumps({"msg":"インスタンスが見つかりません","instanceId":instance_id}))
continue
inst = reservations[0]["Instances"][0]
tags = {t.get("Key",""): t.get("Value","") for t in inst.get("Tags", [])}
val = (tags.get("auto_stop") or "").strip().lower()
if val == "false":
logger.info(json.dumps({"msg":"停止をスキップ: auto_stop=false","instanceId":instance_id}))
continue
state = inst.get("State", {}).get("Name")
if state != "running":
logger.info(json.dumps({"msg":"停止をスキップ: running ではない","instanceId":instance_id,"state":state}))
continue
try:
ec2.stop_instances(InstanceIds=[instance_id])
logger.info(json.dumps({"msg":"インスタンスを停止しました","instanceId":instance_id}))
except Exception as e:
logger.error(json.dumps({"msg":"StopInstances の呼び出しに失敗","instanceId":instance_id,"error":str(e)}))
return {"statusCode": 200, "body": "Processed"}
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref LambdaFunction
Action: lambda:InvokeFunction
Principal: logs.amazonaws.com
SourceArn: !GetAtt EC2WriteLogGroup.Arn
LogSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
DependsOn:
- LambdaInvokePermission
- LambdaFunction
- EC2WriteLogGroup
Properties:
LogGroupName: !Sub '${Environment}-${ProjectId}-ec2-log-group-write-logs'
FilterName: !Sub '${Environment}-${ProjectId}-ec2-log-group-write-logs-filter'
FilterPattern: '{ $.event = "no-active-sessions" }'
DestinationArn: !GetAtt LambdaFunction.Arn
Outputs:
PublicSubnetId:
Value: !Ref PublicSubnet
Export:
Name: !Sub '${Environment}-${ProjectId}-auto-stop-public-subnet-id'
SecurityGroupId:
Value: !Ref EC2SecurityGroup
Export:
Name: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-sg-id'
InstanceProfileName:
Value: !Ref EC2InstanceProfile
Export:
Name: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-instance-profile-name'
KeyPairName:
Value: !Ref EC2KeyPair
Export:
Name: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-keypair-name'
ファイルを保存しましょう。
1-2. AWS CloudFormationコンソールを開く
- AWS マネジメントコンソールにログイン
- サービスメニューから「AWS CloudFormation」を選択
- 「スタックの作成」→「新しいリソースを使用(標準)」を選択
1-3. テンプレートのアップロード
- 「テンプレートの準備完了」を選択
- 「テンプレートファイルのアップロード」を選択
- 作成したAWS CloudFormationテンプレートファイルを選んでアップロード
- 「次へ」を選択
1-4. スタック詳細の指定
スタック名: ec2-auto-stop-base-stack
パラメーターはこんな感じで設定してください。
| パラメーター名 | 推奨値 | 説明 |
|---|---|---|
| Environment | dev |
環境識別子 |
| ProjectId | 001 |
プロジェクトID |
| VpcCidr | 10.0.0.0/16 |
VPCのCIDRブロック |
| PublicSubnetCidr | 10.0.1.0/24 |
パブリックサブネットのCIDR |
| LogRetentionInDays | 7 |
ログ保持期間(日数) |
「次へ」を選択しましょう。
1-5. スタックオプションの設定
- 「AWS CloudFormationによってAWS IAMリソースが作成される場合があることを承認します」にチェックを入れ、「次へ」を選択
1-6. 確認して作成
- 設定内容をざっと確認し、「送信」を選択
1-7. デプロイ完了の確認
スタックのステータスが「CREATE_COMPLETE」になるまで、待ちましょう。(約3~5分かかります)
ステップ2: EC2インスタンススタックのデプロイ
次は、監視対象となるEC2インスタンスを作っていきます。

2-1. AWS CloudFormationテンプレートファイルの作成
ローカルPCでec2-auto-stop-instance-stack.yamlという名前のファイルを作って、以下の内容をコピー&ペーストしてください。
ここを押すと展開します
AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2 instance stack'
Parameters:
Environment:
Type: String
Default: dev
ProjectId:
Type: String
Default: '001'
EC2ProjectId:
Type: String
Default: '001'
InstanceType:
Type: String
Default: t3.micro
AllowedValues: [ t3.micro, t3.small, t3.medium, t3.large ]
ImageId:
Type: String
Default: ami-03d1820163e6b9f5d
AllowedPattern: "^ami-[0-9a-f]{17}$"
EbsVolumeSize:
Type: Number
Default: 8
MinValue: 8
MaxValue: 50
AutoStopTag:
Type: String
Default: 'true'
AllowedValues: ['true','false']
IdleMinutes:
Type: Number
Default: 30
Description: 'Idle detection window in minutes. If no activity occurs for this period, the stop event is considered by Lambda.'
MinValue: 10
MaxValue: 120
DebounceMinutes:
Type: Number
Default: 5
Description: 'Debounce interval in minutes to suppress duplicate notifications. If a notification was sent within this interval, subsequent notifications are suppressed.'
MinValue: 5
MaxValue: 110
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Stack Configuration
Parameters:
- Environment
- ProjectId
- EC2ProjectId
-
Label:
default: EC2 Configuration
Parameters:
- InstanceType
- ImageId
- EbsVolumeSize
- AutoStopTag
-
Label:
default: Tool Configuration
Parameters:
- IdleMinutes
- DebounceMinutes
Resources:
EC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref ImageId
InstanceType: !Ref InstanceType
KeyName:
Fn::ImportValue: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-keypair-name'
IamInstanceProfile:
Fn::ImportValue: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-instance-profile-name'
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: 0
GroupSet:
- Fn::ImportValue: !Sub '${Environment}-${ProjectId}-auto-stop-ec2-sg-id'
SubnetId:
Fn::ImportValue: !Sub '${Environment}-${ProjectId}-auto-stop-public-subnet-id'
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeSize: !Ref EbsVolumeSize
VolumeType: gp3
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euo pipefail
exec > >(tee /var/log/user-data.log) 2>&1
echo "=== UserData script started: $(date) ==="
# CloudFormation parameters to shell variables
IDLE_MINUTES=${IdleMinutes}
DEBOUNCE_MINUTES=${DebounceMinutes}
LOG_GROUP_NAME=${Environment}-${ProjectId}-ec2-log-group-write-logs
# Create dedicated environment file for systemd services
cat > /etc/default/check_connections.conf <<ENV_EOF
AUTO_STOP_IDLE_MINUTES=${!IDLE_MINUTES}
AUTO_STOP_DEBOUNCE_MINUTES=${!DEBOUNCE_MINUTES}
AUTO_STOP_LOG_GROUP_NAME=${!LOG_GROUP_NAME}
ENV_EOF
chmod 644 /etc/default/check_connections.conf
echo "Environment file created: /etc/default/check_connections.conf"
echo "AUTO_STOP_IDLE_MINUTES=$(grep '^AUTO_STOP_IDLE_MINUTES=' /etc/default/check_connections.conf | cut -d '=' -f2)"
echo "AUTO_STOP_DEBOUNCE_MINUTES=$(grep '^AUTO_STOP_DEBOUNCE_MINUTES=' /etc/default/check_connections.conf | cut -d '=' -f2)"
echo "AUTO_STOP_LOG_GROUP_NAME=$(grep '^AUTO_STOP_LOG_GROUP_NAME=' /etc/default/check_connections.conf | cut -d '=' -f2)"
echo "=== UserData script completed: $(date) ==="
Tags:
- Key: Name
Value: !Sub '${Environment}-${EC2ProjectId}-linux-auto-stop-sv'
- Key: Environment
Value: !Ref Environment
- Key: ProjectId
Value: !Ref ProjectId
- Key: auto_stop
Value: !Ref AutoStopTag
ファイルを保存しましょう。
2-2. 新しいスタックの作成とデプロイ
ステップ1: 基盤スタックのデプロイ の手順を参考に、リソースをデプロイします。
スタック名: ec2-auto-stop-instance-stack
パラメーターはこんな感じで設定してください。
| パラメーター名 | 推奨値 | 説明 |
|---|---|---|
| Environment | dev |
環境識別子(基盤スタックと同じにしてください) |
| ProjectId | 001 |
プロジェクトID(基盤スタックと同じにしてください) |
| EC2ProjectId | 001 |
EC2固有のプロジェクトID |
| InstanceType | t3.micro |
インスタンスタイプ |
| ImageId | ami-03d1820163e6b9f5d |
Amazon Linux 2023のAMI ID |
| EbsVolumeSize | 8 |
EBSボリュームサイズ(GB) |
| AutoStopTag | true |
自動停止を有効化 |
| IdleMinutes | 30 |
アイドル検知の閾値(分) |
| DebounceMinutes | 5 |
重複通知抑制の間隔(分) |
注意: ImageIdはリージョンごとにことなります。Amazon EC2 コンソールを使用した AL2023 の起動の手順を参照し、最新のAMI IDに更新してください。
ステップ3: Session Managerでインスタンスに接続

3-1. EC2インスタンスの確認
- AWS マネジメントコンソールで「Amazon EC2」を開く
- 左ペインから「インスタンス」を選択
- dev-001-linux-auto-stop-svという名前のインスタンスを選択
補足: ステップ2: EC2インスタンススタックのデプロイの直後は、EC2が自動で起動します。もし、「インスタンスの状態」が「停止済み」の場合は、EC2インスタンスを選択後、「インスタンスの状態 > インスタンスを開始」を選択し、EC2を起動させます。
3-2. Session Managerで接続
- 「インスタンスの状態」が「実行中」、「ステータスチェック」が「3/3 のチェックに合格しました」となるまで待つ
- 「接続」ボタンを選択
- 「SSM Session Manager」タブにて「接続」ボタンを選択
ブラウザー上でターミナルセッションが開きます。
ステップ4: 監視スクリプトの作成
さあ、ここからが面白いところです。監視スクリプトを作っていきましょう。
4-1. 設定ファイルの確認
まずは、AWS CloudFormationで自動作成された設定ファイルを確認してみましょう。
sudo cat /etc/default/check_connections.conf
出力例
AUTO_STOP_IDLE_MINUTES=30 AUTO_STOP_DEBOUNCE_MINUTES=5 AUTO_STOP_LOG_GROUP_NAME=dev-001-ec2-log-group-write-logs
補足: check_connections.confには、スタックのパラメーター「アイドル検知の閾値(分)」「重複通知抑制の間隔(分)」および、Amazon CloudWatch Logs のロググループ名が格納されます。この情報は後続のセットアップで利用します。
4-2. 監視スクリプトの作成
それでは、viエディターでスクリプトファイルを作っていきましょう。
sudo vi /usr/local/bin/check_connections.sh
iキーを押して挿入モードに入ったら、以下のスクリプトをコピー&ペーストしてください。
ここを押すと展開します
#!/usr/bin/env bash #------------------------------------------------------------------------------ # スクリプト名: check_connections # # 目的: # - SSH(TCP 22)および Session Managerのアクティブな接続を確認します。 # - アクティブなセッションが検出されず、指定したアイドル時間の閾値を超えた場合に、 # インスタンスIDと可読時刻のみを含むJSONを指定のログディレクトリへ追記します。 # - 通知の重複を避けるため、デバウンス間隔(分)を使用して過剰通知を抑制します。 # # 使い方: # check_connections[DEBOUNCE_MINUTES] # # リターンコード規約: # 0 : 正常終了(通知あり) # 2 : 正常終了(通知不要:セッションがアクティブ、またはアイドル閾値未到達) # 3 : 正常終了(通知不要:デバウンス期間内のため抑制) # 4 : 正常終了(初期化のみ実施:初回実行で最終アクティブ時刻を書き込み) # 501 : エラー(IMDSv2 トークン取得失敗) # 502 : エラー(IMDSv2 インスタンスID取得失敗) # 500+: エラー(その他の致命的エラー) #------------------------------------------------------------------------------ set -euo pipefail fatal() { echo "FATAL: $*" >&2 exit 500 } # 引数の解析 IDLE_MINUTES="${1:-}" STATE_DIR="${2:-}" LOG_DIR="${3:-}" DEBOUNCE_MINUTES="${4:-5}" [[ -n "$IDLE_MINUTES" && -n "$STATE_DIR" && -n "$LOG_DIR" ]] || fatal "Usage: $0 [DEBOUNCE_MINUTES]" ([[ "$IDLE_MINUTES" =~ ^[0-9]+$ ]] && [[ "$IDLE_MINUTES" -gt 0 ]]) || fatal "IDLE_MINUTES は分を表す正の整数で指定してください。" ([[ "$DEBOUNCE_MINUTES" =~ ^[0-9]+$ ]]) || fatal "DEBOUNCE_MINUTES は分を表す0以上の整数で指定してください。" # 設定値 LOG_FILENAME="notify.log" EVENT_NAME="no-active-sessions" SSH_PORT="22" TZ_REGION="Asia/Tokyo" READABLE_TIME_FORMAT="%Y-%m-%d %H:%M:%S" # パス導出 LOG_FILE="${LOG_DIR%/}/${LOG_FILENAME}" LAST_ACTIVE_FILE="${STATE_DIR%/}/last_active_epoch" LAST_ACTIVE_FILE_SEC="${STATE_DIR%/}/last_active_epoch_sec" MARK="${STATE_DIR%/}/last_notified_epoch" mkdir -p "$STATE_DIR" "$LOG_DIR" || fatal "failed to create directories: $STATE_DIR, $LOG_DIR" # IMDSv2: インスタンスID取得 if ! TOKEN=$(curl -sS --fail -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"); then echo "ERROR: failed to get IMDSv2 token" >&2 exit 501 fi if ! INSTANCE_ID=$(curl -sS --fail "http://169.254.169.254/latest/meta-data/instance-id" \ -H "X-aws-ec2-metadata-token: $TOKEN"); then echo "ERROR: failed to get instance-id via IMDSv2" >&2 exit 502 fi # アクティブセッションの判定 has_active_sessions() { if ss -tn state established "( dport = :$SSH_PORT or sport = :$SSH_PORT )" 2>/dev/null | grep -q ESTAB; then return 0 fi if pgrep -f "ssm-session-worker" > /dev/null 2>&1; then return 0 fi return 1 } # 現在時刻の取得 NOW_EPOCH=$(date +%s) NOW_READABLE=$(TZ="$TZ_REGION" date +"$READABLE_TIME_FORMAT") # アクティブなら最終アクティブ時刻を更新 if has_active_sessions; then echo "$NOW_READABLE" > "$LAST_ACTIVE_FILE" || fatal "failed to write $LAST_ACTIVE_FILE" echo "$NOW_EPOCH" > "$LAST_ACTIVE_FILE_SEC" || fatal "failed to write $LAST_ACTIVE_FILE_SEC" exit 2 fi # 初回実行時の初期化 if [[ ! -f "$LAST_ACTIVE_FILE_SEC" ]]; then echo "$NOW_READABLE" > "$LAST_ACTIVE_FILE" || fatal "failed to write $LAST_ACTIVE_FILE" echo "$NOW_EPOCH" > "$LAST_ACTIVE_FILE_SEC" || fatal "failed to write $LAST_ACTIVE_FILE_SEC" exit 4 fi # アイドル時間の計算 LAST_ACTIVE_EPOCH="$(cat "$LAST_ACTIVE_FILE_SEC")" || fatal "failed to read $LAST_ACTIVE_FILE_SEC" IDLE_SECONDS=$(( NOW_EPOCH - LAST_ACTIVE_EPOCH )) THRESHOLD_SECONDS=$(( IDLE_MINUTES * 60 )) if [[ "$IDLE_SECONDS" -lt "$THRESHOLD_SECONDS" ]]; then exit 2 fi # デバウンス抑制 DEBOUNCE_SECONDS=$(( DEBOUNCE_MINUTES * 60 )) if [[ -f "$MARK" ]]; then LAST_NOTIFIED="$(cat "$MARK")" || fatal "failed to read $MARK" if (( NOW_EPOCH - LAST_NOTIFIED < DEBOUNCE_SECONDS )); then exit 3 fi fi # 通知(JSONログ出力) echo "{\"event\":\"$EVENT_NAME\",\"instanceId\":\"$INSTANCE_ID\",\"readableTime\":\"$NOW_READABLE\"}" \ | tee -a "$LOG_FILE" >/dev/null || fatal "failed to append $LOG_FILE" echo "$NOW_EPOCH" > "$MARK" || fatal "failed to write $MARK" exit 0
Escキーを押して、:wqと入力して保存しましょう。
4-3. スクリプトの確認と実行権限の付与
スクリプトがちゃんと保存されたか確認して、実行権限をつけてあげます。
# スクリプトの内容を確認 sudo cat /usr/local/bin/check_connections.sh # 実行権限を付与 sudo chmod +x /usr/local/bin/check_connections.sh
ステップ5: スクリプトの動作テスト
本番で使う前に、スクリプトがちゃんと動くかテストしてみましょう。
5-1. テスト環境の準備
まずはテスト用のディレクトリを準備します。
# テストディレクトリの初期化 sudo rm -rf /tmp/check_sessions_test /tmp/check_connections_test sudo mkdir -p /tmp/check_sessions_test /tmp/check_connections_test # バックアップを作成 sudo cp -a /usr/local/bin/check_connections.sh /usr/local/bin/check_connections.sh.bak
5-2. テスト用にスクリプトを一時修正
なぜ修正するの?
このスクリプトは本来、SSHとSession Managerの両方の接続を検知します。でも、テスト中はSession Managerで接続したままスクリプトを実行したいので、Session Manager検知部分だけをコメントアウトして、疑似的に「接続なし」の状況を作ります。こうすることで、接続したままテストができるんです。
それでは修正してみましょう。
sudo vi /usr/local/bin/check_connections.sh
iキーを押して、以下の部分に「#」を追加し、コメントアウトします。
修正前
# アクティブセッションの判定
has_active_sessions() {
if ss -tn state established "( dport = :$SSH_PORT or sport = :$SSH_PORT )" 2>/dev/null | grep -q ESTAB; then
return 0
fi
if pgrep -f "ssm-session-worker" > /dev/null 2>&1; then
return 0
fi
return 1
}
修正後
アクティブセッションの判定
has_active_sessions() {
if ss -tn state established "( dport = :$SSH_PORT or sport = :$SSH_PORT )" 2>/dev/null | grep -q ESTAB; then
return 0
fi
# if pgrep -f "ssm-session-worker" > /dev/null 2>&1; then
# return 0
# fi
return 1
}
Escキーを押して、:wqと入力して保存しましょう。
5-3. テストケース1: 初期化(終了コード4)
何をテストするの?: スクリプトの初回実行時に、状態ファイルがちゃんと作られるかチェックします。
どういうシナリオ?: 状態ファイル(last_active_epoch_sec)がない状態でスクリプトを実行すると、現在時刻を記録して初期化してくれます。これは「初回起動時の基準時刻設定」です。
さっそく試してみましょう。
# 初期状態を作成(ファイル削除) sudo rm -f /tmp/check_sessions_test/last_active_epoch_sec # 実行 sudo /usr/local/bin/check_connections.sh 1 /tmp/check_sessions_test /tmp/check_connections_test echo "終了コード: $?" # 状態ファイルの確認 sudo cat /tmp/check_sessions_test/last_active_epoch sudo cat /tmp/check_sessions_test/last_active_epoch_sec
処理フロー

- 初回実行時は通知を行わず、次回以降の判定基準となる時刻を記録するだけです。
期待される結果
- 終了コード 4(初期化完了)
- last_active_epochファイルに現在時刻(読みやすい形式)が記録される
- last_active_epoch_secファイルに現在時刻(epoch秒)が記録される
5-4. テストケース2: 閾値未到達(終了コード2)
何をテストするの?: アイドル時間が閾値に達していない場合、通知が行われないことを確認します。
どういうシナリオ?: 最終アクティブ時刻を「現在時刻」に設定して、アイドル閾値を10分に設定します。この場合、アイドル時間は0分なので、閾値(10分)に達していないため、通知は行われません。
やってみましょう。
# 現在時刻をセット echo "$(date +%s)" | sudo tee /tmp/check_sessions_test/last_active_epoch_sec >/dev/null # 実行(閾値を10分に設定) sudo /usr/local/bin/check_connections.sh 10 /tmp/check_sessions_test /tmp/check_connections_test echo "終了コード: $?"
処理フロー

- 最終アクティブ時刻: 現在時刻
- 現在時刻: 実行時の時刻
- アイドル時間: 0分
- 閾値: 10分
- 判定: 0分 < 10分 → 通知不要
期待される結果
- 終了コード 2(通知不要)
- notify.logにログが出力されない
5-5. テストケース3: デバウンス抑制(終了コード3)
何をテストするの?: 短時間に重複した通知が送信されないよう、デバウンス機能がちゃんと動くか確認します。
どういうシナリオ?: 最終アクティブ時刻を1時間前に設定して(アイドル閾値を超える)、最終通知時刻を現在時刻に設定します。この場合、アイドル閾値は超えていますが、直近で通知済みなので、デバウンス機能により通知が抑制されます。
デバウンスの意義: 同じインスタンスに対して短時間に何度も停止命令が送られるのを防ぎます。
試してみましょう。
# 1時間前をセット echo "$(( $(date +%s) - 3600 ))" | sudo tee /tmp/check_sessions_test/last_active_epoch_sec >/dev/null # 直近通知時刻を現在時刻に設定 echo "$(date +%s)" | sudo tee /tmp/check_sessions_test/last_notified_epoch >/dev/null # 実行 sudo /usr/local/bin/check_connections.sh 1 /tmp/check_sessions_test /tmp/check_connections_test echo "終了コード: $?"
処理フロー

- 最終アクティブ時刻: 1時間前
- アイドル時間: 60分(閾値1分を超過)
- 最終通知時刻: 現在時刻
- デバウンス間隔: 5分(デフォルト)
- 判定: 前回通知から5分未満 → 通知抑制
期待される結果
- 終了コード 3(デバウンス抑制)
- notify.logにログが出力されない
5-6. テストケース4: 通知あり(終了コード0)
何をテストするの?: すべての条件を満たした場合に、ちゃんと通知が行われるか確認します。
どういうシナリオ?: 最終アクティブ時刻を1時間前に設定して(アイドル閾値を超える)、デバウンスマークを削除します(前回通知なし)。この場合、アイドル閾値を超えていて、デバウンス抑制もないので、通知が行われます。
さあ、やってみましょう。
# 1時間前をセット echo "$(( $(date +%s) - 3600 ))" | sudo tee /tmp/check_sessions_test/last_active_epoch_sec >/dev/null # デバウンスマーク削除 sudo rm -f /tmp/check_sessions_test/last_notified_epoch # 実行 sudo /usr/local/bin/check_connections.sh 1 /tmp/check_sessions_test /tmp/check_connections_test echo "終了コード: $?" # ログファイルの確認 sudo cat /tmp/check_connections_test/notify.log
処理フロー

- 最終アクティブ時刻: 1時間前
- アイドル時間: 60分(閾値1分を超過)
- デバウンスマーク: なし
- 判定: アイドル閾値超過 & デバウンス期間外 → 通知実行
期待される結果
- 終了コード 0(通知あり)
- notify.logに以下のようなJSON出力
{"event":"no-active-sessions","instanceId":"i-xxxxxxxxxxxxxxxxx","readableTime":"YYYY-MM-DD hh:mm:ss"}
JSONフィールドの意味
- event: イベント種別(no-active-sessions = アクティブセッションなし)
- instanceId: 対象のEC2インスタンスID(IMDSv2から自動取得)
- readableTime: 通知時刻(Asia/Tokyoタイムゾーン)
このJSONがAmazon CloudWatch Logsに送信されて、AWS Lambda関数がトリガーされてEC2停止処理が実行されます。
5-7. テスト環境のクリーンアップ
テストが終わったら、お掃除しておきましょう。
# テストディレクトリの削除 sudo rm -rf /tmp/check_sessions_test /tmp/check_connections_test
ステップ6: systemdサービスとタイマーの設定
さあ、スクリプトを定期的に動かす仕組みを作っていきましょう。
6-1. サービスユニットの作成
まずはサービスユニットを作ります。
sudo vi /etc/systemd/system/check_connections.service
iキーを押して、以下の内容を入力してください。
[Unit]
Description=Run check_connections.sh to detect no-active-sessions
[Service]
Type=oneshot
# 環境ファイルから読み込み
EnvironmentFile=/etc/default/check_connections.conf
# 固定環境
Environment=STATE_DIR=/var/lib/check_sessions
Environment=LOG_DIR=/var/log/check_connections
# 終了コード 0, 2, 3, 4 を成功として扱う
# 0: 通知あり, 2: 通知不要(アクティブまたは閾値未到達)
# 3: 通知不要(デバウンス抑制), 4: 初期化完了
SuccessExitStatus=0 2 3 4
# スクリプト実行
ExecStart=/usr/local/bin/check_connections.sh \
${AUTO_STOP_IDLE_MINUTES} \
${STATE_DIR} \
${LOG_DIR} \
${AUTO_STOP_DEBOUNCE_MINUTES}
Escキーを押して、:wqと入力して保存しましょう。
6-2. サービスユニットの確認
ちゃんと保存されたか確認してみます。
sudo cat /etc/systemd/system/check_connections.service
6-3. タイマーユニットの作成
次はタイマーユニットを作ります。
sudo vi /etc/systemd/system/check_connections.timer
iキーを押して、以下の内容を入力してください。
[Unit] Description=Run check_connections.service every 5 minutes [Timer] OnBootSec=5min OnUnitActiveSec=5min AccuracySec=30s Persistent=true [Install] WantedBy=timers.target
Escキーを押して、:wqと入力して保存しましょう。
6-4. タイマーユニットの確認
こちらも確認してみます。
sudo cat /etc/systemd/system/check_connections.timer
6-5. systemdへの反映と有効化
systemdに新しいサービスとタイマーを認識させて、自動起動を有効化しましょう。
# systemdに反映 sudo systemctl daemon-reload # タイマー有効化・起動 sudo systemctl enable check_connections.timer sudo systemctl start check_connections.timer # 動作確認 sudo systemctl status check_connections.timer --no-pager sudo systemctl status check_connections.service --no-pager || true
各コマンドの説明
| コマンド | 説明 |
|---|---|
| daemon-reload | systemdに新しいユニットファイルを読み込み |
| enable | システム起動時にタイマーが自動起動するよう設定 |
| start | タイマーを今すぐ起動 |
| status | 現在の状態を確認 |
check_connections.timerのステータス確認
タイマーが正常に起動すると、こんな感じの出力が表示されます。
● check_connections.timer - Run check_connections.service every 5 minutes
Loaded: loaded (/etc/systemd/system/check_connections.timer; enabled; preset: disabled)
Active: active (waiting) since Tue 2026-02-03 07:30:00 UTC; 2min ago
Trigger: Tue 2026-02-03 07:35:00 UTC; 2min 30s left
Triggers: ● check_connections.service
確認ポイント
- Active: active (waiting) - タイマーが待機中(正常です)
check_connections.serviceのステータス確認
サービスの実行状況も確認できます。出力内容はスクリプトの終了コードによって変わります。
出力例(終了コード2の場合)
○ check_connections.service - Run check_connections.sh to detect no-active-sessions
Loaded: loaded (/etc/systemd/system/check_connections.service; static)
Active: inactive (dead) since Tue 2026-02-03 07:50:12 UTC; 32s ago
TriggeredBy: ● check_connections.timer
Process: 3865 ExecStart=/usr/local/bin/check_connections.sh ${AUTO_STOP_IDLE_MINUTES} ${STATE_DIR} ${LOG_DIR} ${AUTO_STOP_DEBOUNCE_MINUTES} (code=exited, status=2)
Main PID: 3865 (code=exited, status=2)
CPU: 24ms
Feb 03 07:50:12 ip-10-0-1-47.ap-northeast-1.compute.internal systemd[1]: Starting check_connections.service - Run check_connections.sh to detect no-active-sessions...
Feb 03 07:50:12 ip-10-0-1-47.ap-northeast-1.compute.internal systemd[1]: check_connections.service: Deactivated successfully.
Feb 03 07:50:12 ip-10-0-1-47.ap-northeast-1.compute.internal systemd[1]: Finished check_connections.service - Run check_connections.sh to detect no-active-sessions.
確認ポイント
- Active: inactive (dead) - Type=oneshotのサービスは実行完了後に停止状態になります(これが正常です)
終了コードによる出力パターン
| 終了コード | 意味 |
|---|---|
| 0 | 通知あり(EC2停止イベント発生) |
| 2 | 通知不要(アクティブまたは閾値未到達) |
| 3 | 通知不要(デバウンス抑制) |
| 4 | 初期化完了(初回実行時) |
6-6. 手動実行テスト
きちんと動作するか、手動で試してみましょう。
# サービスを手動実行 sudo systemctl start check_connections.service # 次回のタイマー実行時間(UTC表示)を確認 systemctl list-timers | grep check_connections
ステップ7: CloudWatch Agentの設定
次は、ログをAmazon CloudWatchに送る設定をしていきましょう。
7-1. 設定ファイルの読み込み
まずは環境変数を読み込みます。
# 環境変数を読み込み
. /etc/default/check_connections.conf
# ロググループ名を変数に格納
LOG_GROUP_NAME="${AUTO_STOP_LOG_GROUP_NAME}"
7-2. CloudWatch Agentのインストール
CloudWatch Agentをインストールしましょう。
# CloudWatch AgentをインストールAmazon Linux 2023) sudo dnf install -y amazon-cloudwatch-agent
7-3. CloudWatch Agent設定ファイルの作成
設定ファイルを作っていきます。
sudo tee /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json >/dev/null <<CW_EOF
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/check_connections/notify.log",
"log_group_name": "${AUTO_STOP_LOG_GROUP_NAME}",
"log_stream_name": "check-connections-{instance_id}"
}
]
}
}
},
"agent": {
"region": "ap-northeast-1"
}
}
CW_EOF
注意: regionは使っているAWSリージョンに合わせて変更してください。
7-4. タイマーの一時停止とログクリア
タイマーを一旦止めて、ログをクリアします。
# タイマーを停止 sudo systemctl stop check_connections.timer # ログファイルを削除 sudo rm -f /var/log/check_connections/notify.log
7-5. CloudWatch Agentの起動
さあ、CloudWatch Agentを起動してみましょう。
# CloudWatch Agentを停止(既に起動している場合) sudo systemctl stop amazon-cloudwatch-agent || true # 設定を適用して起動 sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ -a fetch-config -m ec2 -s \ -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json # ステータス確認 sudo systemctl status amazon-cloudwatch-agent --no-pager || true
確認ポイント
- Active: active (running) - サービスが動作中(正常です)
ステップ8: 動作確認用の設定変更
テストのために、アイドル時間を短く設定してみましょう。
8-1. 設定ファイルのバックアップ
まずはバックアップを取っておきます。
sudo cp -a /etc/default/check_connections.conf /etc/default/check_connections.conf.bak
8-2. 設定の変更
設定を変更してみましょう。
sudo vi /etc/default/check_connections.conf
iキーを押して、以下のように変更します。
修正前
AUTO_STOP_IDLE_MINUTES=30 AUTO_STOP_DEBOUNCE_MINUTES=5
修正後
AUTO_STOP_IDLE_MINUTES=5 AUTO_STOP_DEBOUNCE_MINUTES=1
Escキーを押して、:wqと入力して保存しましょう。
5.動作確認
さあ、いよいよ実際に動かしてみましょう。
ステップ一覧
- 自動停止機能のテスト
- auto_stopタグによる制御テスト
- 本番設定への復元
ステップ1: 自動停止機能のテスト
処理フロー
- OS内のフロー

- AWS上のフロー

- T=0分:EC2起動、Session Manager接続
- T=5分:初回スクリプト実行(接続中なので最終アクティブ時刻を記録)
- T=5~10分:Session Manager切断(アイドル状態開始)
- T=10~15分:2回目のスクリプト実行(アイドル時間5分 = 閾値5分 → 停止判定)
- T=10~15分+数秒:AWS Lambda実行、EC2停止開始
1-1. スクリプトをバックアップから復元
まずはスクリプトをバックアップから復元して、Session Manager検知を有効化します。
# バックアップから復元(Session Manager検知を有効化) sudo cp -a /usr/local/bin/check_connections.sh.bak /usr/local/bin/check_connections.sh# systemdに反映 sudo systemctl daemon-reload
1-2. タイマーの起動
タイマーを起動してみましょう。
# タイマーを起動 sudo systemctl start check_connections.timer # 次回の実行時間を確認 systemctl list-timers | grep check_connections
出力例
NEXT LEFT LAST PASSED UNIT ACTIVATES Mon YYYY-MM-DD hh:mm:ss JST 4min 30s left - - check_connections.timer check_connections.service
1-3. Session Managerセッションを閉じる
- ブラウザーのターミナルセッションにて「終了」を押下
- 「セッションの終了」にて「終了」を押下
SSH接続している場合も切断してください。
注意: ターミナルセッションのブラウザのタブで閉じると、セッションが終了されません。その場合、セッションを終了するを実施して下さい。
期待される停止タイミング
- EC2起動後、約10分で自動停止します
1-4. Amazon CloudWatch Logsでの確認
CloudWatchでも確認してみましょう。
- AWS マネジメントコンソールで「CloudWatch」を開く
- 左ペインから「ログ > Log Management」→「ロググループ」を選択
- dev-001-ec2-log-group-write-logsを開く
- 作成されたログストリーム(check-connections-i-xxxxxxxxx)を選択
約15分後 こんな感じのログイベントが追記されます。
{
"event": "no-active-sessions",
"instanceId": "i-xxxxxxxxxxxxxxxxx",
"readableTime": "YYYY-MM-DD hh:mm:ss"
}
1-5. AWS Lambda関数のログ確認
- AWS マネジメントコンソールで「AWS Lambda」を開く
- dev-001-lambda-ec2-stop関数を選択
- 「モニタリング」タブを選択
- 「CloudWatch のログを表示」を選択
- 作成されたログストリーム(YYYY/MM/DD/dev-001-lambda-ec2-stop~)を選択
以下のようなログが出力されていることを確認します。
{
"msg": "インスタンスを停止しました",
"instanceId": "i-xxxxxxxxxxxxxxxxx"
}
1-6. EC2インスタンスの停止確認
AWS マネジメントコンソールで「Amazon EC2」を開いて、インスタンスのステータスが「停止済み」となっていることを確認しましょう。
ステップ2: auto_stopタグによる制御テスト
次は、タグで停止を制御できるか試してみましょう。
2-1. auto_stopタグをfalseに変更
タグを変更してみます。
- インスタンスを選択した状態で「タグ」タブを選択
- 「タグを管理」を選択
- auto_stopタグの値をtrueからfalseに変更
- 「変更を保存」を選択
2-2. EC2インスタンスの起動
まずはインスタンスを起動します。
- AWS マネジメントコンソールで「Amazon EC2」を開く
- dev-001-linux-auto-stop-svという名前のインスタンスを選択
- 「インスタンスの状態」→「インスタンスを開始」を選択
- 「インスタンスの状態」が「実行中」、「ステータスチェック」が「3/3 のチェックに合格しました」となるまで待つ
2-3. Amazon CloudWatch Logsでの確認
CloudWatchでも確認してみましょう。
- AWS マネジメントコンソールで「CloudWatch」を開く
- 左ペインから「ログ > Log Management」→「ロググループ」を選択
- dev-001-ec2-log-group-write-logsを開く
- 作成されたログストリーム(check-connections-i-xxxxxxxxx)を選択
約5分後 こんな感じのログイベントが追記されます。
{
"event": "no-active-sessions",
"instanceId": "i-xxxxxxxxxxxxxxxxx",
"readableTime": "YYYY-MM-DD hh:mm:ss"
}
2-4. AWS Lambda関数のログ確認
AWS Lambda関数のログで、こんなメッセージが出力されていることを確認してください。
{
"msg": "停止をスキップ: auto_stop=false",
"instanceId": "i-xxxxxxxxxxxxxxxxx"
}
EC2インスタンスは停止されず、実行中のままです。タグで制御できています。
注意: auto_stopタグによる制御は、一時的な機能です。セッションが無い状態で、EC2を起動し続けると、notify.logが肥大化し、ディスク容量がひっ迫します。
2-5. auto_stopタグをtrueに戻す
テストが終わったら、タグを元に戻しておきましょう。
- AWS マネジメントコンソールで「Amazon EC2」を開く
- インスタンスを選択
- 「タグ」タブからauto_stopタグの値をfalseからtrueに変更
- 「変更を保存」を選択
完璧です。
ステップ3: 本番設定への復元
テストが完了したら、本番用の設定に戻しましょう。
3-1. 設定ファイルの復元
Session Managerで接続して、以下を実行してください。
# バックアップから復元 sudo cp -a /etc/default/check_connections.conf.bak /etc/default/check_connections.conf # systemdに反映 sudo systemctl daemon-reload sudo systemctl restart check_connections.timer
3-2. 設定内容の確認
ちゃんと戻っているか確認してみましょう。
sudo cat /etc/default/check_connections.conf
以下のように戻っていることを確認します。
AUTO_STOP_IDLE_MINUTES=30 AUTO_STOP_DEBOUNCE_MINUTES=5 AUTO_STOP_LOG_GROUP_NAME=dev-001-ec2-log-group-write-logs
6.環境削除手順
テストが完了したら、以下の手順で環境をきれいにお掃除しましょう。
ステップ一覧
- EC2インスタンスの停止
- AWS CloudFormationスタックの削除
ステップ1: EC2インスタンスの停止
まずはインスタンスを停止します。
- AWS マネジメントコンソールで「Amazon EC2」を開く
- インスタンスを選択
- 「インスタンスの状態」→「インスタンスを停止」を選択
- インスタンスが「停止済み」になるまで待つ
ステップ2: AWS CloudFormationスタックの削除
2-1. EC2スタックの削除
- AWS マネジメントコンソールで「AWS CloudFormation」を開く
- スタック名: ec2-auto-stop-instance-stackを選択
- 「削除」ボタンを選択
- 確認ダイアログで「削除」を選択
- ステータスが「DELETE_COMPLETE」になるまで待つ(約3~5分)
2-2. 基盤スタックの削除
- 2-1. EC2スタックの削除 の手順を参考に、スタック名: ec2-auto-stop-base-stackを削除します。
大事なポイント: スタックは必ず上記の順序で削除してください。EC2スタックが基盤スタックのリソースに依存しているので、逆順で削除するとエラーになります。
まとめ
お疲れさまでした。自動停止システム構築ハンズオンが完了しました。いかがでしたでしょうか。
このハンズオンでは、以下の技術を組み合わせて、コスト最適化のための自動停止システムを構築しました。
| 技術・サービス | 役割 |
|---|---|
| AWS CloudFormation | インフラのコード化(VPC、AWS IAMロール、AWS Lambda関数など) |
| Bashスクリプト | SSH/Session Manager接続の監視 |
| systemd | 定期実行の仕組み |
| Amazon CloudWatch Logs + AWS Lambda | イベント駆動型の自動停止処理 |
| タグベース制御 | 柔軟な停止対象の管理 |
このシステムにより、使用していないEC2インスタンスを自動的に停止することで、無駄なコストを削減できます。アイドル時間やデバウンス間隔は環境に合わせて調整可能で、auto_stopタグで簡単にオン・オフを切り替えられます。
ぜひ、いろいろ試してみてください。
プラスワンのハンズオン
さらに理解を深めたい方は、付録:Session Managerポートフォワーディング経由のSSH接続を参考に、Session Managerのポートフォワーディング機能を使ったSSH接続でも、監視スクリプトが正しく接続を検知して自動停止が動作するか確認してみてください。
付録:Session Managerポートフォワーディング経由のSSH接続
Session Managerのポートフォワーディング機能を使って、ローカルPCからSSH接続する方法を説明します。Visual Studio Codeでリモート開発したい方におすすめです。
ステップ一覧
- キーペアのダウンロード
- SSH Configファイルの設定
- Visual Studio Code Remote-SSH拡張機能のインストール
- Session Managerポートフォワーディングの開始
- Visual Studio Code Remote SSHで接続
ステップ1: キーペアのダウンロード
AWS CloudFormationで作成されたキーペアの秘密鍵を取得します。
1-1. キーペアIDの確認
- AWS マネジメントコンソールで「Amazon EC2」を開く
- 左ペインから「ネットワーク & セキュリティ」→「キーペア」を選択
- dev-001-ec2-keypairを検索して選択
- 「キーペアID」をコピー(key-xxxxxxxxxxxxxxxxxの形式)
1-2. AWS Systems Manager Parameter Storeから秘密鍵を取得
- AWS マネジメントコンソールで「AWS Systems Manager」を開く
- 左ペインから「パラメーターストア」を選択
- 先程、コピーしたキーペアIDで検索して選択
- 「復号化された値を表示」をオンにして、表示された値をコピー
1-3. 秘密鍵ファイルの保存
- エクスプローラーで「C:\Users\【ユーザー名】\.ssh」フォルダーを開く
- dev-001-ec2-keypair.pemという名前で新規ファイルを作成
- コピーした秘密鍵の内容を貼り付けて保存
- ファイルを右クリック→「プロパティ」→「読み取り専用」にチェック→「OK」
補足: .sshフォルダーがない場合は作成してください。
ステップ2: SSH Configファイルの設定
SSH接続の設定を記述します。
2-1. Configファイルの作成・編集
- エクスプローラーで「C:\Users\【ユーザー名】.ssh」フォルダーを開く
- configファイルがなければ作成(拡張子なし)
- テキストエディターで開いて、以下の内容を入力または追記
補足: 【ユーザー名】*の部分は実際のユーザー名に置き換えてください
Host dev-001-linux-auto-stop-sv
HostName localhost
User ec2-user
Port 10022
IdentityFile C:\Users\【ユーザー名】\.ssh\dev-001-ec2-keypair.pem
IdentitiesOnly yes
StrictHostKeyChecking no
UserKnownHostsFile NUL
ステップ3: Visual Studio Code Remote-SSH拡張機能のインストール
Visual Studio CodeでSSH接続できるようにします。
- Visual Studio Codeを開く
- 左サイドバーの「拡張機能」アイコンを選択
- 検索ボックスにRemote - SSHと入力
- Microsoft公式の「Remote - SSH」拡張機能をインストール
- インストール完了後、VS Codeを再起動
ステップ4: Session Managerポートフォワーディングの開始
接続フロー

4-1. インスタンスIDの取得とポートフォワーディング開始
PowerShellを開いて、以下のコマンドを実行します。(このウィンドウは開いたままにしてください)
# インスタンスIDを取得
$INSTANCE_ID = aws ec2 describe-instances `
--filters "Name=tag:Name,Values=dev-001-linux-auto-stop-sv" `
--query "Reservations[0].Instances[0].InstanceId" `
--output text `
--region ap-northeast-1
echo "Instance ID: $INSTANCE_ID"
# ポートフォワーディング開始
aws ssm start-session `
--target $INSTANCE_ID `
--document-name AWS-StartPortForwardingSession `
--parameters '{\"portNumber\":[\"22\"],\"localPortNumber\":[\"10022\"]}' `
--region ap-northeast-1
成功すると以下のように表示されます
Instance ID: i-xxxxxxxxxxxxxxxxx Starting session with SessionId: your-session-id Port 10022 opened for sessionId your-session-id. Waiting for connections...
大事なポイント: このPowerShellウィンドウは閉じないでください。ポートフォワーディングが動作し続けます。
ステップ5: Visual Studio Code Remote SSHで接続
5-1. Visual Studio Codeから接続
- Visual Studio Codeを開く
- F1キーを押してコマンドパレットを開く
- Remote-SSH: Connect to Host...と入力して選択
- dev-001-linux-auto-stop-svを選択
- 新しいウィンドウが開いて、接続が開始
- 初回「Select the platform~」と表示しますのでLinuxを選択
- 接続に成功すると、VS Codeの左下に「SSH: dev-001-linux-auto-stop-sv」と表示
- Ctrl + @キーを押下し、ターミナルを起動
接続の終了
1. Visual Studio Code接続の終了
- Visual Studio Codeの左下の「SSH: dev-001-linux-auto-stop-sv」を選択
- 「リモート接続を終了する」を選択
2. Session Managerポートフォワーディングの終了
- ポートフォワーディングを実行しているPowerShellウィンドウでCtrl + Cキーを押下
この記事は私が書きました
印鑰 幸太
記事一覧全ての AWS 認定を取得。AWSサービスでは、AWS CloudFormationが好きです。ジム通いが趣味です。
# systemdに反映
sudo systemctl daemon-reload