本連載では、以下のイメージ構成に沿ってアプリケーション開発を進めています。

開発するアプリケーションの構成イメージ

前回までに、VPC環境/アプリケーションロードバランサ(ALB)を構築しました。今回はECSコンテナで実行するSpringアプリケーションの実装方法について解説します。

Springを使用したコンテナアプリケーションの実装方法

ECSの実態はDockerコンテナなので、ECSで実行するSpringアプリケーションは、単純にLinuxベースのOSで実行するアプリケーションを「Spring Boot」を使って実装し、ExcutableJar形式で実行するコンテナイメージを作成する方法が簡易です。

ただ、ECSを用いると、単にDockerをEC2上で実行する場合に比べて、クラスタのポートの管理やコンテナ実行がAWSのマネージドサービスになり、コンテナアプリケーション間のサービス連携はALBを介して行うほうが良いという特徴があります。アプリケーションを実装する際は、こうした点を理解しておくことが必要です。

なお、実際に作成したアプリケーションはGitHub上にコミットしています。以降、解説で使用しているソースコードでは、import文などの記述を一部省略しているので、実行コードを作成する場合は、必要に応じて適宜GitHub上のソースコードも参照してください。

pom.xmlの記述

Spring Bootを使用してアプリケーションを構築するには、まず、Mavenプロジェクトのpom.xmlで必要なライブラリの依存性を定義します。今回は、プライベートサブネットに配置する「単純なAPIを持つアプリケーション(Backend)」と、パブリックサブネットに配置する「簡単なHTMLを返すWebアプリケーション(BFF:Backend For Frontend)」を作成するので、各アプリケーションプロジェクトには、以下のライブラリを定義します。

【プライベートサブネット(Backend)アプリケーションのpom.xml】

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

【パブリックサブネット(BFF)アプリケーションのpom.xml】

<dependencies>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

どちらも「spring-boot-starter-web」が必須ですが、BFFアプリケーションはHTMLページを生成するので、テンプレートエンジンである「Thymeleaf」も含めておきましょう。また、BFFでは、ALBのDNS名をプロパティファイルに記述するので、「spring-boot-configuration-processor」も依存関係に含めておいてください。

さらに、アプリケーションをExcutableJar実行形式とするために、「spring-boot-maven-plugin」もpom.xmlのビルドオプションに追加しておく必要があります。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

アプリケーションの実装

では、アプリケーションを実装していきます。まず、プライベートサブネットでバックエンドAPIサーバとして、「/api/v1/users」というURLのパスでリクエストを受け取り、ユーザのリストを返却する簡単なアプリケーションを作成しておきます。

SpringBootでこれを実装するには、Controllerクラスと起動/設定ファイルクラスが最低限必要です。

【Backendアプリケーションのリクエストを受け付けるControllerクラス】

package org.debugroom.mynavi.sample.ecs.backend.app.web;

import org.debugroom.mynavi.sample.ecs.backend.app.model.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class BackendRestController {

    @GetMapping("/users")
    public List<User> getUsers(){
        List<User> users = new ArrayList<>();
        users.add(User.builder().userId("1").userName("Taro").build());
        users.add(User.builder().userId("2").userName("Jiro").build());
        return users;
    }
}

【BackendアプリケーションのSpirngBoot起動クラス】

package org.debugroom.mynavi.sample.ecs.backend.config;

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

@SpringBootApplication
public class App {

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

}

また、Controllerクラスを読み取ってREST APIとして動作させるために、以下のように「WebMvcConfigurer」を実装し、「@ComponentScan」でコントローラのパッケージを指定したクラスを上述のSpirngBoot起動クラスと同じパッケージに配置しておきましょう。

【BackendアプリケーションのWebMVC設定クラス】

package org.debugroom.mynavi.sample.ecs.backend.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ComponentScan("org.debugroom.mynavi.sample.ecs.backend.app.web")
public class MvcConfig implements WebMvcConfigurer {
}

ここまではSpringBootで作るシンプルなREST APIアプリケーションですが、Backendアプリケーションの「application.yml」にserver.servlet.context-pathプロパティを設定しておいてください。この設定を追加することで、URLが「http://localhost:8080/backend/api/v1/users」になり、前回設定したALBのパスベースのルーティングを考慮したかたちになります(以下参照)。

applicaiton.yml
server:
  servlet:
   context-path: /backend

続いて、パブリックサブネットに配置するBFFアプリケーションを実装します。アプリケーションの完成イメージは下記の通り、index.htmlページにある、ボタンを押下すると、BackendアプリケーションのGET API(/backend/api/v1/users)を呼び出し、結果をHTMLページに埋め込んで返す処理を行います。

アプリケーションの完成イメージ

index.htmlページから送信したリクエストを受けてBackendのAPIを呼び出し、戻った結果をusers.htmlに出力して、クライアントにHTMLページを返却するBFFアプリケーションのControllerは、次の通りです。

package org.debugroom.mynavi.sample.ecs.backendforfrontend.app.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RestOperations;

@Controller
public class BackendForFrontendController {

    @Autowired
    RestOperations restOperations;

    @RequestMapping(method = RequestMethod.GET, value = "users")
    public String getUsers(Model model){
        String service = "/backend/api/v1/users";
        model.addAttribute("users",
            restOperations.getForObject(service, User[].class));
        return "users";
    }
}

SpringBoot起動クラスの構成はBackendとほぼ同じです。ただし、Controllerでは、Backendアプリケーションを呼び出し、結果をThymeleafのテンプレートに渡してHTMLページを返却する点が異なります。

このControllerでは、RestTemplateのインタフェースである「org.springframework.web.client.RestOperations」を使用してBackendサービスを呼び出しています。ここで実装を工夫して、ALBのDNSドメインをプロパティファイルから取得して設定するかたちにしておけば、Javaソースコード上でAWS環境に依存しないアプリケーション実装にすることが可能です。具体的には、BFFアプリケーションのapplicaiton.ymlに以下のように記述しておきます。

service:
  dns: http://internal-mynavi-sample-private-alb-XXXXXXX.ap-northeast-1.elb.amazonaws.com

プロパティの取得は「org.springframework.boot.context.properties.ConfigurationProperties」などを使って、ymlに定義したプロパティ定義を自動でBeanにインジェクションするようにしておくと良いでしょう。

 package org.debugroom.mynavi.sample.ecs.backendforfrontend.app.web;

 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;

 @Component
 @ConfigurationProperties(prefix="service")
 public class ServiceProperties {

   private String dns;

}

また、「RestOpearations」の生成時に設定クラス上で「org.springframework.boot.web.client.RestTemplateBuilder」のrootUri()メソッドで、下記のように、ALBのルートとなるURLをプロパティから取得して設定しておけば、URL設定の記述も1カ所で済みます。

package org.debugroom.mynavi.sample.ecs.backendforfrontend.config;

import org.debugroom.mynavi.sample.ecs.backendforfrontend.app.web.ServiceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestOperations;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@ComponentScan("org.debugroom.mynavi.sample.ecs.backendforfrontend.app.web")
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    ServiceProperties properties;

    @Bean
    public RestOperations restOperations(RestTemplateBuilder restTemplateBuilder){
       return restTemplateBuilder.rootUri(properties.getDns()).build();
    }
 }

上記のポイントを踏まえ、アプリケーションを実装しておけば、パブリックサブネットのBFFアプリケーションからプライベートサブネット内のBackendのAPIの呼び出しをごく少量のコーディングで実装できます。また、AWS環境に依存した内容は設定ファイルのALBのURL1カ所に限定されるので、ポータリビリティの高いアプリケーションにすることが可能です。

これで、アプリケーションを実装できました。次回は、実装したSpringアプリケーションのDockerコンテナイメージを作り、レジストリへプッシュします。

著者紹介


川畑 光平(KAWABATA Kohei)

某システムインテグレータにて、金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professionalなどの資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。