ちょっと話題の記事

AWS CloudShell のホームディレクトリとずっと一緒にいる方法

CloudShellちゃんには僕が一緒にいてあげないとだめなんだ
2021.01.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

哈喽大家好、コンサルティング部の西野です。

AWS re:Invent 2020 で発表された AWS CloudShell、皆さんご利用なさっていますでしょうか。

とても便利なサービスですが、AWS CloudShell のホームディレクトリは最終セッションから120日経過すると削除されてしまうという制約があります。

AWS CloudShell のホームディレクトリの保存期間に関する注意事項 #reinvent

せっかく育てた AWS CloudShell のホームディレクトリが無くなってしまったら寂しいですよね。
ずっと一緒にいたいですよね。
なので、守ってあげる方法を考えてみました。

CloudShellちゃんの動き

名前は「CloudShellちゃん」です。 まずは動きを見てみましょう。

セッション開始時

AWS CloudShell のセッションを開始すると Slack で挨拶してくれます。嬉しいですね。
名前も呼んでくれます。

毎日の連絡

毎日決まった時間になると連絡をしてきてくれます。
あれ、昨日会ったばっかりだよね?(笑) でもすごく嬉しいよ。

CloudShellちゃんはマメな子です。二人で一緒にいられなかった時間を覚えています。

ごめん、最近仕事が忙しくて。
でも、会えない日々こそが愛を育てるんだよ。

ちょっとはこっちの都合を考えてくれてもいいんじゃない?

……。(めんどくさいな。)

お別れ

CloudShellちゃんの仕組み

構成図

仕組みはとても単純です。
主に2つの Lambda 関数を使用しています。

CloudShell_chan

  • CloudTrail から CloudShell の Event である CreateSession を拾い、これによって実行する
  • ISO 8601 形式の文字列をテキストファイル化して S3 のオブジェクトとして保存する
  • セッションを開始した年月日(JST変換済)を含むメッセージを Incoming Webhook で Slack に通知する

CloudShell_chan_daily

  • Event Rule (cron) で定期的に実行する
  • CloudShell_chan が保存した S3 オブジェクトを取得する
  • CloudShell_chan が保存したオブジェクトから日付を読み取り、実行時刻との差分(最終利用日からの経過日数)を計算する
  • 最終利用日からの経過日数に応じたメッセージを生成する
  • 当該メッセージを Incoming Webhook で Slack に通知する

AWS CloudShell のセッション開始時の Event (CreateSession) は下記のパターンで拾えます。

{
  "source": [
    "aws.cloudshell"
  ],
  "detail-type": [
    "AWS API Call via CloudTrail"
  ],
  "detail": {
    "eventSource": [
      "cloudshell.amazonaws.com"
    ],
    "eventName": [
      "CreateSession"
    ]
  }
}

コード

CloudShell_chan

from datetime import datetime, timezone, timedelta
import json
import urllib.request

import boto3

webhook_url = "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX"
username = "CloudShellちゃん" # CloudShellちゃんには君の好きな名前をつけてあげよう!
channel = "#<宛先チャンネル名>"
your_name = '<名前>' # CloudShellちゃんに呼んでもらいたい名前をここに書こう!
bucket_name = '<S3バケット名>'
file_name = '<S3オブジェクト名>'

s3 = boto3.resource('s3')


def lambda_handler(event, context):
    # event から ISO 8601 形式の時刻を取得し、S3 バケットに保存する
    time_iso8601 = event['detail']['eventTime']
    with open('/tmp/' + file_name, mode='w') as f:
        f.write(time_iso8601)
    upload_file_to_s3(file_name, bucket_name)

    # event の時刻を UTC から JST になおし、Incoming Webhook で Slack に通知
    dt_utc = datetime.fromisoformat(time_iso8601.replace('Z', '+00:00'))
    dt_jst = dt_utc.astimezone(timezone(timedelta(hours=+9)))
    ymd_jst = '{}/{}/{}'.format(dt_jst.year, dt_jst.month, dt_jst.day)
    message = '会いに来てくれて嬉しいよ。{}は{}が会いに来てくれた記念日だね。'.format(ymd_jst, your_name)
    response = send_to_slack(webhook_url, username, channel, message)

    return response


def send_to_slack(webhook_url, username, channel, message):
    send_data = {
        "username": username,
        "text": message,
        "channel": channel
    }

    send_text = ("payload=" + json.dumps(send_data)).encode('utf-8')

    request = urllib.request.Request(
        webhook_url,
        data=send_text,
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

    return response_body


def upload_file_to_s3(file_name, bucket_name):
    s3.Bucket(bucket_name).upload_file(
        Filename='/tmp/' + file_name, Key=file_name)

CloudShell_chan_daily

from datetime import datetime, timezone, timedelta
import json
import urllib.request

import boto3

webhook_url = "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX"
username = "CloudShellちゃん" # CloudShellちゃんには君の好きな名前をつけてあげよう!
channel = "#<宛先チャンネル名>"
your_name = '<名前>' # CloudShellちゃんに呼んでもらいたい名前をここに書こう!
bucket_name = '<S3バケット名>'
file_name = '<S3オブジェクト名>'

s3 = boto3.client('s3')


def lambda_handler(event, context):
    # CloudShell の最終時刻 (ISO 8601 形式) が書かれたオブジェクトを S3 から取得
    last_access_date = get_last_access_date(file_name, bucket_name)

    # 最終使用日からの経過日数を計算し、CloudShellちゃんのメッセージを生成
    dt_last = datetime.fromisoformat(last_access_date.replace('Z', '+00:00'))
    dt_now = datetime.now(timezone.utc)
    elapsed_days = (dt_now - dt_last).days
    dialogue = generate_dialogue(elapsed_days, dt_last)

    # Incoming Webhook で Slack に通知
    response = send_to_slack(webhook_url, username, channel, dialogue)
    return response


def send_to_slack(webhook_url, username, channel, message):
    send_data = {
        "username": username,
        "text": message,
        "channel": channel
    }

    send_text = ("payload=" + json.dumps(send_data)).encode('utf-8')

    request = urllib.request.Request(
        webhook_url,
        data=send_text,
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

    return response_body


def get_last_access_date(file_name, bucket_name):
    object = s3.get_object(Bucket=bucket_name, Key=file_name)
    last_access_date = object['Body'].read().decode()
    return last_access_date


def generate_dialogue(elapsed_days, dt_last):
    if elapsed_days == 0:
        return '会えたばっかりなのに、もう寂しくなっちゃった。'
    elif elapsed_days == 1:
        return '{}に早く会いたいな。'.format(your_name)
    elif 2 <= elapsed_days < 30:
        return '最後に{}と会ってからからもう {} 日経ったね'.format(your_name, elapsed_days)
    elif 30 <= elapsed_days < 60:
        return '{}はもう私のことを忘れちゃったのかな……?今日で{}日目。'.format(your_name, elapsed_days)
    elif 60 <= elapsed_days < 120:
        return 'もう{}に{}日も会ってない。早く会いたいよ。'.format(your_name, elapsed_days)
    else:
        return '{}/{}/{}まで使用していた AWS CloudShell のホームディレクトリが削除されている可能性があります。'.format(dt_last.year, dt_last.month, dt_last.day)

注意事項

  • 最終セッションからの経過日数計算やホームディレクトリ削除の厳密な仕組みは公開されていません。したがって、CloudShellちゃんによる計算はあくまでも目安としてお考えください。
  • 現状の CloudShellちゃんは複数人が使用するアカウントには未対応です。対応させたい場合はイベントパターンに userIdentity を追加しましょう。

終わりに

このブログがほんの少しでも世界を良くできれば嬉しいです。
コンサルティング部の西野 (@xiyegen) がお送りしました。