はじめに

学習を兼ねて、AWS上で、ネットワークとサーバーを構築して、Web APIの機能を持つアプリケーションを動かすデモを作成したので、手順をまとめておきます。

APIアプリケーションはKotlin製で、Ktorを利用しています。

システム構成

AWSの構成は以下の図の通りです。
APIアプリケーションを動かすだけであれば、この構成よりも最低限の内容で事足りるかと思いますが、実務で使用されていそうなAWSのサービスをいくつか試してみたかったので、このような構成としています。

  • 東京リージョンにAmazon VPCを構築
  • ap-northeast-1aに Public / Private サブネットを1つずつ構築
  • ap-northeast-1cに Public / Private サブネットを1つずつ構築
  • Amazon EC2インスタンスをそれぞれのPrivate Subnetに構築
  • Application Load Balancerで、インターネットからのリクエストをそれぞれのEC2インスタンスへルーティング
  • Kotlin + Ktor による Web API サーバーをそれぞれのEC2インスタンス上で動かす
  • Amazon S3 に Web API アプリケーションの実行ファイルを格納し、EC2インスタンスからアクセスしてダウンロードする
  • EC2インスタンスへのアクセスはAmazon Systems Manager による踏み台レス運用とする

ネットワーク構築

1. Amazon VPCを作成

  • IPv4 CIDRを10.0.0.16で作成

2. サブネットを作成

2-1. パブリック用に2つ作成

  • 先述で作成したVPCに関連づける
  • 東京リージョン内の別々のアベイラビリティゾーンで2つ作成
    • ap-northeast-1a
      • IPv4 CIDRを10.0.1.0/24
    • ap-northeast-1c
      • IPv4 CIDRを10.0.2.0/24

2-2. プライベート用に2つ作成

  • 先述で作成したVPCに関連づける
  • 東京リージョン内の別々のアベイラビリティゾーンで2つ作成
    • ap-northeast-1a
      • IPv4 CIDRを10.0.3.0/24
    • ap-northeast-1c
      • IPv4 CIDRを10.0.4.0/24

3. パブリックサブネットをインターネットに接続

  • インターネットゲートウェイを作成
  • ルートテーブルを作成
    • 先述で作成したVPCに関連づける
    • サブネットを関連づける
      • 先述で作成したパブリックサブネット2つを関連づける
    • ルートを編集する
      • 送信先のCIDRを0.0.0.0/0とし、ターゲットを先述で作成したインターネットゲートウェイを指定する

4. ロードバランサーを作成

4-1. ターゲットグループを2つ作成

  • ターゲットタイプはインスタンスとする
  • ロードバランサーとターゲット間の通信するプロトコルはHttp
  • ターゲットがトラフィックを受信するポートを、
    • 1つ目のターゲットグループでは8080と指定
    • 2つ目のターゲットグループでは8081と指定
  • ヘルスチェック設定
    • 対応させるプロトコルはHttp
    • パスは、
      • 1つ目のターゲットグループでは/api/statusと指定
      • 2つ目のターゲットグループでは/admin/statusと指定

4-2. Application Load Balancerを作成

  • スキーム設定
    • インターネット向け
    • ロードバランサーのIPタイプはIPv4
  • ネットワークマッピング設定
    • VPCは、先述で作成したVPCを指定
    • アベイラビリティゾーンとサブネット
      • ap-northeast-1aを指定し、作成したパブリック用のサブネットを指定
      • ap-northeast-1cを指定し、作成したパブリック用のサブネットを指定
    • セキュリティグループを設定
      • ALB用のセキュリティグループを作成し指定する
        • 作成するセキュリティグループはインバウンドで全てのIPからのHttpを受け付けるようにする
    • リスナーとルーティング
      • リスナーはプロトコルをHttpとし、ポートを80とする
      • リッスンしたリクエストをルーティングするターゲットに先述で作成したターゲットグループを指定

サーバー構築

1. Amazon EC2インスタンスを2つ作成

  • AMIはAmazon Linux 2023 kernel-6.1
  • インスタンスタイプはt3.micro
  • キーペアはRSAのpemファイルを作成して使用する
  • ネットワーク設定
    • VPCは先述で作成したVPCを指定
    • サブネットは先述で作成したプライベートサブネットを指定する
      • 1つ目のEC2インスタンスでは、アベイラビリティゾーンAのプライベートサブネットを指定
      • 2つ目のEC2インスタンスでは、アベイラビリティゾーンCのプライベートサブネットを指定
    • ファイアーウォールで、セキュリティグループを設定
      • セキュリティグループを作成し指定する
        • 1つ目のEC2インスタンスでは
          • インバウンドでALB用のセキュリティグループをソースとして、カスタムTCP8080ポートで待ち受けるようにする
          • アウトバウンドで、全てのプロトコルで全てのIPを送信先として指定する
        • 2つ目のEC2インスタンスでは
          • インバウンドでALB用のセキュリティグループをソースとして、カスタムTCP8081ポートで待ち受けるようにする
          • アウトバウンドで、全てのプロトコルで全てのIPを送信先として指定する
    • プライマリIPを
      • 1つ目のEC2インスタンスでは10.0.3.10に設定
      • 2つ目のEC2インスタンスでは10.0.4.10に設定

2. EC2インスタンスにAmazon System Managerを使用してアクセスできるようにする

  • SSM Agentのインストール
    • 今回はAmazon Linux 2023を利用していて、プリインストールされているため不要
  • IAMロールを作成
    • AmazonSSMManagedInstanceCoreポリシーを許可する
  • 作成したIAMロールを2つのEC2インスタンスにアタッチする
  • セキュリティグループを作成
    • 作成するセキュリティグループはインバウンドで先述で作成したVPCに割り当てたIP CIDRからのHttpsを受け付けるようにする
  • VPCエンドポイントを4つ作成
    • タイプはAWSのサービスを指定
    • サービスは以下の4つを指定(VPCエンドポイントをサービス毎に1つ作成する)
    com.amazonaws.region.ssm
    com.amazonaws.region.ec2messages
    com.amazonaws.region.ssmmessages
    com.amazonaws.region.s3
    
    • VPCは先述で作成したVPCを指定する
    • エンドポイントを作成するサブネットは
      • アベイラビリティゾーンAのプライベートサブネットを指定
      • アベイラビリティゾーンCのプライベートサブネットを指定

APIアプリケーション実装

1. Ktorを利用するための設定

val ktorVersion by extra("2.3.12")

dependencies {
    implementation("io.ktor:ktor-server-netty:${ktorVersion}")
    implementation("io.ktor:ktor-server-core:${ktorVersion}")
}

2. Ktorを利用して、Http通信ができるようにする

  • アベイラビリティゾーンAで動かすAPIサーバ
    • ポート8080
    • IP0.0.0.0
    • エンドポイント/api/status
  • アベイラビリティゾーンCで動かすAPIサーバ
    • ポート8081
    • IP0.0.0.0
    • エンドポイント/admin/status

以下はアベイラビリティゾーンAで動かすAPIサーバのソースです。
アベイラビリティゾーンCで動かすAPIサーバとの差分はポートとエンドポイントが異なるだけです。

package com.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(
        factory = Netty,
        port = 8080,
        host = "0.0.0.0",
        module = { module() }
    ).start(wait = true)
}

fun Application.module() {
    routing {
        get("/api/status") {
            call.respondText("{\"status\":\"ok\"}", io.ktor.http.ContentType.Application.Json)
        }
    }
}

APIアプリケーションをEC2上で動くようにする

1. アプリケーションのJarファイルをEC2インスタンス上に格納する

1-1. Amazon S3上にJarファイルをアップロードする

2つのAPIアプリケーションで、依存関係を全て含めてフルビルドしたJarファイルを作成する
com.github.johnrengelman.shadowを利用するので、build.gradleに依存関係を追加

plugins {
    id("com.github.johnrengelman.shadow") version "8.1.1"
}

./gradlew shadowJarコマンドでFat Jarを作成する。
ビルドするとbuild/libs配下にJarファイルが生成される。

次にS3でバケットを作成し、JarファイルをS3のコンソール画面上からアップロードする。

EC2インスタンス上で、S3に格納されているJarファイルをダウンロードする

EC2インスタンスにアタッチしているIAMロールに、S3の読み取り権限を付与する
AmazonS3ReadOnlyAccess

System Managerを使用して2つのEC2インスタンスに接続する。
接続したらコンソール上で、以下のコマンドで、S3からJarファイルをダウンロードする

mkdir -p /opt/myapp
sudo chown ec2-user:ec2-user /opt/myapp
sudo aws s3 cp s3://api-server-test-20260104/KtorApiApp-1.0-SNAPSHOT.jar /opt/myapp

2. EC2インスタンス上にJDKをインストールする

アプリケーションはKotlinなので、2つのEC2インスタンス上で実行するためにJDKをインストールする

sudo yum update -y
sudo yum install java-21-amazon-corretto -y

3. EC2インスタンス上で、アプリケーションを起動する

2つのEC2インスタンスにSystem Managerで接続後に、以下のコマンドを叩き、Jarファイルを実行する。

java -jar your-app.jar

4. APIアプリケーションに対してHttpリクエストを送信してレスポンスが返るか確認する

Webブラウザで以下のUrlにアクセスして、疎通していることを確認する

// request
http://<アプリケーションロードバランサーのDNS名>/api/status

// response
{"status":"ok"}
// request
http://<アプリケーションロードバランサーのDNS名>/admin/status

// response
{"status":"ok"}

参考

https://ktor.io/docs/server-engines.html

https://ktor.io/

https://qiita.com/kmdsbng/items/ab093146a62afa34b739

https://qiita.com/hisato_imanishi/items/9993c86e02f5cbf4deae