Spring Boot中LocalStack Testcontainer测试报错:Secrets Manager找不到密钥
问题描述
在Spring Boot项目中使用LocalStack进行AWS相关测试时,执行HomeController的任意测试用例均出现以下报错:
com.amazonaws.services.secretsmanager.model.ResourceNotFoundException: Secrets Manager can't find the specified secret. (Service: AWSSecretsManager; Status Code: 404; Error Code: ResourceNotFoundException; Request ID: null; Proxy: null)
怀疑是LocalStackContainer在执行相关操作前未完成启动,请问该如何解决?
相关代码
BaseIntegrationTest类
@ContextConfiguration(initializers = BaseIntegrationTest.Initializer.class) public class BaseIntegrationTest { public static final String LOCALSTACK_HOSTNAME = "localhost"; public static final String LOCALSTACK_REGION = "eu-west-3"; public static final String LOCALSTACK_ACCESS_KEY = "test"; public static final String LOCALSTACK_SECRET_KEY = "test"; private static LocalStackContainer localStackContainer = new LocalStackContainer() .withServices(LocalStackContainer.Service.S3) .withServices(LocalStackContainer.Service.S3) .withServices(LocalStackContainer.Service.SECRETSMANAGER) .withExposedPorts(4566, 4566) .withEnv("HOSTNAME_EXTERNAL", LOCALSTACK_HOSTNAME) .withEnv("DEFAULT_REGION", LOCALSTACK_REGION) .withEnv("AWS_ACCESS_KEY_ID", LOCALSTACK_ACCESS_KEY) .withEnv("AWS_SECRET_ACCESS_KEY", LOCALSTACK_SECRET_KEY);; static { localStackContainer.start(); } static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { String localStackHost; try { localStackHost = initializeLocalStack(); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,localStackHost); } } private static String initializeLocalStack() throws IOException, InterruptedException { localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 secretsmanager create-secret --name aws/secret --secret-string '{\"my_uname\":\"username\",\"my_pwd\":\"password\"}'"); localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket bucketname --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-3"); final Integer mappedPort = localStackContainer.getMappedPort(4566); return "cloud.aws.secrets-manager.end-point.uri=http://localhost:" + mappedPort; } }
BaseRestControllerTest类
@SpringBootTest @AutoConfigureMockMvc public abstract class BaseRestControllerTest extends BaseIntegrationTest { }
HomeControllerTest类
class HomeControllerTest extends BaseRestControllerTest { }
解决方案
你的怀疑是对的,LocalStack调用start()后不会立即完成所有服务初始化,此时执行创建secret的命令会因服务未就绪失败,导致测试时找不到目标secret。可以通过以下步骤修复:
等待服务就绪后再执行初始化命令
LocalStack提供waitUntilServiceIsReady方法,可指定等待特定服务完全启动。修改initializeLocalStack方法,在执行创建命令前先等待SecretsManager和S3就绪:private static String initializeLocalStack() throws IOException, InterruptedException { // 等待SecretsManager服务就绪 localStackContainer.waitUntilServiceIsReady(LocalStackContainer.Service.SECRETSMANAGER); // 等待S3服务就绪 localStackContainer.waitUntilServiceIsReady(LocalStackContainer.Service.S3); localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 secretsmanager create-secret --name aws/secret --secret-string '{\"my_uname\":\"username\",\"my_pwd\":\"password\"}'"); localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket bucketname --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-3"); final Integer mappedPort = localStackContainer.getMappedPort(4566); return "cloud.aws.secrets-manager.end-point.uri=http://localhost:" + mappedPort; }修正容器启动时机与配置冗余
不要在静态代码块中直接启动容器,而是在初始化方法中判断容器状态后再启动;同时移除重复的withServices(S3)配置:private static LocalStackContainer localStackContainer = new LocalStackContainer() .withServices(LocalStackContainer.Service.S3, LocalStackContainer.Service.SECRETSMANAGER) .withEnv("HOSTNAME_EXTERNAL", LOCALSTACK_HOSTNAME) .withEnv("DEFAULT_REGION", LOCALSTACK_REGION) .withEnv("AWS_ACCESS_KEY_ID", LOCALSTACK_ACCESS_KEY) .withEnv("AWS_SECRET_ACCESS_KEY", LOCALSTACK_SECRET_KEY);调整初始化方法中的启动逻辑:
private static String initializeLocalStack() throws IOException, InterruptedException { if (!localStackContainer.isRunning()) { localStackContainer.start(); } localStackContainer.waitUntilServiceIsReady(LocalStackContainer.Service.SECRETSMANAGER); localStackContainer.waitUntilServiceIsReady(LocalStackContainer.Service.S3); // 后续创建命令不变 }校验初始化命令执行结果
执行execInContainer后,添加退出码校验,确保secret和bucket创建成功,避免静默失败:ExecResult secretResult = localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 secretsmanager create-secret --name aws/secret --secret-string '{\"my_uname\":\"username\",\"my_pwd\":\"password\"}'"); if (secretResult.getExitCode() != 0) { throw new RuntimeException("创建Secret失败: " + secretResult.getStderr()); } ExecResult bucketResult = localStackContainer.execInContainer("aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket bucketname --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-3"); if (bucketResult.getExitCode() != 0) { throw new RuntimeException("创建Bucket失败: " + bucketResult.getStderr()); }改用AWS SDK替代Shell命令
避免直接执行shell命令,使用AWS SDK客户端与LocalStack交互,能更可靠地处理服务状态:// 创建SecretsManager客户端 AWSSecretsManager secretsManager = AWSSecretsManagerClientBuilder.standard() .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration( localStackContainer.getEndpointOverride(LocalStackContainer.Service.SECRETSMANAGER).toString(), LOCALSTACK_REGION )) .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(LOCALSTACK_ACCESS_KEY, LOCALSTACK_SECRET_KEY))) .build(); // 创建secret secretsManager.createSecret(new CreateSecretRequest() .withName("aws/secret") .withSecretString("{\"my_uname\":\"username\",\"my_pwd\":\"password\"}"));
内容的提问来源于stack exchange,提问作者Sercan Noyan Germiyanoğlu
相关产品推荐
相关产品推荐

