-j ACCEPT: jump의 약자로, 위 조건이 맞다면 패킷을 허용(ACCEPT)하라는 명령
# 규칙 목록 보기
sudo iptables -L -v -n --line-numbers
# 특정 규칙 삭제
sudo iptables -D INPUT [라인번호]
# 모든 규칙 초기화
sudo iptables -F
# 특정 IP 차단
sudo iptables -A INPUT -s [차단할IP] -j DROP
iptables 명령은 입력 즉시 적용이지만, 서버 재부팅 시 사라짐
반드시 netfileter-persistent save 명령을 통해 설정을 파일로 저장해야 재부팅 후에도 방화벽이 유지
sudo netfilter-persistent save
sudo netfilter-persistent reload
재부팅 후에도 유지하도록 설정을 저장하는 명령어
save는 현재 메모리상에 적용되어 있는 실시간 방화벽 규칙(iptables)을 파일로 저장
reload는 저장된 설정 파일(/etc/iptables/rules.v4)을 읽어와서 방화벽 시스템에 다시 로드
# start.bat 혹은 sh로 사용할 jar 실행 명령어
java -Xms4G -Xmx4G -jar server.jar nogui
java -Xms16G -Xmx16G -jar fabric-server-mc.1.21.11-loader.0.18.4-launcher.1.1.1.jar nogui
# 새 가상 화면 만들기
screen -S minecraft
# 가상 화면 접속하기
screen -r minecraft
# 가상 화면 나가기
Ctrl + A D
터미널을 종료해도 서버가 유지되게 하는 서버 백그라운드 구동
스크린을 생성하고 서버 메모리를 할당
실행 가능하게 모드 변경
정상적으로 eula.txt까지 설정하고 난 뒤 모습
# 포트 상태 확인
sudo netstat -tnlp | grep 25565
sudo ss -tunlp | grep 25565
# 리눅스 자체 방화벽(Uncomplicated FireWall) 내리기
# OCI 클라우드 방화벽과 리눅스 자체 방화벽 이중 방화벽 상태
sudo ufw disable
기타 확인 사항
pause-when-empty-seconds
접속 중인 플레이어가 없다면 서버를 일시 중지한다.(tick freeze와 같음) 초 단위로 조절한다.
0으로 설정하면 접속 중인 플레이어가 없어도 서버는 계속 돌아간다.
/docker-entrypoint.sh: line 15: /dev/tcp/mongo/27017: Invalid argument
Fri Dec 5 06:58:33 UTC 2025 retrying to connect to mongo:27017 (10/10)
/docker-entrypoint.sh: line 15: mongo: Try again
/docker-entrypoint.sh: line 15: /dev/tcp/mongo/27017: Invalid argument
No custom config.js found, loading config.default.js
Welcome to mongo-express 1.0.2
------------------------
Mongo Express server listening at http://0.0.0.0:8081
Server is open to allow connections from anyone (0.0.0.0)
basicAuth credentials are "admin:pass", it is recommended you change this in your config.js!
admin/admin도 내가 처음에 yaml에 작성한 값도 아닌 기본값이 admin/pass라는 사실
해결 완료
7-2. pgAdmin 웹 접속 오류
ERROR : Failed to create the directory /var/lib/pgadmin/sessions:
[Errno 13] Permission denied: '/var/lib/pgadmin/sessions'
HINT : Create the directory /var/lib/pgadmin/sessions, ensure it is writeable by
'pgadmin', and try again, or, create a config_local.py file
and override the SESSION_DB_PATH setting per
https://www.pgadmin.org/docs/pgadmin4/9.10/config_py.html
[2025-12-05 07:24:29 +0000] [1] [ERROR] Worker (pid:2357) exited with code 1
[2025-12-05 07:24:29 +0000] [1] [ERROR] Worker (pid:2357) exited with code 1.
[2025-12-05 07:24:29 +0000] [2358] [INFO] Booting worker with pid: 2358
PGAdmin 4는Python/Flask 기반의 애플리케이션이며, 웹 서버로Gunicorn을 사용합니다.
컨테이너를 시작한 직후에 첫 번째 접속이 이루어지면, Gunicorn 워커 프로세스가 웹 요청을 처리하기 위해 Python 가상 환경을 로드하고 Flask 애플리케이션을 초기화하는 데 시간이 소요됩니다.
2. 세션 및 내부 DB 로딩
PGAdmin은 사용자의 세션 정보, 이전에 등록된 서버 목록, 내부 설정 등을 저장하기 위해내부 SQLite 데이터베이스를 사용합니다 (볼륨 ./database/pgadmin에 저장됨).
사용자가 5050 포트로 접속하면, PGAdmin은 이 내부 DB를 로드하고 세션을 인증하는 등의 초기 준비 단계를 거쳐야 합니다. 이 과정이 웹 페이지를 즉시 반환하는 다른 서비스보다 느립니다.
3. 첫 접속 이후 속도 개선
일반적으로 PGAdmin은첫 접속 시에만지연이 크게 발생합니다. 한 번 접속하고 세션이 활성화된 상태라면, 이후 접속이나 페이지 이동은 훨씬 빠르게 처리될 것입니다.
해결 완료
7-3. pgAdmin 권한 설정 오류
만약 chmod의 값을 777이 아니라 775로 한다면 위와 같은 문구를 확인할 수 있다.
{
"success": 0,
"errormsg": "(sqlite3.OperationalError) attempt to write a readonly database\n[SQL: UPDATE user SET locked=? WHERE user.id = ?]\n[parameters: (0, 1)]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)",
"info": "",
"result": null,
"data": null
}
쓰기 권한이 부족하다는 뜻
근데 chmod 777로 해버리면 보안 문제가 발생하니
pgadmin UID는 5050
postgres UID는 999를 이용해서 해야 할 듯..?
7-4. pgAdmin 폴더가 VM에 필요한 이유?
pgadmin이 depends on이 database-postgres로 되어있다. 내가 이해하기로는 pgadmin은 database-postgres의 데이터를 조회하고 pgadmin은 웹으로 보여주는 기능이다. database/postgres랑 database/pgadmin 폴더가 따로 필요한가?
결론적으로는database/pgadmin 폴더가 따로 필요하다.
PGAdmin이 database-postgres의 데이터를 조회하고 웹으로 보여주는 것이 주 기능이지만,
1. 🗄️ database/postgres 폴더 (PostgreSQL 데이터 보존) 이 폴더는 PostgreSQL 데이터베이스 자체의 모든 데이터를 저장합니다. 용도: database-postgres 컨테이너가 사용하는 데이터 볼륨입니다 (./database/postgres:/var/lib/postgresql/data). 저장 내용: 실제 사용자 테이블, 인덱스, 데이터베이스 구조 등 PostgreSQL 서버가 운영하는 데 필요한 모든 데이터 파일이 저장됩니다. 필요성: 이 볼륨이 없으면 컨테이너가 재시작될 때마다 데이터베이스의 모든 데이터가 사라집니다. 2. ⚙️ database/pgadmin 폴더 (PGAdmin 설정 보존) 이 폴더는 데이터베이스 데이터가 아니라 PGAdmin 애플리케이션 자체의 설정을 저장합니다. 용도: pgadmin 컨테이너가 사용하는 데이터 볼륨입니다 (./database/pgadmin:/var/lib/pgadmin). 저장 내용: PGAdmin 사용자 정보: 로그인 시 사용한 관리자 계정 이메일 및 암호화된 비밀번호. 서버 연결 정보: PGAdmin에 등록한 database-postgres를 포함한 모든 외부 PostgreSQL 서버의 연결 설정 정보 (호스트, 포트, 사용자 이름 등). 세션 데이터, 로그, 구성 파일 등 PGAdmin 웹 애플리케이션의 상태와 설정 데이터. 필요성: 이 볼륨이 없으면 PGAdmin 컨테이너가 재시작될 때마다 매번 최초 접속 시의 설정 화면이 뜨게 되며, 이전에 등록했던 모든 PostgreSQL 서버 연결 정보가 사라집니다. 💡 컨테이너 의존성과 볼륨의 역할 depends_on: database-postgres: PGAdmin이 PostgreSQL 컨테이너가 완전히 실행된 후에 시작되도록 순서를 보장하여 연결 오류를 방지하는 역할을 합니다. 볼륨 (volumes): 각 컨테이너 내부의 데이터를 호스트 머신의 특정 폴더에 연결(마운트)하여 컨테이너가 삭제되거나 재생성되어도 데이터나 설정이 유지되도록 하는 역할을 합니다. 요약하자면, PostgreSQL 서버의 데이터와 PGAdmin 관리 도구의 설정/상태 데이터는 완전히 별개이기 때문에, 영구적으로 보존하기 위해 각각 별도의 볼륨 폴더가 필요합니다.
7-5. 그럼 Mongo Express 폴더는 VM에 필요없는 이유?
여기서 드는 궁금증 그렇다면 왜 Mongo Express는 폴더가 따로 필요 없는가?
요약하자면 Mongo Express와 pgAdmin은 역할과 데이터 저장 방식이 다르기 때문이다.
1. 웹 인터페이스일 뿐입니다. Mongo Express는 PostgreSQL의 PGAdmin이나 Airflow의 웹서버와 마찬가지로, 백엔드 데이터베이스(database-mongo)의 데이터를 웹 브라우저를 통해 보여주고 조작할 수 있게 해주는 단순한 웹 인터페이스/클라이언트 도구입니다. 2. 별도의 설정 데이터가 거의 없습니다. PGAdmin이 서버 연결 목록, 사용자 관리, 세션 상태 등을 자체적으로 데이터베이스 파일에 저장해야 하는 반면, Mongo Express는 보통 다음과 같이 작동합니다. 설정: Docker Compose 파일에 명시된 환경 변수(ME_CONFIG_MONGODB_SERVER, ADMINUSERNAME, ADMINPASSWORD 등)를 시작 시점에 로드하여 사용합니다. 영구 데이터: Mongo Express가 영구적으로 보존해야 할 사용자 설정, 세션, 로그 기록이 거의 없습니다. 컨테이너가 재시작되어도 환경 변수만 있으면 데이터베이스에 다시 연결할 수 있습니다. 따라서 컨테이너가 죽거나 새로 만들어져도, 설정 파일이 담긴 볼륨이 없어도 기능하는 데 문제가 없습니다.
상태가 없는(Stateless) 도구에 가까워 영구적인 설정을 저장할 필요가 없어, 별도의 볼륨 폴더가 없다.
7-6. Docker를 이용한 PostgreSQL 자동 복원 시 발생하는 DB 이름 문제
admin과 postgres, 그리고 maintaince
정확하게는 YAML에서 기본 DB 이름을 admin과 postgres로 했을 때의 차이
7-7. pg_dump에서 발생한 sql 복원 파일 DROP, ALTER 문제
ubuntu@drawbridge-vm:~/my-project/test$ docker compose logs database-postgres
database-postgres | The files belonging to this database system will be owned by user "postgres".
database-postgres | This user must also own the server process.
database-postgres |
database-postgres | The database cluster will be initialized with locale "en_US.utf8".
database-postgres | The default database encoding has accordingly been set to "UTF8".
database-postgres | The default text search configuration will be set to "english".
database-postgres |
database-postgres | Data page checksums are disabled.
database-postgres |
database-postgres | fixing permissions on existing directory /var/lib/postgresql/data ... ok
database-postgres | creating subdirectories ... ok
database-postgres | selecting dynamic shared memory implementation ... posix
database-postgres | selecting default max_connections ... 100
database-postgres | selecting default shared_buffers ... 128MB
database-postgres | selecting default time zone ... UTC
database-postgres | creating configuration files ... ok
database-postgres | running bootstrap script ... ok
database-postgres | sh: locale: not found
database-postgres | 2025-12-07 10:55:42.207 UTC [36] WARNING: no usable system locales were found
database-postgres | performing post-bootstrap initialization ... ok
database-postgres | syncing data to disk ... ok
database-postgres | initdb: warning: enabling "trust" authentication for local connections
database-postgres | You can change this by editing pg_hba.conf or using the option -A, or
database-postgres | --auth-local and --auth-host, the next time you run initdb.
database-postgres |
database-postgres |
database-postgres | Success. You can now start the database server using:
database-postgres |
database-postgres | pg_ctl -D /var/lib/postgresql/data -l logfile start
database-postgres |
database-postgres | waiting for server to start....2025-12-07 10:55:43.584 UTC [42] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-07 10:55:43.591 UTC [42] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-07 10:55:43.608 UTC [43] LOG: database system was shut down at 2025-12-07 10:55:43 UTC
database-postgres | 2025-12-07 10:55:43.615 UTC [42] LOG: database system is ready to accept connections
database-postgres | done
database-postgres | server started
database-postgres |
database-postgres | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/azure_db_dump.sql
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | set_config
database-postgres | ------------
database-postgres |
database-postgres | (1 row)
database-postgres |
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | 2025-12-07 10:55:43.678 UTC [54] ERROR: publication "postgres_fabricpublication" does not exist
database-postgres | 2025-12-07 10:55:43.678 UTC [54] STATEMENT: DROP PUBLICATION postgres_fabricpublication;
database-postgres | psql:/docker-entrypoint-initdb.d/azure_db_dump.sql:19: ERROR: publication "postgres_fabricpublication" does not exist
database-postgres |
database-postgres | PostgreSQL Database directory appears to contain a database; Skipping initialization
database-postgres |
database-postgres | 2025-12-07 10:55:44.164 UTC [1] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-07 10:55:44.164 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
database-postgres | 2025-12-07 10:55:44.164 UTC [1] LOG: listening on IPv6 address "::", port 5432
database-postgres | 2025-12-07 10:55:44.171 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-07 10:55:44.178 UTC [27] LOG: database system was interrupted; last known up at 2025-12-07 10:55:43 UTC
database-postgres | 2025-12-07 10:55:44.217 UTC [27] LOG: database system was not properly shut down; automatic recovery in progress
database-postgres | 2025-12-07 10:55:44.221 UTC [27] LOG: invalid record length at 0/1708648: wanted 24, got 0
database-postgres | 2025-12-07 10:55:44.221 UTC [27] LOG: redo is not required
database-postgres | 2025-12-07 10:55:44.247 UTC [1] LOG: database system is ready to accept connections
database-postgres | 2025-12-07 10:55:49.148 UTC [40] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:55:54.186 UTC [47] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:55:59.228 UTC [54] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:04.271 UTC [61] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:09.312 UTC [68] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:14.354 UTC [75] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:19.399 UTC [82] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:24.440 UTC [89] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:29.477 UTC [96] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:34.514 UTC [103] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 10:56:39.548 UTC [110] FATAL: database "admin" does not exist
문제가 발생
nano나 vi에서 편집기도 파일 수정
ubuntu@drawbridge-vm:~/my-project/test$ docker compose logs database-postgres
database-postgres | The files belonging to this database system will be owned by user "postgres".
database-postgres | This user must also own the server process.
database-postgres |
database-postgres | The database cluster will be initialized with locale "en_US.utf8".
database-postgres | The default database encoding has accordingly been set to "UTF8".
database-postgres | The default text search configuration will be set to "english".
database-postgres |
database-postgres | Data page checksums are disabled.
database-postgres |
database-postgres | fixing permissions on existing directory /var/lib/postgresql/data ... ok
database-postgres | creating subdirectories ... ok
database-postgres | selecting dynamic shared memory implementation ... posix
database-postgres | selecting default max_connections ... 100
database-postgres | selecting default shared_buffers ... 128MB
database-postgres | selecting default time zone ... UTC
database-postgres | creating configuration files ... ok
database-postgres | running bootstrap script ... ok
database-postgres | sh: locale: not found
database-postgres | 2025-12-07 11:17:44.478 UTC [36] WARNING: no usable system locales were found
database-postgres | performing post-bootstrap initialization ... ok
database-postgres | syncing data to disk ... ok
database-postgres |
database-postgres |
database-postgres | Success. You can now start the database server using:
database-postgres |
database-postgres | pg_ctl -D /var/lib/postgresql/data -l logfile start
database-postgres |
database-postgres | initdb: warning: enabling "trust" authentication for local connections
database-postgres | You can change this by editing pg_hba.conf or using the option -A, or
database-postgres | --auth-local and --auth-host, the next time you run initdb.
database-postgres | waiting for server to start....2025-12-07 11:17:45.672 UTC [42] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-07 11:17:45.675 UTC [42] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-07 11:17:45.684 UTC [43] LOG: database system was shut down at 2025-12-07 11:17:45 UTC
database-postgres | 2025-12-07 11:17:45.692 UTC [42] LOG: database system is ready to accept connections
database-postgres | done
database-postgres | server started
database-postgres |
database-postgres | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/azure_db_dump.sql
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | set_config
database-postgres | ------------
database-postgres |
database-postgres | (1 row)
database-postgres |
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | SET
database-postgres | DROP PUBLICATION
database-postgres | 2025-12-07 11:17:45.766 UTC [54] ERROR: relation "public.sessions" does not exist
database-postgres | 2025-12-07 11:17:45.766 UTC [54] STATEMENT: ALTER TABLE ONLY public.sessions DROP CONSTRAINT sessions_user_id_fkey;
database-postgres | psql:/docker-entrypoint-initdb.d/azure_db_dump.sql:20: ERROR: relation "public.sessions" does not exist
database-postgres |
database-postgres | PostgreSQL Database directory appears to contain a database; Skipping initialization
database-postgres |
database-postgres | 2025-12-07 11:17:46.296 UTC [1] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-07 11:17:46.297 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
database-postgres | 2025-12-07 11:17:46.297 UTC [1] LOG: listening on IPv6 address "::", port 5432
database-postgres | 2025-12-07 11:17:46.302 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-07 11:17:46.309 UTC [27] LOG: database system was interrupted; last known up at 2025-12-07 11:17:45 UTC
database-postgres | 2025-12-07 11:17:46.323 UTC [27] LOG: database system was not properly shut down; automatic recovery in progress
database-postgres | 2025-12-07 11:17:46.325 UTC [27] LOG: invalid record length at 0/1708648: wanted 24, got 0
database-postgres | 2025-12-07 11:17:46.325 UTC [27] LOG: redo is not required
database-postgres | 2025-12-07 11:17:46.346 UTC [1] LOG: database system is ready to accept connections
database-postgres | 2025-12-07 11:17:51.282 UTC [40] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 11:17:56.320 UTC [47] FATAL: database "admin" does not exist
database-postgres | 2025-12-07 11:18:01.367 UTC [54] FATAL: database "admin" does not exist
다시 발생한 오류
하나만 문제가 아니라서 전부 DROP에 예외를 처리
pg_dump로 생성된 덤프 파일은 객체가 존재한다고 가정하고 DROP 명령을 사용한다.
하지만 로컬 환경에서는 객체들이 전부 없기에 스크립트의 모든 DROP 명령이 오류를 유발하고 복원 스크립트 실행이 중단된다.
존재하는 모든 PUBLICATION, TABLE, INDEX, SEQUENCE, EXTENSION, SCHEMA의 DROP과 ALTER에 대해 했다.
85개의 line에 문제가 있어서 전부 추가
7-8. pg_dump에서 발생한 sql 복원 파일 DROP, ALTER 문제 2
6-7에서 DROP과 ALTER는 전부 해결했지만 FATAL admin does not exist 문제는 계속해서 발견
database-postgres | DROP SCHEMA
database-postgres | psql:/docker-entrypoint-initdb.d/azure_db_dump.sql:109: ERROR: could not open extension control file "/usr/local/share/postgresql/extension/azure_cdc.control": No such file or directory
database-postgres | DROP SCHEMA
database-postgres | DROP SCHEMA
database-postgres | DROP SCHEMA
database-postgres | DROP SCHEMA
database-postgres | DROP EXTENSION
database-postgres | DROP SCHEMA
database-postgres | DROP EXTENSION
database-postgres | DROP EXTENSION
database-postgres | 2025-12-07 15:10:48.089 UTC [55] ERROR: could not open extension control file "/usr/local/share/postgresql/extension/azure_cdc.control": No such file or directory
database-postgres | 2025-12-07 15:10:48.089 UTC [55] STATEMENT: CREATE EXTENSION IF NOT EXISTS azure_cdc WITH SCHEMA pg_catalog;
database-postgres |
database-postgres | PostgreSQL Database directory appears to contain a database; Skipping initialization
database-postgres |
database-postgres | 2025-12-07 15:10:48.598 UTC [1] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-07 15:10:48.602 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
database-postgres | 2025-12-07 15:10:48.602 UTC [1] LOG: listening on IPv6 address "::", port 5432
database-postgres | 2025-12-07 15:10:48.609 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-07 15:10:48.620 UTC [26] LOG: database system was interrupted; last known up at 2025-12-07 15:10:48 UTC
database-postgres | 2025-12-07 15:10:48.640 UTC [26] LOG: database system was not properly shut down; automatic recovery in progress
database-postgres | 2025-12-07 15:10:48.645 UTC [26] LOG: redo starts at 0/1708D98
database-postgres | 2025-12-07 15:10:48.646 UTC [26] LOG: invalid record length at 0/1708E90: wanted 24, got 0
database-postgres | 2025-12-07 15:10:48.646 UTC [26] LOG: redo done at 0/1708E48 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
database-postgres | 2025-12-07 15:10:48.671 UTC [1] LOG: database system is ready to accept connections
Azure CDC 관한 오류, 개념 설명
이거를 없애야 함
# OWNER TO "Drawbridge" 구문을 OWNER TO admin 으로 일괄 변경합니다.
# 따옴표(")는 sed 내에서 이스케이프해야 합니다.
sed -i 's/OWNER TO "Drawbridge"/OWNER TO admin/g' $SQL_FILE
ㅇㅇ
구성 요소
역할
설명
sed
Stream Editor
파일의 내용을 수정하는 리눅스 명령어입니다.
-i
In-place
수정 내용을 화면에 출력하는 대신,원본 파일에 즉시 저장하도록 지시합니다.
's/pattern/replacement/g'
Substitution
s(substitute) 명령입니다. 파일 내에서 pattern을 찾아 replacement로 바꿉니다.
pattern
OWNER TO "Drawbridge"
찾을 문자열 패턴입니다.
replacement
OWNER TO admin
패턴을 대체할 문자열입니다.
g
Global
찾은 패턴을각 줄에서 단 한 번만바꾸는 대신,모든 발생 횟수를 바꾸도록 지시합니다 (일괄 변경).
$SQL_FILE
파일 변수
수정할 파일의 경로(azure_db_dump.sql 파일 경로를 저장한 변수)입니다.
설명
#!/bin/bash
# 컨테이너 내부 경로: /docker-entrypoint-initdb.d/
# SQL_FILE="02_azure_db_dump.sql"
SQL_FILE="/docker-entrypoint-initdb.d/02_azure_db_dump.sql"
echo "[DEBUG] 01_fix_dump.sh Starting dump file modifications..."
# 1. OWNER TO 일괄 수정
sed -i 's/OWNER TO \"Drawbridge\"/OWNER TO admin/g' "$SQL_FILE"
sed -i 's/OWNER TO azure_pg_admin/OWNER TO admin/g' "$SQL_FILE"
echo "[DEBUG] OWNER TO 일괄 수정 완료."
# 2-1. Azure 확장 CREATE 구문 주석 처리
# CREATE EXTENSION IF NOT EXISTS azure_cdc...
# CREATE EXTENSION IF NOT EXISTS azure_storage...
sed -i '/CREATE EXTENSION IF NOT EXISTS azure/s/^/-- /g' "$SQL_FILE"
echo "[DEBUG] CREATE EXTENSION azure 구문 주석 처리 완료."
# 2-2. Azure 확장 COMMENT 구문 주석 처리
# COMMENT ON EXTENSION azure_cdc...
# COMMENT ON EXTENSION azure_storage...
sed -i '/COMMENT ON EXTENSION azure/s/^/-- /g' "$SQL_FILE"
echo "[DEBUG] COMMENT ON EXTENSION azure 구문 주석 처리 완료."
# 3-1. 기타 확장 CREATE 구문 주석 처리
# CREATE EXTENSION IF NOT EXISTS pgaadauth WITH SCHEMA pg_catalog;
# CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA pg_catalog;
# CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
sed -i '/CREATE EXTENSION IF NOT EXISTS pg/s/^/-- /g' "$SQL_FILE"
echo "[DEBUG] CREATE EXTENSION pg 구문 주석 처리 완료."
# 3-2. 기타 확장 COMMENT 구문 주석 처리
# COMMENT ON EXTENSION pgaadauth IS 'Microsoft Entra ID Authentication';
# COMMENT ON EXTENSION pg_cron IS 'Job Scheduler for PostgreSQL';
# COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
sed -i '/COMMENT ON EXTENSION pg/s/^/-- /g' "$SQL_FILE"
echo "[DEBUG] COMMENT ON EXTENSION pg 구문 주석 처리 완료."
echo "[DEBUG] 01_fix_dump.sh Modifications complete."
전체 명령 코드
cron EXTENSION이 없다는 에러가 발생하기는 한다
하지만 데이터는 정상 복원 완료.
...인 줄 알았으나 테이블까지만 복원되고 데이터는 없었다.
7-10. CosmosDB 데이터 MongoDB로 복원 중 발생한 decode array 에러
database-mongo | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01_mongo_dump.sh
database-mongo | [INFO] Waiting for MongoDB to fully start using mongosh...
... (MongoDB 연결 성공 로그)
database-mongo | [INFO] MongoDB is up - executing data import.
database-mongo | 2025-12-10T07:16:33.835+0000 Failed: cannot decode array into a primitive.D
database-mongo | 2025-12-10T07:16:33.835+0000 0 document(s) imported
라는 형태의 로그 발생
이 오류 메시지 (cannot decode array into a primitive.D)는 MongoDB 가져오기 도구(mongoimport)가 가져오려는 데이터 파일의 형식과 지정한 데이터 형식이 일치하지 않을 때 주로 발생합니다.
ui-mongo-express | /docker-entrypoint.sh: connect: Connection refused
ui-mongo-express | /docker-entrypoint.sh: line 15: /dev/tcp/database-mongo/27017: Connection refused
ui-mongo-express | Wed Dec 10 07:45:57 UTC 2025 retrying to connect to database-mongo:27017 (8/10)
ui-mongo-express | /docker-entrypoint.sh: connect: Connection refused
ui-mongo-express | /docker-entrypoint.sh: line 15: /dev/tcp/database-mongo/27017: Connection refused
ui-mongo-express | Wed Dec 10 07:45:58 UTC 2025 retrying to connect to database-mongo:27017 (9/10)
ui-mongo-express | /docker-entrypoint.sh: connect: Connection refused
ui-mongo-express | /docker-entrypoint.sh: line 15: /dev/tcp/database-mongo/27017: Connection refused
ui-mongo-express | Wed Dec 10 07:45:59 UTC 2025 retrying to connect to database-mongo:27017 (10/10)
ui-mongo-express | /docker-entrypoint.sh: connect: Connection refused
ui-mongo-express | /docker-entrypoint.sh: line 15: /dev/tcp/database-mongo/27017: Connection refused
ui-mongo-express | No custom config.js found, loading config.default.js
ui-mongo-express | Welcome to mongo-express 1.0.2
ui-mongo-express | ------------------------
ui-mongo-express |
ui-mongo-express |
ui-mongo-express | Mongo Express server listening at http://0.0.0.0:8081
ui-mongo-express | Server is open to allow connections from anyone (0.0.0.0)
ui-mongo-express | basicAuth credentials are "admin:pass", it is recommended you change this in your config.js!
1. 시간차 (Race Condition)Docker Compose는 기본적으로 서비스들을 거의 동시에 시작합니다. 하지만 MongoDB 서버 (database-mongo)가 완전히 부팅되고 27017 포트에서 연결을 받을 준비를 마치는 데는 시간이 걸립니다.mongo-express 컨테이너는 MongoDB보다 빠르게 시작했습니다.mongo-express는 시작하자마자 database-mongo:27017로 연결을 시도했습니다.하지만 그 시점에 database-mongo는 아직 초기화 중이었기 때문에, 연결 요청을 거부했습니다. $\rightarrow$ Connection refused 또는 ECONNREFUSED 발생2. 초기 연결 재시도 메커니즘 (Retry Logic)mongo-express의 로그 상단을 보면 다음과 같은 재시도 로직이 보입니다.ui-mongo-express | Waiting for database-mongo:27017... ui-mongo-express | Wed Dec 10 07:43:46 UTC 2025 retrying to connect to database-mongo:27017 (2/10) ... (10번 반복) mongo-express의 entrypoint.sh 또는 초기화 스크립트에는 MongoDB가 준비될 때까지 일정 횟수 (여기서는 10번) 연결을 시도하는 대기/재시도 로직이 내장되어 있습니다.$\Rightarrow$ 결론: Connection refused가 반복된 시점은 database-mongo 컨테이너가 완전히 준비되기 전에 mongo-express가 먼저 연결을 시도했기 때문입니다.🌟 최종 성공 이유반복적인 연결 실패 메시지에도 불구하고 결국 mongo-express가 성공적으로 접속할 수 있었던 이유는 다음과 같습니다.MongoDB 준비 완료: mongo-express의 재시도 로직이 끝난 후, 결국 database-mongo가 모든 초기화 작업을 완료하고 27017 포트를 열었습니다.연결 재개: 재시도 로직이 끝난 후에도 mongo-express 자체적으로 연결을 시도하거나, 혹은 연결 오류가 발생할 때마다 애플리케이션 레벨에서 연결을 다시 시도하는 과정이 있었고, 결국 MongoDB가 준비된 시점에 연결이 성공했습니다.이러한 문제를 해결하기 위해 Docker Compose에서는 depends_on 외에도 **healthcheck**와 condition: service_healthy 옵션을 사용하여, 종속된 서비스가 단순히 시작된 것뿐만 아니라 '준비 완료' 상태가 될 때까지 기다리도록 설정하는 것이 일반적인 모범 사례입니다.
정상적으로 기존의 CosmosDB의 데이터베이스를 복원했다.
내부 collection들과 데이터들도 정상적으로 복원했다.
7-11. PostgreSQL restore 후 테이블은 있지만 데이터가 없는 에러
이전에 테이블만 보고 데이터가 전부 옮겨졌다고 착각했다.
데이터까지 봤어야 했는데, 무슨 바람이 불었는지 데이터를 확인해보았고... 그 결과 데이터는 없었다.
database-postgres | 2025-12-10T08:17:45.400383444Z COPY 297
database-postgres | 2025-12-10T08:17:45.414991626Z COPY 1294
database-postgres | 2025-12-10T08:17:45.415174827Z 2025-12-10 08:17:45.415 UTC [2568] ERROR: schema "cron" does not exist
database-postgres | 2025-12-10T08:17:45.415190787Z 2025-12-10 08:17:45.415 UTC [2568] STATEMENT: COPY cron.job (jobid, schedule, command, nodename, nodeport, database, username, active, jobname) FROM stdin;
database-postgres | 2025-12-10T08:17:45.415395028Z psql:/docker-entrypoint-initdb.d/02_azure_db_dump.sql:19201095: ERROR: schema "cron" does not exist
database-postgres exited with code 3 (restarting)
database-postgres | 2025-12-10T08:17:46.237532882Z
database-postgres | 2025-12-10T08:17:46.237570443Z PostgreSQL Database directory appears to contain a database; Skipping initialization
database-postgres | 2025-12-10T08:17:46.237574643Z
database-postgres | 2025-12-10T08:17:46.260004239Z 2025-12-10 08:17:46.259 UTC [1] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-10T08:17:46.260062599Z 2025-12-10 08:17:46.259 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
database-postgres | 2025-12-10T08:17:46.260359842Z 2025-12-10 08:17:46.260 UTC [1] LOG: listening on IPv6 address "::", port 5432
database-postgres | 2025-12-10T08:17:46.266473844Z 2025-12-10 08:17:46.266 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-10T08:17:46.272875409Z 2025-12-10 08:17:46.272 UTC [27] LOG: database system was interrupted; last known up at 2025-12-10 08:17:39 UTC
database-postgres | 2025-12-10T08:17:46.284278328Z 2025-12-10 08:17:46.284 UTC [27] LOG: database system was not properly shut down; automatic recovery in progress
database-postgres | 2025-12-10T08:17:46.288706399Z 2025-12-10 08:17:46.288 UTC [27] LOG: redo starts at 1/AE0999E0
database-postgres | 2025-12-10T08:17:47.615519133Z 2025-12-10 08:17:47.615 UTC [27] LOG: invalid record length at 1/D52CB170: wanted 24, got 0
database-postgres | 2025-12-10T08:17:47.615549333Z 2025-12-10 08:17:47.615 UTC [27] LOG: redo done at 1/D52CB148 system usage: CPU: user: 0.61 s, system: 0.71 s, elapsed: 1.32 s
database-postgres | 2025-12-10T08:17:50.749444592Z 2025-12-10 08:17:50.748 UTC [1] LOG: database system is ready to accept connections
다행히도 docker compose logs -ft 옵션으로 계속 로그를 찍고 있었어서 확인할 수 있었다.
pg_cron을 주석 처리했는데(pgaadauth, pg_cron, pg_crypto extension을 pg 기반 sed로 주석 처리) 여기서 문제가 발생했다.
cron 스키마가 계속해서 없다는 에러가 발생한다.
sed -i \
-e 's/OWNER TO \"Drawbridge\"/OWNER TO admin/g' \
-e 's/OWNER TO azure_pg_admin/OWNER TO admin/g' \
-e '/CREATE EXTENSION IF NOT EXISTS azure/s/^/-- /g' \
-e '/COMMENT ON EXTENSION azure/s/^/-- /g' \
-e '/CREATE EXTENSION IF NOT EXISTS pg/s/^/-- /g' \
-e '/COMMENT ON EXTENSION pg/s/^/-- /g' \
-e '/^COPY cron\.job/,/^\.$/ s/^/-- /g' \
-e '/^COPY cron\.job_run_details/,/^\.$/ s/^/-- /g' \
-e '/^\.$/s/^/-- /g' \
"$SQL_FILE"
이런 형태로 sql 주석 처리 스크립트를 짜서 변경했다.
database-postgres | 2025-12-13T18:56:55.868374535Z
database-postgres | 2025-12-13T18:56:55.868391455Z PostgreSQL init process complete; ready for start up.
database-postgres | 2025-12-13T18:56:55.868394615Z
database-postgres | 2025-12-13T18:56:55.890161129Z 2025-12-13 18:56:55.890 UTC [1] LOG: starting PostgreSQL 14.20 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
database-postgres | 2025-12-13T18:56:55.890287849Z 2025-12-13 18:56:55.890 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
database-postgres | 2025-12-13T18:56:55.890294529Z 2025-12-13 18:56:55.890 UTC [1] LOG: listening on IPv6 address "::", port 5432
database-postgres | 2025-12-13T18:56:55.894183397Z 2025-12-13 18:56:55.894 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
database-postgres | 2025-12-13T18:56:55.901092325Z 2025-12-13 18:56:55.900 UTC [2580] LOG: database system was shut down at 2025-12-13 18:56:55 UTC
database-postgres | 2025-12-13T18:56:55.908089735Z 2025-12-13 18:56:55.907 UTC [1] LOG: database system is ready to accept connections
그랬더니 정상적으로 실행한 것처럼 보인다.
7-12. WAL_MAX_SIZE로 인한 Shut down 문제 발생?
database-postgres | 2025-12-13T18:54:00.297553440Z 2025-12-13 18:54:00.297 UTC [43] LOG: checkpoints are occurring too frequently (25 seconds apart)
database-postgres | 2025-12-13T18:54:00.297585400Z 2025-12-13 18:54:00.297 UTC [43] HINT: Consider increasing the configuration parameter "max_wal_size".
database-postgres | 2025-12-13T18:54:28.916353466Z 2025-12-13 18:54:28.916 UTC [43] LOG: checkpoints are occurring too frequently (28 seconds apart)
database-postgres | 2025-12-13T18:54:28.916400187Z 2025-12-13 18:54:28.916 UTC [43] HINT: Consider increasing the configuration parameter "max_wal_size".
database-postgres | 2025-12-13T18:55:58.161870593Z 2025-12-13 18:55:58.161 UTC [43] LOG: checkpoints are occurring too frequently (29 seconds apart)
database-postgres | 2025-12-13T18:55:58.161910673Z 2025-12-13 18:55:58.161 UTC [43] HINT: Consider increasing the configuration parameter "max_wal_size".
database-postgres | 2025-12-13T18:56:24.461408456Z 2025-12-13 18:56:24.461 UTC [43] LOG: checkpoints are occurring too frequently (26 seconds apart)
database-postgres | 2025-12-13T18:56:24.461449336Z 2025-12-13 18:56:24.461 UTC [43] HINT: Consider increasing the configuration parameter "max_wal_size".
database-postgres | 2025-12-13T18:56:48.886490914Z 2025-12-13 18:56:48.886 UTC [43] LOG: checkpoints are occurring too frequently (24 seconds apart)
database-postgres | 2025-12-13T18:56:48.886527194Z 2025-12-13 18:56:48.886 UTC [43] HINT: Consider increasing the configuration parameter "max_wal_size".
database-postgres | 2025-12-13T18:56:52.142191989Z COPY 2083155
database-postgres | 2025-12-13T18:56:52.167559447Z COPY 1186
database-postgres | 2025-12-13T18:56:54.764739567Z waiting for server to shut down....2025-12-13 18:56:54.764 UTC [41] LOG: received fast shutdown request
database-postgres | 2025-12-13T18:56:54.767656388Z 2025-12-13 18:56:54.767 UTC [41] LOG: aborting any active transactions
database-postgres | 2025-12-13T18:56:54.773284667Z 2025-12-13 18:56:54.773 UTC [41] LOG: background worker "logical replication launcher" (PID 48) exited with exit code 1
database-postgres | 2025-12-13T18:56:55.599597603Z 2025-12-13 18:56:55.599 UTC [43] LOG: shutting down
database-postgres | 2025-12-13T18:56:55.776632849Z .2025-12-13 18:56:55.776 UTC [41] LOG: database system is shut down
database-postgres | 2025-12-13T18:56:55.867182367Z done
database-postgres | 2025-12-13T18:56:55.867209847Z server stopped
7-11에서 혹시 몰라 로그를 조금 더 올려보았다.
그러니 WAL_MAX_SIZE 관련으로 checkpoints가 너무 자주 일어난다는 WARNING이 발생했다.
그리고 잠시 뒤에 aborting이 뜨더니... shutting down으로 database까지 꺼진다???
옵션
설명
max_wal_size
WAL 디렉토리 안에 WAL 파일들의 총 용량 최대 크기를 지정할 수 있습니다. soft limit이기 때문에 특별한 상황에 초과할 수 있습니다. 기본값 1GB
min_wal_size
해당 값 이하로는 WAL파일을 정리하지 않습니다. (최초 생성이나 WAL RESET 상황은 제외합니다.) 기본값 80MB
결론부터 말씀드리면, max_wal_size가 너무 작다고 해서 PostgreSQL 서버가 강제로 "셧다운(Shutdown)"되지는 않습니다.
하지만 간접적으로 서버 운영에 심각한 문제를 일으켜 서비스 중단(Outage)이나 비정상 종료(Crash)의 원인이 될 수 있습니다.
1. 잦은 체크포인트의 위험 (성능 저하)
max_wal_size가 너무 작으면(특히 쓰기 작업량이 많은 환경에서) 다음과 같은 성능 문제가 발생합니다.
잦은 체크포인트 요청: PostgreSQL이 max_wal_size에 도달할 때마다 강제로 체크포인트를 시작합니다. 이는 로그에서 본 checkpoints are occurring too frequently 경고의 원인입니다.
I/O 폭증 (I/O Spike): 체크포인트는 메모리(Shared Buffers)의 변경된 모든 데이터를 디스크(데이터 파일)에 기록하는 작업입니다. 이 작업이 빈번하게 발생하면 디스크 I/O 부하가 급증하게 됩니다.
성능 저하 및 지연: I/O 부하가 너무 높아지면, 데이터베이스는 다른 쿼리(쓰기 및 읽기)를 처리하는 속도가 느려지거나 일시적으로 멈출 수 있습니다. 이는 사용자가 느끼는 서비스 지연이나 응답 없음 상태를 초래합니다.
⚠️ 위험 증가: 만약 데이터베이스가 부하를 견디지 못하고 운영체제(OS) 수준에서 OOM(Out of Memory) Killer에 의해 프로세스가 종료되거나, 디스크 I/O가 멈춰버리는 등 시스템 장애가 발생하면 비정상적인 서버 종료(Crash)가 발생할 수 있습니다.
2. 디스크 공간 고갈의 위험 (주요 서버 중단 원인)
max_wal_size 설정 자체가 아니라, WAL 아카이빙(Archiving)이나 복제(Replication) 문제가 함께 발생했을 때 심각한 서버 중단을 일으킬 수 있습니다.
max_wal_size는 WAL 세그먼트를 재활용하거나 삭제하는 시점을 결정하는 **소프트 한계(Soft Limit)**입니다.
만약 WAL을 계속 생성하는데 디스크 아카이브 명령이 실패하거나, 복제 슬롯이 오랫동안 활성화되어 오래된 WAL 파일을 지우지 못하게 되면, WAL 파일은 계속해서 증가하여 max_wal_size를 넘어섭니다.
결국 서버가 실행되는 디스크 공간이 **고갈(Disk Full)**되면 PostgreSQL은 더 이상 WAL을 쓸 수 없게 되어 **강제 종료(Crash)**됩니다. 이는 max_wal_size가 작아서 발생하는 문제라기보다는 디스크 공간 관리 문제입니다.
로그 타임스탬프
로그 내용
2025-12-13T18:56:54.764739567Z
waiting for server to shut down....
2025-12-13T18:56:54.764 UTC [41] LOG: received fast shutdown request
종료 명령 수신! 외부(Docker, Kubernetes, 스크립트 등)에서 PostgreSQL 서버 프로세스에 종료(Shutdown) 요청을 보냈습
2025-12-13T18:56:54.767 UTC [41] LOG: aborting any active transactions
진행 중인 트랜잭션 취소.
2025-12-13T18:56:55.599 UTC [43] LOG: shutting down
백그라운드 프로세스가 종료 절차를 밟고 있음.
2025-12-13T18:56:55.776 UTC [41] LOG: database system is shut down
데이터베이스 시스템 종료 완료.
2025-12-13T18:56:55.867182367Z done
2025-12-13T18:56:55.867209847Z server stopped
서버 프로세스 종료 완료.
데이터베이스 초기화 스크립트(initdb 또는 사용자 정의 스크립트)의 완료입니다.
초기화 스크립트 실행: 컨테이너가 처음 시작될 때, 데이터베이스 구조 생성(CREATE TABLE)과 대용량 데이터 로딩(COPY)을 수행하는 스크립트(예: Docker Entrypoint 스크립트 내의 SQL 실행)가 실행되었습니다.
스크립트 완료: 마지막 COPY 1294 명령이 18:56:52.588Z에 완료된 후, 스크립트가 성공적으로 끝났습니다.
아마 중단에 끝난 게 아니라 sql 복구 스크립트가 끝나서 재실행한 것 같다는 이야기
그래서 sql 맨 아래로 가보니까 딱히 shutting down에 관한 건 없었음
아마 sql 복구 dump 파일이자 명령어니 그건 당연한 건가 싶기도 함
그래서 전체 로그를 다시 읽어보니까
COPY 완료: 모든 COPY 명령이 18:56:52.588Z에 성공적으로 완료되었습니다.
종료 요청 수신: 2025-12-13T18:56:54.764 UTC에 received fast shutdown request 로그가 기록
정상 종료 완료: 2025-12-13T18:56:55.776 UTC에 database system is shut down 로그와 함께 서버가 종료
sed -i \
-e 's/OWNER TO \"Drawbridge\"/OWNER TO admin/g' \
-e 's/OWNER TO azure_pg_admin/OWNER TO admin/g' \
-e '/CREATE EXTENSION IF NOT EXISTS azure/s/^/-- /g' \
-e '/COMMENT ON EXTENSION azure/s/^/-- /g' \
-e '/CREATE EXTENSION IF NOT EXISTS pg/s/^/-- /g' \
-e '/COMMENT ON EXTENSION pg/s/^/-- /g' \
# -e '/^COPY cron\.job/,/^\.$/ s/^/-- /g' \ < 제거
# -e '/^COPY cron\.job_run_details/,/^\.$/ s/^/-- /g' \ < 제거
-e '/^\.$/s/^/-- /g' \
"$SQL_FILE"
문제가 되는 부분이 cron 부분이라고 생각했다.
왜냐하면 저 제거한 2줄짜리 주석은 한 줄 주석이 아니라 블록 단위 주석 처리이기 때문이다.
내가 의도한 부분은 A (다음 줄) A 이렇게 되어있는, 다음 줄까지 걸쳐있는 한두 줄을 주석 처리하고 싶었던 건데,
그게 아니라 A ~~ BCDEF ~~ A 이런 느낌으로 sed 표현식이 받아들여서 전체 블록이 주석이 된 것 같다는 느낌?
여기가 문제
흠... 뭐가 문제지...
다시 한 번 처음부터 확인
보니까 확실히 숫자가 큰 부분의 COPY 데이터에 주석이 붙었다.
그래서 한 번 19951545 라인으로 가서 보기로 했다.
허허 그랬더니 전부 주석이 붙어있다.
위로 올라가서 어디까지 이어져있나 한 번 확인해본다.
아니나 다를까 cron.job이 문제였다.
혹시나 해서 해당 단락의 끝이 어디인지 이동해보았다.
이유를 알았다.
그동안 Ctrl + b (위로 한 페이지 이동), Ctrl + f (아래로 한 페이지 이동)로만 이동하다가
{ (해당 블록의 처음으로 이동)과 } (해당 블록의 마지막으로 이동) 기능을 알아내서 이동해보니 확실해졌다.
cron.job부터 문서의 마지막까지 '하나의 블록'으로 취급 받고 있다.
그리고 모종의 이유로 sed 구문이 잘못 들어가서 전체가 주석이 되었다.
sed -i \
-e 's/OWNER TO \"Drawbridge\"/OWNER TO admin/g' \
-e 's/OWNER TO azure_pg_admin/OWNER TO admin/g' \
-e '/CREATE EXTENSION IF NOT EXISTS azure/s/^/-- /g' \
-e '/COMMENT ON EXTENSION azure/s/^/-- /g' \
-e '/CREATE EXTENSION IF NOT EXISTS pg/s/^/-- /g' \
-e '/COMMENT ON EXTENSION pg/s/^/-- /g' \
-e '/^COPY cron\.job/,/^\.$/ s/^/-- /g' \
-e '/^COPY cron\.job_run_details/,/^\.$/ s/^/-- /g' \
"$SQL_FILE"
사용한 sed 명령어
-e '/^COPY cron\.job/,/^\.$/ s/^/-- /g' \
그중에서 이걸 살펴본다.
-e는 여러 개의 sed 명령을 순차적으로 실행할 때 사용한다.
-e '/^COPY cron\.job/,/^\.$/ s/^/-- /g'
내용
설명
^COPY cron\.job
정규 표현식으로 해석하는 패턴
^
라인의 시작을 의미
COPY cron
리터럴(literal) 텍스트로 COPY cron 구문을 찾으라는 뜻
\.
점(.) 문자는 정규 표현식에서 모든 문자를 의미한다. 앞에 \를 붙여 점 문자 자체로 인식하게 한다.
job
리터럴(literal) 텍스트로 job 구문을 찾으라는 뜻
시작 패턴 내용
다행히도 silver, gold, public 스키마에 대해서 데이터는 전부 들어왔다.
database-postgres | 2025-12-15T09:01:29.407472323Z 2025-12-15 09:01:29.407 UTC [2140] ERROR: schema "cron" does not exist at character 26
database-postgres | 2025-12-15T09:01:29.407483483Z 2025-12-15 09:01:29.407 UTC [2140] STATEMENT: SELECT pg_catalog.setval('cron.jobid_seq', 1, false);
database-postgres | 2025-12-15T09:01:29.407562203Z psql:/docker-entrypoint-initdb.d/02_azure_db_dump.sql:19996720: ERROR: schema "cron" does not exist
database-postgres | 2025-12-15T10:34:22.134563093Z 2025-12-15 10:34:22.134 UTC [2391] ERROR: canceling autovacuum task
database-postgres | 2025-12-15T10:34:22.134602933Z 2025-12-15 10:34:22.134 UTC [2391] CONTEXT: while scanning block 132806 of relation "bronze.brz_hf_meta"
database-postgres | 2025-12-15T10:34:22.134605973Z automatic vacuum of table "admin.bronze.brz_hf_meta"
database-postgres | 2025-12-15T10:34:27.933539677Z 2025-12-15 10:34:27.933 UTC [2006] WARNING: wal_level is insufficient to publish logical changes
database-postgres | 2025-12-15T10:34:27.933560517Z 2025-12-15 10:34:27.933 UTC [2006] HINT: Set wal_level to logical before creating subscriptions.
database-postgres | 2025-12-15T10:34:27.934066641Z psql:/docker-entrypoint-initdb.d/02_azure_db_dump.sql:19996950: WARNING: wal_level is insufficient to publish logical changes
database-postgres | 2025-12-15T10:34:27.934088801Z HINT: Set wal_level to logical before creating subscriptions.
database-postgres | 2025-12-15T10:34:27.934541244Z CREATE PUBLICATION
database-postgres | 2025-12-15T10:34:27.934610405Z ALTER PUBLICATION
database-postgres | 2025-12-15T10:34:27.934965687Z 2025-12-15 10:34:27.934 UTC [2006] ERROR: schema "cron" does not exist
database-postgres | 2025-12-15T10:34:27.934981527Z 2025-12-15 10:34:27.934 UTC [2006] STATEMENT: GRANT USAGE ON SCHEMA cron TO azure_pg_admin WITH GRANT OPTION;
database-postgres | 2025-12-15T10:34:27.935049488Z psql:/docker-entrypoint-initdb.d/02_azure_db_dump.sql:19996959: ERROR: schema "cron" does not exist
# 컨테이너 이름: database-postgres
# 사용자 이름: admin
# 데이터베이스 이름: admin
docker exec -i database-postgres psql -U admin -d admin < azure_db_dump.sql
복원 실행: docker exec을 사용하여 컨테이너 내부에서 psql 명령을 실행하고 SQL 덤프 파일을 파이프(|)로 연결합니다.
docker exec -i: 컨테이너 내부에서 명령을 실행하고 표준 입력(-i)을 허용한다.
psql -U admin -d admin: admin 사용자 및 admin 데이터베이스로 접속한다.
< azure_db_dump.sql: 로컬 VM에 있는 덤프 파일을 읽어 psql의 표준 입력으로 전달한다.
ERROR: schema "gold" does not exist
ERROR: relation "public.users" does not exist
ERROR: relation "public.users" does not exist
ERROR: relation "public.user_skill_scores" does not exist
ERROR: relation "public.sessions" does not exist
ERROR: relation "public.job_seekers" does not exist
ERROR: relation "public.company_members" does not exist
ERROR: relation "public.companies" does not exist
ERROR: relation "public.companies" does not exist
ERROR: schema "meta" does not exist
ERROR: schema "bronze" does not exist
1. Azure/클라우드 전용 객체 오류
이 오류는 Docker 환경의 PostgreSQL에는 존재하지 않는 Azure 클라우드 환경의 특수 기능 때문에 발생합니다. 이는 기능상 무시해도 됩니다.
오류 유형: ERROR: extension "pgaadauth" does not exist, ERROR: extension "azure" does not exist, ERROR: schema "cron" does not exist, ERROR: publication "postgres_fabricpublication" does not exist, ERROR: role "azure_pg_admin" does not exist 등
원인: Azure PostgreSQL은 관리형 서비스이므로 인증(pgaadauth), 로깅(pg_cron), Azure 연동(azure), 논리 복제(publication) 등을 위한 **전용 확장(Extension)**과 **내장 역할(Role)**을 사용합니다. 도커 컨테이너의 표준 PostgreSQL 이미지에는 이들이 포함되어 있지 않아 생성/삭제 명령에서 실패합니다.
해결: 이는 무시해도 됩니다. 애플리케이션의 핵심 데이터(테이블, 데이터) 복원에는 영향을 미치지 않습니다.
2. 객체 존재/부재 오류 (순서 문제)
이 오류는 덤프 파일 실행 순서와 관련된 충돌이며, 덤프에 -c (CLEAN) 옵션을 사용했을 때 자주 발생합니다.
오류 유형: ERROR: relation "public.sessions" does not exist, ERROR: index "ux_user_skill" does not exist
원인: 덤프 파일은 객체를 생성하기 전에 DROP 명령을 넣습니다. 하지만 만약 이전에 복원 시도가 실패했거나, 대상 객체가 애초에 없는 상태라면 DROP 명령이 실패합니다. (예: "야, 세션 테이블 삭제해!" -> "세션 테이블이 없는데요?")
해결: 이 오류 역시 무시해도 됩니다. DROP 명령은 실패했지만, 다음 단계의 CREATE 명령은 성공적으로 실행됩니다.
3. 역할(Role) 및 권한 오류
데이터 복원 및 객체 생성에는 성공했으나, 권한 설정에서 실패한 오류입니다.
오류 유형: ERROR: role "Drawbridge" does not exist, ERROR: role "azure_pg_admin" does not exist
원인: Azure DB는 덤프된 객체(테이블, 스키마 등)에 대해 Drawbridge 같은 특정 역할을 소유자(Owner)로 지정했거나 해당 역할에게 GRANT 권한을 부여하도록 덤프 파일에 기록되어 있습니다. 하지만 Docker 컨테이너의 PostgreSQL에는 Drawbridge라는 역할이 존재하지 않습니다.
해결: 데이터베이스 객체의 소유자 변경이나 권한 부여는 실패했지만, 테이블과 데이터 자체는 생성되었습니다.
# 호스트 VM에 새로운 초기화 디렉터리 생성 (예: my-init-scripts)
mkdir -p ./database/my-init-scripts
# 덤프 파일을 이 디렉터리로 이동
mv azure_db_dump.sql ./database/my-init-scripts/
아니면 시작 시 복원하는 방법도 존재한다.
# docker-compose.yml 파일 수정 예시
services:
database-postgres:
image: postgres:latest
# ... (기존 환경 변수 생략)
volumes:
# 1. 실제 데이터가 저장되는 볼륨 (기존 설정)
- ./database/postgres:/var/lib/postgresql/data
# 2. 초기화 스크립트 볼륨 추가 (새로운 설정)
# 컨테이너가 처음 시작될 때, 이 디렉터리 내부의 .sql 파일을 자동으로 실행합니다.
- ./database/my-init-scripts:/docker-entrypoint-initdb.d
# ...
초기화 스크립트 감지: PostgreSQL 공식 이미지의 시작 스크립트는 /docker-entrypoint-initdb.d 디렉터리(호스트의 ./database/my-init-scripts에 연결됨)를 확인합니다.
덤프 파일 실행: 이 디렉터리 안에 있는 azure_db_dump.sql 파일이 감지되면, PostgreSQL 서버가 초기화된 직후 자동으로 해당 SQL 스크립트가 실행됩니다.
이 때, 덤프 파일에 포함된 모든 CREATE TABLE, INSERT 명령이 실행되어 데이터가 복원됩니다.
서비스 정상 실행: 복원이 완료된 후 PostgreSQL 서비스가 완전히 실행됩니다.
# 1. 컨테이너 종료 및 삭제
docker compose down
# 2. 기존 데이터 볼륨 삭제 (기존 데이터가 있다면 이 단계에서 삭제됩니다.)
sudo rm -rf ./database/postgres
# 3. 새로운 설정으로 컨테이너 다시 실행 (자동 복원 스크립트 실행)
docker compose up -d
복원 시 주의사항 (재실행 문제)
이 자동 복원 방식은 **컨테이너가 처음 실행될 때(즉, /var/lib/postgresql/data가 비어있을 때)**만 작동합니다.
만약 이미 PostgreSQL 컨테이너를 실행하여 데이터(./database/postgres 폴더)가 생성된 상태라면, 자동 복원 스크립트는 다시 실행되지 않습니다.
해결책: 자동 복원 기능을 사용하려면, 반드시 PostgreSQL 데이터 볼륨을 삭제하고 컨테이너를 다시 실행해야 합니다.
ssh-key-YYYY-MM-DD.key의 개인키와 ssh-key-YYYY-MM-DD.key.pub의 공용키를 받았을 것이다.
Key comment (키 설명)
역할: 키를 식별하기 위한 주석(Comment) 또는 이름표 역할
설정: 보통 사용자 이름과 키 생성 날짜(rsa-key-2025-11-27 등)가 자동으로 입력됩니다.
사용: 여러 개의 SSH 키를 사용할 때, 이 키가 어떤 서버(예: oracle-arm-vm-tokyo)에 사용되는 키인지 한눈에 알 수 있다.
Key passphrase (키 암호/비밀번호)
역할: 개인 키(Private Key) 자체를 암호화하여 2차 보안 장치를 추가하는 역할입니다.
설정: 사용자가 설정하는 비밀번호입니다. (일반 계정 비밀번호와는 별개)
사용: PuTTY로 VM에 접속할 때, 개인 키 파일을 등록한 후 추가로 이 암호를 입력해야만 키가 해독되어 접속이 가능합니다.
생성한 키 등록
서버 저장 후 접속
정보 입력 후 접속하기
OS 이미지
PuTTY login as 값
Oracle Linux
opc
Ubuntu
ubuntu
Red Hat (RHEL)
cloud-user 혹은 ec2-user
CentOS
centos
AlmaLinux
cloud-user
Rocky Linux
cloud-user
Windows
opc 또는 Administrator
Marketplace
제공하는 업체마다 상이, 문서 확인 필요
My Images
이미지 생성 시 지정한 이름
이미지 별 login as 값은 위와 같으니 참고하면 된다.
Linux 명령어로 확인 가능
exit이나 logout으로 종료 가능
2. Window CMD로 접속(1, 2번 중 택 1)
ssh -i PEM_ABS_PATH IMAGE@VM_PUBLIC_IP
위와 같은 명령어로 접속 가능
The authenticity of host 'YOUR_ORACLE_PUBLIC_IP (YOUR_ORACLE_PUBLIC_IP)' can't be established.
ED25519 key fingerprint is SHA256:CcQL2uRBFTkw7YOGdZWQzgwBrceltb56iqcm8rv9i20.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
ㅇㅇ
Bad permissions. Try removing permissions for user: BUILTIN\\Users (S-1-5-32-545) on file YOUR_PRIVATE_KEY.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions for 'YOUR_PRIVATE_KEY' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "YOUR_PRIVATE_KEY": bad permissions
IMAGE@VM_PUBLIC_IP: Permission denied (publickey).
ㅇㅇ
# 상속을 비활성화하고, 파일에 정의된 모든 기존 사용자 권한을 제거하는 명령어
icacls "<YOUR_PRIVATE_KEY_ABS_PATH>" /inheritance:r
# 파일을 소유한 현재 사용자에게만 읽기(R) 권한을 명시적으로 부여하는 명령어
icacls "<YOUR_PRIVATE_KEY_ABS_PATH>" /grant:r "%USERNAME%":R
ㅇㅇ
ㅇㅇ
ㅇㅇ
3. Docker
# 패키지 업데이트
sudo apt update
# 필수 패키지 설치
sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
# Docker 공식 GPG 키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Docker Stable 저장소 설정
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker 엔진 설치
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io -y
# Docker Compose V2는 Docker CLI에 통합되어 docker compose 명령으로 사용
# Docker Compose 설치
sudo apt install docker-compose-plugin -y
# 설치 버전 정보 확인
docker compose version
실행해야 하는 전체 명령어
# 패키지 업데이트
sudo apt update
로컬 시스템에 저장된 패키지 저장소(repository) 목록 및 인덱스 파일을 최신 정보로 갱신한다.
Docker 저장소를 추가하고 https 통신을 수행하는 데 필요한 기본 패키지들을 설치한다.
-y 옵션은 설치 시 나오는 확인 질문에 자동으로 Yes라고 답하여 설치 과정을 자동화한다.
apt-transport-https는 HTTPS를 지원하는 옵션
apt가 HTTPS 프로토콜을 사용하여 저장소에 접근할 수 있게 한다.
ca-certifiactes는 인증서 검증을 하는 옵션
서버의 SSL/TLS 인증서의 유효성을 검증하는 데 필요한 CA 인증서를 제공하여 보안 통신을 진행한다.
curl은 파일 전송 도구다.
지정된 URL에서 데이터를 다운로드/업로드한다. 여기서는 Docker의 GPG 키를 다운로드할 때 사용한다.
software-properties-common는 저장소 관리 도구다.
리눅스 저장소(PPA 등)을 추가하거나 제거할 때 필요한 유틸리티를 제공한다.
혹시라도 sudo apt install 명령을 실행하고 위와 같은 화면이 나올 수 있다.
이는 Ubuntu 시스템에서 패키지 업데이트(설치)를 수행한 후, 커널 재시작(업그레이드)이 필요하다는 알림이다.
networkd-dispatcher.service(네트워크 설정 변경 감시 및 이벤트 발생 시 스크립트를 실행하는 서비스)와
unattended-upgrades.service(백그라운드에서 보안 패치 등을 자동으로 처리하는 서비스)가 재시작이 필요하다.
서비스를 선택하고 재시작을 하면 이후 install 할 때 같은 알림이 안 뜬다.
# Docker 공식 GPG 키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
curl -fsSL https://...gpg는 GPG 키 다운로드하려는 명령어다.
└ GnuPG(GNU Privacy Guard)에서 사용되는 암호화 및 디지털 서명 도구
curl 명령을 사용하여 Docker 공식 다운로드 링크에서 공개 GPG 키를 다운로드한다.
이 키는 다운로드할 패키지가 docker에 의해 서명되었음을 확인하는 데 사용한다.
-f는 fail 옵션으로 HTTP 오류 발생 시 자동 종료한다.
-s는 silent 옵션으로 진행 표시줄이나 오류를 출력하지 않는다.
-S는 show error 옵션으로 -s와 함께 사용하며 오류는 표시한다.
-L는 location 옵션으로 서버가 redirect할 경우, 해당 위치로 따라간다.
| 는 Linux에서 사용하는 파이프 명령이다.
A | B의 경우 A의 결과를 B로 건네거나, A 이후 B를 이어서 실행하라는 명령이다.
sudo는 최고 관리자 권한 명령어를 실행한다.
시스템의 보안 관련 키(GPG) 파일을 저장해야 하므로 관리자(root) 권한으로 명령을 실행한다.
gpg는 암호화 및 서명을 담당하는 프로그램이다.
여기서는 키 포맷을 다루는 데 사용한다.
--dearmor는 키 변환 옵션이다.
다운로드한 GPG 키는 보통 텍스트 기반의 ASCII Armor 포맷이다.
이 옵션으로 해당 키를 Linux 시스템이 사용하는 바이너리 형태의 키링 포맷으로 변환한다.
-o /usr/shar/keyrings/ ... 로 출력 파일 위치를 지정한다.
docker-archive-keyring.gpg라는 이름으로 변환한 GPG 키를 지정한 디렉터리에 저장한다.
이 디렉터리는 시스템이 신뢰하는 저장소 키를 보관하는 표준 위치다.
# Docker Stable 저장소 설정
# 다운로드할 Docker 패키지의 무결성과 신뢰성을 보장하고, 시스템에 Docker 공식 다운로드 위치를 지정
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
echo "deb ... stable"은 저장소 경로를 지정하는 명령어다.
Docker 패키지가 위치한 공식 repository의 경로를 설정하는 문자열이다.
deb는 Debian 패키지 약어다. 즉 .deb 형식의 패지키 저장소임을 나타낸다.
[arch=...]는 아키텍처를 지정한다. 시스템의 CPU 아키텍쳐(amd64, arm64 등)에 맞는 Docker 버전을 다운로드한다.
$(dpkg --print-architecture)는 현재 아키텍쳐를 자동으로 출력한다.
signed-by=...로 저장한 GPG 서명 키를 지정하여 보안을 강화한다.
https://...stable은 저장소 URL로 Docker 공식 안정(stable) 버전 저장소 주소다.
$(lsb_release -cs)는 현재 Ubuntu/Debian 시스템의 코드명(jammy, focal 등)을 자동으로 출력한다.
tee는 표준 입력 내용을 받아서
1) 지정된 파일에 내용을 기록하고, 2) 그 내용을 표준 출력으로 동시에 전달해주는 명령어다.
echo의 출력을 바로 redirection(>)으로 쓰려고 하면 sudo 권한이 적용되지 않는다.
tee는 sudo 권한으로 실행하면서 파일 쓰기를 안전하게 수행할 수 있다.
/etc/apt/sources.list.d 폴더는 APT 패키지 관리자가 추가 저장소 목록을 보관하는 표준 위치다.
/docker.list는 새로 추가할 Docker 저장소 정보가 기록될 파일이다.
>를 통해 tee 명령의 표준 출력을 다른 곳으로 보낸다.
/dev/null는 Linux에서 일종의 블랙홀 역할을 한다. tee의 표준 출력으로 내보내는 불필요 메시지가 화면에 출력하지 않게 한다.
여기서 없애는 메시지는 저장소 설정 문자열이다.
# Docker 엔진 설치
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io -y
Docker 저장소 패키지 목록을 시스템이 인식하도록 새롭게 update한다.
sudo apt install로 Docker를 설치한다.
docker-ce는 Docker의 기본 엔진(데몬)이다. ce는 Community Edition의 약어다.
docker-ce-cli는 Docker 데몬과 통신하는 명령어다.
containerd.io는 컨테이너의 생명주기를 관리하는 CNCF의 핵심 컨테이너 런타임이다.
# Docker Compose V2는 Docker CLI에 통합되어 docker compose 명령으로 사용
# Docker Compose 설치
sudo apt install docker-compose-plugin -y
Docker compose 명령어를 사용할 수 있게 하는 compose 플러그인을 설치한다.
# 설치 버전 정보 확인
docker compose version
이후 정상 설치 되었는지 버전을 확인한다.
정상적으로 VM에 Docker를 설치했다.
4. Docker Compose Yaml Copy
# 로컬 컴퓨터의 CLI (터미널/PowerShell)에서 실행
# 이전에 안내된 스크립트를 사용하여 파일을 /home/opc/my-service/ 로 전송합니다.
scp -i /path/to/key.pem docker-compose.yml .env opc@<VM_Public_IP>:/home/opc/my-service/
scp -r -i /path/to/key.pem webapp airflow opc@<VM_Public_IP>:/home/opc/my-service/
ㅇㅇ
# 상속을 비활성화하고, 파일에 정의된 모든 기존 사용자 권한을 제거하는 명령어
icacls "<YOUR_PRIVATE_KEY_ABS_PATH>" /inheritance:r
ㅇㅇ
# 파일을 소유한 현재 사용자에게만 읽기(R) 권한을 명시적으로 부여하는 명령어
icacls "<YOUR_PRIVATE_KEY_ABS_PATH>" /grant:r "%USERNAME%":R
ㅇㅇ
ㅇㅇ
# docker-compose.yml 파일이 위치한 디렉터리에서 실행
docker compose up --build -d
ㅇㅇ
혹시라도 이런 오류가 발생한다면, 권한 설정을 해줘야 한다.
이는 Docker 데몬과 통신할 수 있는 권한이 없기 때문에 발생하는 에러 사항이다.
# 사용자를 docker 그룹에 추가
sudo usermod -aG docker $USER
# 권한 변경 시, 세션을 종료 후 재시작 필요
exit
관리자 권한(sudo)으로 명령어를 실행한다.
usermod는 사용자 정보를 수정하는 명령어고, -aG docker는 사용자를 docker 그룹(G)에 추가(a)하는 옵션이다.
권한이 바뀌었다면 반드시 세션을 재시작해줘야 적용된다.
# Docker 권한이 있는 테스트
docker run hello-world
# 사용자를 시스템의 특정 그룹에서 제거하는 명령어
sudo gpasswd -d $USER docker
Stateless (상태 비저장) 설명 보안 규칙에서 Stateless를 켜고 끌 수 있게 되어 있는데, 이는 해당 규칙이 **트래픽의 상태(State)**를 추적할지 여부를 결정하는 중요한 옵션입니다. Stateful (상태 저장) 규칙 (기본값, 권장) 대부분의 방화벽 규칙은 Stateful입니다. 동작: 방화벽이 특정 세션의 상태를 기억합니다. 인그레스(들어오는) 규칙 설정 시: 만약 외부에서 VM으로 들어오는(Ingress) 트래픽을 허용하면, 그 트래픽에 대한 **응답(Reply)으로 나가는 트래픽(Egress)**은 자동으로 허용됩니다. 장점: 관리자가 나가는 규칙(Egress Rule)을 따로 설정할 필요가 없어 규칙 관리가 단순하고 효율적입니다. Stateless (상태 비저장) 규칙 (Stateless) 이미지에서 토글을 켜지 않은 상태가 기본적으로 Stateful입니다. 만약 Stateless를 활성화(토글 켬)하면 다음과 같이 작동합니다. 동작: 방화벽이 트래픽의 세션 상태를 기억하지 않고, 패킷 하나하나를 규칙에 따라 검사합니다. 양방향 규칙 필요: 인그레스 규칙을 허용했더라도, VM이 외부로 응답(Egress)을 보내려면 반드시 별도의 Egress 규칙을 설정하여 응답 트래픽을 허용해야 합니다. 장점: 세션 추적에 필요한 리소스를 절약하므로 매우 높은 성능이 요구되거나 단방향 통신만 필요한 특정 고급 환경에서 사용됩니다.
Source Port Range (소스 포트 범위) 상세 설명 Source Port Range는 클라우드 방화벽 규칙(특히 OCI의 보안 목록)에서 트래픽을 시작하는(보내는) 장치가 사용하는 포트 번호를 지정하는 항목입니다. 이 항목은 트래픽이 들어오는 포트를 지정하는 Destination Port Range와 역할이 정반대이며, 특히 Stateless(상태 비저장) 규칙을 다룰 때 그 중요성이 커집니다. 1. Source Port Range의 역할 Source Port는 통신을 시작하는 클라이언트 장치 (사용자의 웹 브라우저, Airflow 스케줄러, 다른 서버 등)가 임의로 할당하는 포트입니다. 클라이언트 측 포트: 웹 브라우저나 애플리케이션이 외부 서버(OCI VM)와 통신을 시작할 때, 자신의 로컬 포트 중에서 임시로 할당받는 포트 번호입니다. 범위: 이 포트 번호는 일반적으로 $\text{1024}$ 이상의 포트 중에서 무작위로 할당됩니다 (에페머럴 포트, Ephemeral Port). 2. 왜 Source Port를 따로 지정하지 않는가? 일반적인 Stateful (상태 저장) 인그레스(Ingress, 들어오는) 규칙을 설정할 때, Source Port Range는 대부분 비워두거나 All로 설정합니다. 그 이유는 다음과 같습니다. 임의성: 클라이언트가 어떤 포트를 사용할지 미리 알 수 없으며, 요청이 들어올 때마다 포트 번호가 달라집니다. 따라서 특정 Source Port를 지정하면 대부분의 합법적인 트래픽이 차단됩니다. 보안: 방화벽의 주 목적은 **Destination Port (도착지 포트)**를 막아 서버의 특정 서비스 접근을 통제하는 것이지, 클라이언트가 사용하는 임시 포트를 제한하는 것이 아닙니다. 예시: 당신의 PC에서 VM의 $\text{8080}$ 포트(Destination Port)로 Airflow 접속 요청을 보낼 때, 당신의 PC는 $\text{45678}$번 포트(Source Port)를 사용할 수도 있고, 다음 번에는 $\text{59123}$번 포트를 사용할 수도 있습니다. 3. Source Port Range를 지정해야 하는 특별한 경우 Source Port Range를 특정 값으로 제한해야 하는 경우는 매우 드물며, 주로 다음과 같은 상황에서 발생합니다. 특정 서버만 허용: 만약 당신의 특정 서버 A만 OCI VM의 $\text{8080}$ 포트에 접속하도록 허용하고 싶다면, Source CIDR에 서버 A의 IP를 지정하고, Source Port Range는 비워둡니다. (Source Port는 대부분의 경우 중요하지 않습니다.) Stateless 규칙 사용 시 응답 트래픽 제어: Stateful 방화벽은 인그레스 규칙만 설정하면 응답(Egress)을 자동으로 허용합니다. 하지만 Stateless 방화벽을 사용한다면, 응답 트래픽(VM $\to$ 외부)을 위한 Egress 규칙을 설정해야 합니다. 이때 Egress 규칙의 Destination Port는 외부 클라이언트의 Source Port가 되므로, 이 값이 $\text{1024-65535}$와 같은 넓은 범위로 설정될 수 있습니다.
이러고 새로고침하면 정상적으로 접속 가능
Services
Port Number
Purpose
Container Image
Nginx Proxy Manager
80 81 443
일반 HTTP 접속 및 Let's Encrypt 인증 NPM 관리자 GUI 접속 HTTPS 보안 접속
Name (인스턴스 이름): instance-20251123-1128 → Oracle이 자동으로 임시 이름을 넣어준 것. 원하면 마음대로 바꿀 수 있어요
2) Create in compartment drawbridge (root) → 계정 안에서 VM이 속할 “폴더 같은 공간”입니다. 대부분 사람은 root compartment 하나만 씁니다. → 그냥 저 상태로 두면 돼요. (특별히 여러 프로젝트 구분할 게 아니면 변경할 필요 없음)
Availability Domain(가용 도메인) = 같은 리전 안의 데이터센터 구역
Oracle Cloud 리전(region)은 보통 여러 개의 “AD(Availability Domain)”로 나뉘어 있고, 각 AD는 서로 분리된 데이터센터 구역이에요.
예: 서울 리전 → AD1, AD2, AD3
토론토 리전 → AD1만 있을 수도 있음(지역마다 AD 개수가 다름)
VM을 어떤 방식의 Capacity(자원 형태)로 생성할지 선택하는 단계
On-demand capacity
일반 VM. 안정적. 언제든 사용 가능
Preemptible capacity
값싼 대신 Oracle이 언제든 VM 뺏어갈 수 있음
Capacity reservation
특정 자원을 미리 예약(유료)
Dedicated host
전용 서버(비쌈, Free Tier 불가)
Compute cluster
HPC(고성능 컴퓨팅)용
Cluster placement group(클러스터 배치 그룹) 설정
Cluster placement group은 고성능 컴퓨팅(HPC) 용도예요.
여러 VM을 아주 가까운 물리적 위치에 묶어서
RDMA 같은 초고속 네트워크로 연결하고 싶을 때 사용하는 기능입니다.
머신러닝 대규모 클러스터
계산 과학/시뮬레이션
HPC 노드
→ 일반적인 Web/DB/Airflow/DevOps 환경에서는 완전 불필요.
Fault Domain(장애 도메인) 을 선택하는 단계
✔ Fault Domain이 뭐냐?
하나의 Availability Domain(AD) 안에 있는 서버 랙 그룹(전원/네트워크 분리된 구역)
AD = 큰 데이터센터
FD(FAULT-DOMAIN) = 그 안의 작은 물리적 구역
즉, 동일 AD 안에서 완전히 분리된 하드웨어 그룹들이에요
✔ 개발자/개인 사용자에게 중요한가?
거의 전혀 중요하지 않습니다. DB/웹 서버 1대만 돌리는 상황에서는 FD는 아무 의미 없음.
다만 이런 경우에는 신경 써야 함:
여러 VM을 띄워서 고가용성을 구성할 때
VM 여러 대를 서로 다른 Fault Domain에 배치하고 싶을 때
→ 하지만 지금은 VM 한 대 만들고 옮겨서 돌리는 상황이므로 그냥 기본값 OK.
VM에 어떤 OS 이미지를 설치할지 선택하는 단계(Image)
다양한 서버 이미지 제공
Canonical Ubuntu 22.04 Docker, PostgreSQL, Airflow, npmplus 전부 가장 안정적으로 돌아가는 OS
LTS(Long Term Support)라 2027년까지 안정 지원
Docker, Node.js, Postgres 설치 문서가 전부 Ubuntu 기준
Oracle ARM(A1)에서도 완벽 호환
Free Tier에서 성능 가장 잘 나옴
Airflow 공식 문서도 Ubuntu 기반 추천
Canonical Ubuntu 22.04
기본 패키지 포함한 정상 Ubuntu
이거 추천 (지금 선택한 것)
Canonical Ubuntu 22.04 Minimal
매우 최소 패키지만 포함
나중에 패키지 직접 더 설치해야 해서 귀찮음
aarch64
ARM 전용 빌드
Free Tier A1은 ARM이지만 Full 버전은 자동으로 ARM 지원
Shape(인스턴스 타입) 선택 화면
🔹 1) Virtual machine (기본 선택된 것)
우리가 원하는 옵션
일반적인 VM
OS 설치하고 PostgreSQL/Docker/Airflow/Web 등을 돌릴 수 있음
Ampere A1 Flex 역시 이 카테고리 안에 있음
🔹 2) Bare metal machine (베어메탈)
물리 서버를 통째로 빌리는 방식
Free Tier 불가
너무 비싸고 필요 없음
Shape series (CPU 종류 선택 영역)
여기 4가지가 나와:
① AMD
x86(AMD) CPU
Free Tier에는 초라한 1GB RAM짜리 VM.Standard.E2.1.Micro만 가능
만약 그럼에도 airflow-webserver, airflow-scheduler, postgres 중에 자꾸 꺼지는 게 있다면
위와 같은 명령어를 통해서 가장 최근 50자의 로그를 읽어서 꺼지는 에러를 해결해보자.
webserver, scheduler가 모두 돌아간다면 8080 포트 localhost에 정상적으로 작성한 DAG가 뜬다.
특정 DAG를 눌러서 Trigger DAG로 직접 실행할 수 있다.
이런 식으로 DAG 내의 task들과 실행 결과를 볼 수 있다.
# 스케줄러를 재시작하는 cmd 명령어
docker compose restart airflow-scheduler
# airflow가 인식하는 DAG 목록 확인하는 cmd 명령어(webserver 기준)
docker compose exec airflow-webserver ls -l /opt/airflow/dags
# airflow가 인식하는 DAG 목록 확인하는 cmd 명령어(scheduler 기준)
docker compose exec airflow-scheduler ls -l /opt/airflow/dags
# webserver가 DAG를 인식했는지 확인하는 cmd 명령어
docker compose exec airflow-webserver airflow dags list
그밖에 새롭게 DAG를 추가하고 webserver에 안 뜰 때 쓰는 명령어들
# 파이썬 함수를 실행하는 작업
# 데이터 처리 로직, 파일 읽기/쓰기, Pandas, API 호출 등 대부분의 로직
PythonOperator(
task_id="str",
python_callable=func_name
)
# 기본 Bash/쉘 명령 실행
# 셸 명령 실행, Python 스크립트를 CLI로 실행, 간단한 파일 작업
BashOperator(
task_id="list_files",
bash_command="ls -al /opt/airflow"
)
# if문을 통해 어떤 task를 실행할지 결정
# 파일 존재 여부에 따라 작업 경로 변경, 비즈니스 로직에 따른 작업 선택
BranchPythonOperator(
task_id="decide",
python_callable=func_name
)
# 실제로는 아무것도 안 하는 DAG 구조(Flow) 조정에 사용하는 빈 작업
# 시작점(start), 종료점(end), 병렬 작업 합류점(fan-in), branch 후 merge, ETL Flow 기본 틀 잡기
EmptyOperator(
task_id="start"
)
# 메일 발송, 성공/실패 알림, 리포트 결과 전달 등에 사용
EmailOperator(
task_id="send_email",
to="user@example.com",
subject="Done!",
html_content="Pipeline completed."
)
# 어떤 조건이 충족될 때까지 기다리는 작업
# FileSensor: 특정 파일이 생길 때까지 기다림
# S3KeySensor: S3에 키가 생길 때까지 기다림
# ExternalTaskSensor: 다른 DAG의 Task가 끝날 때까지 기다림
# HttpSensor: HTTP 응답 코드 확인
# SqlSensor: DB 쿼리 결과 조건 충족될 때까지 기다림
FileSensor(
task_id="wait_file",
filepath="/opt/airflow/data/data.csv",
poke_interval=30,
)
# DB에 직접 쿼리 실행하는 작업
# PostgresOperator
# MySqlOperator
# BigQueryInsertJobOperator
PostgresOperator(
task_id="run_sql",
postgres_conn_id="my_db",
sql="SELECT COUNT(*) FROM users;"
)
# 실무에서 쓸 법한 k8s, docker, spark 연동
# DockerOperator: 컨테이너 하나 실행
# KubernetesPodOperator: K8s Pod을 실행(가장 강력한 방법)
# SparkSubmitOperator: Spark job 제출
# Dataproc / Glue / EMR Operators: 클라우드 빅데이터 플랫폼 자동화할 때 사용
USE AdventureWorks;
GO
CREATE NONCLUSTERED COLUMNSTORE INDEX [IX_SalesOrderDetail_ColumnStore]
ON Sales.SalesOrderDetail
(UnitPrice, OrderQty, ProductID)
GO
사용한 쿼리
2가지 방법으로 생성한 index를 확인할 수 있다.
SELECT * FROM sys.indexes WHERE name = 'IX_SalesOrderDetail_ColumnStore';
GO
사용한 쿼리
In - memory OLTP
In-memory database(왼쪽), On-disk database(오른쪽)
데이터베이스와 테이블은 어디에 저장하느냐(RAM, SSD, HDD)에 따라 특징이 달라진다.
그리고 해당 저장소에 어떤 DBMS를 쓰는지에 따라서도 고유한 접근 시간, 속도, 가격 차이가 생긴다.
Memory 기반은 데이터를 읽을 때, disk에서 메모리 버퍼 영역으로 데이터를 로드할 필요가 없다.
때문에 빠른 속도를 지원한다. 그밖에도 아래와 같은 특성 때문에 높은 트랜잭션 처리량을 요구하는 금융, 게임, 웹에서 애용한다.
ㆍ 메인 메모리 저장: 데이터와 인덱스가 서버의 메인 메모리(RAM)에 상주한다.
ㆍ 디스크 I/O 최소화: 데이터를 읽을 때 디스크에 접근할 필요가 없으므로, 디스크 I/O로 인한 병목 현상이 사라진다.
ㆍ 성능 극대화: 디스크 기반보다 트랜잭션 처리(OLTP) 성능이 수십 배까지 높아진다.
이번 실습에서는 메모리 최적화 테이블인 dbo.ShoppingCart를 만든다.
메모리 최적화 테이블 생성절(MEMORY_OPTIMIZED=ON)로 생성해본다.
In-memory OLTP를 사용하기 위해서는 최소 130의 호환성 수준이 필요하다.
이를 위해 호환성 수준을 탐색한다.
SELECT sdb.compatibility_level
FROM sys.databases as sdb
WHERE sdb.name = Db_Name();
사용한 쿼리
ALTER DATABASE CURRENT
SET COMPATIBILITY_LEVEL = 130;
이 쿼리로 사용 수준을 130으로 올려야 한다.
ALTER DATABASE CURRENT
SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT=ON
트랜잭션이 디스크 기반 테이블과 메모리 기반 테이블에 둘 다 상관이 있다면,
트랜잭션 중 메모리 최적화된 부분은 트랜잭션 격리 수준을 SNAPSHOT 레벨로 설정하여 동작해야 한다.
위 쿼리를 통해 메모리 최적화 테이블의 수준을 확실히 강제화 시킨다.
데이터베이스에 여럿이 동시 접근을 할 경우, 문제가 생기는데
SNAPSHOT으로 설정하면 한 시점에서만 값을 보여줘서 일관성을 제공한다??
동시성 사용이 높아진다. 그리고 빨라진다?
이걸 설정을 안 하면 오류가 발생?
ALTER DATABASE AdventureWorks
ADD FILEGROUP AdventureWorks_mod
CONTAINS memory_optimized_data
메모리 최적화 테이블을 만들기 위해서는 사전에 메모리 최적화 파일 그룹을 먼저 만들어 줘야 한다.
참고로 메모리 최적화 파일 그룹은 DB당 하나만 생성할 수 있다.
ALTER DATABASE AdventureWorks
ADD FILE (NAME='AdventureWorks_mod', FILENAME='/var/opt/mssql/data/AdventureWorks_mod')
TO FILEGROUP AdventureWorks_mod
만든 파일 그룹에 메모리 최적화 테이블 전용 파일을 만든다.
Putty로 Linux 가상 머신에 접속하여 확인하면 파란색 글자의 '메모리 최적화 전용 파일'을 볼 수 있다.
참고로 파일 그룹은 하나만 가능하지만, 파일은 여러 개가 가능하다.
CREATE TABLE dbo.ShoppingCart (
ShoppingCartId INT IDENTITY(1, 1) PRIMARY KEY NONCLUSTERED,
UserId INT NOT NULL INDEX ix_UserId NONCLUSTERED HASH WITH (BUCKET_COUNT=1000000),
CreatedDate DATETIME2 NOT NULL,
TotalPrice MONEY
) WITH (MEMORY_OPTIMIZED=ON)
GO
이제 in-memory table을 만든다.
In-memory table을 만들 때는 PK 값을 필수로 요구한다.
Query Store
Query Store는 쿼리, 실행계획, 런타임 통계에 대해 자세한 정보를 수집할 수 있게 해주는 기능이다.
Query Store는 이러한 문제를 해결하기 위해, 데이터베이스의 '블랙박스' 역할을 한다.
ㆍ 실행 계획 변화 추적: 쿼리의 성능이 변했을 때, 쿼리 스토어에 기록된 과거 실행 계획과 현재 실행 계획을 비교하여 원인을 파악할 수 있습니다.
ㆍ 성능 문제 식별: 실행 시간이 오래 걸리거나 CPU를 많이 사용하는 쿼리를 쉽게 식별하여 튜닝 대상을 빠르게 찾을 수 있습니다.
ㆍ 성능 회귀 분석: 특정 패치나 인덱스 변경 후 성능이 저하되었을 경우, 변경 전후의 성능 데이터를 비교하여 문제를 진단하고 이전의 좋은 실행 계획을 강제로 적용할 수 있습니다.
Query Store는 성능 분석에 막대한 양의 정보를 제공하지만, 시스템 성능에 영향을 줄 수도 있기 때문에 기본적으로는 비활성화되어 있습니다.
따라서 이 기능을 사용하려면 데이터베이스 단위로 직접 활성화해주어야 합니다.
ALTER DATABASE AdventureWorks
SET QUERY_STORE = ON (
OPERATION_MODE = READ_WRITE,
QUERY_CAPTURE_MODE = ALL,
MAX_STORAGE_SIZE_MB = 100,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS=30)
);
위 쿼리로 query store를 켜준다.
SELECT TOP 20 * FROm [Person].[Person];
SELECT COUNT(*) FROM [Production].[Product];
SELECT * FROM [Sales].[SalesOrderHeader] WHERE TotalDue > 100;
이후 위와 같은 예제 쿼리를 여러 번 수행해본다.
ㅇㅇ
이런 정보가 나온다.
SELECT Txt.query_text_id, Txt.query_sql_text, Pl.plan_id, Qry.*
FROM sys.query_store_plan AS Pl
JOIN sys.query_store_query AS Qry ON Pl.query_id = Qry.query_id
JOIN sys.query_store_query_text AS Txt ON Qry.query_text_id = Txt.query_text_id;
사용한 쿼리는 위와 같다.
DMV(Dynamic Managment View)
동적 관리뷰는 서버의 상태 정보를 반환해 줘서 서버 인스턴스의 건강 상태와 문제를 진단하고 성능을 튜닝하는 목적으로 제공됩니다.
DMV는 마치 SQL Server 인스턴스에 부착된 '실시간 모니터'와 같습니다.
DMV는 데이터베이스 엔진의 현재 상태 정보를 반환해주는 뷰(View)입니다.
이를 통해 서버의 건강 상태를 진단하고, 성능 문제를 파악하며, 쿼리 튜닝의 실마리를 찾을 수 있습니다.
DMV는 왜 필요한가요?
성능 문제가 발생했을 때, 관리자들은 '지금 이 순간 SQL Server에서 무슨 일이 벌어지고 있는지'를 파악해야 합니다.
DMV는 이러한 질문에 대한 해답을 제공합니다.
ㆍ 실시간 진단: DMV는 버퍼 캐시, 메모리 사용량, 잠금(Lock), 대기(Wait) 상태 등 서버의 다양한 내부 상태를 실시간으로 보여줍니다.
ㆍ 문제점 식별: 특정 쿼리가 리소스를 과도하게 사용하거나, 특정 프로세스가 잠금을 유발하는 등의 문제를 DMV를 통해 정확하게 식별할 수 있습니다.
ㆍ 성능 튜닝의 시작: DMV가 제공하는 정보는 성능 저하의 원인을 파악하고, 어떤 쿼리를 먼저 튜닝해야 할지 결정하는 데 중요한 기준이 됩니다.
ㅇㅇ
SELECT wait_type, wait_time_ms
FROM sys.dm_os_wait_stats;
ㅇㅇ
ㅇㅇ
HA(High Availability)
주로 서비스를 하는 서버와 백업으로 준비된 서비스 서버의 데이터가 항상 동기화 되어야 업무 관련 어플리케이션 동작의 일관성이 유지 될 수 있습니다. 주 서비스 서버에서 보조 서버로 시스템이 전환될 때 서비스가 지속되게 하기 위한 데이터 동기화 방식은 여러가지가 있을 수 있습니다. 데이터베이스와 같이 데이터 자체의 일관성이 중요한 어플리케이션에서 무난하게 데이터 일관성을 보장하기 Corosync, Pacemaker에 기반한 클러스터링 기법을 주로 리눅스 용 클러스터링 방법으로 사용합니다.
window는 failover cluster manager로 고가용성을 관리했다.
고가용성 관리를 위해 VM 2대를 만든다.
각 VM마다 외부 접속을 허용하는 인바운드 규칙으로 1433 포트 번호를 추가한다.
정확하게는 SQL Database 클라이언트 애플리케이션을 호스팅하는 데스크톱 컴퓨터에서 열어야 하는 유일한 포트를 의미한다.
각각의 Putty를 통해 접속한다.
각 VM의 IP를 확인한다.
여기서는 10.208.0.5/24와 10.208.0.6/24에 해당한다.
상호 간에 통신이 가능한지 확인한다.
vi /etc/hosts로 도메인 기반 리다이렉션이 가능하다.
# advanced package tools 및 기본 패키지 설치
sudo apt update -y
sudo apt install curl apt-transport-https software-properties-common -y
# MS SQL Server를 설치하기 위해 MS Ubuntu 서버 repository 경로 등록
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | sudo tee /etc/apt/sources.list.d/mssql-server-2022.list
# 저장소 갱신
sudo apt-get update
# MS SQL Server 설치
sudo apt-get install -y mssql-server
# MS SQL Server Setup 진행
sudo /opt/mssql/bin/mssql-conf setup
상호 통신을 확인했다면 SQLserver를 각각 설치하고 Setup을 진행한다.
비밀번호는 동일하게 SQLserver123으로 진행했다.
# 가용성 그룹 설정 ON
sudo /opt/mssql/bin/mssql-conf set hadr.hadrenabled 1
# MS SQL Server 재시작
sudo systemctl restart mssql-server
ㅇㅇ
이후 SSMS에서 각각의 VM에 접속한다.
CREATE LOGIN dbm_login WITH PASSWORD = 'Password123%'
CREATE USER dbm_user FOR LOGIN dbm_login;
서버 1, 서버 2 모두 진행
-- 마스터 키와 인증서 생성
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'Password123%';
CREATE CERTIFICATE dbm_certificate WITH SUBJECT = 'dbm';
-- 인증서 백업(linux 파일 경로)
BACKUP CERTIFICATE dbm_certificate
TO FILE = '/var/opt/mssql/data/dbm_certificate.cer'
WITH PRIVATE KEY (
FILE = '/var/opt/mssql/data/dbm_certificate.pvk',
ENCRYPTION BY PASSWORD = 'Password123%'
)
서버 1에서만 진행
각각 진행하지 않는 이유는 같은 인증서와 개인키를 써야 하는데, 각각 생성하면 다른 인증서와 개인키를 생성한다.
즉 한 쪽에서만 생성하고 이후에 다른 쪽 서버로 옮겨줘야 한다.
1에서만 cer(인증서)와 pvk(개인키)가 있어야 한다.
passwd root
vi /etc/ssh/sshd_config
service sshd restart
Endpoint 작업을 수행하기 전에 먼저 간단하게 방화벽을 양쪽 서버 모두 내려주는 작업을 한다.
실제로는 특정 포트만 허용하는 것이 바람직하기에 선택지 2번으로 포트를 허용하는 것을 추천한다.
-- ENDPOINT를 생성하는 쿼리
CREATE ENDPOINT [Hadr_endpoint]
AS TCP (LISTENER_IP = (YOUR_VM_INTERNAL_IP), LISTENER_PORT = 5022)
FOR DATA_MIRRORING (
ROLE = ALL,
AUTHENTICATION = CERTIFICATE dbm_certificate,
ENCRYPTION = REQUIRED ALGORITHM AES
)
ALTER ENDPOINT [Hadr_endpiont] STATE = STARTED
GRANT CONNECT ON ENDPOINT::[Hadr_endpoint] TO [dbm_login]
-- ENDPOINT를 확인하는 쿼리
SELECT * FROM sys.endpoints
위 쿼리는 가용성 그룹을 위한 listener endpoint를 생성하는 쿼리다.
YOUR_VM_INTERNAL_IP에 위에서 확인한 ip 주소를 적어넣는다.
각 VM에 대해서 SSMS에서 전부 진행해준다.
위처럼 endpoint를 확인할 수 있으면 성공이다.
ALTER ENDPOINT Hadr_endpoint STATE = STARTED;
만약 다양한 사유로 생성한 endpoint가 STOPPED 상태라면 위 쿼리로 활성화한다.
서버 1에서만 실행
좌측에서 가용성 부분에서 확인 가능하다.
CREATE AVAILABILITY GROUP [Server1]
WITH (DB_FAILOVER = ON, CLUSTER_TYPE = NONE)
FOR REPLICA ON
N'HA-VM-2' WITH (
ENDPOINT_URL = N'tcp://HA-VM-2:5022',
AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
FAILOVER_MODE = MANUAL,
SEEDING_MODE = AUTOMATIC,
SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL)
),
N'HA-VM-3' WITH (
ENDPOINT_URL = N'tcp://HA-VM-3:5022',
AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
FAILOVER_MODE = MANUAL,
SEEDING_MODE = AUTOMATIC,
SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL)
)
ALTER AVAILABILITY GROUP [Server1] GRANT CREATE ANY DATABASE
사용한 쿼리
실제 DB를 만들 때 가용성 그룹에 추가하여 관리한다.
가용성 그룹에 추가하려는 DB가 완전 복구 모드인지, 유효한 로그 백업이 있는지 확인해야 한다.
테스트할 DB를 만들었다면 데이터베이스 백업을 수행해야 한다.
-- DB 생성, 완전 복구 모드로 변환, disk에 백업
CREATE DATABASE [test_database]
ALTER DATABASE [test_database] SET RECOVERY FULL
BACKUP DATABASE [test_database] TO DISK = N'NUL'
-- 가용성 그룹에 추가
ALTER AVAILABILITY GROUP [Server1] ADD DATABASE [test_database]
서버 1에서만 실행, 사용한 쿼리
해당 DB가 동기화 상태인지 확인한다.
이후에 'ALTER AVAILABILITY GROUP [Server1] FORCE_FAILOVER_ALLOW_DATA_LOSS;'를 통해
서버 2번에서 강제 failover로 Primary로 변경하는 것도 가능하다.
SQL Server in Container and k8s
SQL Server 컨테이너화의 주요 특징 및 장점
ㆍ 빠른 배포 및 경량화: OS 설치 및 설정 과정이 생략되어 몇 초 만에 SQL Server 인스턴스를 시작할 수 있다.
ㆍ 개발/테스트 환경의 일관성: 개발, QA, 운영 환경이 동일한 컨테이너 이미지로 구성되어 로컬 문제를 방지합니다.
ㆍ 운영 효율성 향상: Dockerfile 을 통해 환경 설정을 코드화(IaC: Infrastructure as Code)할 수 있어, 반복 수동 작업을 줄인다.
ㆍ 효율적인 리소스 사용: 호스트 OS 의 커널을 공유하므로 VM보다 훨씬 가벼우며, 필요한 리소스만 할당하여 사용 가능하다.
ㆍ 고가용성 및 확장성: 쿠버네티스로 SQL Server 인스턴스를 손쉽게 복제, 관리하여 장애 발생 시 자동 복구 구현이 가능하다.
SQL Server 컨테이너화의 단점 및 고려사항
ㆍ 성능 오버헤드: OS 위에 컨테이너 런타임이 추가되므로, 성능 오버헤드가 발생할 수 있다.
ㆍ 운영 복잡성 증가: Docker를 넘어 쿠버네티스까지 도입할 경우, 오케스트레이션에 대한 학습과 운영 노하우가 필요하다.
ㆍ 영구 데이터 관리: 컨테이너의 본질은 임시다. 데이터 영속성(Persistence)을 위해 스토리지 개념을 명확히 이해해야 한다.
└ 볼륨(Volume), PersistentVolumeClaim(PVC) 등
ㆍ 보안 및 모니터링: 컨테이너 환경에 최적화된 보안 설정 및 모니터링 체계를 별도로 구축해야 힌다.
가상 머신(VM)과 컨테이너의 차이점
ㆍ 가상 머신(VM): 하드웨어 위에 하이퍼바이저를 통해 Guest OS를 통째로 올리는 방식, 무겁고 부팅 시간이 오래 걸린다.
ㆍ 컨테이너: 호스트 OS의 커널을 공유하며, 애플리케이션과 필요한 라이브러리만 패키징, 훨씬 가볍고, 리소스 효율성이 좋다.
실습용 VM을 하나 만든다.
외부에서 접속이 가능하게끔 1433 인바운드 포트 규칙을 허용한다.
설정한 아이디와 비밀번호를 기반으로 putty로 접속한다.
# 시스템 업데이트 및 필수 패키지 설치
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
# Docker 공식 GPG 키 추가
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Docker Repository 추가
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 시스템 업데이트 및 Docker Engine 설치
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
# Docker 설치 확인 및 사용자 권한 설정
docker -v # sudo docker run hello-world
sudo usermod -aG docker $USER # sudo 없이 docker 명령어를 사용하기 위해 현재 사용자를 docker 그룹에 추가
# SQL Server 컨테이너 실행
docker run -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=Contoso!0000' -p 1433:1433 --name mssql-test -d mcr.microsoft.com/mssql/server:2022-latest
# 컨테이너 확인
docker ps
위 명령어를 따라 linux 기반으로 VM에 docker를 설치하고 실행할 수 있다.
로컬 SSMS로 접속하면 이제 컨테이너에서 생성한 MS SQL에 접속할 수 있다.
(주소는 Azure VM의 공용 IP 주소, 계정은 SA, 비밀번호는 명령어에 따라 Contoso!0000다.)
SQL Server Docker Volume
Container는 기본적으로 임시적이다.
프로덕션 환경에서 데이터베이스 컨테이너를 사용할 때, 데이터를 컨테이너 외부의 영구적인 저장소에 보관해야 한다.
└ Production Environment: 서비스나 제품이 실제로 사용자에게 제공되고 운영되는 실제 환경
Docker에서 가장 일반적인 방법은 Docker Volume을 사용한다.
Docker Volume은 컨테이너가 생성되고 삭제되는 것과 무관하게 데이터를 영구적으로 저장할 수 있는 디렉터리다.
Volume은 호스트 OS의 특정 디렉터리를 컨테이너의 디렉터리에 연결(마운트)하는 방식으로 동작한다.
Volume의 특징들은 다음과 같다.
ㆍ 컨테이너의 수명 주기와 독립적이다.
ㆍ Docker가 관리하는 별도의 영역에 생성한다.
ㆍ 컨테이너 간에 데이터를 공유할 때 유용하다.
ㆍ 호스트 머신의 파일 시스템에 직접 접근하는 bind mount보다 성능이 좋고, 보안성이 뛰어나다.
# 기존 컨테이너 및 이미지 정리
docker stop mssql-test
docker rm mssql-test
# Docker Volume 생성 및 확인
docker volume create mssql_data
docker volume ls
# Volume 기반 SQL Server Container 실행
docker run -e "ACCEPT_EULA=Y" \
-e 'MSSQL_SA_PASSWORD=Contoso!0000' \
-p 1433:1433 \
--name mssql-with-volume \
-v mssql_data:/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latest
# 컨테이너 확인
docker ps # STATUS가 Up인지 확인
Volume 기반으로 컨테이너를 생성한다.
Volume을 위해 VM MS SQL에 재연결 후 DB, 테이블, 데이터를 생성해본다.
CREATE DATABASE testDB;
GO
USE testDB;
GO
CREATE TABLE testTable (ID INT, Name NVARCHAR(50));
INSERT INTO testTable VALUES (1, 'test_sentence');
GO
SELECT * FROM testTable;
GO
사용한 쿼리
# 검증을 위해 컨테이너 정지 후 삭제
docker stop mssql-with-volume
docker rm mssql-with-volume
# 새로운 컨테이너 생성
docker run -e "ACCEPT_EULA=Y" -e 'MSSQL_SA_PASSWORD=Contoso!0000' -p 1433:1433 --name mssql-validation -v mssql_data:/var/opt/mssql -d mcr.microsoft.com/mssql/server:2022-latest
Volume에 데이터를 보존하고 있나 확인하기 위해, 삭제 후 재생성한다.
로컬 SSMS에서 재연결하고 확인해보면 데이터가 살아있다.
k8s
# kubectl 설치
curl -LO "https://dl.k8s.io/release/v1.30.1/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
kubectl version
# minikube 설치
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube version
# minikube 클러스터 시작
minikube start --driver=docker --force
호환성 수준은 DB가 어떤 SQL Server 버전의 쿼리 동작 방식과 기능 집합을 따를지를 정하는 설정이다.
호환성 수준 값
해당 SQL Server 버전
80
SQL Server 2000
90
SQL Server 2005
100
SQL Server 2008
110
SQL Server 2012
120
SQL Server 2014
130
SQL Server 2016
140
SQL Server 2017
150
SQL Server 2019
160
SQL Server 2022
호환성 수준에 따른 버전은 위와 같다.
USE [master];
GO
ALTER DATABASE [AdventureWorks2017] SET QUERY_STORE = ON;
GO
ALTER DATABASE [AdventureWorks2017] SET QUERY_STORE (OPERATION_MODE = READ_WRITE);
GO
ALTER DATABASE [AdventureWorks2017] SET COMPATIBILITY_LEVEL = 100;
GO
사용한 쿼리는 위와 같다.
USE [master];
GO
ALTER DATABASE [AdventureWorks2017] SET COMPATIBILITY_LEVEL = 160;
GO
RESTORE DATABASE AdventureWorks2017
FROM DISK = 'C:\LabFiles\Monitor and optimize\AdventureWorks2017.bak'
WITH RECOVERY,
MOVE 'AdventureWorks2017'
TO 'C:\LabFiles\Monitor and optimize\AdventureWorks2017.mdf',
MOVE 'AdventureWorks2017_log'
TO 'C:\LabFiles\Monitor and optimize\AdventureWorks2017_log.ldf';
사용한 쿼리다. bak를 저장한 위치에 따라 경로를 바꿔서 실행하면 된다.
조각화 상태가 있는지 확인하는 쿼리
USE AdventureWorks2017
GO
SELECT
i.name AS Index_Name, -- 인덱스 이름
avg_fragmentation_in_percent, -- 평균 조각화율
db_name(database_id) AS Database_Name, -- 데이터베이스 이름
i.object_id, -- 인덱스가 속한 객체의 ID (테이블 등)
i.index_id, -- 인덱스 ID
index_type_desc -- 인덱스 유형 설명 (예: CLUSTERED, NONCLUSTERED)
FROM
sys.dm_db_index_physical_stats(
db_id('AdventureWorks2017'), -- 대상 데이터베이스 ID
object_id('person.address'), -- 대상 테이블의 객체 ID
NULL, NULL, -- 인덱스,파티션 ID는 NULL(모든 인덱스 포함)
'DETAILED' -- 'DETAILED' 모드로 상세 조각화 정보 수집
) ps
INNER JOIN sys.indexes i
ON ps.object_id = i.object_id
AND ps.index_id = i.index_id -- 인덱스 메타데이터와 조각화 정보 조인
WHERE avg_fragmentation_in_percent > 50 -- 조각화율이 50% 초과인 인덱스만 선택
사용한 쿼리
USE AdventureWorks2017
GO
INSERT INTO [Person].[Address]
([AddressLine1], [AddressLine2], [City], [StateProvinceID], [PostalCode], [SpatialLocation], [rowguid], [ModifiedDate])
SELECT
AddressLine1,
AddressLine2,
'Amsterdam',
StateProvinceID,
PostalCode,
SpatialLocation,
newid(),
getdate()
FROM
Person.Address;
GO
쿼리로 데이터 추가하여 index 일부러 파편화하기
다시 확인해보면 조각난 index를 볼 수 있다.
상단의 쿼리는 조각 모음 쿼리다.
하단은 조각난 상태의 index에서 쿼리 실행 결과다.
USE AdventureWorks2017
GO
ALTER INDEX [IX_Address_StateProvinceID] ON [Person].[Address] REBUILD PARTITION = ALL
WITH (
PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF,
ONLINE = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON
)
조각화제거를 하고, 통계도기본적으로다시계산한다. (STATISTICS_NORECOMPUTE = OFF)
SET STATISTICS IO, TIME ON
GO
USE AdventureWorks2017
GO
SELECT
DISTINCT (StateProvinceID),
count(StateProvinceID) AS CustomerCount
FROM person.Address
GROUP BY StateProvinceID
ORDER BY count(StateProvinceID) DESC;
GO
sudo su
cd /etc/skel
vi .vimrc
vi .bashrc # PS1="\h[\u \w]$ "
cd /root
cp /etc/skel/.vimrc /root
vi .bashrc # PS1="\h[\u \w]$ " (다음라인) alias vi='vim'
ㅇㅇ
필수는 아니고 글자 크기나 이런 걸 정하는 파일
# advanced package tools 및 기본 패키지 설치
sudo apt update -y
sudo apt install curl apt-transport-https software-properties-common -y
# MS SQL Server를 설치하기 위해 MS Ubuntu 서버 repository 경로 등록
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | sudo tee /etc/apt/sources.list.d/mssql-server-2022.list
# 저장소 갱신
sudo apt-get update
# MS SQL Server 설치
sudo apt-get install -y mssql-server
# MS SQL Server 실행
sudo /opt/mssql/bin/mssql-conf setup
ㅇㅇ
비번 SQLserver123
ㅇㅇ
# 패키지 설치
curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo apt-get install mssql-tools18 unixodbc-dev
# 환경 변수 추가
echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bash_profile
source ~/.bash_profile
echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc
source ~/.bashrc
# SQLCMD로 SQL Server에 접속
sqlcmd -S localhost -U SA -P '비밀번호' -C
ㅇㅇ
ㅇㅇ
SQL Server on Linux는 보통 /var/opt/mssql/data 경로의 하위에 mdf 파일 및 ldf 파일을 보관한다.
MDF(Primary Data File) 파일은 데이터베이스의 핵심 데이터를 저장한다.
(테이블, 뷰, 저장 프로시저 등) 하나의 데이터베이스당 기본적으로 1개의 MDF 파일이 반드시 존재해야 한다. LDF(Transaction Log File) 파일은 트랜잭션 로그를 저장한다. 데이터 변경 내역, 트랜잭션 복구/롤백에 사용한다. SQL Server는 LDF 파일을 사용해서 복원, 롤백, 복구 등의 작업을 수행한다.
ㅇㅇ
ㅇㅇ
chown mssql:mssql FabrikamFiber*으로 소유권자를 변경해야 한다.
cd /var/opt/mssql
mkdir backup
cd backup
wget https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2014.bak
chown mssql:mssql AdventureWorks2014.bak
cd ..
chown mssql:mssql backup
ㅇㅇ
ㅇㅇ
혹시라도 접속이 안 되면 포트 확인하기
ㅇㅇ
ㅇㅇ
또는 SQL 명령으로 백업
USE master;
GO
RESTORE DATABASE AdventureWorks
FROM DISK = '/var/opt/mssql/backup/AdventureWorks2014.bak'
WITH
MOVE 'AdventureWorks2014_Data' TO '/var/opt/mssql/data/AdventureWorks2014_Data.mdf',
MOVE 'AdventureWorks2014_Log' TO '/var/opt/mssql/data/AdventureWorks2014_Log.ldf';
GO
ㅇㅇ
ㅇㅇ
/var/opt/mssql 경로에 Samples 폴더를 mkdir로 만들고 vi로 예제 txt 파일을 하나 만든다.
이렇게 적은 내용은 cat 명령으로 확인할 수 있다.
Linux와 Windows는 newline을 다르게 관리한다.
Preview 단계에서는 SQL Server on Linux는 오직 Windows 포맷으로만 newline을 인식할 수 있다.
만약 Windows 에서 작성한 txt 파일을 복사하여 넣은 것이 아니라, txt를 직접 리눅스에서 만들어 줬다면 변환 작업이 필요하다.
이렇게 DC 서버로 승격하는 설정을 하고 나면, DC 설정을 적용하기 위해 재부팅을 시작한다.
재부팅에 꽤 오랜 시간이 걸리니 유의하자.
재부팅을 기다리는 사이에 아까 생성한 DC VM의 IP와 DNS를 이어준다.
'네트워킹 > 네트워크 설정' 탭에 들어가면 '개인 IP 주소'가 있다.
이 IP 주소는 DC-VM-1dt008-1의 주소를 가리킨다. 이 주소를 복사한다.
복사한 주소를 '설정 > DNS 서버' 탭에 들어가서 사용자 지정으로 바꾼 다음, IP 주소를 추가한다.
이제 DNS를 사용하여 접속하면 자동으로 IP 매핑을 하여 접속할 수 있다.
이제 3개의 Subnet과 DC VM 1개의 설정이 끝났다.
환경에 대한 설정이 끝났으니, 이제 프로그램을 설치하고 관리할 사람이 필요하다. 이를 위해 2개의 계정을 만든다.
하나는 감독 계정으로, 모든 VM에 로그인해서 SQL Server를 설치하고, 클러스터와 가용성 그룹을 설정하는 계정이다.
다른 하나는 일꾼 계정으로, SQL Server 서비스를 실행하고 데이터를 처리하는 계정이다.
Pythonic하게 비유를 하자면 이제까지 파이썬 설치와 환경변수 설정을 했다면
Master(감독 계정)는 라이브러리나 extension을 설치하고 코드를 짜는 사람인 거고,
Slave(일꾼 계정)는 작성한 코드를 계속해서 실행해주는 사람이다.
이때 Master는 설치를 주로 하기에 install이라는 이름의 계정으로 생성했다.
DC VM의 Server Manager 창을 위처럼 다시 띄운다.
우상단의 'Tools > Active Directory Administrative Center' 탭으로 들어간다.
'좌측의_내가_작성한_최상위_도메인 > Users > New > User'로 계정을 만들기 위해 들어간다.
감독 계정을 만든다.
Full name과 User AccountName에 설치를 주로 할 계정이기에 install이라는 이름을 지어줬다.
비밀번호는 admin123%으로 했지만, 본인이 기억할 수 있는 거라면 뭐든 상관없다.
이 계정을 이제 MASTER 계정이라고 명명한다.
일꾼 계정을 만든다.
Full name과 User AccountName에 코드를 주로 돌리기에 SQLslave라는 이름을 지어줬다.
비밀번호는 slave123%으로 했지만, 본인이 기억할 수 있는 거라면 뭐든 상관없다.
이 계정을 이제 SLAVE 계정이라고 명명한다.
MASTER 계정은 모든 VM에 접속해서 설치, 설정을 해야 하는 계정이다.
그렇기에 모든 곳에 들어가서 작업할 수 있는 권한을 설정해야 한다.
'Builtin > Properties' 탭으로 들어간다.
MASTER 계정에 대한 권한 설정 세부 사항이다.
위와 같은 순서대로 접속하여 Permissions에 Full Control을 골라준다.
계정 생성까지 끝났으니 이제 실제 SQL Server를 실을 VM을 만들 차례다.
이렇게 생성하는 VM은 SQL VM이라고 명명하겠다.
단순 참고 사항으로 on-premises 같은 경우 공용 IP 주소를 사용하지 않는다.
보안을 위해 대부분 개인 IP 주소만 사용한다.
외부에서 직접 접속할 수 없게 만들어 해킹과 같은 위협으로부터 VM을 보호한다고 한다.
Azure Resource에서 '가상 머신'을 검색해서 여러 설정을 진행한 뒤 생성한다.
DC VM을 만들 때와 같이 '가상머신 이름', '이미지', '크기', '관리자 계정'을 신경써서 만들어야 한다.
DC VM 때와 마찬가지로 자원을 절약해야 한다면, 디스크 설정을 변경하자.
네트워킹 탭에서 아까 만들어둔 10.5.n.0에 해당하는 서브넷을 이제 사용할 때가 됐다.
서브넷 탭에서 1번과 2번 중에 원하는 서브넷으로 이어준다.
이후 검토 + 만들기로 SQL VM을 생성한다.
같은 방식으로 2번째 SQL VM도 생성한다.
다만 이때 '기본 사항 > 가용성 영역'에서 Zone 2으로 변경하고 만들어도 된다. (굳이 안 해도 된다는 말과 같다.)
이 가용성 영역은 VM을 만드는 데이터 센터의 위치를 말한다.
그러니까 1번 Azure 데이터 센터에 만들래? 2번 Azure 데이터 센터에 만들래?라고 물어보는 것이다.
당연히 어느 데이터 센터에 만들든 상관없지만, 가용성 영역을 나눈다면 이 자체로 HA(고가용성)을 추구하는 것이다.
어찌됐든 편한 방법으로 2개의 SQL VM을 생성하자.
아까 DC-subnet, SQL-subnet-1, SQL-subnet-2를 만들었고,
각각의 subnet에는 subnet mask를 24로 설정하여 256개의 단말(네트워크)을 등록할 수 있었다.
물론 5개의 예약 IP 때문에 실질적으로 사용 가능한 주소는 251개 뿐이었다.
이제 생성한 2개의 SQL VM을 SQL-subnet의 단말로서 등록해야 한다.
마치 공유기(subnet)에서 핸드폰(SQL VM)과 연결하여 네트워킹하는 것처럼 말이다.
이를 위해 '네트워크 > 네트워크 설정 > 네트워크 인터페이스/IP 구성'으로 들어간다.
IP 설정을 추가하여 어떤 주소로 사용할지 정해야 한다.
0, 1, 2, 3, 255는 이미 예약 IP로 사용하고 있기에 다섯 개를 제외한 그 어떤 숫자를 써도 상관없다.
여기서는 임의로 10.5.1.7로 설정했다.
3번째 octet이 1인 이유는 첫 번째 subnet에 등록한 SQL VM이기 때문이다.
마찬가지로 2번째 SQL VM에 대해서도 10.5.2.n으로 설정한다.
이제 두 개의 SQL VM을 '도메인'에 가입(join)하는 과정을 진행한다. 앞서 만든 SQL VM들을 root.domain.com이라는 거대한 '네트워크 마을(도메인)'의 정식 주민으로 등록하는 과정이다.
위 사진처럼 SQL VM의 RDP로 각각 접속하여 Server Manager 창이 뜰 때까지 기다린다.
이후 좌측의 Local Server 탭으로 들어가서 상단의 Workgroup을 눌러준다.
Change로 들어가 Member of Domain을 눌러 DC VM에서 설정한 Domain 이름으로 연결한다.
관리자 계정과 암호에는 DC VM의 도메인에 가입하는 과정이기 때문에 DC 계정을 입력한다.
만약 모든 VM의 ID와 PW를 동일하게 설정했다면, ROOT\DomainAdmin 이런 식으로 명시해줘야 인식한다.
SQL-VM-1과 SQL -VM-2 모두 RDP로 접속하여 Workgroup을 바꿔준다.
도메인에 가입해야 이유는 다음과 같다.
└ 중앙 관리: Domain Controller가 모든 VM을 한꺼번에 관리할 수 있다. 보안 정책, 사용자 권한 설정을 한 번에 적용할 수 있다. └ 보안 강화: 도메인에 속한 모든 컴퓨터끼리는 서로 안전하게 통신할 수 있게 된다. └ 쉬운 접근: 특정 VM의 이름을 직접 외울 필요 없이, 도메인 이름으로 쉽게 찾아서 접근할 수 있다.
DC subnet, DC VM, SQL Sub net, SQL VM을 전부 만들고 설정했다.
이제 MASTER 계정을 2개의 SQL VM에 등록하여 설치, 관리하게 만들어야 한다.
각 SQL VM에 RDP로 접속하여 Server Manager 창으로 들어간다.
우상단의 'Tools > Computer Management' 탭으로 들어간다.
'Local Users and Groups > Groups > Administrators > Add'로 들어간다.
그리고 MASTER 계정의 아이디(install)을 추가한다.
그러기 위해서는 도메인 네트워크의 permissions이 필요하기에 DC 계정으로 허락한다.
이 과정을 2개의 SQL VM에 대해 전부 설정한다.
그리고 모든 VM에 로그인하기 위해서 로그인 설정을 진행한다.
각 SQL VM에 RDP로 접속하여 Server Manager 창으로 들어간다.
우상단의 'Tools > Active Directory Users and Computers' 탭으로 들어간다.
'자신이_만든_도메인 > Users > 자신의_MASTER_계정(여기서는 install)'으로 들어간다.
Account 탭으로 들어가서 첫 번째 설정을 끄고, 세 번째 설정을 켠다.
User must change password at next logon은 처음 로그인하기 전 반드시 비밀번호를 바꿔야 하는 설정이고,