EC2 + PostgreSQL + Spring Boot: SSM Parameter Store로 DB 접속정보 관리하기
DB 비밀번호를 application.yml에 직접 적으면 Git에 실수로 올라가기 쉽고, 서버마다 파일을 따로 관리해야 합니다.
이 글에서는 AWS Systems Manager(SSM) Parameter Store에 DB 접속정보를 저장하고, Spring Boot 앱이 시작할 때 이 값을 가져오도록 구성합니다.
완성되면 이렇게 동작합니다.
브라우저 ──▶ Spring Boot (EC2:8080) ──▶ PostgreSQL (같은 EC2:5432)
│
└── 시작 시 SSM Parameter Store에서 DB URL / 계정 / 비밀번호 조회
(EC2에 연결된 IAM Role로 인증, Access Key 불필요)
화면에는 PostgreSQL에서 조회한 현재 시간이 표시됩니다.
목차
- 준비 사항과 사용 버전
- EC2 인스턴스 생성
- PostgreSQL 설치 및 설정
- Parameter Store에 접속정보 저장
- IAM Role 생성 및 EC2에 연결
- Spring Boot 앱 작성
- 빌드, 배포, 실행
- 동작 확인
- 트러블슈팅
- 정리 및 다음 단계
1. 준비 사항과 사용 버전
| 항목 | 버전 / 값 |
|---|---|
| OS | Amazon Linux 2023 |
| Java | Amazon Corretto 21 |
| PostgreSQL | 16 |
| Spring Boot | 4.0.x |
| Spring Cloud AWS | 4.0.x |
| 리전 | ap-northeast-2 (서울) |
버전 호환성 주의
Spring Cloud AWS는 Spring Boot 버전에 맞춰 사용해야 합니다.
- Spring Boot 4.0.x → Spring Cloud AWS 4.0.x
- Spring Boot 3.5.x → Spring Cloud AWS 3.4.x
최신 호환표는 Spring Cloud AWS GitHub에서 확인하세요.
이 글에서 사용하는 이름은 다음과 같습니다. 원하는 이름으로 바꿔도 됩니다.
| 항목 | 값 |
|---|---|
| DB 이름 | timedb |
| DB 사용자 | timeapp |
| Parameter Store 경로 | /timeapp/prod/ |
| IAM Role 이름 | timeapp-ec2-role |
2. EC2 인스턴스 생성
AWS 콘솔 → EC2 → 인스턴스 시작에서 다음과 같이 설정합니다.
- AMI: Amazon Linux 2023
- 인스턴스 유형:
t3.small이상 권장 (Java + PostgreSQL을 한 대에서 실행하므로t3.micro는 메모리가 빠듯할 수 있습니다) - 키 페어: 새로 만들거나 기존 키 사용
- 보안 그룹 인바운드 규칙
| 유형 | 포트 | 소스 | 용도 |
|---|---|---|---|
| SSH | 22 | 내 IP | 서버 접속 |
| 사용자 지정 TCP | 8080 | 내 IP | 앱 확인 |
PostgreSQL 포트(5432)는 열지 않습니다. 앱과 DB가 같은 서버에 있으므로 외부에서 DB에 접근할 필요가 없습니다.
IAM Role은 5단계에서 연결하므로 지금은 비워 둬도 됩니다.
인스턴스가 실행되면 SSH로 접속합니다.
ssh -i my-key.pem ec2-user@<EC2_PUBLIC_IP>
3. PostgreSQL 설치 및 설정
3-1. 설치
sudo dnf update -y
sudo dnf install -y postgresql16 postgresql16-server
3-2. 초기화 및 시작
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql
sudo systemctl status postgresql
active (running)이 보이면 정상입니다.
3-3. DB와 사용자 생성
sudo -u postgres psql
CREATE USER timeapp WITH PASSWORD 'ChangeMe-Strong-Pass!';
CREATE DATABASE timedb OWNER timeapp;
\q
3-4. 비밀번호 인증 허용
Amazon Linux의 기본 설정은 로컬 TCP 접속에 ident 인증을 사용합니다. JDBC는 TCP(localhost)로 접속하므로 비밀번호 인증(scram-sha-256)으로 바꿔야 합니다.
sudo vi /var/lib/pgsql/data/pg_hba.conf
아래 두 줄의 마지막 값을 scram-sha-256으로 수정합니다.
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections:
host all all ::1/128 scram-sha-256
설정을 반영합니다.
sudo systemctl restart postgresql
3-5. 접속 테스트
psql -h localhost -U timeapp -d timedb -c "SELECT now();"
비밀번호를 입력하고 현재 시간이 나오면 DB 준비는 끝났습니다.
4. Parameter Store에 접속정보 저장
핵심 규칙: 파라미터 이름 = Spring 프로퍼티 이름
Spring Cloud AWS는 지정한 경로(prefix) 아래의 파라미터를 읽어서, prefix를 뺀 나머지 이름을 Spring 프로퍼티 키로 사용합니다.
| Parameter Store 이름 | Spring 프로퍼티 |
|---|---|
/timeapp/prod/spring.datasource.url |
spring.datasource.url |
/timeapp/prod/spring.datasource.username |
spring.datasource.username |
/timeapp/prod/spring.datasource.password |
spring.datasource.password |
방법 A: AWS 콘솔
AWS 콘솔 → Systems Manager → Parameter Store → 파라미터 생성
| 이름 | 유형 | 값 |
|---|---|---|
/timeapp/prod/spring.datasource.url |
String | jdbc:postgresql://localhost:5432/timedb |
/timeapp/prod/spring.datasource.username |
String | timeapp |
/timeapp/prod/spring.datasource.password |
SecureString | ChangeMe-Strong-Pass! |
- 계층(Tier)은 Standard를 선택합니다. Standard 파라미터는 추가 요금이 없습니다.
- SecureString의 KMS 키는 기본값
alias/aws/ssm을 그대로 사용합니다.
방법 B: AWS CLI (로컬 PC에서 실행)
aws ssm put-parameter --region ap-northeast-2 \
--name "/timeapp/prod/spring.datasource.url" \
--type String \
--value "jdbc:postgresql://localhost:5432/timedb"
aws ssm put-parameter --region ap-northeast-2 \
--name "/timeapp/prod/spring.datasource.username" \
--type String \
--value "timeapp"
aws ssm put-parameter --region ap-northeast-2 \
--name "/timeapp/prod/spring.datasource.password" \
--type SecureString \
--value "ChangeMe-Strong-Pass!"
CLI로 비밀번호를 입력하면 셸 히스토리에 남을 수 있습니다. 실제 운영 비밀번호는 콘솔에서 입력하는 편이 안전합니다.
5. IAM Role 생성 및 EC2에 연결
EC2에서 실행되는 앱이 Parameter Store를 읽으려면 권한이 필요합니다. Access Key를 서버에 두지 않고, EC2에 IAM Role을 붙이는 방식을 사용합니다.
5-1. 정책 생성
IAM → 정책 → 정책 생성 → JSON 탭에 아래 내용을 입력합니다. <ACCOUNT_ID>는 본인 AWS 계정 ID로 바꿉니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadTimeAppParameters",
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameters",
"ssm:GetParameter"
],
"Resource": [
"arn:aws:ssm:ap-northeast-2:<ACCOUNT_ID>:parameter/timeapp/prod",
"arn:aws:ssm:ap-northeast-2:<ACCOUNT_ID>:parameter/timeapp/prod/*"
]
}
]
}
정책 이름은 timeapp-ssm-read로 지정합니다.
KMS 권한은? SecureString을 기본 키(
alias/aws/ssm)로 암호화했다면 별도의kms:Decrypt권한이 필요 없습니다. 고객 관리형 KMS 키를 썼다면 해당 키에 대한kms:Decrypt권한을 추가해야 합니다.
5-2. Role 생성
IAM → 역할 → 역할 생성
- 신뢰할 수 있는 엔터티: AWS 서비스 → EC2
- 권한 정책: 방금 만든
timeapp-ssm-read선택 - 역할 이름:
timeapp-ec2-role
5-3. EC2에 Role 연결
EC2 콘솔 → 인스턴스 선택 → 작업 → 보안 → IAM 역할 수정 → timeapp-ec2-role 선택 → 저장
5-4. EC2에서 권한 확인
EC2에 SSH로 접속한 상태에서 실행합니다. Amazon Linux 2023에는 AWS CLI가 기본으로 설치되어 있습니다.
aws ssm get-parameters-by-path \
--region ap-northeast-2 \
--path /timeapp/prod/ \
--with-decryption \
--query "Parameters[].Name"
세 개의 파라미터 이름이 출력되면 권한 설정은 완료입니다.
6. Spring Boot 앱 작성
로컬 PC에서 작업합니다.
6-1. 프로젝트 생성
start.spring.io에서 다음과 같이 생성합니다.
- Project: Gradle – Groovy
- Language: Java
- Spring Boot: 4.0.x (최신 패치 버전)
- Group:
com.example/ Artifact:timeapp - Java: 21
- Dependencies: Spring Web, JDBC API, PostgreSQL Driver
6-2. 프로젝트 구조
timeapp
├── build.gradle
└── src/main
├── java/com/example/timeapp
│ ├── TimeappApplication.java
│ └── TimeController.java
└── resources
└── application.yml
6-3. build.gradle
start.spring.io가 만든 파일에 Spring Cloud AWS 의존성을 추가합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.6' // start.spring.io가 생성한 버전 사용
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencyManagement {
imports {
// Spring Boot 4.0.x에 맞는 Spring Cloud AWS 4.0.x
mavenBom 'io.awspring.cloud:spring-cloud-aws-dependencies:4.0.2'
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'org.postgresql:postgresql'
// SSM Parameter Store 연동
implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- Spring Boot 4부터 웹 스타터 이름이
spring-boot-starter-webmvc로 바뀌었습니다. start.spring.io가 생성한 이름을 그대로 쓰면 됩니다.- 버전 번호는 글 작성 시점 기준입니다. 최신 패치 버전으로 바꿔 사용하세요.
6-4. application.yml
비밀번호가 어디에도 없다는 점이 핵심입니다.
spring:
application:
name: timeapp
config:
import: "aws-parameterstore:/timeapp/prod/"
cloud:
aws:
region:
static: ap-northeast-2
server:
port: 8080
spring.config.import: 앱 시작 시/timeapp/prod/아래의 모든 파라미터를 읽어 Spring 프로퍼티로 등록합니다.- 리전은 EC2 메타데이터에서 자동으로 찾을 수도 있지만, 명시해 두면 로컬 실행 시에도 헷갈리지 않습니다.
- 파라미터를 읽지 못하면 앱이 시작되지 않습니다. 설정 누락을 배포 단계에서 바로 알 수 있으므로 의도된 동작입니다. 로컬 개발처럼 Parameter Store 없이 실행해야 할 때는
optional:aws-parameterstore:/timeapp/prod/처럼optional:을 붙입니다.
6-5. TimeappApplication.java
start.spring.io가 생성한 파일을 그대로 사용합니다.
package com.example.timeapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TimeappApplication {
public static void main(String[] args) {
SpringApplication.run(TimeappApplication.class, args);
}
}
6-6. TimeController.java
DB에서 현재 시간을 조회해 HTML로 보여줍니다.
package com.example.timeapp;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TimeController {
private final JdbcTemplate jdbcTemplate;
public TimeController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
public String now() {
Map<String, Object> row = jdbcTemplate.queryForMap("""
SELECT
to_char(now() AT TIME ZONE 'Asia/Seoul', 'YYYY-MM-DD HH24:MI:SS') AS seoul_time,
to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS utc_time,
version() AS db_version
""");
return """
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>DB 현재 시간</title>
<style>
body { font-family: sans-serif; max-width: 640px; margin: 60px auto; padding: 0 16px; }
.time { font-size: 2rem; font-weight: bold; margin: 8px 0 24px; }
.muted { color: #666; font-size: 0.9rem; }
</style>
</head>
<body>
<h1>PostgreSQL 현재 시간</h1>
<p>서울 (KST)</p>
<div class="time">%s</div>
<p>UTC</p>
<div class="time">%s</div>
<p class="muted">%s</p>
<p class="muted">접속정보 출처: AWS SSM Parameter Store</p>
</body>
</html>
""".formatted(row.get("seoul_time"), row.get("utc_time"), row.get("db_version"));
}
}
EC2의 기본 시간대는 UTC이므로, PostgreSQL의
now()도 기본적으로 UTC 기준으로 표시됩니다. 여기서는AT TIME ZONE으로 서울 시간과 UTC를 함께 보여줍니다.
7. 빌드, 배포, 실행
7-1. 로컬에서 빌드
./gradlew clean bootJar
build/libs/timeapp-0.0.1-SNAPSHOT.jar 파일이 생성됩니다.
로컬 PC에는 IAM Role이 없으므로, 이 상태로 로컬에서 실행하면 Parameter Store를 읽지 못해 시작에 실패합니다. 로컬 실행용 설정은 10. 정리 및 다음 단계를 참고하세요.
7-2. EC2에 Java 설치
sudo dnf install -y java-21-amazon-corretto-headless
java -version
7-3. JAR 업로드
로컬 PC에서 실행합니다.
scp -i my-key.pem build/libs/timeapp-0.0.1-SNAPSHOT.jar ec2-user@<EC2_PUBLIC_IP>:/home/ec2-user/timeapp.jar
7-4. 먼저 직접 실행해 보기
java -jar ~/timeapp.jar
로그에 Tomcat started on port 8080이 보이면 성공입니다. 확인 후 Ctrl + C로 종료합니다.
7-5. systemd 서비스로 등록
SSH 접속을 끊어도 앱이 계속 실행되고, 서버가 재부팅되면 자동으로 시작되도록 등록합니다.
sudo mkdir -p /opt/timeapp
sudo mv ~/timeapp.jar /opt/timeapp/timeapp.jar
sudo chown -R ec2-user:ec2-user /opt/timeapp
sudo vi /etc/systemd/system/timeapp.service
[Unit]
Description=Time App (Spring Boot)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
User=ec2-user
WorkingDirectory=/opt/timeapp
ExecStart=/usr/bin/java -jar /opt/timeapp/timeapp.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
서비스 파일에 DB 비밀번호가 전혀 없다는 점을 확인하세요. 접속정보는 모두 Parameter Store에서 가져옵니다.
sudo systemctl daemon-reload
sudo systemctl enable --now timeapp
sudo systemctl status timeapp
로그 확인:
journalctl -u timeapp -f
8. 동작 확인
EC2 안에서 먼저 확인합니다.
curl http://localhost:8080/
그다음 브라우저에서 접속합니다.
http://<EC2_PUBLIC_IP>:8080/
서울 시간과 UTC, PostgreSQL 버전이 표시되면 완성입니다.
비밀번호 변경 테스트
Parameter Store 방식의 장점은 코드나 서버 파일을 수정하지 않고 접속정보를 바꿀 수 있다는 점입니다.
-
PostgreSQL 비밀번호 변경
sudo -u postgres psql -c "ALTER USER timeapp WITH PASSWORD 'New-Strong-Pass!';" -
Parameter Store 값 변경 (콘솔에서
/timeapp/prod/spring.datasource.password편집) -
앱 재시작
sudo systemctl restart timeapp
JAR와 설정 파일은 그대로인데 새 비밀번호로 정상 접속됩니다.
이 구성에서는 앱이 시작할 때 한 번 값을 읽습니다. 값을 바꾼 뒤에는 앱을 재시작해야 반영됩니다.
9. 트러블슈팅
| 증상 (로그) | 원인 | 해결 |
|---|---|---|
Unable to load credentials from any of the providers |
EC2에 IAM Role이 연결되지 않음 | 5-3단계에서 Role 연결 확인. 연결 직후라면 1~2분 기다린 뒤 재시작 |
AccessDeniedException ... ssm:GetParametersByPath |
정책의 Resource ARN이 경로와 다름 | 리전, 계정 ID, 경로(/timeapp/prod, /timeapp/prod/*) 확인 |
AccessDeniedException ... kms:Decrypt |
고객 관리형 KMS 키로 SecureString 암호화 | 해당 키에 대한 kms:Decrypt 권한 추가 |
Failed to configure a DataSource: 'url' attribute is not specified |
파라미터 이름이 프로퍼티 키와 다름 | 파라미터 이름이 /timeapp/prod/spring.datasource.url 형태인지 확인 (오타, 대소문자 포함) |
FATAL: Ident authentication failed for user "timeapp" |
pg_hba.conf가 ident 인증 상태 |
3-4단계대로 scram-sha-256으로 변경 후 PostgreSQL 재시작 |
FATAL: password authentication failed |
Parameter Store의 비밀번호와 DB 비밀번호가 다름 | 두 값을 일치시키고 앱 재시작 |
브라우저에서 접속 안 됨 (curl localhost는 성공) |
보안 그룹에 8080 미허용 | 인바운드 규칙에 8080 / 내 IP 추가 |
| 로컬 PC에서 실행 시 시작 실패 | 로컬에는 IAM Role이 없음 | 아래 "로컬 개발 환경" 참고 |
어떤 값이 실제로 로드됐는지 확인하고 싶다면, EC2에서 5-4단계의 CLI 명령에 --query "Parameters[].[Name,Value]"를 붙여 실행해 보세요. (비밀번호가 화면에 출력되므로 주의하세요.)
10. 정리 및 다음 단계
정리
- DB 접속정보는 Parameter Store에 저장하고, 비밀번호는 SecureString으로 암호화했습니다.
- 앱은
spring.config.import: aws-parameterstore:/timeapp/prod/한 줄로 접속정보를 가져옵니다. - EC2는 IAM Role로 인증하므로 서버 어디에도 AWS Access Key나 DB 비밀번호가 없습니다.
- 접속정보를 바꿀 때는 Parameter Store 값만 수정하고 앱을 재시작하면 됩니다.
로컬 개발 환경
로컬에서는 Parameter Store 대신 로컬 DB를 쓰도록 프로파일을 나누는 방법이 간단합니다.
application.yml (공통, prod 기본)
spring:
application:
name: timeapp
config:
import: "optional:aws-parameterstore:/timeapp/prod/"
cloud:
aws:
region:
static: ap-northeast-2
application-local.yml (로컬 전용)
spring:
config:
import: "" # Parameter Store를 사용하지 않음
datasource:
url: jdbc:postgresql://localhost:5432/timedb
username: timeapp
password: local-dev-password
./gradlew bootRun --args='--spring.profiles.active=local'
application-local.yml에는 로컬 개발용 비밀번호만 넣고, 운영 비밀번호는 절대 넣지 마세요.
또는 로컬 PC에 AWS CLI 자격 증명(aws configure)이 설정되어 있다면, 같은 Parameter Store의 개발용 경로(예:/timeapp/dev/)를 읽도록 구성할 수도 있습니다.
다음 단계
- 환경별 경로 분리:
/timeapp/dev/,/timeapp/prod/처럼 나누고 프로파일별로 다른 경로를 import - Secrets Manager: 비밀번호 자동 교체(rotation)가 필요하다면
aws-secretsmanager:import로 전환 - RDS로 이전: DB를 별도 관리형 서비스로 분리할 때도 Parameter Store의 URL 값만 바꾸면 됩니다
리소스 정리
실습 후 비용이 발생하지 않도록 정리합니다.
- EC2 인스턴스 종료
- Parameter Store의
/timeapp/prod/*파라미터 삭제 - IAM Role
timeapp-ec2-role과 정책timeapp-ssm-read삭제 - 보안 그룹 삭제 (필요 시)