使用Spring Boot和AWS App Runner,在AWS Systems Manager Parameter Store中管理Spring Data Redis的连接信息
AWS App Runner Advent Calendar 2022 的文章!(轻声说)
本文中我计划介绍一些关于如何在App Runner上部署Spring Boot应用程序的技巧。
不知不觉中,时间已经到了2023年,App Runner 现在支持使用 SSM Parameter Store 和 AWS Secrets Manager 进行配置管理。因此,我打算利用这个功能尝试使用 Spring Data Redis 连接到 ElastiCache for Redis。
创建和设置Spring Boot应用程序
首先,创建一个使用Redis作为数据存储的Spring Boot应用程序。由于使用了App Runner,我打算使用托管的Java运行时。
目前,在 App Runner 的 Java 托管运行时中,只支持 Java 11 以前的版本。然而,Spring Boot 3.0 的运行需要 Java 17 或更高版本(当然,如果您在 App Runner 中使用容器映像,则可以使用基于 Java 17 的镜像)。
因此,这次我们将使用Spring Boot 2.7。下面是构建应用程序的Gradle文件。
plugins {
id 'org.springframework.boot' version '2.7.7'
}
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-data-redis')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
在代码中,我们仅仅将数据写入Redis。
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class ActuatorDemoApplication {
// Redis クライアントの注入
private final RedisTemplate<String, String> redisTemplate;
ActuatorDemoApplication(final RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static void main(String[] args) {
SpringApplication.run(ActuatorDemoApplication.class, args);
}
@GetMapping("/")
public String root() {
return "To healthy: /healthy";
}
// Redis にデータを書き込む
@GetMapping("/healthy")
public String setHealty() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("health", "healthy");
return "Status: " + ops.get("health");
}
// Redis にデータを書き込む
@GetMapping("/unhealthy")
public String setUnhealty() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("health", "unhealthy");
return "Status: " + ops.get("health");
}
}
以下是Spring Boot的Redis引用设置。在App Runner中,连接信息作为环境变量设置在应用程序中,因此在这里也使用环境变量。
spring:
redis:
host: ${CACHE_HOST:localhost}
port: ${CACHE_PORT:6379}
当启动这个 Spring Boot 应用程序并访问 /healthy 或 unhealthy 时,将会访问 Redis 并写入值。
./gradlew bootRun
...
curl http://localhost:8080/healthy
curl http://localhost:8080/unhealthy
管理ElastiCache环境以及连接信息。
因为我很喜欢AWS CDK,所以我决定使用CDK来构建ElastiCache for Redis,并将其连接信息保存到SSM Parameter Store中。
我已经按照以下方式创建了堆栈。
import * as cdk from 'aws-cdk-lib';
import { StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';
export class ActuatorDemoInfraStack extends cdk.Stack {
// 後で App Runner の Stack で使用する変数
readonly vpc: ec2.IVpc;
readonly cacheSecurityGroup: ec2.SecurityGroup;
readonly cacheHostParameter: ssm.StringParameter;
readonly cachePortParameter: ssm.StringParameter;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Redis を配置する、Private な VPC を作成
this.vpc = new ec2.Vpc(this, 'Vpc', {
subnetConfiguration: [
{
name: 'cache',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// ElastiCache および ElastiCache へ接続するための Security Group
const subnetGroup = new elasticache.CfnSubnetGroup(this, "CacheSubnetGroup", {
subnetIds: this.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }).subnetIds,
description: "Group of subnets to place Cache into",
});
this.cacheSecurityGroup = new ec2.SecurityGroup(this, "CacheSecurityGroup", { vpc: this.vpc });
this.cacheSecurityGroup.addIngressRule(this.cacheSecurityGroup, ec2.Port.tcp(6379), "Ingress to the cache");
// Redis の作成
const cacheCluster = new elasticache.CfnCacheCluster(this, "CacheCluster", {
engine: "redis",
cacheNodeType: "cache.t3.micro",
numCacheNodes: 1,
cacheSubnetGroupName: subnetGroup.ref,
vpcSecurityGroupIds: [this.cacheSecurityGroup.securityGroupId],
});
// Redis のホスト名とポート番号を SSM Parameter Store に保存
this.cacheHostParameter = new ssm.StringParameter(this, "CacheHostParameter", {
stringValue: cacheCluster.attrRedisEndpointAddress,
});
this.cachePortParameter = new ssm.StringParameter(this, "CachePortParameter", {
stringValue: cacheCluster.attrRedisEndpointPort,
});
}
}
当部署该堆栈时,ElastiCache 环境将被创建在私有的 VPC 中。
要连接到这个VPC内的ElastiCache,需要创建一个VPC连接器,并且确保从App Runner环境发出的出站流量通过VPC。
并且,要从SSM Parameter Store中读取参数,还需要将参数的读取权限赋予实例角色。
由于CDK可以在变量中引用环境信息,因此进行此类操作非常方便。我们将按照以下方式创建堆栈。
// 上記の ElastiCache 環境を構築したスタック
const infraStack = new ActuatorDemoInfraStack(app, 'ActuatorDemoInfraStack');
// 上記のスタックから接続情報を読み出しつつ、 VPC Connector や SSM への読み出し許可を作成するスタックを構築
new ActuatorDemoServiceStack(app, 'ActuatorDemoServiceStack', {
vpc: infraStack.vpc,
securityGroups: [infraStack.cacheSecurityGroup],
host: infraStack.cacheHostParameter,
port: infraStack.cachePortParameter,
});
以下是我们在上述中创建的 ActuatorDemoServiceStack。
// ElastiCache への接続に必要な情報をまとめる
export class CacheConnection {
readonly vpc: ec2.IVpc;
readonly securityGroups: ec2.SecurityGroup[];
readonly host: ssm.StringParameter;
readonly port: ssm.StringParameter;
constructor(vpc: ec2.IVpc, securityGroups: ec2.SecurityGroup[], host: ssm.StringParameter, port: ssm.StringParameter) {
this.securityGroups = securityGroups;
this.host = host;
this.port = port;
}
}
// App Runner サービスの構築に必要なリソースを作成
export class ActuatorDemoServiceStack extends cdk.Stack {
constructor(scope: Construct, id: string, cacheConnection: CacheConnection, props?: StackProps) {
super(scope, id, props);
// VPC Connector の作成
const connector = new apprunner.CfnVpcConnector(this, 'VpcConnector', {
subnets: cacheConnection.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }).subnetIds,
vpcConnectorName: 'CacheHostConnector',
securityGroups: cacheConnection.securityGroups.map(s => s.securityGroupId),
});
// ElastiCache 接続情報が格納された SSM Parameter Store の読み出しを許可する Role を作成
const instanceRole = new iam.Role(this, 'InstanceRole', {
assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
inlinePolicies: {
ssmParameter: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [ "ssm:GetParameters" ],
resources: [
cacheConnection.host.parameterArn,
cacheConnection.port.parameterArn,
],
}),
],
}),
},
});
}
}
实际上,如果能够通过CDK来完全创建App Runner服务,那将会更加完美。但不幸的是,由于CloudFormation尚未支持SSM和Secrets Manager的配置,因此通过CDK来直接实现可能会很困难。
new apprunner.CfnService(this, 'Service', {
sourceConfiguration: {
authenticationConfiguration: {
connectionArn: this.node.tryGetContext('connectionArn'),
},
codeRepository: {
repositoryUrl: this.node.tryGetContext('repositoryUrl'),
sourceCodeVersion: {
type: "BRANCH",
value: "main",
},
codeConfiguration: {
configurationSource: "API",
codeConfigurationValues: {
runtime: "CORRETTO_11",
runtimeEnvironmentSecrets: [ // この設定値がまだ存在しない…
{
name: "CACHE_HOST",
value: cacheConnection.host.parameterArn,
},
{
name: "CACHE_PORT",
value: cacheConnection.port.parameterArn,
},
],
buildCommand: "cd actuator-demo && ./gradlew bootJar",
startCommand: "cd actuator-demo && java -jar build/libs/actuator-demo.jar",
}
},
},
},
instanceConfiguration: {
instanceRoleArn: instanceRole.roleArn,
},
networkConfiguration: {
egressConfiguration: {
egressType: "VPC",
vpcConnectorArn: connector.attrVpcConnectorArn,
},
},
});
因此,在使用CDK创建VPC连接器和IAM角色的同时,我们将在控制台上创建App Runner服务。
在App Runner的控制台中,转到“创建服务”,然后指定GitHub仓库。


接下来,请输入 SSM Parameter Store 的参数名称,并设置上述应用程序引用的环境变量。

另外,还可以在源代码的根目录下放置apprunner.yaml文件,并在其中进行设置,以引用SSM Parameter Store和Secrets Manager的值。个人而言,我不希望在提交到GitHub的文件中写入Parameter Store或Secrets Manager的ARN和参数名称,因此我尝试通过控制台和API进行此类设置。
在安全设置中,输入要为 App Runner 实例配置的 IAM 角色。如果您已经部署了上述 CDK,则应该会在下拉菜单中看到已设置适当权限的 IAM 角色,选择该角色即可。

最后,网络设置。输入连接到ElastiCache的VPC连接器。如果上述CDK已部署,只需从下拉菜单中选择相应的VPC连接器即可。

部署使用此配置后,将分配一个应用程序的 URL,并且应用程序将启动并连接到 SSM Parameter Store 获取与 ElastiCache 相关的引用信息。
总结
在这篇文章中,我们确认了由于可以在SSM Parameter Store和Secrets Manager中设置环境变量,将 Redis 连接到 Spring Boot 应用程序中的方法通过使用 App Runner来实现。
通过连接到VPC、支持私有终端节点,并支持外部环境信息的转移,App Runner能够扩展工作负载的选择。请务必尝试一下。