Spring Cloud Consulさわる。Service DiscoveryとLoadBalancerのチュートリアルレベルのことをやる。ただし、LoadBalancerの挙動(ex. 2つ以上のserivce切り替えなど)まではやらない。
Consulとは、一言で表現するのが難しい(俺自身が良く理解してない)が、マイクロサービスの各要素をserviceとして集中管理するプロダクト、と思う(たぶん)。なおspringとは無関係のプロダクト。
それで、ConsulとSpring Cloud Consulの関係だが、Consulの面倒なオペレーションを簡易化したのがSpring Cloud Consul、といった位置づけらしい。俺自身、実際やったわけでは無いが、マニュアル読む限りConsulに対して手作業であれこれやるのは面倒なので、Spring Cloud Consulがそこらへんいい感じにやってくれる、という理解で最初は良いと思われる。
なお、このエントリは「やったらとりあえず動いた」レベルなので、間違ったこと書いてる可能性があります。その辺は注意してください。
consulの起動
consulのチュートリアル https://learn.hashicorp.com/consul/getting-started/install を見てconsulを起動しておく。
$ consul agent -dev
consulにservice登録
consulにserviceとしてweb-apiを登録する。あとあと、consulのclientからこのweb-apiにアクセスする。
Spring Initializrでweb, consul discoveryを追加する。以下は生成されたものをコピペしたもの。
plugins {
id 'org.springframework.boot' version '2.2.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
sourceCompatibility = '11'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "Hoxton.SR4")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
web-apiとして適当なエンドポイント/
を作っておく。
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class Application {
@RequestMapping("/")
public String home() {
return "Hello world";
}
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
/src/main/resources/application.properties
に各種設定をする。
spring.application.name=myapp
spring.cloud.consul.discovery.instance-id=hoge
spring.cloud.consul.discovery.hostname=localhost
spring.cloud.consul.discovery.port=8080
spring.application.name
- この値がcosulのserviceとして登録される。
spring.cloud.consul.discovery.instance-id
- 良くわかってないがserviceのinstance idらしい。
spring.cloud.consul.discovery.hostname
とspring.cloud.consul.discovery.port
- clientのところで再度ふれる。
起動する。http://localhost:8500/v1/catalog/services
にアクセスするとmyapp
serviceが登録されたのがわかる。
{
"consul": [],
"myapp": [
"secure=false"
]
}
clientからアクセスする
clientからconsulのservice id経由でweb-apiにアクセスする。
ただ、方法が幾つかあり、おそらく用途とか趣味とかで切り替えるのだと思う。
DiscoveryClient
こちらにもconsul discoveryの依存性を追加する。
plugins {
id 'org.springframework.boot' version '2.2.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
sourceCompatibility = '11'
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "Hoxton.SR4")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
clientのソースはこんな感じ。@EnableDiscoveryClient
を付与するとよしなにDiscoveryClient
の準備を整えてくれるらしい。
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class Application {
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/")
public String home() {
List<ServiceInstance> instances = discoveryClient.getInstances("myapp");
System.out.println(instances);
System.out.println(instances.get(0).getUri());
return "hoge";
}
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
/src/main/resources/application.properties
に各種設定をする。
spring.application.name=myclient
server.port=8081
spring.cloud.consul.discovery.instance-id=myclient-id
spring.cloud.loadbalancer.ribbon.enabled=false
spring.application.name
とspring.cloud.consul.discovery.instance-id
- clientもconsulのserviceとして登録の必要がある(たぶん。こうしないとdiscoveryClient.getInstances
が動かなかった)
spring.cloud.loadbalancer.ribbon.enabled
- 後述。
これを実行すると以下がコンソールに出力される。
[DefaultServiceInstance{instanceId='hoge', serviceId='myapp', host='localhost', port=8080, secure=false, metadata={secure=false}}]
http://localhost:8080
consul経由でservice myapp
のURLであるhttp://localhost:8080
が取得できた。あとはこれを基にRestTemplateとかでアクセスすればよい。なお、ここのhostとportはservice登録側のspring.cloud.consul.discovery.hostname
とspring.cloud.consul.discovery.port
になっている。
@LoadBalanced + RestTemplate
DiscoveryClient
のコードを見ると分かるとおり、アプリケーション的な使い勝手は良くない。ので、RestTemplate
にconsulのservice idを直接書ける方法が用意されている。このエントリでいうと、http://myapp/
と書くとhttp://localhost:8080
にアクセスが行くイメージ。
上のjavaコードに追記する。
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class Application {
@Autowired
RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/")
public String home() {
List<ServiceInstance> instances = discoveryClient.getInstances("myapp");
System.out.println(instances);
System.out.println(instances.get(0).getUri());
System.out.println(this.restTemplate.getForObject("http://myapp/", String.class));;
return "hoge";
}
@LoadBalanced
@Bean
public RestTemplate loadbalancedRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
これを実行するとHello world
と返ってくる。
load balanceの実際の動きまでは確認してない。ただ、consulのservice idを経由してweb-apiにアクセスするので、そこでload balanceが動くんだろうな、てことはなんとなく予想できる。
はまりどころ
java.lang.IllegalStateException: No instances available for myapp2
DiscoveryClient
は動くのにRestTemplate
は以下のような例外が出て動かなかった。
java.lang.IllegalStateException: No instances available for myapp2
at org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient.execute(RibbonLoadBalancerClient.java:119) ~[spring-cloud-netflix-ribbon-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient.execute(RibbonLoadBalancerClient.java:99) ~[spring-cloud-netflix-ribbon-2.2.2.RELEASE.jar:2.2.2.RELEASE]
これはspring.cloud.loadbalancer.ribbon.enabled=false
したら直った。たぶんだけど、Ribbonがmaintenance入りしてるので何かしら悪さしてるのだと思われる。
なんかhostがhost.docker.internalになる
spring.cloud.consul.discovery.hostname
を設定する。これがデフォルト値で、どうもdockerを前提にしているのだと思う。
discoveryClient.getInstancesがなんも返ってこない
エントリ中にあるとおり、clientもconsulに登録してあげないとダメだった。