使用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仓库。

create-service.png
build-config.png

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

ssm-parameter.png

另外,还可以在源代码的根目录下放置apprunner.yaml文件,并在其中进行设置,以引用SSM Parameter Store和Secrets Manager的值。个人而言,我不希望在提交到GitHub的文件中写入Parameter Store或Secrets Manager的ARN和参数名称,因此我尝试通过控制台和API进行此类设置。

在安全设置中,输入要为 App Runner 实例配置的 IAM 角色。如果您已经部署了上述 CDK,则应该会在下拉菜单中看到已设置适当权限的 IAM 角色,选择该角色即可。

security.png

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

network.png

部署使用此配置后,将分配一个应用程序的 URL,并且应用程序将启动并连接到 SSM Parameter Store 获取与 ElastiCache 相关的引用信息。

总结

在这篇文章中,我们确认了由于可以在SSM Parameter Store和Secrets Manager中设置环境变量,将 Redis 连接到 Spring Boot 应用程序中的方法通过使用 App Runner来实现。

通过连接到VPC、支持私有终端节点,并支持外部环境信息的转移,App Runner能够扩展工作负载的选择。请务必尝试一下。

广告
将在 10 秒后关闭
bannerAds