안녕하세요🙌! 개발자 갈레입니다!
이번 글에서는 스프링 프로젝트에서 AWS SecretsManager를 이용하여 보안 프로퍼티를 주입하게 된 과정과 결과에 대해 알아볼겁니다!
들어가며
글을 읽으면서 여러분들이 스스로의 프로젝트에 맞는 적절한 도구를 선택하고 pros and cons를 생각하며 적용까지 할 수 있는 시야 혹은 사고 과정 틀을 얻을 수 있으면 합니다. 이를 얻으면 여러분의 프로젝트가 제것과 다를지 몰라도, 논리적인 사고 과정은 큰 틀을 벗어나지 않기 때문에 분명 도움이 될거라 생각합니다. 그럼 시작해볼까요!
궁금증(호기심)을 가지면 좋은 컨텐츠
- 보안 프로퍼티를 왜 신경써야할까?
- 보안 프로퍼티 관리 도구는 어떤 특징을 가져야할까?
- 현재 프로젝트에 적용하기 좋은 방식은?
글을 읽으시면서 핵심 부분(노란)과 꼬리질문(초록) 부분에 집중하며 읽어주세요! 개인적으로 키워드에서 꼬리질문을 하는게 개념을 깊이있게 이해하는 과정이라고 생각합니다:)
목차
1. 문제 상황 분석
- EC2 내 보안 프로퍼티 저장 문제
- 보안 프로퍼티 분석
2. 선택 가능한 기술 분석
- Github Actions Secrets
- AWS SecretsManager
3. 현재 프로젝트에 적용
- 유지보수, 안정성
- SecretsManager & Spring 연동 과정
- 적용 사례
1. 문제 상황 분석
EC2 내 보안 프로퍼티 저장 문제
보안 프로퍼티를 EC2 내에 저장하고 있었습니다. 왜 EC2 내에 저장하고 있었냐구요? Github public repository에 보안 프로퍼티를 직접 저장하고 있는 것'보단' 낫다고 판단했기 때문이죠. EC2에 저장하면, EC2가 날아가거나 문제가 생길 시, 개발자가 추가적인 일을 하면 되지만(물론 product 문제도 당연히 생깁니다), Github public repository에 보안 관련 정보를 저장하면 악의적인 사용자에 의해 시스템이 공격 받을 수 있습니다. 그렇다면 EC2에 저장하면 어떤 단점이 있을까요?
EC2에 보안 프로퍼티를 저장하면 유지보수를 하기가 굉장히 어려워집니다. 동료 개발자들은 보안 프로퍼티를 수정하려면 EC2 내부에 ssh로 접속해서 manual하게 정보를 바꿔줘야해요. 또한 EC2 확장하기도 어려워집니다. EC2 scale out 시, 보안 프로퍼티를 동기화 시켜줘야하죠. 네 맞습니다 굉장히 불편하죠. 그래서 우린 보안 프로퍼티를 분리할 필요가 생기게 되는거죠. 중복되는건 함수화 하는 것 처럼요! 그렇다면 어떻게 보안 프로퍼티를 분리할까요? 보안 프로퍼티 특성을 분석해봅시다!
보안 프로퍼티 특성 분석
보안 프로퍼티를 프로젝트로 분리하기 위해 프로젝트 내 보안 프로퍼티 저장 및 적용 프로세스를 살펴봅시다! 아래 <그림2>은 application-jwt.properties 파일입니다. 파일 내용에는 jwt.secret.key, jwt.access.expiration.time 등 보안과 직결되는 데이터가 다수 있습니다. 스프링에서는 프로퍼티를 애플리케이션과 분리시키기 위해 Externalized Configuration을 제공합니다. Java System properties, OS 환경 변수, Application properties outside of your packaged jar 등을 이용하면, 쉽게 보안 프로퍼티를 주입할 수 있습니다. 예를 들어, jwt.secret.key={JWT_SECRET_KEY:key}에서 JWT_SECRET_KEY가 OS 환경 변수로 설정돼있으면, jwt.secret.key는 해당 변수로 설정되는거죠. 물론 프로퍼티 주입 우선 순위를 고려해야겠지만요! '이러한 여러 방법으로 프로퍼티를 외부에서 주입할 수 있구나!' 정도만 알면 될듯 합니다. 이를 직접 해야할까요? 아마 지원하는 도구가 있을겁니다. 도구가 없다면 그때 직접하면 되겠죠! 그렇다면 이를 지원하는 도구를 찾아보죠!
2. 선택 가능한 기술 분석
- Github Actions Secrets
- AWS SecretsManager
선택 가능한 기술 분석은 아래 링크를 참조해주세요.
https://www.notion.so/Github-action-secrets-AWS-SecretsManager-b0b1cd69e6ed463c91fbaef020154d71
바쁘신 분들을 위해 세문단 요약을 하겠습니다. 더 자세히 알고 싶다면 위 링크를 참조해주세요:)
Github Actions Secrets
Github actions secret은 기존 Github actions의 CI workflow를 사용하기 때문에 새로운 툴 도입이 필요가 없습니다. 빠르게 구현할 수 있다는 장점이 있죠. 또한 무료입니다. 구현하기도 간편한데 무료면 더할 나위 없는 이점일겁니다. 하지만 Github actions secret는 단일 디렉토리라는 단점이 있습니다. 아래 <그림3>에서 보시다 싶이 책임을 분리하기가 어렵죠. AWS_ACCESS_KEY는 인증 & 인가 정보이지만 만약에 JWT_TOKEN_SECRET과 같은 보안 정보가 같은 Repository secrets에 있으면 유지보수하기 점점 어려워질겁니다. 현재는 보안 정보가 4-5개 밖에 없어서 괜찮을 수 있지만 프로젝트가 점점 커져서 200-300개가 된다면 Github actions secret는 유지보수하기에 정말 안좋은 툴이될겁니다.
Github Actions의 CI/CD 내 역할을 고려해봐야합니다. Github Actions의 CI/CD 내 역할이 무엇일까요? Github Actions가 스프링 애플리케이션의 보안 프로퍼티를 모두 책임 질 위치인가요? <그림4>에서 볼 수 있듯이 Github Actions는 CI/CD 흐름 제어 역할을 합니다. Github Repo와 소통하여 데이터를 가져온 후, S3에 결과물을 전달하고 CodeDeploy에 배포 요청을 하죠. Github Actions의 역할은 다른 서버와 인터페이스로 소통하는 겁니다. 스프링 애플리케이션의 보안 프로퍼티 저장과는 결이 맞지 않죠.그렇다면 Github Actions에는 어떤 정보를 저장해야할까요? 바로 다른 서버와 소통할 때 사용되는 인증 & 인가 정보입니다. <그림5>를 보면 Github Actions Secrets는 다른 서버의 인증 및 인가 정보만 저장하고 있습니다. 여기서 누군가 궁금해할 수 있습니다!
Q. <그림4>를 보면 Github Actions는 AWS SecretsManager, AWS S3와 AWS CodeDeploy에게 요청을 보내는데 왜 <그림5>의 인증 및 인가 정보는 하나 밖에 없나요?
A. 간단한 구현을 위해 AWS에서 IAM 사용자를 하나만 만들었기 때문입니다. 만약에 필요에 따라 권한을 여러개로 분리해야한다면 2개가 될 수도 있고 3개가 될 수 있습니다. 독자 분께서 생각하시는 적절한 방향으로 IAM 사용자를 만드시면 됩니다:) 중요한건 Github Actions Secrets엔 다른 서버와 소통할 때 사용되는 인증 & 인가 정보가 저장되야하는겁니다.
AWS SecretsManager
SecretsManager는 cloud.spring에서 공식 지원하고 있기 때문에 호환성에 장점이 있습니다. profile을 나눠 관리할 수 있어서 유지 보수성에 좋다고 판단했습니다. github action secrets의 인증&인가 정보와 책임을 분리 할 수 있다는 장점 또한 존재했습니다. 물론 SecretsManager는 API 호출로 보안 정보를 가져오는 방식이라 네트워크 비용 소모로 인한 추가 비용 발생의 단점이 존재합니다. 하지만 유지보수와 호환성의 장점을 생각한다면 이는 감수할 만 하다고 판단했습니다. 결국 유지보수와 안정성을 고려하여 AWS의 SecretsManager를 이용하여 보안 프로퍼티를 관리하기로 결정했습니다.
3. 현재 프로젝트에 적용
유지보수, 안정성
현재 프로젝트에 적용하기 전에 우리가 유의해야할 점은 무엇일까요? 전 유지보수라고 생각합니다. 앞으로 동료 개발자가 해당 기능을 편하게 사용하고 확장할 수 있어야합니다. 유지 보수하기 가장 좋은 방법은 어떤걸까요? Spring에서는 Active profile로 특정 프로파일을 키고 끌 수 있는 기능을 지원합니다. 즉 SecretsManager를 사용하여 Active profile과 연동할 수 있으면 정말 좋겠군요! 해당 방법을 찾아본 결과 선택 가능한 기술 분석에서 설명 했던 AWS SecretsManager와 cloud.spring 호환 페이지에서 이를 설명하고 있었습니다.
SecretsManager & Spring 연동 과정
cloud.spring에 따르면 특정 조건을 만족하면 AWS SecretsManager와 spring이 연동됩니다. 그 과정을 한번 이해하고 적용시켜보도록 하죠! cloud.spring에 따르면 Secrets Manager Configuration를 지키면, 애플리케이션이 시작할 때 스프링 PropertySource를 사용할 수 있다고 합니다. 자세한 작동 원리는 해당 페이지에 따로 없었습니다. 다만, 아래 Github Actions workflow에서 작동 과정을 유추할 수 있었습니다. name: Configure AWS Credentials가 build 보다 뒤에 있어야 배포가 정상적으로 됐습니다. 즉 Github Actions의 Runner가 AWS IAM 사용자 권한을 획득한 후 스프링 프로젝트를 빌드 할 때 프로젝트에 작성된 SecretsManager 메타 데이터를 활용하여 jar 파일 내부에 환경 변수 형태로 주입하는게 아닐까 생각했습니다. 만약 사실과 다른게 있다면 언제든 피드백 바랍니다:)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
적용 사례
자 이제 드디어 실제로 적용해보죠! 현재 제 프로젝트에는 세가지 종류(default, jwt, real-db)의 profile이 존재합니다. 각 프로파일이 대응되는 보안 암호 저장소를 만들어보죠! <그림3>을 복사해서 <그림4>로 가져왔습니다! <그림4>를 보면 우린 세개의 다른 보안 암호를 만들었어요. 근데 보안 암호 이름이 독특하죠? 왜 저런 규칙으로 만들었을까요? 혹은 어떤 보안 암호 네이밍을 해야 cloud.spring과 소통할 수 있을까요?
여러분 당연히 의존성도 추가해야합니다. 제 블로그 보고 모든걸 clone한다는 생각 보단 제가 어떤 원인에서 어떤 결과로 이어지는 사고 과정에 집중해주시면 좋을듯합니다:) 이를 알아야 결국 여러분들도 자기 주도적으로 문제 해결을 할 수 있으니까요! 하하 다시 적용 사례로 돌아가보죠:)
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap:3.1.3'
implementation 'org.springframework.cloud:spring-cloud-starter-aws-secrets-manager-config:2.2.6.RELEASE'
cloud.spring에서는 보안 암호 네이밍 규칙을 정해줍니다. /secret/으로 시작해야 cloud.spring이 '내가 소통해야할 보안 암호구나~'라고 알기 때문이죠. 그 후에 application이라고 작성하면 모든 앱이 해당 정보를 가져가게 되고 특정 서비스만 해당 정보를 가져가게 하고 싶으면 my-service(제 경우엔 resell-platform)를 붙여 적으면됩니다. 거기에다가 해당 서비스의 특정 프로파일이 활성화됐을 때 Active하게 만들고 싶은 보안 암호가 있다면 '_'를 붙이고 profile 환경 이름을 뒤에 붙여주죠. 제 경우엔 resell-platform 서비스가 jwt 프로파일 환경이 active됐을 때 활성화 시켜 주고 싶은 보안 암호 이름을 '/secret/resell-platform_jwt'라고 지었습니다.
<그림6>에 따르면 bootstrap.properties 혹은 bootstrap.yml에서 aws.secretsManager.name을 설정해줘야합니다. 왜냐면 저희는 resell-platform이라는 특정 서비스만이 가질 보안 암호를 설정하기 때문이죠! 제 추측으론 AWS Credential을 얻은 Github Actions의 Runner가 bootstrap.yml의 정보를 보고 AWS SecretsManager에 접근하는 것 같습니다. bootstrap.yml을 resouce 폴터에 생성 후 데이터르 적어줍시다.
# bootstrap.yml
aws:
secretsmanager:
name: resell-platform
자 마지막으로 /secret/resell-platform_jwt에 관련 보안 암호키를 넣어줍시다! 암호 키는 아무렇게나 작성해도 될까요? 규칙이 필요할까요? 보안 암호 키 또한 특정 규칙을 가져야합니다. 숫자로 시작하면 안되고 spring.url 처럼 '점'을 사용하면 이후에 Underscore로 바뀌는 규칙이 존재하기 때문이죠. 뭘 사용하든 결국 대문자 Snake case가 됩니다. 따라서 전 해당 방식으로 처음부터 설정해줬습니다:)
자 이렇게 설정하고 제가 Active하게 만들 프로파일을 지정해주면 끝이납니다!
드디어 유지 보수 및 안정성을 고려한 보안 프로퍼티 파일 주입 with AWS SecretsManager, Spring의 모든 사고 과정과 구현 결과까지 끝냈습니다😃!
긴 여정을 따라 와주셔서 감사합니다🙇♂️.
부디 여러분들이 해당 글을 읽으셨을 때 초기에 제가 말씀 드렸던, 왜? Bullet point는? 색깔의 의미를 잘 이해하며 읽으셨으면 합니다. 사실상 두개를 하는 능력이 전 정말 중요하다고 생각합니다. 개념을 제대로 이해하여 문제를 해결할 수 있는 실마리
이죠.
그럼 다음 번에 또 봅시다👋👋!
Thanks, im 김민석(갈레)!
4. 래퍼런스
- https://www.notion.so/Github-action-secrets-AWS-SecretsManager-b0b1cd69e6ed463c91fbaef020154d71