SpringBootで開発したREST APIをLambda×API Gatewayの環境にデプロイし、Route53でカスタムドメイン化する①

# やりたいこと
SpringBootで開発したREST APIアプリケーションをLambdaにデプロイし、API Gatewayで公開したい。
API GatewayのエンドポイントのURLをRoute53を使ってカスタムドメイン化したい。
![RestAPI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/6097f90f-34a5-581b-0ec8-a5aa198917b7.jpeg)
# ①でやること
少し長くなるので①と②に分けます。
①ではLambda環境で動作するSpringBootアプリケーションの開発、Lambdaへのデプロイ、API Gatewayとの連携まで行います。
②では、API GatewayとRoute53を使った、APIのエンドポイントのカスタムドメイン化を行います。

# SpringBoot開発環境
・Java21
・SpringBoot3.4.0
・gradle
・Eclipse

# 完成したアプリケーション
Githubで公開しています。
今回はLambda環境で動かすために必要な設定のみ取り扱うため、詳しくは下記をご参照ください。

https://github.com/SatoHiroto526/LambdaFunctionSample

# 通常のSpringBootアプリとLambda環境で動かすSpringBootアプリの相違点
通常は、下記クラスのようなクラスがキックされることでアプリが実行されるようです。
```
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LambdaFunctionSampleApplication {

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

}
```

しかし、Lambda環境ではこのようなクラスをキックすることができず、ClassNotFoundExceptionがスローされてしまいます。

そこで、Lambda環境でも実行できるようにする設定が必要です。

# builde.gradleの設定
以下がgladleの設定です。

```
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.0'
	id 'io.spring.dependency-management' version '1.1.6'
	//shadowJarを作成できるようにする。
	id 'com.github.johnrengelman.shadow' version '7.0.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	//Lambda上で実行できるようにするための設定できるようにする。
	implementation 'com.amazonaws:aws-lambda-java-core:1.1.0'
}

tasks.named('test') {
	useJUnitPlatform()
}
```

付け加えたのは2個所。
1つ目は、shadowJarを作成できるようにする設定です。
Lambdaにデプロイする際は、shadowJarとしてビルドする必要があるようです。
（なんでかは全く分かりません、、、）
```
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.0'
	id 'io.spring.dependency-management' version '1.1.6'
	//shadowJarを作成できるようにする。
	id 'com.github.johnrengelman.shadow' version '7.0.0'
}
```
2つ目はLambda環境で実行できるようにするための設定を行うライブラリに関してです。
すなわち、aws-lambda-java-coreを投入します。
```
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	//Lambda上で実行できるようにするための設定できるようにする。
	implementation 'com.amazonaws:aws-lambda-java-core:1.1.0'
}
```
以上でbuilde.gradleの設定は完了です。

# Contolollerの編集
次にContlollerを編集します。
以下が完成したControllerです。
```
package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.amazonaws.services.lambda.runtime.Context;
import com.example.demo.entity.SampleEntity;
import com.example.demo.service.SampleService;

import lombok.Data;

@RestController
//inputとcontextのgetter/setterを使えるようにする。
@Data
//Lambda環境で@Serviceクラスをインジェクションするために必要
@ComponentScan({"com.example.demo.service"})
@RequestMapping("/sample")
public class SampleController {
	
	private SampleService service;
	//Lambdaがキックするクラスで使用するために定義
	private Object input;
	private Context context;
	
	//コンストラクタインジェクション（フィールドインジェクションが使えないため）
	@Autowired
	public SampleController(SampleService service){
		this.service = service;
	}
	
	@GetMapping("/api")
	public List<SampleEntity> returnList(){
		return service.createList();
	}

}
```
まずはクラスに付与されたアノテーションから見ていきます。
```
@RestController
//inputとcontextのgetter/setterを使えるようにする。
@Data
//Lambda環境で@Serviceクラスをインジェクションするために必要
@ComponentScan({"com.example.demo.service"})
@RequestMapping("/sample")
```
Lombokの@Dataに関しては、Lambdaがキックするクラスで使われるObject型の変数inputとContext型の変数contextのgetter/setterを補うものです。
また、@ComponentScanは、DIコンテナに登録されたオブジェクトをインジェクションする際に必要になります。Lambda環境では、@ComponentScanを使わないとDIコンテナに登録されたクラスを使うことができないようです。

次にフィールドとコンストラクタに関して。
```
    private SampleService service;
	//Lambdaがキックするクラスで使用するために定義
	private Object input;
	private Context context;
	
	//コンストラクタインジェクション（フィールドインジェクションが使えないため）
	@Autowired
	public SampleController(SampleService service){
		this.service = service;
	}
 ```
 フィールドは上記で述べた通り、Lambdaがキックするクラスで使われるObject型の変数inputとContext型の変数contextを定義しています。
 また、コンストラクタは@Autowiredを用いたコンストラクタインジェクションを行っていますが、Lambda環境で動かすためにはフィールドインジェクションは使えないようなのでこのような書き方をしています。（相変わらず理由は全く分かりません、、、）

# Lambdaがキックするクラス（エントリーポイント）
以下がソースコードです。
```
package com.example.demo.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.example.demo.controller.SampleController;

@SpringBootApplication
//LambdaがSpringBootアプリケーションを起動する際にキックするクラス
//RequestHandlerをimplements
public class StartApp implements RequestHandler<Object, Object> {
	
	//RequestHandlerのメソッド
	@Override
	public Object handleRequest(Object input, Context context) {
		String args[] = new String[0];
		
		try(ConfigurableApplicationContext ctx = SpringApplication.run(SampleController.class, args)) {
			SampleController app = ctx.getBean(SampleController.class);
			app.setInput(input);
			app.setContext(context);
			Object result = app.returnList();
			return result;
		}catch(Exception e) {
			return "error.";
		}
	}

}
```
まずはアノテーションから見ていきましょう。
```
@SpringBootApplication
```
Lambdaがアプリケーションをキックする際のエンドポイントとなるクラスのため@SpringBootApplicationを付与します。

次にクラス宣言に関して
```
public class StartApp implements RequestHandler<Object, Object> 
```
gradleに追加したaws-lambda-java-coreライブラリのインターフェースであるRequestHandler<Object, Object> をimplementsしています。

RequestHandlerで定義されたメソッドhandleRequest(Object input, Context context) をOverrideすることでLambdaがキックできるようになるようですね。
```
//RequestHandlerのメソッド
	@Override
	public Object handleRequest(Object input, Context context) {
		String args[] = new String[0];
		
		try(ConfigurableApplicationContext ctx = SpringApplication.run(SampleController.class, args)) {
			SampleController app = ctx.getBean(SampleController.class);
			app.setInput(input);
			app.setContext(context);
			Object result = app.returnList();
			return result;
		}catch(Exception e) {
			return "error.";
		}
	}
```
正直中身はよくわかりませんが、先ほど作成したControllerクラスとそのメソッドをいい感じ記述すれば動くようです。（笑）
（catchブロックの中身にRuntimeExceptionをthrowする記述を追記してもいいなって思いました。）

最後に実際に動かしてみます。
![スクリーンショット 2024-11-24 114318.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/e5c3d6cb-4963-5935-0370-97fa5b0cecc3.png)
ローカルの開発環境では問題なく動きました！

# ビルドする
アプリケーションが完成したので、gradleでビルドします。
ここで注意なのですが、普通のJarでビルドしてもLambda環境では動かないようなので下記のようにshadowJarでビルドしなければならないようです。
（ここでかなり沼りました。）
![shadowJar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/55b0c2c2-3d82-80af-a34b-af95f800f028.png)

shadowJarをクリックするとビルドが開始され、下記場所にJarファイルが作成されます。
![shadowJar完成場所.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/7e7a19c4-78dc-ea51-061e-f65fa0a2fcda.png)

あとはお好きな場所にエクスポートすればOKです！

# Lambdaへデプロイ
AWSマネジメントコンソールからLambdaを開きます。
関数→関数の作成を開きます。
関数名は任意の名前、ランタイムはJava21を選択し、それ以外はデフォルトにしました。
![スクリーンショット 2024-11-24 110131.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/cc66aba0-d06d-1b3e-4a06-6bcab48227cb.png)

次に作成した関数を開き、コード→コードソース→アップロード元→.zipまたはjarファイルから、先ほどビルドしたJarファイルをアップロードします。
（ちょっと時間かかります。）
![Lambdaにアップロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/297cc935-c7b7-734d-c783-3d8baa0f26e7.png)

次に、エントリーポイントとなるクラスのパスをLambdaで指定します。
コード→ランタイム設定を編集を開き、ハンドラを修正します。
![forランタイム設定画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/250c3ca5-5c94-0344-055c-dbe7e9d6d011.png)

今回は、com.example.demo.app.StartApp.javaをLambdaがキックするように設定したいので、example.Helloをcom.example.demo.app.StartAppに変更しました。
![ランタイム設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/13bd3de4-d8ee-3dba-156d-c52fc0470515.png)

ここまでできたらLambda上でテストをします。
テスト→テストイベント→テストでテストを実行します。
![Lambdaテスト画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/4c2e968d-734b-6c09-5043-cc01a5997290.png)

下記のように問題なく実行できていればOKです！
![Lambdaテスト成功.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/3b646b7d-299b-6301-9e36-87449b860540.png)

# API GatewayとLambdaの連携
マネジメントコンソールからAPI Gatewayを開きます。
API→APIを作成→REST APIを作成を選択します。
![RESTAPI選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/7dab6d50-09e1-6c76-0513-eaa36dd0f55e.png)

今回はAPI名のみを入力し、それ以外はすべてデフォルトにしました。
![API作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/da339fc8-4bdc-8ab3-a3c9-2f4506c0079c.png)

次に作成したAPIを開き、メソッドの作成を開きます。
![forメソッド作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/7b19b70c-5cc5-ad10-c6d0-1fffb30f7102.png)

メソッドタイプはGET（APIの仕様に合ったもの）、統合タイプはLambda、そしてLambda関数には先ほど作成したものを指定し、メソッドを作成します。
![メソッド作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/d6747dad-3727-2fa5-c564-7329abcb98d8.png)

メソッドが作成されると、下記のようにリクエストとレスポンスの図が表示されます。
![メソッド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/4dc25c27-997e-6518-1270-132e04b4c5ed.png)

また、Lambda側からもAPI Gatewayがトリガーになっていることを確認することができます。
![Lambda画面にAPIgatewayがつながっている.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/13bcc8c2-2713-99bf-ad80-0214659588f0.png)

最後にAPIデプロイを行います。
APIデプロイを開き、ステージに新しいステージを選択し、任意のステージ名を入力してデプロイします。
![APIデプロイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/629f7a28-cc33-9e80-013a-de30ba833b9f.png)

デプロイ後、下記のように表示されるエンドポイントをクリックし、APIが問題なく動作していればOKです！
![エンドポイント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/f4d6f9fa-4c07-eee1-4e5d-ed7dfc4c8adc.png)

# 最後に
ここまでで、API Gatewayのデフォルトのエンドポイントを使用してSprngBootで作成したREST APIをデプロイすることができました！
次回は、Route53（とACM）を使って独自ドメインからAPIを呼び出せるようにしたいと思います！
続きはこちらから↓↓↓

https://qiita.com/hiroto_mis/items/147af96a27fb8b1e0229

# 参考にさせていただいた記事
https://qiita.com/miyuki_samitani/items/f01f1bd49334f97fe84c

https://qiita.com/syoichi_kitasaka/items/a08077fb523d33425bda