AWS Secret Manager그리고 Lambda Extension
안녕하세요! 이번 포스팅에서는 AWS Secret Manger 그리고 Lambda extension 사용에 대해서 정리해보려고 합니다.
회사에서 사용 중인 Lambda 서비스에서 하드코딩으로 관리되고 있던 값들을 정리하기 위해 AWS Secret Manager를 사용하기로 논의가 되었습니다. 해당 서비스가 자주 업데이트 되는 서비스가 아니었어서 유지보수 하기 어려운 형태였는데요. 시간이 지나고 보니 생각보다 중요한 레거시(?)가 돼서 지금이라도 유지보수를 위해 정비를 하게 되었습니다.
1. 하드코딩 된 값들을 제거함으로써 dev, stg, prd 환경에서 동일한 코드로 관리
2. 이런 값들을 Secret으로 빼서 보안성 높이기
크게 위 두가지 목표를 가지고 작업을 시작했습니다.
1. AWS Secret Manager란?
AWS Secret Manager는 데이터베이스 보안 인증정보 혹은 api key와 같은 정보들을 안전하게 암호화하고 중앙집중식으로 관리할 수 있는 서비스입니다. 말 그대로 시크릿 매니저입니다ㅋㅋ
Secret Manager 사용하는 방법도 크게 어렵진 않은데요. 콘솔로 Secret Manager를 생성하고 키/값을 추가하면 끝입니다. 이후 Secret Manger 이름을 정의하고 생성하면 끝! 저는 test/lambda/jay로 이름을 지정했습니다~
Secret Manager를 생성하면 여러 언어로 샘플코드까지 줘서 테스트하는데 크게 어렵지 않습니다. 해당 코드를 복사하고 Lambda에 추가해서 배포해보겠습니다.
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
export const handler = async (event) => {
const secretName = "test/lambda/jay"; // 시크릿 매니저 이름
const client = new SecretsManagerClient({
region: "ap-northeast-2",
});
let result;
try {
result = await client.send(
new GetSecretValueCommand({
SecretId: secretName,
VersionStage: "AWSCURRENT", // VersionStage defaults to AWSCURRENT if unspecified
})
);
} catch (error) {
throw error;
}
const secret = result.SecretString;
const response = {
statusCode: 200,
body: JSON.stringify(secret),
};
return response;
};
위 코드는 샘플코드 그대로 가져왔으며 Secret Manager에 접근해서 secret들을 가져온 뒤 response로 전달하고 있습니다. 실제로 그렇게 동작할까요?
익셉션(Exception)이 발생했네요 ㅋㅋㅋ 내용을 확인해 보면...
jayLambdaTest is not authorized to perform: secretsmanager:GetSecretValue on resource: test/lambda/jay because no identity-based policy allows the secretsmanager:GetSecretValue action"
역시 엄격한 AWS! 위 내용은 Lambda에서 test/lambda/jay 라는 Secret Manager의 GetSecretValue 액션을 허용하는 정책이 없기 때문에 접근이 불가하다는 말입니다. 당연하죠~?! 저희는 서비스 별로 권한을 분리하고 특정 서비스에서는 특정 리소스만 사용하게 해야 보안도 높일 수 있고 관리도 편합니다.
현재는 기본 권한 정책만 존재하기 때문에 test/lambda/jay lambda에 접근할 수 있는 권한정책을 추가해 보겠습니다.
저희는 GetSecretValue 액션만 필요하기 때문에 해당 옵션만 선택하고 리소스를 특정합니다. 여기서 특정 리소스는 접근하고자 하는 Secret Manager의 ARN을 말합니다. 내용을 모두 입력 후 저장한 후 다시 Lambda를 호출해 보겠습니다.
호출 결과를 보시면 정상적으로 Secret Manager의 값들을 가져온 것을 확인할 수 있습니다!! 오예!!
2. SecretManager를 그대로 쓰면 안 되는 이유!
위 내용에서 끝난 줄 알았지만 사실 끝이 아닙니다! Secret Manager 호출하는 것도 모두 '돈'입니다 ㅋㅋㅋ 저희는 회사에 소속되어 있기 때문에 AWS 리소스를 최적화해서 사용할 의무가 있습니다~
엇... 이렇게 보니까 생각보다 안 비싸네... 여하튼 트래픽이 늘어나고 사용하는 보안암호(Secret)들이 많아질수록 요금은 올라가겠죠? 그리고 매번 Lambda에서 Secret Manager를 호출하는 것 자체가 응답속도에 영향을 주는 행위입니다!
그래서 왜 이렇게 말이 길었느냐?! 위 문제들을 어느 정도 해소해 줄 수 있는 방법이 있습니다! 바로 Lambda Extension 중 하나인 AWS-Parameters-and-Secrets-Lambda-Extension 입니다!!
AWS-Parameters-and-Secrets-Lambda-Extension을 사용하면 시크릿을 검색한 후 로컬 캐시에 저장합니다. 그런 다음 캐시 된 값은 만료될 때까지 이후 호출에 사용됩니다. 캐시된 값은 TTL(time-to-live)이 경과한 이후에 만료되며 SECRETS_MANAGER_TTL 환경변수로 TTL 시간을 조절할 수 있습니다. (default 300초)
익스텐션 이름 그대로 AWS Parameter Store도 동일하게 사용할 수 있습니다. 이렇게 캐시를 사용하게 되면 Secret Manager API를 계속 호출하지 않으니 비용도 감소되고 Lambda의 응답시간도 빨라질 것입니다.
자! 그럼 AWS-Parameters-and-Secrets-Lambda-Extension을 추가해 보도록 하겠습니다
이제 코드를 수정해 보겠습니다. Lambda에서는 더 이상 직접적으로 Secret Manager를 호출하는 게 아니라 Extension을 통해서 호출하게 됩니다. (캐싱된 값이 없을 때만)
export const handler = async (event) => {
const secretName = "test/lambda/jay"; // 시크릿 매니저 이름
const url = `http://localhost:${process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT}/secretsmanager/get?secretId=${secretName}`;
const options = {
headers: { 'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN },
method: 'GET',
};
let result;
try {
const response = await fetch(url, options);
if (!response.ok) {
const json = await response.json().catch((err) => err.message);
console.error('Invalid response :', json);
throw new Error(`Invalid ${response.status} response`);
}
result = await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
return {
statusCode: 200,
body: JSON.parse(result.SecretString)
};
};
Lambda Extension 호출은 localhost를 호출하게 되며 Secret Manager, Parameter Store 별 API endpoint가 존재합니다. 자세한 내용은 여기서 확인 가능합니다.
3. 참고