ちばのてっく

積極的にアウトプット

Terraformでキーペアつくってみた

EC2にSSH接続するためのキーペアをTerraformでつくってみた。

結論

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.43"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

resource "tls_private_key" "this" {
  algorithm = "ED25519"
}

resource "local_file" "private" {
  filename        = "id_ed25519"
  content         = tls_private_key.this.private_key_openssh
  file_permission = "0600"
}

resource "aws_key_pair" "this" {
  key_name   = "by-terraform"
  public_key = tls_private_key.this.public_key_openssh
}

ポイント

  • tls_private_key
    • ssh-keygenコマンドに相当し、SSH接続に必要な秘密鍵と公開鍵を作成してくれる。
    • algorithmには、RSA、ECDSA、ED25519を選択できる。最も強度の高いED25519を採用した。
  • local_file
    • ファイルを作成してくれる。
    • contentにtls_private_keyで作成した秘密鍵を指定し、SSH接続用の秘密鍵をファイル出力する。
    • file_permissionはデフォルトでは777。ガバガバだとSSH接続時にエラーになると思うので、600に変更する。
  • aws_key_pair
    • キーペアを作成してくれる。
    • public_keyにtls_private_keyで作成した公開鍵を指定する。

Systems Managerのパブリックパラメータから最新のAMI IDを取得してみた

最新のAMI IDを取得するために、Systems Manager(以下、SSM)のパブリックパラメータが利用できそうなので試してみた。

はじめに結論

AWS CLIの場合

aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64 --query 'Parameters[].Value' --
output text

Terraformの場合

data "aws_ssm_parameter" "linux_latest" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
}

output "linux_latest" {
  value = data.aws_ssm_parameter.linux_latest.id
}

やってみた

AWSドキュメントのこのあたりを参考にする。

aws ssm get-parameters-by-pathコマンドで、SSMのパブリックパラメータが取得できるとのこと。

利用できるパブリックパラメータは、オプションに--path /aws/service/listを加えることで確認可能。

入力

aws ssm get-parameters-by-path --path /aws/service/list

出力

{
    "Parameters": [
### 割愛 ###            
        {
            "Name": "/aws/service/list/ami-windows-latest",
            "Type": "String",
            "Value": "/aws/service/ami-windows-latest/",
            "Version": 1,
            "LastModifiedDate": "2021-03-12T08:07:38.727000+09:00",
            "ARN": "arn:aws:ssm:ap-northeast-1::parameter/aws/service/list/ami-windows-latest",
            "DataType": "text"
        },
### 割愛 ###
        {
            "Name": "/aws/service/list/ami-amazon-linux-latest",
            "Type": "String",
            "Value": "/aws/service/ami-amazon-linux-latest/",
            "Version": 1,
            "LastModifiedDate": "2021-03-12T08:07:37.393000+09:00",
            "ARN": "arn:aws:ssm:ap-northeast-1::parameter/aws/service/list/ami-amazon-linux-latest",
            "DataType": "text"
        }
    ]
}

特定のパブリックパラメータを確認するには、パスからlistを削除する。例えば/aws/service/list/ami-amazon-linux-latestであれば、/aws/service/ami-amazon-linux-latestになる。また、AWS CLIはJMETHPathに従ったクエリが利用可能。

入力

aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest --query 'Parameters[].Name'

出力

[
    "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-6.1-arm64",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-6.1-x86_64",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-default-arm64",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-s3",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-default-x86_64",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-minimal-hvm-x86_64-s3",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-minimal-pv-x86_64-s3",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-pv-x86_64-s3",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-arm64-gp2",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-minimal-hvm-arm64-ebs",
    "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-minimal-hvm-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-minimal-pv-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/amzn-ami-pv-x86_64-ebs",
    "/aws/service/ami-amazon-linux-latest/amzn2-ami-minimal-hvm-x86_64-ebs"
]

これまでの経験から、おそらくはもっとも普遍的なAMIかと勝手に思っているため、「/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64」のAMI IDを取得することにする。最下層のパラメータへアクセスする際は、get-parameters-by-path --pathではなく、get-parameters --nameを使う。

入力

aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64 --query 'Parameters[].Value' --
output text

出力

ami-031134f7a79b6e424

terraformの場合は、data句aws_ssm_parameerを使う。

入力

data "aws_ssm_parameter" "linux_latest" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
}
output "linux_latest" {
  value = data.aws_ssm_parameter.linux_latest.id
}

出力

Changes to Outputs:
  + linux_latest = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"

AWS最新情報をLINE通知させてみた

プル型ではなくプッシュ型でAWSの最新情報をキャッチアップしたいと思い、身近なツールであるLINEで通知するアプリケーションをつくってみた。

仕組みとしては、RSSで配信されているAWSの最新情報を、EventBridge + Lambdaで定期的に確認し新しいものがあればLINE通知する、といったもの。

キーワードは以下の通り。

  • RSS
  • Messaging API(LINE)
  • Terraform
  • Lambda + EventBridge

準備運動

RSS

Pythonにはfeedparserというライブラリが存在するため、これを使う。 feedparser.parse(任意のURL)とすれば、FeedParserDictオブジェクトを取得できる。

参考:https://feedparser.readthedocs.io/en/latest/index.html

試しに「RSSフィードで購読する」で確認できたURLで、FeedParserDictオブジェクトを取得してみた。(可視性あげるためにJSON形式で整形した&量多いので抜粋した)

{
    "bozo": false,
    "entries": [
        {
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/fujitsu-and-aws-deepen-global-partnership-to-accelerate-legacy-applications-modernization-on-the-cloud/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/fujitsu-and-aws-deepen-global-partnership-to-accelerate-legacy-applications-modernization-on-the-cloud/",
            "id": "84d117fbd5b1f61fa1ebf495d2643756e4ee4afd",
            "guidislink": false,
            "title": "富士通と AWS、クラウドでのレガシーシステムのモダナイゼーション加速に向けてグローバルパートナーシップを拡大",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "富士通と AWS、クラウドでのレガシーシステムのモダナイゼーション加速に向けてグローバルパートナーシップを拡大"
            },
            "summary": "<p>「Modernization Acceleration Joint Initiative」を通して、お客様の DX を支援</p>",
            "summary_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "<p>「Modernization Acceleration Joint Initiative」を通して、お客様の DX を支援</p>"
            },
            "published": "Mon, 18 Mar 2024 01:58:27 +0000",
            "published_parsed": [
                2024,
                3,
                18,
                1,
                58,
                27,
                0,
                78,
                0
            ],
            "tags": [],
            "authors": [
                {
                    "email": "aws@amazon.com"
                }
            ],
            "author": "aws@amazon.com",
            "author_detail": {
                "email": "aws@amazon.com"
            }
        },
        {
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/amazon-cognito-europe-zurich-region/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/amazon-cognito-europe-zurich-region/",
            "id": "f8f89768f2f327c71d8f139d045d572abb31bd57",
            "guidislink": false,
            "title": "Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能に",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能に"
            },
            "summary": "<p>Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能になりました。Amazon Cognito では、ウェブアプリケーションやモバイルアプリケーションに認証、認可、ユーザー管理の機能を簡単に追加できます。Amazon Cognito は、数百万のユーザーに対応してスケールし、SAML 2.0 や OpenID Connect などの規格を介して、Apple、Facebook、Google、Amazon などのソーシャル ID プロバイダーやエンタープライズ ID プロバイダーを使用したサインインをサポートします。</p>",
            "summary_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "<p>Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能になりました。Amazon Cognito では、ウェブアプリケーションやモバイルアプリケーションに認証、認可、ユーザー管理の機能を簡単に追加できます。Amazon Cognito は、数百万のユーザーに対応してスケールし、SAML 2.0 や OpenID Connect などの規格を介して、Apple、Facebook、Google、Amazon などのソーシャル ID プロバイダーやエンタープライズ ID プロバイダーを使用したサインインをサポートします。</p>"
            },
            "published": "Fri, 22 Mar 2024 19:32:00 +0000",
            "published_parsed": [
                2024,
                3,
                22,
                19,
                32,
                0,
                4,
                82,
                0
            ],
            "tags": [
                {
                    "term": "general:products/amazon-cognito,marketing:marchitecture/security-identity-and-compliance",
                    "scheme": null,
                    "label": null
                }
            ],
            "authors": [
                {
                    "email": "aws@amazon.com"
                }
            ],
            "author": "aws@amazon.com",
            "author_detail": {
                "email": "aws@amazon.com"
            }
        },
######################## 行数多いので間を割愛 ########################
    ],
    "feed": {
        "links": [
            {
                "rel": "alternate",
                "type": "text/html",
                "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/"
            }
        ],
        "link": "https://aws.amazon.com/jp/about-aws/whats-new/recent/",
        "title": "最近の発表",
        "title_detail": {
            "type": "text/plain",
            "language": null,
            "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
            "value": "最近の発表"
        },
        "subtitle": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。",
        "subtitle_detail": {
            "type": "text/html",
            "language": null,
            "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
            "value": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。"
        },
        "authors": [
            {
                "name": "Amazon Web Services",
                "email": "aws@amazon.com"
            }
        ],
        "author": "aws@amazon.com (Amazon Web Services)",
        "author_detail": {
            "name": "Amazon Web Services",
            "email": "aws@amazon.com"
        },
        "updated": "Fri, 22 Mar 2024 19:33:37 +0000",
        "updated_parsed": [
            2024,
            3,
            22,
            19,
            33,
            37,
            4,
            82,
            0
        ],
        "published": "Fri, 22 Mar 2024 19:33:37 +0000",
        "published_parsed": [
            2024,
            3,
            22,
            19,
            33,
            37,
            4,
            82,
            0
        ],
        "docs": "http://blogs.law.harvard.edu/tech/rss",
        "image": {
            "href": "https://a0.awsstatic.com/main/images/logos/aws_logo_smile_179x109.png",
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/recent/",
            "title": "最近の発表",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "最近の発表"
            },
            "subtitle": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。",
            "subtitle_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。"
            }
        }
    },
    "headers": {
        "content-type": "application/rss+xml;charset=UTF-8",
        "transfer-encoding": "chunked",
        "connection": "close",
        "server": "Server",
        "date": "Sat, 23 Mar 2024 03:35:31 GMT",
        "x-amz-rid": "6PBTC6Z0XPV7YBSP6A0J",
        "set-cookie": "aws-priv=eyJ2IjoxLCJldSI6MCwic3QiOjB9; Version=1; Comment=\"Anonymous cookie for privacy regulations\"; Domain=.aws.amazon.com; Max-Age=31536000; Expires=Sun, 23 Mar 2025 03:35:31 GMT; Path=/; Secure",
        "x-frame-options": "SAMEORIGIN",
        "x-xss-protection": "1; mode=block",
        "strict-transport-security": "max-age=63072000",
        "x-amz-id-1": "6PBTC6Z0XPV7YBSP6A0J",
        "last-modified": "Sat, 23 Mar 2024 02:57:54 GMT",
        "content-security-policy-report-only": "default-src *; connect-src *; font-src * data:; frame-src *; img-src * data:; media-src *; object-src *; script-src 'nonce-xsp7AIieRU2oQ8SMHCbQKA==' *; style-src 'unsafe-inline' *; report-uri https://prod-us-west-2.csp-report.marketing.aws.dev/submit",
        "x-content-type-options": "nosniff",
        "vary": "Content-Type,Accept-Encoding,User-Agent",
        "x-cache": "Miss from cloudfront",
        "via": "1.1 7637a60a07b64cdf45697b2f5cacacee.cloudfront.net (CloudFront)",
        "x-amz-cf-pop": "NRT57-P1",
        "x-amz-cf-id": "DFFSpWXI_mkCtGqBy_xI_7p1D7QlqZM01Dw5yyp2gIpec2X4B0hObw=="
    },
    "updated": "Sat, 23 Mar 2024 02:57:54 GMT",
    "updated_parsed": [
        2024,
        3,
        23,
        2,
        57,
        54,
        5,
        83,
        0
    ],
    "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
    "status": 200,
    "encoding": "UTF-8",
    "version": "rss20",
    "namespaces": {}
}

各記事に相当するentries内に、publishedという日時の項目があったため、これを使って通知する記事を抽出しようと思う。

Messaging API(LINE)

Line Developersでアカウントつくって、チャネルつくって、といったところは割愛する。)

メッセージを送信する方法は5種類あるが、今回は友達登録者全員にメッセージを送るブロードキャストメッセージを採用することにする。

  • 応答メッセージ
  • プッシュメッセージ:1対1
  • マルチキャストメッセージ:1対多(ユーザーID指定)
  • ナローキャストメッセージ:1対多(絞り込み配信)
  • ブロードキャストメッセージ:1対多(すべての友だち)

引用:メッセージの送信方法
https://developers.line.biz/ja/docs/messaging-api/sending-messages/

Messaging APIPythonで利用するにあたっては、line-bot-sdk-pythonを利用する。
試しに動かしてみたところ、期待通りhogeというメッセージがLINEに届いた。

from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    TextMessage,
    BroadcastRequest
)

channel_access_token = "<自作したチャネルで発行したトークン>"
channel_secret = "<自作したチャネルで発行したシークレット>"

configuration = Configuration(access_token=channel_access_token)
api_client = ApiClient(configuration) 
line_bot_api = MessagingApi(api_client)
handler = WebhookHandler(channel_secret)

line_bot_api.broadcast(
    BroadcastRequest(
        messages=[TextMessage(text="hoge")]
    )
)

なお、送信できるブロードキャストメッセージの上限は、60リクエスト/時ということを一応押さえておくと良い。(いろいろ試していたらこの上限にひっかかって処理に失敗し、Too Many Requestsが返ってきた、、)

参考:https://developers.line.biz/ja/reference/messaging-api/#rate-limits

つくってみた

イメージとしてはこんな感じ。

EventBridgeは、Schedulerを利用して、毎朝8時にLambdaを実行する。
Lambdaは、feedparserで取得した各記事のpublishedから、24時間以内に公開された記事がないか確認し、あればMessaging API(LINE)経由でLINE友達登録者にブロードキャストメッセージを送信する。

ディレクトリ構造

.
├── app.py
├── layer
│   ├── python
│   └── requirements.txt
└── main.tf

app.py

import feedparser
import os
from datetime import datetime
from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    TextMessage,
    BroadcastRequest
)

channel_access_token = os.getenv('CHANNEL_ACCESS_TOKEN')
channel_secret = os.getenv('CHANNEL_SECRET')

configuration = Configuration(access_token=channel_access_token)
api_client = ApiClient(configuration) 
line_bot_api = MessagingApi(api_client)
handler = WebhookHandler(channel_secret)

now = datetime.now()

# 対象RSS
feeds = [
    "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/"
]

def lambda_handler(event, context):
    # FeedParserDictオブジェクトを取得
    for feed in feeds:
        f = feedparser.parse(feed)
        # 各記事のlink、title、publishedを辞書に格納する
        dicts = [{'url': entry['link'], 'title': entry['title'], 'published': entry['published']} for entry in f['entries']]
        for dict in dicts:
            # publishedをstrからdatetime型へ変換
            dict_pudlished = datetime.strptime(dict['published'],'%a, %d %b %Y %H:%M:%S %z') # https://docs.python.org/ja/3/library/datetime.html
            # publishedの日時が24時間以内であれば、該当記事のlinkとtitleをブロードキャストメッセージで送信
            if now.timestamp() - dict_pudlished.timestamp() < 86400:
                line_bot_api.broadcast(
                    BroadcastRequest(
                        messages=[TextMessage(text = str(dict['title']) + str(dict['url']))]
                    )
                )
    return "OK"

requirements.txt

line-bot-sdk==3.7.0
feedparser==6.0.11

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.42"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

#
# eventbridge
#

# EventBridgeを定期実行するルールを定義
resource "aws_cloudwatch_event_rule" "rss" {
  name                = "rss-by-terraform"
  schedule_expression = "cron(0 23 * * ? *)" # 毎日AM8:00
}

# EventBridgeがキックするターゲットを定義
resource "aws_cloudwatch_event_target" "rss" {
  rule      = aws_cloudwatch_event_rule.rss.name
  target_id = "rss-by-terraform"
  arn       = aws_lambda_function.rss_function.arn
}

#
# IAM
#

# LambdaがCloudWatch Logsにログを出力する権限を定義
resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda-by-terraform"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  ]
}

# LambdaがIAMロールを利用する権限を定義
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

#
# Layer
#

# 事前にリストアップした外部ライブラリをインストールする
resource "null_resource" "rss_layer" {
  provisioner "local-exec" {
    command = "pip3 install -r layer/requirements.txt --target=layer/python"
  }
}

# Lambdaレイヤーに必要なものをzip化する
data "archive_file" "rss_layer" {
  type        = "zip"
  source_dir  = "layer"
  output_path = "layer_by_tf.zip"
}

# Lambdaレイヤーを定義
resource "aws_lambda_layer_version" "rss_layer" {
  filename            = data.archive_file.rss_layer.output_path
  layer_name          = "rss_layer-by-terraform"
  source_code_hash    = data.archive_file.rss_layer.output_base64sha256
  compatible_runtimes = ["python3.12"]
}

#
# Function
#

# Lambda関数に必要なものをzip化する
data "archive_file" "rss_function" {
  type        = "zip"
  source_file = "app.py"
  output_path = "app_by_tf.zip"
}

# Lambda関数を定義
resource "aws_lambda_function" "rss_function" {
  depends_on = [
    aws_lambda_layer_version.rss_layer
  ]

  filename         = data.archive_file.rss_function.output_path
  function_name    = "rss-by-terraform"
  role             = aws_iam_role.iam_for_lambda.arn
  handler          = "app.lambda_handler"
  source_code_hash = data.archive_file.rss_function.output_base64sha256
  runtime          = "python3.12"
  timeout          = 30 #seconds
  layers = [
    aws_lambda_layer_version.rss_layer.arn
  ]
  environment {
    variables = {
      CHANNEL_SECRET       = "hoge"
      CHANNEL_ACCESS_TOKEN = "hoge"
    }
  }

  lifecycle {
    ignore_changes = [
      environment
    ]
  }
}

# EventBridgeがLambdaをキックする許可を定義
resource "aws_lambda_permission" "rss_function" {
  function_name = aws_lambda_function.rss_function.function_name
  action        = "lambda:InvokeFunction"
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.rss.arn
}

# アカウントIDなどを取得
data "aws_caller_identity" "current" {}

Terraformで最新の無料利用対象AMI IDを出力してみた

先日の記事で、Terraformで利用するために、最新のAMI IDをAWS CLIで取得する方法を調べたが、Terraformだけで完結できそうだと気付いたためやってみる。

はじめに結論

このtfファイルを実行することで、先日の記事と同じ結果が得られる。

作成するtfファイル

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.42"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

# ↓ここから
data "aws_ami" "latest" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-2*"]
  }
}

output "latest_ami_id" {
  value = data.aws_ami.latest.id
}
# ↑ここが大事

terraform planの結果

Changes to Outputs:
  + latest_ami_id = "ami-031134f7a79b6e424"

(なお、先日の記事から数日しか経っていないが、すでに無料利用対象の最新AMIが更新されているようで、得られたAMI IDは異なっていた。)

やってみた

Terraformドキュメントに、AMIに関するData Sourceであるaws_amiが存在したので参考にする。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami

aws_amiで利用できる引数は以下の通り。AWS CLIのdescribe-imagesコマンドで見たことのあるものが揃っているが、「most_recent」と「name_regrex」はTerraform独自の引数となる。

  • owners
  • most_recent
  • executable_users
  • include_deprecated
  • filter
  • name_regex

most_recent

most_recentがtrueの場合、返却対象のAMIが複数あったときに最新のAMI1つのみを返却する。なお、返却されるIDが1つでないとTerraformは失敗する。
先日の記事でいうと、AWS CLIでqueryオプションを使ってJMESPathでごにょごにょ書いていた箇所に相当する。

name_regex

name_regexは、AWSが返却するAMI IDのリストに対して正規表現で絞り込みを行う。
自由度の高い絞り込みができる一方で、一応の注意点としてはローカルで絞り込みを行うという点。対象となるAMI IDの量やTerraform実行環境のスペック次第では少し重い処理になる。そのため、他の引数で返却されるIDを絞って減らしておくことが推奨されている。

脇道にそれるが、name_regex使うとどれだけパフォーマンスに影響が出るのか、ちょっとやってみた。

以下2パターンで、返却時間をtimeコマンドで確認してみた。

data "aws_ami" "filter" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-2*"]
  }
}

→ user 0m7.279s

data "aws_ami" "regex" {
  most_recent = true
  owners      = ["amazon"]
  name_regex  = "^al2023-ami-2.*"
}

→ user 0m12.970s

確かにパフォーマンスに少し影響が出た。大量のEC2を異なるAMIで起動するようなときなどはちょっと気を使っても良いかもしれない。

AWS CLIコマンドとの紐づけて整理

先日の記事のコマンドaws ec2 describe-images --owners amazon --filters 'Name=name,Values=al2023-ami-2*' --query 'sort_by(Images,&CreationDate)[-1].[ImageId]' --output textと紐づけて整理してみる。

AWS CLI--owners amazonは、
Terraformではowners = ["amazon"]となり、

AWS CLI--filters 'Name=name,Values=al2023-ami-2*'は、
Terraformでは

  filter {
    name   = "name"
    values = ["al2023-ami-2*"]
  }

となり、

AWS CLI--query 'sort_by(Images,&CreationDate)[-1].[ImageId]'は、
Terraformではmost_recent = trueとなった。

残課題

先日の記事に同じ。

残課題
今回のように近しい命名規則のAMIが追加された場合や、AWS側でNameの命名規則を変えた>場合に、期待通りの出力結果が得られなくなる可能性がある。無料利用対象という条件で絞り込むような、スマートな方法があるのかもしれない。

トークンバケットとリーキーバケットの違いを調べてみた

システム設計の面接試験を読んでいて、レートリミッターのアルゴリズムについてはじめはうまく理解できなかったため、頭の整理がてら調べてみる。

そもそもレートリミッターとは

アクセスが集中することによる過負荷を防ぐことを目的として使われる。
プロモーション時のスパイクやDDoS攻撃などでAPIへのアクセスが集中すると、その裏で稼働するサーバーがリソース不足に陥り他ユーザーへのレスポンスが遅延したり、パブリッククラウドのようなオンデマンドなコンピューティングを利用していれば利用コストが跳ね上がってしまったりしかねない。
そこで、クライアントとサーバーの間でアクセス量をコントロールする層の役割を果たすのが、このレートリミッターである。

調べてみた

代表的なものは以下。今回は、基本形と思われるトークバケットとリーキーバケットについて調べる。

  • トークバケット
  • リーキーバケット
  • 固定ウィンドウカウンタ
  • スライディングウィンドウログ
  • スライディングウィンドウカウンタ

トークバケット

一定間隔で供給されるトークンをバケットに貯めておき、トークンを消費することでリクエストを処理するアルゴリズム

Amazon API Gatewayをはじめとして、Web APIでよく利用されるらしいのがトークバケット

API Gateway は、トークバケットアルゴリズムを使用してトークンでリクエストをカウントし、API へのリクエストを調整します。
引用:API リクエストを調整してスループットを向上させる https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-request-throttling.html

リクエストが来た際にトークンがあれば、トークンを消費してリクエストを中継する。そのため、リクエストのバーストに備えた分のトークンを貯めておければバーストを捌ける。一方で、リクエストが来た際にトークンがなければ、リクエストは即座に破棄される。なお、トークンは一定量以上をバケットに貯めておくことはできない。

2つのパラメータでアルゴリズムを調整することになる。

リーキーバケット

上限帯域が決まっており、単位時間においてその帯域内であればリクエストを順次処理していくアルゴリズム

トークバケットとあわせて解説されていることが多い印象。正確ではないかもしれないが、ざっくりいうとFIFOキューという理解。Shopifyで使われているらしい。

参考:レート制限について https://www.shopify.com/jp/blog/partner-rate-limits

リクエストが来た際にバケットに空きがあればバケットへリクエストを格納していく。そして格納したリクエストは一定のペースで処理される。そのため、バーストが発生した場合にバーストに追随して処理量を増やすことはできない。ただし、バケットサイズ内にバーストのリクエスト量が収まっていれば、一定のペースで処理されていくため、いずれは処理される。

リクエストのバーストが発生しても、単位時間あたりの処理数は変わらない。そのため、一定負荷を中継し続けたいようなユースケースで利用できる。

2つのパラメータでアルゴリズムを調整することになる。

まとめ

  • トークバケット
    • バーストに追随して処理量を増やせる
    • 処理/中継するリクエスト量を一定に保てない
  • リーキーバケット
    • バーストに追随して処理量を増やせない
    • 処理/中継するリクエスト量を一定に保てる

DynamoDBのレスポンスに含まれるCountとScannedCountの違いを調べてみた

先日の記事を書きながら、DynamoDBにScanした際のレスポンスにCountとScannedCountという似たような名前で同じ値を返却しているものがあることが気になっていたため、調べてみた。

Scanレスポンス

先日の記事からコピペ。"Count": 2,"ScannedCount": 2と返却されていた。

{
  "Count": 2,
  "Items": [
    {
      "color": {
        "S": "yellow"
      },
      "target": {
        "S": "banana"
      }
    },
    {
      "color": {
        "S": "red"
      },
      "target": {
        "S": "apple"
      }
    }
  ],
  "ResponseMetadata": {
    "HTTPHeaders": {
      "connection": "keep-alive",
      "content-length": "132",
      "content-type": "application/x-amz-json-1.0",
      "date": "Wed, 13 Mar 2024 22:29:09 GMT",
      "server": "Server",
      "x-amz-crc32": "2357452850",
      "x-amzn-requestid": "9E048FPDU66OTKVD4QQP98LSBRVV4KQNSO5AEMVJF66Q9ASUAAJG"
    },
    "HTTPStatusCode": 200,
    "RequestId": "9E048FPDU66OTKVD4QQP98LSBRVV4KQNSO5AEMVJF66Q9ASUAAJG",
    "RetryAttempts": 0
  },
  "ScannedCount": 2
}

結論

以下の違いがある。

  • Count:フィルター適用前の項目数
  • ScannedCount:フィルター適用後の項目数

フィルターを利用していない場合は、CountとScannedCountの値は同一になる。そのため、先日の記事ではCountとScannedCountが同じ値になっていた。

なお、DynamoDBが一度に返却するItemは上限1MBまであり、それ以上のItemを返却するためにはリクエストを分割する必要がある。CountおよびScannedCountの値は分割した単一リクエストごとの値となり、リクエスト全体の合計値ではないことに注意が必要。なので、リクエスト全体の合計値を取得したい場合はアプリケーション側で集計する処理を行う必要がある。

参考

結果での項目のカウント
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Query.Other.html#Query.Count

Flask起動したときに表示されるメッセージの意味を調べてみた

Flaskを起動したときに表示されるメッセージの意味をまったく理解してないため、調べてみる。

調べてみた

これのこと。なお、デバッグモードはONにしている。(デフォルトはOFF)

* Serving Flask app 'hello'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on <http://127.0.0.1:5000>
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 115-419-723

1行ずつみていく。


* Serving Flask app 'hello'

実行するコードのファイル名を表している(はず)。


* Debug mode: on

デバッグモードのON/OFFを表している。


WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.

開発環境用のサーバーであること、商用環境用の場合はWSGIサーバーを使え、というメッセージ。なお、このWARNINGメッセージが出力されていても、サーバーは問題なく稼働できている。

WSGIというものを根本的によくわかっていないため、少し調べてみたが、実際に使ってみないとちょっと理解が深まりそうになかった、、
とりあえずざっくり、「WSGIPythonのアプリケーションとサーバーの間の共通的なインターフェースであり、Flaskのみでホスティングしている簡易なものよりもセキュリティ面など(他は?)で優位(なぜ?)。」くらいの理解にとどめた。


* Running on <http://127.0.0.1:5000>

立ち上げたサーバーがlocalhostの5000番ポートでhttp通信を受け付けていることを表す。


Press CTRL+C to quit

立ち上げたサーバーを停止する場合はCTRL+Cを入力しろ、ということを表す。


* Restarting with stat

よくわからなかった、、
statはファイルシステム関連のシステムコールの文脈で使われている?
なお、デバッグモードOFF時は出力されない。

参考?:stat https://docs.python.org/ja/3.12/library/stat.html


* Debugger is active!

デバッガーがアクティブであることを表す。
デバッガーを使うことで、エラーが表示されたブラウザ上でデバッグを行うことができる。
なお、デバッグモードOFF時は出力されない。

参考:組み込みのデバッガ https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/debugging.html#the-built-in-debugger


* Debugger PIN: 115-419-723

デバッガーを使う際に入力を求められるPINを表す。
なお、デバッグモードOFF時は出力されない。

残課題

  • WSGIについて手を動かしながら理解を深める
  • Restarting with statの意味を探る
  • ブラウザでデバッガー使ってみる