みなさんこんにちは!
この記事では、Spring WebFlux、Spring Security、Spring Cloud Netflix Eureka(Service Discovery)、Hystrix(Circuit Breaker)、Ribbon(Client Side Load Balancer)、External Configuration(git repository)を使用して、Reactive RESTfulミキサーサービスを作成するための基本コンポーネントを紹介します、Spring Cloud Sleuth、Spring Cloud Gateway、Spring Boot Reactive MongoDB。 Spring Boot Adminと監視用のZipkinも同様です。
このレビューは、Spring Microservices in ActionおよびHands-On Spring 5 Security for Reactive Applicationsの書籍を研究した後に行われました。
この記事では、ゲームのリストを取得する、プレーヤーのリストを取得する、プレーヤーIDからゲームを作成する、応答を長時間待つ場合のロールバック(Hystrixフォールバック)を確認する要求の3つのクエリを使用して、基本アプリケーションを作成します。 そして、Reactive Applicationの書籍Hands-On Spring 5 Securityに基づくJWTトークンを介した認証の実装。
この記事は経験豊富なユーザーを対象としているため、IDEで各アプリケーションを作成する方法については説明しません。
プロジェクト構造
プロジェクトは2つのモジュールで構成されています。 spring-servers
モジュールは、プロジェクト間で安全にコピーできます。 コードと構成はほとんどありません。 tictactoe-services
モジュールには、アプリケーションのモジュールとマイクロtictactoe-services
が含まれています。 auth-module
とdomain-module
をサービスに追加すると、マイクロサービスの自律性に関するマイクロサービスアーキテクチャの原則の1つに違反することにすぐに気付くでしょう。 しかし、これらのモジュールの開発段階では、これが最も最適なソリューションだと思います。
Gradle設定
Gradle構成全体が1つのファイルにあるのが好きなので、1つのbuild.gradle
プロジェクト全体を構成しました。
buildscript { ext { springBootVersion = '2.1.1.RELEASE' gradleDockerVersion = '0.20.1' } repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("gradle.plugin.com.palantir.gradle.docker:gradle-docker:${gradleDockerVersion}") } } allprojects { group = 'com.tictactoe' apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'com.palantir.docker' apply plugin: 'com.palantir.docker-run' apply plugin: 'com.palantir.docker-compose' } docker.name = 'com.tictactoe' bootJar.enabled = false sourceCompatibility = 11 repositories { mavenCentral() maven { url "https://repo.spring.io/milestone" } } subprojects { ext['springCloudVersion'] = 'Greenwich.M3' sourceSets.configureEach { sourceSet -> tasks.named(sourceSet.compileJavaTaskName, { options.annotationProcessorGeneratedSourcesDirectory = file("$buildDir/generated/sources/annotationProcessor/java/${sourceSet.name}") }) } repositories { mavenCentral() maven { url "https://repo.spring.io/milestone" } } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compileOnly('org.projectlombok:lombok') annotationProcessor('org.projectlombok:lombok') } } project(':spring-servers') { bootJar.enabled = false task cleanAll { dependsOn subprojects*.tasks*.findByName('clean') } task buildAll { dependsOn subprojects*.tasks*.findByName('build') } dockerCompose { template 'docker-compose.spring-servers.template.yml' dockerComposeFile 'docker-compose.spring-servers.yml' } } project(':tictactoe-services') { bootJar.enabled = false task cleanAll { dependsOn subprojects*.tasks*.findByName('clean') } task buildAll { dependsOn subprojects*.tasks*.findByName('build') } } // Tictactoe Modules project(':tictactoe-services:domain-module') { bootJar.enabled = false jar { enabled = true group 'com.tictactoe' baseName = 'domain-module' version = '1.0' } dependencies { implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') implementation('org.springframework.boot:spring-boot-starter-validation') implementation('com.fasterxml.jackson.core:jackson-annotations:2.9.3') implementation 'com.intellij:annotations:+@jar' compileOnly('org.projectlombok:lombok') testCompile group: 'junit', name: 'junit', version: '4.12' } } project(':tictactoe-services:auth-module') { bootJar.enabled = false jar { enabled = true baseName = 'auth-module' version = '1.0' } dependencies { implementation project(':tictactoe-services:domain-module') implementation('org.springframework.boot:spring-boot-starter-webflux') implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon') implementation('org.springframework.security:spring-security-oauth2-core') implementation('org.springframework.security:spring-security-oauth2-jose') implementation 'com.intellij:annotations:+@jar' testImplementation('org.springframework.boot:spring-boot-starter-test') testImplementation('io.projectreactor:reactor-test') testImplementation('org.springframework.security:spring-security-test') } } project(':tictactoe-services:user-service') { bootJar { launchScript() baseName = 'user-service' version = '0.1.0' } dependencies { implementation project(':tictactoe-services:domain-module') implementation project(':tictactoe-services:auth-module') } } project(':tictactoe-services:game-service') { bootJar { launchScript() baseName = 'game-service' version = '0.1.0' } dependencies { implementation project(':tictactoe-services:domain-module') implementation project(':tictactoe-services:auth-module') } } project(':tictactoe-services:webapi-service') { bootJar { launchScript() baseName = 'webapi-service' version = '0.1.0' } dependencies { implementation project(':tictactoe-services:domain-module') implementation project(':tictactoe-services:auth-module') } } // Spring Servers project(':spring-servers:discovery-server') { bootJar { launchScript() baseName = 'discovery-server' version = '0.1.0' } dependencies { implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-server') implementation('org.springframework.boot:spring-boot-starter-security') compile('javax.xml.bind:jaxb-api:2.3.0') compile('javax.activation:activation:1.1') compile('org.glassfish.jaxb:jaxb-runtime:2.3.0') testImplementation('org.springframework.boot:spring-boot-starter-test') } } project(':spring-servers:config-server') { bootJar { launchScript() baseName = 'config-server' version = '0.1.0' } dependencies { implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.springframework.cloud:spring-cloud-config-server') implementation('org.springframework.cloud:spring-cloud-starter-config') implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') testImplementation('org.springframework.boot:spring-boot-starter-test') } } project(':spring-servers:gateway-server') { bootJar { launchScript() baseName = 'gateway-server' version = '0.1.0' } dependencies { implementation('org.springframework.boot:spring-boot-starter-webflux') implementation('org.springframework.boot:spring-boot-starter-actuator') implementation('org.springframework.cloud:spring-cloud-starter-gateway') implementation('org.springframework.cloud:spring-cloud-starter-config') implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon') implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') testImplementation('org.springframework.boot:spring-boot-starter-test') } } project(':spring-servers:admin-server') { ext['springBootAdminVersion'] = '2.1.1' bootJar { launchScript() baseName = 'admin-server' version = '0.1.0' } dependencies { implementation('org.springframework.boot:spring-boot-starter-web') implementation('org.springframework.boot:spring-boot-starter-security') implementation('de.codecentric:spring-boot-admin-starter-server') implementation('org.springframework.cloud:spring-cloud-starter-config') implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') testImplementation('org.springframework.boot:spring-boot-starter-test') testImplementation('org.springframework.security:spring-security-test') } dependencyManagement { imports { mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}" } } } subprojects { subproject -> if (file("${subproject.projectDir}/docker/Dockerfile").exists()) { docker { // workingbit - replace with your dockerhub's username name "workingbit/${subproject.group}.${subproject.bootJar.baseName}" tags 'latest' dockerfile file("${subproject.projectDir}/docker/Dockerfile") files tasks.bootJar.archivePath, 'docker/run.sh' buildArgs "JAR_FILE": "${subproject.bootJar.baseName}-${subproject.bootJar.version}.jar", "RUN_SH": "run.sh" } } else { docker.name = 'noop' } if (subproject.name.endsWith('service')) { dependencies { implementation('org.springframework.boot:spring-boot-starter-actuator') implementation('org.springframework.boot:spring-boot-starter-webflux') implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') implementation('org.springframework.boot:spring-boot-starter-security') implementation('org.springframework.security:spring-security-oauth2-core') implementation('org.springframework.security:spring-security-oauth2-jose') implementation('org.springframework.cloud:spring-cloud-starter-config') implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') implementation('org.springframework.cloud:spring-cloud-starter-netflix-hystrix') implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon') implementation('org.springframework.cloud:spring-cloud-starter-sleuth') implementation('org.springframework.cloud:spring-cloud-starter-zipkin') implementation('org.springframework.security:spring-security-rsa') implementation('com.intellij:annotations:+@jar') implementation('org.apache.commons:commons-lang3:3.8.1') runtimeOnly('org.springframework.boot:spring-boot-devtools') testImplementation('org.springframework.boot:spring-boot-starter-test') testImplementation('de.flapdoodle.embed:de.flapdoodle.embed.mongo') testImplementation('io.projectreactor:reactor-test') } } }
共通の構成ファイルを使用すると、1つの場所で、マイクロサービス(この場合、「service」で終わる名前のサービス)に共通の依存関係を作成できます。 しかし、これは再びマイクロサービスの自律の原則に違反しています。 一般的な依存関係に加えて、サブプロジェクトにタスクを追加できます。 gradle.plugin.com.palantir.gradle.docker:gradle-docker
プラグインタスクをDocker
動作するように追加しました。
認証モジュール
次に、JWT認証モジュールを検討します。 このモジュールのauth
パッケージの説明は、上記で示したリアクティブ認証に関する本に記載されています。
ああ、構成パッケージについて詳しく見ていきconfig
。
「複雑な」プロパティのクラスApplicationClientsProperties.java
@Data @Component @ConfigurationProperties("appclients") public class ApplicationClientsProperties { private List<ApplicationClient> clients = new ArrayList<>(); @Data public static class ApplicationClient { private String username; private String password; private String[] roles; } }
このクラスには、inMemoryデータベース構成の「複雑な」プロパティが含まれます。
AuthModuleConfig.javaモジュール構成クラス
@Data @Configuration @PropertySource("classpath:moduleConfig.yml") public class AuthModuleConfig { @Value("${tokenExpirationMinutes:60}") private Integer tokenExpirationMinutes; @Value("${tokenIssuer:workingbit-example.com}") private String tokenIssuer; @Value("${tokenSecret:secret}") // length minimum 256 bites private String tokenSecret; }
リソースファイルでは、これらの変数を指定する必要があります。 私の構成では、トークンは10時間後に無効になります。
MicroserviceServiceJwtAuthWebFilter.java Filter Matchers構成クラス
public class MicroserviceServiceJwtAuthWebFilter extends JwtAuthWebFilter { private final String[] matchersStrings; public MicroserviceServiceJwtAuthWebFilter(JwtService jwtService, String[] matchersStrings) { super(jwtService); this.matchersStrings = matchersStrings; } @Override protected ServerWebExchangeMatcher getAuthMatcher() { List<ServerWebExchangeMatcher> matchers = Arrays.stream(this.matchersStrings) .map(PathPatternParserServerWebExchangeMatcher::new) .collect(Collectors.toList()); return ServerWebExchangeMatchers.matchers(new OrServerWebExchangeMatcher(matchers)); } }
この設計中に、JWTを操作するサービスと、このフィルターが処理するパスのリストがこのフィルターに転送されます。
リアクティブスプリングブートセキュリティMicroserviceSpringSecurityWebFluxConfig.java構成クラス
@ConditionalOnProperty(value = "microservice", havingValue = "true") @EnableReactiveMethodSecurity @PropertySource(value = "classpath:/application.properties") public class MicroserviceSpringSecurityWebFluxConfig { @Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls; /** * Bean which configures whiteListed and JWT filter urls * Also it configures authentication for Actuator. Actuator takes configured AuthenticationManager automatically * which uses MapReactiveUserDetailsService to configure inMemory users */ @Bean public SecurityWebFilterChain springSecurityFilterChain( ServerHttpSecurity http, JwtService jwtService ) { MicroserviceServiceJwtAuthWebFilter userServiceJwtAuthWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange() .pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange() .pathMatchers("/actuator/**").hasRole("SYSTEM") .and() .httpBasic() .and() .addFilterAt(userServiceJwtAuthWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); } }
ここには3つの興味深い注釈があります。
@ConditionalOnProperty(value = "microservice", havingValue = "true")
構成ファイル内のmicroservice変数に応じてこのモジュールを接続する注釈。注釈に示されています。 これは、一部のモジュールで一般的なトークンチェックを無効にするために必要です。 このアプリケーションでは、これはSecurityWebFilterChain
Beanの独自の実装を持つwebapi-service
です。
@PropertySource(value = "classpath:/application.properties")
このアノテーションにより、このモジュールがインポートされるメインサービスからプロパティを取得することもできます。 つまり、変数
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
子孫のマイクロサービス構成から値を取得します。
また、 @PreAuthorize(“hasRole('MY_ROLE')”)
などのセキュリティアノテーションを添付できるアノテーション
@EnableReactiveMethodSecurity
このモジュールでは、 SecurityWebFilterChain
Beanが作成されます。これにより、アクチュエーターへのアクセス、許可されたURL、およびJWTトークンがチェックされるURLが構成されます。 JWTトークンフィルターへのアクセスは開いている必要があることに注意してください。
SpringWebFluxConfig.javaの構成
この構成では、 MapReactiveUserDetailsService
が作成されて、メモリー内のアクチュエーターおよびその他のシステムユーザーが構成されます。
@Bean @Primary public MapReactiveUserDetailsService userDetailsRepositoryInMemory() { List<UserDetails> users = applicationClients.getClients() .stream() .map(applicationClient -> User.builder() .username(applicationClient.getUsername()) .password(passwordEncoder().encode(applicationClient.getPassword())) .roles(applicationClient.getRoles()).build()) .collect(toList()); return new MapReactiveUserDetailsService(users); }
ユーザーリポジトリとSpring Security
をつなぐために必要なReactiveUserDetailsService
。
@Bean public ReactiveUserDetailsService userDetailsRepository(UserRepository users) { return (email) -> users.findByEmail(email).cast(UserDetails.class); }
リアクティブリクエストを実行するためのWebClient
クライアントを作成するためのBean。
@Bean public WebClient loadBalancedWebClientBuilder(JwtService jwtService) { return WebClient.builder() .filter(lbFunction) .filter(authorizationFilter(jwtService)) .build(); } private ExchangeFilterFunction authorizationFilter(JwtService jwtService) { return ExchangeFilterFunction .ofRequestProcessor(clientRequest -> ReactiveSecurityContextHolder.getContext() .map(securityContext -> ClientRequest.from(clientRequest) .header(HttpHeaders.AUTHORIZATION, jwtService.getHttpAuthHeaderValue(securityContext.getAuthentication())) .build())); }
作成中に、2つのフィルターが追加されます。 LoadBalancer
およびReactiveSecurityContext
コンテキストからAuthentication
インスタンスをLoadBalancer
し、そこからトークンを作成するフィルター。フィルターがターゲットサーバーによって認証され、それに応じて承認されます。
そして、MongoDB ObjectId
タイプと日付を操作するために、objectMapper作成ビンを追加しました。
@Bean @Primary ObjectMapper objectMapper() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder.serializerByType(ObjectId.class, new ToStringSerializer()); builder.deserializerByType(ObjectId.class, new JsonDeserializer() { @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Map oid = p.readValueAs(Map.class); return new ObjectId( (Integer) oid.get("timestamp"), (Integer) oid.get("machineIdentifier"), ((Integer) oid.get("processIdentifier")).shortValue(), (Integer) oid.get("counter")); } }); builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return builder.build(); }
マイクロサービスゲームサービス
マイクロサービスゲームサービスの構造は次のとおりです。
ご覧のとおり、1つのApplicationConfig構成ファイルのみ
Configurator ApplicationConfig.java
@Data @Configuration @EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository") @Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class}) public class ApplicationConfig { @Value("${userserviceUrl}") private String userServiceUrl; }
user-service
アドレスを持つ変数が含まれており、2つの興味深いアノテーションがあります。
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
このアノテーションは、コンフィギュレーターMongoDBリポジトリーを示すために必要です。
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
この注釈は、 auth-module
から構成をインポートします。
GameService.javaサービス
このサービスには、次の興味深いコードのみがあります。
@HystrixCommand public Flux<Game> getAllGames() { return gameRepository.findAll(); } @HystrixCommand(fallbackMethod = "buildFallbackAllGames", threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {@HystrixProperty(name = "coreSize", value = "30"), @HystrixProperty(name = "maxQueueSize", value = "10")}, commandProperties = { @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "75"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "7000"), @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "15000"), @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "5")} ) public Flux<Game> getAllGamesLong() { // logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return gameRepository.findAll(); }
このメソッドはランダムに例外をスローし、Hystrixは注釈に従って、次のメソッドの結果を返します。
private Flux<Game> buildFallbackAllGames() { User fakeUserBlack = new User("fakeUserBlack", "password", Collections.emptyList()); User fakeUserWhite = new User("fakeUserBlack", "password", Collections.emptyList()); Game game = new Game(fakeUserBlack, fakeUserWhite); List<Game> games = List.of(game); return Flux.fromIterable(games); }
上記の本で述べたように、何かが壊れている場合、キャッシュされたデータまたは代替データを表示するよりも良いものを表示しましょう。
マイクロサービスwebapi-service
これは、ゲートウェイと外部からは見えない内部マイクロサービスとの間の一種のミドルウェアです。 このサービスの目的は、他のサービスから選択を取得し、それに基づいてユーザーへの応答を形成することです。
設定からレビューを開始します。
SpringSecurityWebFluxConfig.java構成
@Configuration @EnableReactiveMethodSecurity public class SpringSecurityWebFluxConfig { private static final String AUTH_TOKEN_PATH = "/auth/token"; @Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls; @Bean @Primary public SecurityWebFilterChain systemSecurityFilterChain( ServerHttpSecurity http, JwtService jwtService, @Qualifier("userDetailsRepository") ReactiveUserDetailsService userDetailsService ) {
ここでは、 auth-module
前に定義したuserDetailsService
サービスから認証マネージャーを作成しauth-module
。
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
そして、このマネージャーでフィルターを作成し、 x-www-form-urlencoded
エンコードされたユーザーデータを取得するために認証インスタンスコンバーターも追加します。
AuthenticationWebFilter tokenWebFilter = new AuthenticationWebFilter(authenticationManager); tokenWebFilter.setServerAuthenticationConverter(exchange -> Mono.justOrEmpty(exchange) .filter(ex -> AUTH_TOKEN_PATH.equalsIgnoreCase(ex.getRequest().getPath().value())) .flatMap(ServerWebExchange::getFormData) .filter(formData -> !formData.isEmpty()) .map((formData) -> { String email = formData.getFirst("email"); String password = formData.getFirst("password"); return new UsernamePasswordAuthenticationToken(email, password); }) );
成功した認証ハンドラーを追加します。その本質は、 Authentication
から生成されたリクエストヘッダーにJWTトークンを入れて、有効なゲストトークンを使用してのみ認証を行えるようにすることです。
tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService)); MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange()
ホワイトリストからアドレスを解決します。 前に書いたように、JWTフィルターによって処理されるアドレスも開く必要があります
.pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange()
基本認証でアクチュエータと一部のアドレスを保護します
.pathMatchers("/actuator/**").hasRole("SYSTEM") .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST") .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER") .and() .httpBasic() .and() .authorizeExchange()
認証トークンへのアクセスを必須にする
.pathMatchers(AUTH_TOKEN_PATH).authenticated() .and()
フィルターを追加します。 JWTトークンを認証および検証するため。
.addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); }
また、上で書いたように、このサービスは、 application.properites
ファイルでmicoservice=false
変数の値を指定することにより、他のサービスに共通のJWTトークンチェックを無効にします。
トークン発行、登録、および承認コントローラーAuthController.java
このコントローラーは純粋に特定のものであるため、説明しません。
WebApiService.javaサービス
このサービスは、 WebApiMethodProtectedController.jav
コントローラーで呼び出され、興味深いアノテーションが付いています。
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
このアノテーションは、ゲストロールを持つ許可ユーザーのみにメソッドへのアクセスを許可します。
テスト方法
環境を作成します。
トークンを取得
環境内のTOKEN変数を受信したトークンで更新します。
新しいユーザーを登録する
登録後、ユーザートークンを受け取ります。 10時間で期限切れになります。 有効期限が切れたら、新しいものを取得する必要があります。 これを行うには、ゲストトークンを再度リクエストし、環境を更新してリクエストを実行します
次に、ユーザー、ゲームのリストを取得したり、新しいゲームを作成したりできます。 また、Hystrixをテストします。サービスの構成を確認し、gitリポジトリの変数を暗号化します。