개발환경, 한눈에.
프론트 배포부터 API 연결 확인까지.
새로운 AIDCMap을 위한 배포·운영 가이드입니다.
경로로 나누고, 하나로 연결합니다.
정적 파일은 OAC로만 읽습니다.
버킷 직접 공개는 차단합니다.
VPC 원본으로 비공개 ALB에 연결합니다.
API 캐시는 비활성화합니다.
배포 가이드
프론트는 파일로, 백엔드는 컨테이너 이미지로 배포합니다.
시작 전 · AWS CLI 로그인과 도구 준비 +
AWS CLI v2·Docker가 필요합니다. 백엔드 배포 도우미는 Python 3를 사용합니다. 아래 명령은 macOS/Linux 터미널 기준입니다.
SSO 설정에서 시작 URL은 https://gridwiz.awsapps.com/start/, SSO 리전은 ap-northeast-2, 계정은 242441135880, 권한 세트는 gw-aidcmap-operator를 선택하세요.
aws configure sso --profile aidcmap-dev
aws sso login --profile aidcmap-dev
export AWS_PROFILE=aidcmap-dev
export AWS_DEFAULT_REGION=ap-northeast-2
aws sts get-caller-identity출력된 Account가 개발 계정과 일치하는지 확인하세요. 로그인 세션이 만료되면 aws sso login --profile aidcmap-dev를 다시 실행합니다.
정적 빌드 결과를 준비합니다.
프로젝트의 빌드 명령을 실행한 뒤 index.html이 들어 있는 폴더를 확인하세요. 아래는 npm·dist/ 기준 예시입니다. SSR 서버가 필요한 프론트는 S3 단독 배포 대상이 아닙니다.
npm ci
npm run build
# dist/ 대신 실제 빌드 결과 폴더를 사용하세요.개발용 신규 S3에 업로드합니다.
승인된 AIDCMap 개발 계정·역할로 로그인한 후 실행하세요. 기존 gridwiz-aidcmap-dev 버킷과는 별개입니다. 삭제 옵션 없이 변경 파일만 업로드합니다.
aws sts get-caller-identity
aws s3 sync ./dist/ s3://gw-aidcmap-dev-web-242441135880/ \
--cache-control "no-cache" --region ap-northeast-2화면과 API를 함께 확인합니다.
현재 개발 배포는 캐시가 꺼져 있습니다. 새로고침 후 화면을 확인하고, 프론트의 API 기본 주소는 상대 경로 /api로 설정하세요. 실제 프론트를 업로드하면 초기 화면이 대체됩니다. 가이드는 /guide/index.html에서 계속 볼 수 있습니다.
/health예제 소스 다운로드 ↓컨테이너 실행 조건을 맞춥니다.
서버는 0.0.0.0:8080에서 요청을 받아야 합니다. ALB는 /health의 HTTP 200 응답으로 상태를 확인합니다. 브라우저 검증용 /api/health는 아래 JSON을 반환하도록 구현합니다.
{ "status": "ok", "service": "aidcmap-api" }이미지를 빌드하고 ECR에 올립니다.
다운로드한 Node.js 예제 폴더 또는 실제 백엔드 프로젝트에서 실행하세요. ECR 태그는 덮어쓰기가 차단돼 있으므로 배포마다 새로운 태그를 사용합니다.
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="${ACCOUNT_ID}.dkr.ecr.ap-northeast-2.amazonaws.com"
IMAGE_TAG="dev-$(date -u +%Y%m%d%H%M%S)"
IMAGE="${REGISTRY}/gw-aidcmap-dev-api:${IMAGE_TAG}"
aws ecr get-login-password --region ap-northeast-2 \
| docker login --username AWS --password-stdin "$REGISTRY"
docker build --platform linux/amd64 -t "$IMAGE" .
docker push "$IMAGE"
echo "$IMAGE_TAG"ECS에 배포합니다.
최초 서비스 생성은 인프라 담당자가 수행합니다. 이미지 태그·포트·헬스체크 경로를 전달해 스택에 반영합니다. 서비스가 생긴 이후에는 새 작업 정의 리비전을 등록하고 해당 리비전으로 서비스를 갱신합니다.
# 아래 ‘배포 도우미 다운로드’에서 deploy-api.py를 받으세요.
# 먼저 계정·이미지·작업 정의 형식을 검사합니다. 변경은 적용하지 않습니다.
python3 deploy-api.py "$IMAGE_TAG" --dry-run
# 새 작업 정의를 등록하고 해당 리비전으로 서비스를 배포합니다.
python3 deploy-api.py "$IMAGE_TAG"배포 도우미는 현재 작업 정의의 포트·역할·설정을 유지하고 api 컨테이너 이미지만 변경합니다. 새 이미지 태그는 인프라 담당자와 공유해 이후 스택 변경 시 유지하세요.
이미지 태그만 바꿔서 푸시해도 ECS 서비스는 자동 갱신되지 않습니다. DB 연결은 아래 PostgreSQL 가이드를 따르세요. 업무 API와 실제 테이블·마이그레이션은 앱 요구사항에 맞춰 구현합니다.
GitLab OIDC와 배포 역할을 준비합니다.
SSO 사용자 역할과 별도의 배포 전용 IAM 역할을 사용합니다. ZIP의 iam/*-trust.example.json에서 gitlab.example.com과 프로젝트 경로를 실제 값으로 바꾸세요. Audience는 sts.amazonaws.com, 허용 브랜치는 dev입니다.
| 대상 | 역할 이름 예시 | 권한 |
|---|---|---|
| 프론트 | gw-aidcmap-dev-gitlab-web-deploy | 신규 S3 업로드 · /guide/ 덮어쓰기 차단 |
| 백엔드 | gw-aidcmap-dev-gitlab-api-deploy | 전용 ECR 푸시 · 기존 ECS 갱신 · 전용 역할 전달 |
실제 IAM 권한은 변경하지 않았습니다. GitLab 주소·프로젝트·브랜치 확정 후 인프라 담당자가 OIDC 제공자와 역할을 연결해야 합니다. AWS가 GitLab Discovery·JWKS에 접근할 수 있어야 합니다.
브랜치를 보호하고 GitLab 변수를 설정합니다.
GitLab에서 dev 브랜치를 보호하고 Settings → CI/CD → Variables에 아래 변수를 추가합니다. Protected를 켜고, 이미지 빌드도 읽을 수 있도록 환경 범위는 *로 둡니다.
| 변수 | 값 | 대상 |
|---|---|---|
AWS_WEB_DEPLOY_ROLE_ARN | 프론트 배포 전용 역할 ARN | 프론트 / 모노레포 |
AWS_API_DEPLOY_ROLE_ARN | 백엔드 배포 전용 역할 ARN | 백엔드 / 모노레포 |
보호되지 않은 브랜치에서는 예제 파이프라인이 생성되지 않습니다. 장기 AWS 키와 DB 비밀번호는 GitLab 변수에 복사하지 않습니다. 토큰을 아티팩트에 넣지 않고 CI_DEBUG_TRACE도 사용하지 않습니다.
저장소에 예제 파일을 적용합니다.
분리 저장소는 해당 YAML을 .gitlab-ci.yml로 저장하고 ZIP의 ci/를 루트에 함께 커밋하세요. 모노레포는 두 YAML을 원래 이름으로 유지하고 모노레포 예제를 .gitlab-ci.yml로 사용합니다.
프론트는 npm run build와 dist/, 백엔드는 npm test·Dockerfile을 기준으로 합니다. 예제는 포트·DB 환경변수·시크릿 참조를 유지하고 앱 이미지만 바꿉니다.
Linux amd64 Docker executor와 TLS Docker-in-Docker가 필요합니다. Runner 관리자가 privileged = true와 /certs/client 공유 볼륨을 구성해야 합니다. ZIP에 설정 조각이 있습니다. Docker CLI와 dind 이미지는 같은 승인 버전·digest로 고정하세요.
Runner는 RDS에 직접 접속하지 않습니다. 배포 후 HTTPS DB 헬스체크를 사용합니다. DB 마이그레이션은 자동 실행하지 않습니다.
프론트 / S3 YAML +
Node.js 24 build, dist artifact, manual S3 deployment. The guide prefix is preserved; object deletion is not enabled.
stages: [test, build, deploy]
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "dev" && $CI_COMMIT_REF_PROTECTED == "true"'
- when: never
variables:
AWS_DEFAULT_REGION: ap-northeast-2
AWS_ACCOUNT_ID: "242441135880"
S3_BUCKET: gw-aidcmap-dev-web-242441135880
APP_URL: https://devtest.aidcmap.ai
FRONTEND_DIR: "."
frontend_build:
stage: build
image: node:24-alpine
script:
- cd "$FRONTEND_DIR"
- npm ci
- npm run build
- test -f dist/index.html
artifacts:
paths:
- ${FRONTEND_DIR}/dist/
expire_in: 7 days
frontend_deploy:
stage: deploy
image: python:3.12-alpine
variables:
AWS_ROLE_ARN: $AWS_WEB_DEPLOY_ROLE_ARN
id_tokens:
AWS_OIDC_TOKEN:
aud: sts.amazonaws.com
before_script:
- apk add --no-cache aws-cli
- . ci/aws-oidc.sh
needs:
- job: frontend_build
artifacts: true
resource_group: aidcmap-dev-frontend
environment:
name: development/frontend
url: https://devtest.aidcmap.ai
when: manual
allow_failure: false
script:
- |
# Keep /guide/ intact. Do not add --delete.
aws s3 sync "$FRONTEND_DIR/dist/" "s3://$S3_BUCKET/" \
--exclude 'guide/*' --cache-control no-cache
python3 ci/verify.py frontend
백엔드 / ECR + ECS YAML +
Test, immutable image tag, manual ECS deployment, then API and PostgreSQL health verification.
stages: [test, build, deploy]
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "dev" && $CI_COMMIT_REF_PROTECTED == "true"'
- when: never
variables:
AWS_DEFAULT_REGION: ap-northeast-2
AWS_ACCOUNT_ID: "242441135880"
ECR_REPOSITORY: gw-aidcmap-dev-api
APP_URL: https://devtest.aidcmap.ai
BACKEND_DIR: "."
.aws_api:
variables:
AWS_ROLE_ARN: $AWS_API_DEPLOY_ROLE_ARN
id_tokens:
AWS_OIDC_TOKEN:
aud: sts.amazonaws.com
before_script:
- apk add --no-cache aws-cli python3
- . ci/aws-oidc.sh
backend_test:
stage: test
image: node:24-alpine
script:
- cd "$BACKEND_DIR"
- npm ci
- npm test
backend_image:
extends: .aws_api
stage: build
# Pin both Docker images to your organization's approved matching version/digest.
image: docker:cli
services:
- name: docker:dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: /certs
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /certs/client
needs: [backend_test]
script:
- |
REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
# A retry gets a new job ID, so immutable ECR tags never collide.
IMAGE_TAG="$CI_COMMIT_SHORT_SHA-$CI_PIPELINE_ID-$CI_JOB_ID"
AIDC_DOCKER_CONFIG="$(mktemp -d /tmp/aidcmap-docker.XXXXXX)"
export DOCKER_CONFIG="$AIDC_DOCKER_CONFIG"
aws ecr get-login-password \
| docker login --username AWS --password-stdin "$REGISTRY"
docker build --platform linux/amd64 \
-t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" "$BACKEND_DIR"
docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
printf 'IMAGE_TAG=%s\n' "$IMAGE_TAG" > build.env
artifacts:
reports:
dotenv: build.env
expire_in: 7 days
backend_deploy:
extends: .aws_api
stage: deploy
image: python:3.12-alpine
needs:
- job: backend_image
artifacts: true
resource_group: aidcmap-dev-backend
environment:
name: development/backend
url: https://devtest.aidcmap.ai
when: manual
allow_failure: false
script:
- python3 ci/deploy-api.py "$IMAGE_TAG" --dry-run
- python3 ci/deploy-api.py "$IMAGE_TAG"
- python3 ci/verify.py backend
모노레포 / 통합 YAML +
Keep both included YAML files and ci/ at the repository root. Set both role ARN variables.
# Copy this file to .gitlab-ci.yml in a repository containing frontend/ and backend/.
# Keep the two included YAML examples and ci/ at the repository root.
include:
- local: .gitlab-ci.frontend.yml
- local: .gitlab-ci.backend.yml
variables:
FRONTEND_DIR: frontend
BACKEND_DIR: backend
# Both deployments are manual. For a coordinated change, deploy backend first,
# verify its health, then deploy frontend. Database migrations are a separate step.
dev 푸시 후 배포 작업을 실행합니다.
자동 빌드가 끝나면 frontend_deploy 또는 backend_deploy의 실행 버튼을 누르세요. 저장소별 배포 작업은 직렬화합니다. 통합 변경은 백엔드를 배포·검증한 뒤 프론트를 배포합니다.
프론트는 루트 HTTP 200, 백엔드는 /api/health와 /api/health/db의 실제 JSON·TLS 응답을 확인합니다. 업무 앱에서도 헬스체크 계약을 유지하거나 검증 코드를 조정하세요.
리소스 연결 가이드
어떤 자원이 어디에 연결되는지 확인하세요.
| 역할 | 리소스 | 연결 방식 |
|---|---|---|
| 진입점 | devtest.aidcmap.aiRoute 53 → CloudFront | HTTPS · devtest 전용 인증서 |
| 프론트 | gw-aidcmap-dev-web-242441135880S3 · 비공개 · 버전 관리 | CloudFront OAC로 읽기 |
| API 라우팅 | gw-aidcmap-dev-api-albPrivate ALB · VPC 원본 | /api · /api/* → 대상 그룹 → 컨테이너 8080 |
| 이미지 | gw-aidcmap-dev-apiECR · 태그 덮어쓰기 금지 | 작업 정의가 이미지 태그를 참조 |
| 실행 환경 | gw-aidcmap-dev-apiECS 클러스터 · Fargate · 0.25 vCPU / 0.5 GB | 실행 상태 확인 필요 |
| 데이터베이스 | gw-aidcmap-dev-postgresRDS PostgreSQL 17 · db.t4g.micro · gp3 20GB | 연결 상태 확인 필요 |
| DB 인증정보 | gw-aidcmap-dev-db-apiSecrets Manager · 앱 전용 사용자 | ECS 시작 시 주입 · 관리자 인증정보와 분리 |
| 로그 | /ecs/gw-aidcmap-dev-apiCloudWatch Logs · 보관 14일 | Node.js 표준 출력·표준 오류 |
| 외부 통신 | gw-aidcmap-dev-nat전용 VPC · private subnet → NAT | 상태 확인 필요 |
API 경로는 유지됩니다.
/api/users 요청은 백엔드에도 동일한 경로로 전달됩니다. CloudFront가 /api를 제거하지 않습니다.
인증정보는 서버에서 관리합니다.
브라우저나 프론트 빌드에 AWS 키·DB 비밀번호·시크릿을 넣지 마세요. 필요한 값은 ECS 작업 역할과 Secrets Manager 등으로 연결합니다.
기존 환경은 유지합니다.
dev.aidcmap.ai의 배포 경로는 별도입니다. 신규 앱 검증이 끝난 후에 교체 절차를 진행합니다.
인프라 담당자 체크리스트 +
- 최초 백엔드 배포 전 NAT와 ECR 이미지 준비 상태 확인
- 컨테이너 포트·ALB 헬스체크·실제 Node.js 리스닝 포트 일치
- Task Execution Role은 ECR 이미지·로그, Task Role은 실제 앱의 AWS 접근에 사용
- 시크릿을 작업 정의에 연결할 때 Execution Role에 해당 시크릿만 읽기 허용
- 앱의 외부 URL은 https://devtest.aidcmap.ai로 지정하고 프록시 신뢰 설정 검토
- 프론트의 SPA 또는 정적 내보내기 방식에 맞춰 페이지 새로고침 경로 처리 확인
- 서비스 이름과 Project=aidcmap, Environment=dev 태그 유지
Node.js와 PostgreSQL 연결
전용 DB 사용자와 TLS 연결을 사용합니다. 비밀번호는 프론트에 전달하지 않습니다.
ECS에 연결 설정을 전달합니다.
아래 설정은 이미 검증용 서비스에 연결돼 있습니다. 새 앱 이미지에서도 동일한 환경변수 이름을 사용하면 연결 설정을 유지할 수 있습니다.
| 변수 | 값 또는 참조 | 관리 위치 |
|---|---|---|
DB_HOST | 전용 RDS의 내부 엔드포인트 | ECS 작업 정의 환경변수 |
DB_PORT | 5432 | ECS 작업 정의 환경변수 |
DB_NAME | aidcmap | ECS 작업 정의 환경변수 |
DB_USER | 앱 전용 사용자 · aidcmap_api | Secrets Manager의 username 필드 |
DB_PASSWORD | 앱 전용 비밀번호 · 화면에 표시하지 않음 | Secrets Manager의 password 필드 |
ECS는 시크릿을 작업 시작 시 읽습니다. 비밀번호 변경 시 DB 사용자·시크릿·ECS 재배포를 함께 맞춰야 합니다. API 실행 역할에는 앱 시크릿만 읽기 허용돼 있습니다.
공식 드라이버로 인증서를 검증합니다.
다운로드 예제에는 pg 드라이버, lockfile, AWS RDS CA 번들이 들어 있습니다. npm ci 후 실행하세요. 커넥션 풀은 기본 2개로 제한했습니다.
import pg from 'pg';
import { readFileSync } from 'node:fs';
const pool = new pg.Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT || 5432),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 2,
connectionTimeoutMillis: 5000,
ssl: {
ca: readFileSync(new URL('./rds-ca-bundle.pem', import.meta.url), 'utf8'),
rejectUnauthorized: true,
servername: process.env.DB_HOST,
},
});
const result = await pool.query('SELECT 1 AS connected');
// 사용자 입력은 쿼리 문자열에 붙이지 말고 $1 등의 매개변수를 사용하세요.실제 SQL 응답을 확인합니다.
/api/health/db는 PostgreSQL에서 실제 조회를 실행하고 현재 연결의 TLS 사용 여부를 확인합니다. 호스트·사용자·비밀번호·업무 데이터는 응답하지 않습니다.
{
"status": "ok",
"service": "aidcmap-api",
"database": "connected",
"tls": true
}앱 사용자의 기본 스키마는 app입니다. 실제 테이블은 앱의 마이그레이션으로 이 스키마에 생성하세요. DB 서버 관리 권한은 앱에 부여하지 않았습니다.
실제 요청으로 연결을 확인합니다.
구성 기록과 별개로, 현재 브라우저에서 직접 요청합니다.
HTTPS 진입
현재 도메인과 HTTPS 접속을 확인합니다.
브라우저가 인증서를 검증한 접속인지 확인합니다.S3 정적 파일
GET /guide/verification.txt
Node.js API
GET /api/health
PostgreSQL
GET /api/health/db
연결 검사를 실행하면 실제 HTTP 상태와 응답이 여기에 표시됩니다. 백엔드 또는 DB 연결이 준비되지 않았다면 503 응답이 발생할 수 있습니다.
실패하면 어디를 확인하나요? +
- S3 파일 403/404: 파일 업로드 여부·정확한 파일명·OAC 버킷 정책을 확인하세요.
- API 503: ECS 서비스 실행 여부, 대상 그룹에 등록된 healthy 대상이 있는지 확인하세요.
- API 502/504 또는 시간 초과: 컨테이너 포트·보안 그룹·앱 응답·VPC 원본 연결을 확인하세요.
- HTTP 200이지만 JSON이 아님: API 요청이 정적 페이지로 라우팅되지 않았는지 확인하세요.
- DB 검사 503: ECS의 DB 환경변수·시크릿 주입·DB 보안 그룹·인증서 번들·DB 사용자 권한을 확인하세요. TLS 검증은 끄지 마세요.
- 이미지 가져오기 실패: ECR 태그·CPU 아키텍처·실행 역할·NAT 외부 연결을 확인하세요.