EventBridge Schedulerを使ったEC2停止

はじめに

よくある話ですが、AWSの利用料金を節約するためにEC2の自動停止を実施したいという要望があります。方法は色々あるかなと思いますが、2024年7月時点で一番お手軽なのはEventBridge Schedulerを使う方法かなと考えています。

aws.amazon.com

簡単に動作検証をしてみました。

実装

EC2を停止させるために、EventBridge Schedulerを操作する前にIAMロールを作成します。IAMロール作成の画面にて「カスタム信頼ポリシー」を選んで以下のJSONをコピペします。

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

許可ポリシーはEC2FullAccessを付与してあげれば確実ですが、最小はec2:StopInstancesを与えてあげればOKです。

EventBridge Schedulerを使うのはコンソール上でボタンをポチポチやっていれば簡単に実装できます。最初は1回限りのスケジュールを作成して動作確認を取ると良いかなと思います。

EC2を停止するためには、ターゲットAPIにてEC2を選択後、StopInstancesを選択します。

ターゲットとなるインスタンスIDは一個ずつコピペして設定してあげます。まとめて指定することはできないのかなとちょっと試してみましたが、うまく動きませんでした。

あとは時間になればEC2インスタンスが停止していることを確認すればOKです。

1回限りのスケジュールで動作確認ができれば、cron式で定期的なスケジュールを実行するようにします。

cron式にはいろいろな方言があるので、AWSが定めているcron式のリファレンスを見ながら設定します。

docs.aws.amazon.com

ログについては、CloudTrailに出力されます。以下は出力されたJSONの切り抜き。

だいぶ検索性は低いので実際にはAthenaなどで検索するようにすると良いかなと思います。

考察

EC2の自動停止をEventBridge Schedulerを使って実装しました。コーディングなしに実装できるのは大変楽なのですが、設定にはIAMロールの作成やcron式の入力など地味に引っかかるところがあるので、一度素振りしておいた方が良いかなとおもます。

また、ログについてはかなり貧弱です。EventBridge Scheduler上から確認できればいいんですが、ちょっとなさそうです。

LocalStackを活用したAWSの挙動確認(CloudFormation編)

はじめに

少し前に無償版LocalStackを使ってAWS上の挙動確認をローカル環境でシミュレーションすることをやってみました。

miyohide.hatenablog.com

今回はそれを少し発展させてCloudFormationを実行させてみます。

compose.ymlの作成

環境はDockerで構築します。以下の内容であるcompose.ymlを作成してdocker compose upで起動させておきます。

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
    image: localstack/localstack:3.3.0
    ports:
      - "127.0.0.1:4566:4566"
    environment:
      - DEBUG=${DEBUG:-0}
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

  awscli:
    image: amazon/aws-cli:2.15.36
    entrypoint: "bash"
    volumes:
      - "./.aws:/root/.aws"
      - "./resources:/aws/resources/"
    tty: true
    environment:
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
      - AWS_DEFAULT_REGION=ap-northeast-1
      - AWS_ENDPOINT_URL=http://localstack:4566

AWS CLIのコンテナイメージを使い、このコンテナから操作するイメージです。なお、環境変数としてAWS_ENDPOINTを指定しておくと、awsコマンドが実際のAWS環境ではなくDocker上で動いているLocalStackを参照するようになります。

サンプルの作成(S3)

まずはS3を作成するCloudFormationを実行します。以下のyamlファイルを作成します。今回はこのファイルを./resources/sample-cfn.ymlとして保存しました。

Resources:
  LocalBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: foobar

あとは、aws cloudformation deploy --stack-name cfn-qs-stack --template-file ./resources/sample-cfn.ymlを実行するとOK。無事、LocalStack上でS3バケットが作成されます。

なお、aws cloudformation deployの実行結果は瞬時に返ってくるのではなく、数秒の待ちが発生しました。

bash-4.2# aws s3 ls
2024-07-06 21:14:14 foobar
bash-4.2#

サンプルの作成(EC2)

EC2も作成してみます。以下のyamlファイルを作成します。

AWSTemplateFormatVersion: "2010-09-09"
Description: Provision EC2

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  IGW:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  PubSub:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24

  PubSubRT:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PubSubToInternet:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PubSubRT
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW

  AssoPubSubRT:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PubSub
      RouteTableId: !Ref PubSubRT

  EC2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-hogehoge
      KeyName: "mykey"
      InstanceType: t2.micro
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !Ref PubSub
          GroupSet:
            - !Ref EC2SG

  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: ec2-sg-cf
      GroupDescription: Allow SSH
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: "127.0.0.1"

先ほどと同様にaws cloudformation deploy --stack-name cfn-ec2-4 --template-file ./resources/sample-ec2.ymlを実行するとOKです。

なお、今回もaws cloudformation deployの実行結果は瞬時に返ってくるのではなく、数秒の待ちが発生しました。

実際にEC2が作成されていることはaws ec2 describe-instancesを実行することで確認できます。実行結果は省略します。

なお、あくまでシミュレータですので実際にEC2インスタンスを操作することはできません。

考察

LocalStackに対してCloudFormationを実行することを試してみました。あくまでシミュレータですので過信は禁物ですが、ちょっとした動作確認ぐらいはできるかなと思います。

今回、EC2を作成してみましたが、AMIとしてami-hogehogeを指定しました。これでもLocalStack上は正常終了するので細かい値の妥当性まではLocalStackは実施していないことがわかります。

AWS LambdaでSpring Cloud Functionで作ったアプリを動かす

はじめに

先日、Spring Cloud Functionを使ってAWS Lambda向けのアプリケーション実装について記しました。

miyohide.hatenablog.com

上記の記事では、ローカルで動かしてみたところまでやりましたが、実際にAWS Lambda上で動かしてみます。

AWS Lambda上で動かすための設定

まずは./gradlew buildでjarファイルを作成します。成功すると、build/libs以下に二つのjarファイルが作成されますが、末尾が-aws.jarであるものが対象です。

末尾が-aws.jarとなっているものをLambdaのパッケージとしてアップロードしておきます。

docs.aws.amazon.com

次に、Lambda上でハンドラを指定します。ドキュメントにも記載がありますが、org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequestを指定します。

docs.spring.io

これだけだとmain classが見つからない旨のエラーメッセージが出て起動に失敗します。

エラーメッセージにあるように環境変数MAIN_CLASSを指定してあげます。

すると、無事起動に成功します。

発展的話題

今回、環境変数MAIN_CLASSを指定しましたが、build.gradleにてapplicationプラグインを追記し、以下のように記述すると環境変数を設定しなくても動きました。

application {
    mainClass = 'com.github.miyohide.App'
}

ログを見るとSpringのロゴが確認でき、最初のリクエストが処理されます。最初は341.03msとそこそこの時間がかかります。ただ、2回目以降は2.57ms、1.88msと大幅に処理が速くなっています。

いわゆるウォームスタートの結果が如実に出ている感じです。

docs.aws.amazon.com

考察

実際にSpring Cloud Foundtionを使ったアプリケーションをAWS Lambda上で動かしてみました。マニュアルが書かれているのですが、あまり丁寧な記述ではなく最初の導入ハードルは高いかなと感じます。

また、ビルドした結果のファイルがかなり大きいです。今回は非常に単純な処理だけだったのであまりライブラリも使用していませんが、それでも20MB超えでしたので、いろいろとライブラリを追加していくとクォーターに引っかかることも出てくるかもしれません。

docs.aws.amazon.com

Amazon RDSにてDBインスタンスのパラメータを変更する手順

はじめに

Amazon RDSを使う時に、チューニングやログ出力などでパラメータのデフォルト値から変更したい場合が結構あります。そのとき、パラメータの値を変更しようとするが、デフォルトで用意されたパラメータグループには編集ボタンが出てこずにあれ?ってなるケースを目にしました。以下はPostgreSQL 16の場合ですが、デフォルトパラメータグループを編集しようにも編集ボタンが出てきません。

間際らしいのは、「パラメータ」の横にある「情報」をクリックすると、「1つ以上のパラメータを編集するには、パラメータの[編集]を選択します。」と書かれているところです。

「編集」ボタンが出てこないのは、権限不足だったり、何らかの不具合じゃないかという人も目にしました。ここでは、この対処方法について記します。

公式ドキュメントを見る

公式ドキュメントを見ると、デフォルトパラメータグループは編集できない旨がバッチリ書かれています。

docs.aws.amazon.com

対処方法についてもバッチリ書かれているので、その通りに作業を進めばOKです。

実際にやってみる

実際にやってみます。「パラメータグループ」から「パラメータグループの作成」をクリックします。

必要事項を入力します。名前とデータベースのエンジンやバージョンなどを指定すればよいだけですので、特に迷うところはないかなと思います。

作ったパラメータグループは「カスタムパラメータグループ」に現れます。作ったものを選択して「アクション」メニューから「比較」をクリックします。

すると、デフォルトパラメータグループとの比較になり、差分がないことが確認できます。

作ったパラメータグループにはバッチリ「編集」ボタンも出てきます。

「編集」ボタンをクリックすると、変更可能なものを編集する画面が出てきます。

この画面で値を変更し、DBインスタンスに紐づければOKです。

考察

Amazon RDSで動かしているデータベースのパラメータを編集する方法について記しました。パラメータグループという概念が必要なため、オンプレ経験者とかはちょっとまどろっこしいところもあるのかなと思います。

また、デフォルトパラメータグループが編集できないという記載はコンソール画面には出てこないのもハマりポイントです。知っていれば当たり前なのですが、予備知識がないとあらぬ方向に推察してしまい、例えば権限不足ではないかと疑う人も多いのかなと思いました。

DynamoDB Local: AWS公式のローカルデータベース

はじめに

だらっとDocker Hubを見ていたらDynamoDBのlocal版がDocker Hubにて公開されていることを知りました。

https://hub.docker.com/r/amazon/dynamodb-local

よくよく調べてみたら、DynamoDB localとしてAWSのドキュメントがありました。

docs.aws.amazon.com

今回はこのDynamoDB localを試してみます。

セットアップ

まず、以下の内容でcompose.ymlファイルを作成します。

services:
  dynamodb:
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal

準備は以上です。

起動

起動はdocker compose upを実行すればOKです。以下のようにログが出力されます。

dynamodb-1  | Initializing DynamoDB Local with the following configuration:
dynamodb-1  | Port:     8000
dynamodb-1  | InMemory: false
dynamodb-1  | Version:  2.5.1
dynamodb-1  | DbPath:   ./data
dynamodb-1  | SharedDb: true
dynamodb-1  | shouldDelayTransientStatuses:     false
dynamodb-1  | CorsParams:       null

接続先はhttp://localhost:8000になります。準備はこれだけでOK。

AWS CLIにて接続する

AWS CLIにて接続します。まず、AWS CLIの環境をcompose.ymlにて用意します。先ほど作成したcompose.ymlを修正して以下のように記述します。

services:
  dynamodb:
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    image: "amazon/dynamodb-local:latest"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal

  app:
    image: "amazon/aws-cli:2.16.9"
    entrypoint: ""
    command: "/bin/bash"
    tty: true
    stdin_open: true
    environment:
      - AWS_ENDPOINT_URL=http://dynamodb:8000
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
      - AWS_DEFAULT_REGION=ap-northeast-1

ポイントは、環境変数AWS_ENDPOINT_URLAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGIONの設定です。それぞれの環境変数の意味は以下のドキュメントを参照してください。

環境変数AWS_ENDPOINT_URLの説明は以下を。

docs.aws.amazon.com

環境変数AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGIONの説明は以下を。

docs.aws.amazon.com

docker compose upで起動し、別のターミナルでdocker compose exec app /bin/bashを実行するとAWS CLIが使える環境が手に入ります。

あとは、aws dynamodb list-tablesを実行してテーブル一覧を表示させます。まだ何も作っていないので空が返ってきます。

{
    "TableNames": []
}

aws dynamodb create-tableを使ってテーブルを作成したのちにaws dynamodb list-tablesを実行すると無事テーブルが作成されていることが確認できます。

bash-4.2# aws dynamodb create-table --table-name 'table01' --attribute-definitions '[{"AttributeName":"key","AttributeType": "S"}]' --key-schema '[{"AttributeName":"key","KeyType": "HASH"}]' --provisioned-throughput '{"ReadCapacityUnits": 5,"WriteCapacityUnits": 5}'
(省略)
bash-4.2# aws dynamodb list-tables
{
    "TableNames": [
        "table01"
    ]
}
bash-4.2# 

考察

なかなかクラウド環境にさわれない場合などはこういうローカル環境で動くものが重宝されます。AWS公式が作成して公開してくれるのは大変ありがたいです。

一方で、あくまでシミュレータなので、いくらかの違いがあります。詳細は以下に記されているのでそれをもとに使用の判断をすると良いかなと思います。

docs.aws.amazon.com

AWS LambdaとSpringの融合:Spring Cloud Functionの活用

はじめに

SpringプロジェクトにはServerless用のフレームワークとしてSpring Cloud Functionというものがあります。

spring.io

Webアプリで使い慣れたSpring Bootと同じ考えをAWS LambdaやAzure Functionsなどに適用できることから、魅力的に映る人もいるかなと思います。ここから数回はSpring Cloud Functionを使ってAWS Lambda上で関数を動かすことをやってみます。

プロジェクトの作成

まずはgradle initでGradleプロジェクトを作り、以下のようなbuild.gradleを作ります。

plugins {
    id 'java'
    // dependencyManagement用
    id 'io.spring.dependency-management' version '1.1.5'
    // bootRunのため。
    id 'org.springframework.boot' version '3.2.4'
}

group = 'com.github.miyohide'
version = '1.0'

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2023.0.1")
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-function-web'
    implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws'
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

依存関係をまとめるSpring Cloudのバージョンを変数springCloudVersionで設定して、dependencyManagementで設定。dependenciesにて指定するものとしてorg.springframework.cloud:spring-cloud-starter-function-weborg.springframework.cloud:spring-cloud-function-adapter-awsを指定しておきます。

アプリの実装

アプリは以下のような感じで実装します。

package com.github.miyohide;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.function.Function;

@SpringBootApplication
public class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

  @Bean
  public Function<String, String> reverseString() {
    return value -> new StringBuilder(value).reverse().toString();
  }
}

これだけで./gradlew bootRunでアプリを起動し、別のシェルでcurl localhost:8080/reverseString -H "Content-Type: text/plain" -d "abc"を実行するとcbaが返ります。メソッド名をURLに付与し、-d "abc"で与えた文字列が逆転した結果cbaが返ります。

ちょっと発展

別のクラスに処理を実装してみます。

package com.github.miyohide.functions;

import java.util.function.Function;

public class Greeter implements Function<String, String> {

  @Override
  public String apply(String s) {
    return "Hello " + s;
  }
}

@Componentを付与しなくても、application.propertiesに以下の記述を追加することでapplyメソッドが関数として登録されます。

spring.cloud.function.scan.packages=com.github.miyohide.functions

公式ドキュメントとしては以下のものです。

docs.spring.io

実際試してみます。別のシェルでcurl localhost:8080/greeting -H "Content-Type: text/plain" -d "abc"にアクセスすると、Hello abcが返ります。

考察

とりあえずSpring Cloud Functionを使ってアプリを作ってみました。Springに関する前提知識が多分に求められていますが、すでにSpringに慣れ親しんでいる場合はSpring Cloud Functionを使ってみるのも良いかなと思います。

ちょっと調べた感じ、最小限必要な記述を解説したものはなかったので、build.gradleなどの設定ファイルをつくるのはちょっと苦労しました。何が必要で何が不要なのかがちょっと分かりにくいです。

AWS Lambdaにおけるランタイムバージョンの制御(2)

はじめに

先日、AWS LambdaのRubyランタイムバージョンを確認するということをやりました。

miyohide.hatenablog.com

この記事を書いてから2週間、現在のバージョンはどうなっているかを確認しました。

2024年6月2日時点のRubyのランタイムバージョン

確認したところ、ruby:3.3.v6というバージョンでした。

前回確認した時にはv4でしたので、結構な頻度で上がっていることが確認できます。

前回確認した時のaws-sdkは3.2.0でした。

今回、aws-sdkのバージョンを確認すると3.2.0と変わりませんでしたが、aws-sdk-accessanalyzerやaws-sdk-accountのバージョンは変わっていました。

aws-sdkのバージョンアップ指針を把握しているわけではないのですが、AWS側の任意のタイミングでラインタイムバージョンが上がっているように見受けられます。

ちなみに、5月30日にRuby 3.3.2がリリースされました。

www.ruby-lang.org

ひょっとしたらすでにRubyのバージョンも上がっているのかなと思いましたが、6月2日時点では3.3.1でした。

考察

先日も記しましたが、ラインタイムバージョンは結構な頻度で上がっているかなと思われます。どのようなタイミングで上がるかは分かりませんが、この挙動が要件に合わない場合は、コンテナイメージを動かす形の方が良いかもしれません。ただ、その際各種パッチ作業は利用者側の責任となります。