-
Notifications
You must be signed in to change notification settings - Fork 0
feat: MySQL S3 백업 systemd 파이프라인 구성 #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Hexeong
wants to merge
3
commits into
main
Choose a base branch
from
feat/66-mysql-backup-systemd-pipeline
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| name: MySQL Backup Deploy | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| mode: | ||
| description: validate는 상태만 확인하고 install은 백업 구성을 설치합니다. | ||
| required: true | ||
| default: validate | ||
| type: choice | ||
| options: | ||
| - validate | ||
| - install | ||
|
|
||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: prod-mysql-backup-deploy | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| AWS_REGION: ap-northeast-2 | ||
| LOCAL_SSH_PORT: "2222" | ||
| SESSION_MANAGER_PLUGIN_VERSION: "1.2.835.0" | ||
|
|
||
| jobs: | ||
| deploy: | ||
| if: github.ref == 'refs/heads/main' | ||
| environment: prod-db | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | ||
| with: | ||
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | ||
| aws-region: ${{ env.AWS_REGION }} | ||
|
|
||
| - name: Install Session Manager plugin | ||
| run: | | ||
| set -Eeuo pipefail | ||
| plugin_dir="$(mktemp -d "$RUNNER_TEMP/session-manager-plugin.XXXXXX")" | ||
| trap 'rm -rf -- "$plugin_dir"' EXIT | ||
| plugin_base_url="https://s3.amazonaws.com/session-manager-downloads/plugin/${SESSION_MANAGER_PLUGIN_VERSION}/ubuntu_64bit" | ||
|
|
||
| curl -fsSL "$plugin_base_url/session-manager-plugin.deb" \ | ||
| -o "$plugin_dir/session-manager-plugin.deb" | ||
| curl -fsSL "$plugin_base_url/session-manager-plugin.deb.sig" \ | ||
| -o "$plugin_dir/session-manager-plugin.deb.sig" | ||
|
|
||
| cat >"$plugin_dir/session-manager-plugin.gpg" <<'EOF' | ||
| -----BEGIN PGP PUBLIC KEY BLOCK----- | ||
|
|
||
| mFIEZ5ERQxMIKoZIzj0DAQcCAwQjuZy+IjFoYg57sLTGhF3aZLBaGpzB+gY6j7Ix | ||
| P7NqbpXyjVj8a+dy79gSd64OEaMxUb7vw/jug+CfRXwVGRMNtIBBV1MgU1NNIFNl | ||
| c3Npb24gTWFuYWdlciA8c2Vzc2lvbi1tYW5hZ2VyLXBsdWdpbi1zaWduZXJAYW1h | ||
| em9uLmNvbT4gKEFXUyBTeXN0ZW1zIE1hbmFnZXIgU2Vzc2lvbiBNYW5hZ2VyIFBs | ||
| dWdpbiBMaW51eCBTaWduZXIgS2V5KYkBAAQQEwgAqAUCZ5ERQ4EcQVdTIFNTTSBT | ||
| ZXNzaW9uIE1hbmFnZXIgPHNlc3Npb24tbWFuYWdlci1wbHVnaW4tc2lnbmVyQGFt | ||
| YXpvbi5jb20+IChBV1MgU3lzdGVtcyBNYW5hZ2VyIFNlc3Npb24gTWFuYWdlciBQ | ||
| bHVnaW4gTGludXggU2lnbmVyIEtleSkWIQR5WWNxJM4JOtUB1HosTUr/b2dX7gIe | ||
| AwIbAwIVCAAKCRAsTUr/b2dX7rO1AQCa1kig3lQ78W/QHGU76uHx3XAyv0tfpE9U | ||
| oQBCIwFLSgEA3PDHt3lZ+s6m9JLGJsy+Cp5ZFzpiF6RgluR/2gA861M= | ||
| =2DQm | ||
| -----END PGP PUBLIC KEY BLOCK----- | ||
| EOF | ||
|
|
||
| export GNUPGHOME="$plugin_dir/gnupg" | ||
| install -d -m 700 "$GNUPGHOME" | ||
| gpg --batch --import "$plugin_dir/session-manager-plugin.gpg" | ||
| signing_key_fingerprint="$(gpg --batch --with-colons --fingerprint 2C4D4AFF6F6757EE | awk -F: '$1 == "fpr" {print $10; exit}')" | ||
| if [[ "$signing_key_fingerprint" != "7959637124CE093AD501D47A2C4D4AFF6F6757EE" ]]; then | ||
| echo "::error::Unexpected Session Manager plugin signing key fingerprint" | ||
| exit 1 | ||
| fi | ||
| gpg --batch --verify \ | ||
| "$plugin_dir/session-manager-plugin.deb.sig" \ | ||
| "$plugin_dir/session-manager-plugin.deb" | ||
|
|
||
| package_version="$(dpkg-deb --field "$plugin_dir/session-manager-plugin.deb" Version)" | ||
| if [[ "$package_version" != "${SESSION_MANAGER_PLUGIN_VERSION}-1" ]]; then | ||
| echo "::error::Unexpected Session Manager plugin package version: $package_version" | ||
| exit 1 | ||
| fi | ||
| sudo dpkg -i "$plugin_dir/session-manager-plugin.deb" | ||
| echo "/usr/local/sessionmanagerplugin/bin" >> "$GITHUB_PATH" | ||
|
|
||
| - name: Validate or install MySQL backup | ||
| env: | ||
| DEPLOY_MODE: ${{ inputs.mode }} | ||
| MYSQL_BACKUP_BUCKET: ${{ vars.MYSQL_BACKUP_BUCKET_NAME }} | ||
| MYSQL_DATABASE: ${{ vars.MYSQL_BACKUP_DATABASE_NAME }} | ||
| DB_HOST_FINGERPRINT: ${{ vars.PROD_DB_SSH_HOST_KEY_ED25519 }} | ||
| run: | | ||
| set -Eeuo pipefail | ||
| umask 077 | ||
|
|
||
| : "${MYSQL_BACKUP_BUCKET:?MYSQL_BACKUP_BUCKET_NAME repository variable is required}" | ||
| : "${MYSQL_DATABASE:?MYSQL_BACKUP_DATABASE_NAME repository variable is required}" | ||
| : "${DB_HOST_FINGERPRINT:?PROD_DB_SSH_HOST_KEY_ED25519 repository variable is required}" | ||
| if [[ "$DEPLOY_MODE" != "validate" && "$DEPLOY_MODE" != "install" ]]; then | ||
| echo "::error::Invalid deployment mode" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$MYSQL_BACKUP_BUCKET" =~ ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ ]]; then | ||
| echo "::error::Invalid S3 bucket name" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$MYSQL_DATABASE" =~ ^[A-Za-z0-9_]+$ ]]; then | ||
| echo "::error::Invalid database name" | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$DB_HOST_FINGERPRINT" =~ ^SHA256:[A-Za-z0-9+/]{43}$ ]]; then | ||
| echo "::error::Invalid DB EC2 SSH host key fingerprint" | ||
| exit 1 | ||
| fi | ||
| KEY_DIR="$(mktemp -d "$RUNNER_TEMP/mysql-backup-eic.XXXXXX")" | ||
| SSM_PID="" | ||
| cleanup() { | ||
| if [[ -n "$SSM_PID" ]]; then | ||
| kill "$SSM_PID" 2>/dev/null || true | ||
| wait "$SSM_PID" 2>/dev/null || true | ||
| fi | ||
| rm -rf "$KEY_DIR" | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| ssh-keygen -q -t ed25519 -N '' -f "$KEY_DIR/id_ed25519" | ||
|
|
||
| API_INSTANCE_ID="$(aws ec2 describe-instances \ | ||
| --filters \ | ||
| 'Name=tag:Name,Values=solid-connection-server-prod' \ | ||
| 'Name=instance-state-name,Values=running' \ | ||
| --query 'Reservations[].Instances[].InstanceId' \ | ||
| --output text)" | ||
| DB_INSTANCE_ID="$(aws ec2 describe-instances \ | ||
| --filters \ | ||
| 'Name=tag:Name,Values=solid-connection-db-mysql-prod' \ | ||
| 'Name=instance-state-name,Values=running' \ | ||
| --query 'Reservations[].Instances[].InstanceId' \ | ||
| --output text)" | ||
|
|
||
| if [[ "$API_INSTANCE_ID" == *$'\t'* || -z "$API_INSTANCE_ID" || "$API_INSTANCE_ID" == "None" ]]; then | ||
| echo "::error::Exactly one running Prod API EC2 instance is required" | ||
| exit 1 | ||
| fi | ||
| if [[ "$DB_INSTANCE_ID" == *$'\t'* || -z "$DB_INSTANCE_ID" || "$DB_INSTANCE_ID" == "None" ]]; then | ||
| echo "::error::Exactly one running Prod DB EC2 instance is required" | ||
| exit 1 | ||
| fi | ||
|
|
||
| DB_PRIVATE_IP="$(aws ec2 describe-instances \ | ||
| --instance-ids "$DB_INSTANCE_ID" \ | ||
| --query 'Reservations[0].Instances[0].PrivateIpAddress' \ | ||
| --output text)" | ||
| if [[ -z "$DB_PRIVATE_IP" || "$DB_PRIVATE_IP" == "None" ]]; then | ||
| echo "::error::Prod DB EC2 private IP was not found" | ||
| exit 1 | ||
| fi | ||
|
|
||
| aws ssm start-session \ | ||
| --target "$API_INSTANCE_ID" \ | ||
| --document-name AWS-StartPortForwardingSessionToRemoteHost \ | ||
| --parameters "{\"host\":[\"$DB_PRIVATE_IP\"],\"portNumber\":[\"22\"],\"localPortNumber\":[\"$LOCAL_SSH_PORT\"]}" \ | ||
| >"$KEY_DIR/ssm-tunnel.log" 2>&1 & | ||
| SSM_PID=$! | ||
|
|
||
| for _ in $(seq 1 60); do | ||
| kill -0 "$SSM_PID" 2>/dev/null || { | ||
| cat "$KEY_DIR/ssm-tunnel.log" >&2 | ||
| exit 1 | ||
| } | ||
| nc -z 127.0.0.1 "$LOCAL_SSH_PORT" 2>/dev/null && break | ||
| sleep 1 | ||
| done | ||
| nc -z 127.0.0.1 "$LOCAL_SSH_PORT" | ||
|
|
||
| KNOWN_HOSTS_FILE="$KEY_DIR/known_hosts" | ||
| ssh-keyscan -t ed25519 -p "$LOCAL_SSH_PORT" 127.0.0.1 >"$KNOWN_HOSTS_FILE" 2>/dev/null | ||
| ACTUAL_FINGERPRINT="$(ssh-keygen -lf "$KNOWN_HOSTS_FILE" -E sha256 | awk '{print $2}')" | ||
| if [[ "$ACTUAL_FINGERPRINT" != "$DB_HOST_FINGERPRINT" ]]; then | ||
| echo "::error::DB EC2 SSH host key fingerprint mismatch" | ||
| exit 1 | ||
| fi | ||
|
|
||
| SSH_OPTIONS=( | ||
| -p "$LOCAL_SSH_PORT" | ||
| -i "$KEY_DIR/id_ed25519" | ||
| -o IdentitiesOnly=yes | ||
| -o StrictHostKeyChecking=yes | ||
| -o UserKnownHostsFile="$KNOWN_HOSTS_FILE" | ||
| -o ConnectTimeout=10 | ||
| ) | ||
|
|
||
| if [[ "$DEPLOY_MODE" == "validate" ]]; then | ||
| aws ec2-instance-connect send-ssh-public-key \ | ||
| --region "$AWS_REGION" \ | ||
| --instance-id "$DB_INSTANCE_ID" \ | ||
| --instance-os-user ubuntu \ | ||
| --ssh-public-key "file://$KEY_DIR/id_ed25519.pub" >/dev/null | ||
| REMOTE_VALIDATE_COMMAND="env MYSQL_BACKUP_BUCKET=$(printf '%q' "$MYSQL_BACKUP_BUCKET") MYSQL_DATABASE=$(printf '%q' "$MYSQL_DATABASE") AWS_REGION=$(printf '%q' "$AWS_REGION") bash -s" | ||
| ssh "${SSH_OPTIONS[@]}" [email protected] \ | ||
| "sudo bash -c $(printf '%q' "$REMOTE_VALIDATE_COMMAND")" \ | ||
| < scripts/mysql_backup/validate-remote.sh | ||
| exit 0 | ||
| fi | ||
|
|
||
| CONFIG_FILE="$KEY_DIR/mysql-backup.env" | ||
| { | ||
| printf 'MYSQL_BACKUP_BUCKET=%s\n' "$MYSQL_BACKUP_BUCKET" | ||
| printf 'MYSQL_DATABASE=%s\n' "$MYSQL_DATABASE" | ||
| printf 'AWS_REGION=%s\n' "$AWS_REGION" | ||
| } >"$CONFIG_FILE" | ||
|
|
||
| cp -R scripts/mysql_backup "$KEY_DIR/bundle" | ||
| cp "$CONFIG_FILE" "$KEY_DIR/bundle/mysql-backup.env" | ||
| tar -C "$KEY_DIR/bundle" -czf "$KEY_DIR/mysql-backup.tar.gz" . | ||
|
|
||
| aws ec2-instance-connect send-ssh-public-key \ | ||
| --region "$AWS_REGION" \ | ||
| --instance-id "$DB_INSTANCE_ID" \ | ||
| --instance-os-user ubuntu \ | ||
| --ssh-public-key "file://$KEY_DIR/id_ed25519.pub" >/dev/null | ||
|
|
||
| REMOTE_INSTALL_COMMAND='set -Eeuo pipefail; install_dir="$(mktemp -d /tmp/mysql-backup-install.XXXXXX)"; trap '\''rm -rf -- "$install_dir"'\'' EXIT; tar -xzf - -C "$install_dir"; bash "$install_dir/install.sh" "$install_dir/mysql-backup.env"' | ||
| ssh "${SSH_OPTIONS[@]}" [email protected] \ | ||
| "sudo bash -c $(printf '%q' "$REMOTE_INSTALL_COMMAND")" \ | ||
| <"$KEY_DIR/mysql-backup.tar.gz" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| name: MySQL Backup Test | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Run MySQL backup tests | ||
| run: bash scripts/mysql_backup/tests/run.sh |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Prod MySQL S3 백업 | ||
|
|
||
| DB EC2에서 다음 systemd 작업을 실행합니다. | ||
|
|
||
| - `mysql-backup-dump.timer`: 매일 03:00 KST에 일관된 전체 dump, 체크섬, manifest를 업로드합니다. | ||
| - `mysql-backup-binlog.timer`: 매 5분 경계마다 binlog를 회전하고 닫힌 파일과 manifest를 업로드합니다. | ||
|
|
||
| 백업 파일은 `/mnt/mysql-data/mysql-backup`에서만 임시 생성합니다. S3에는 manifest를 마지막에 업로드하며, 같은 key가 이미 존재하면 체크섬이 같은 경우에만 업로드를 생략합니다. 이 방식으로 재시도 시 Object Lock 버킷에 불필요한 객체 버전이 생기는 것을 방지합니다. | ||
|
|
||
| ## 배포 전 GitHub 설정 | ||
|
|
||
| Repository Secrets: | ||
|
|
||
| - `AWS_ROLE_ARN`: 배포 워크플로우가 AssumeRole할 IAM 역할 ARN | ||
|
|
||
| Repository Variables: | ||
|
|
||
| - `MYSQL_BACKUP_BUCKET_NAME`: 백업 버킷 이름 | ||
| - `MYSQL_BACKUP_DATABASE_NAME`: 백업할 DB 이름. 필수값이며 공개 코드에 기본값을 두지 않습니다. | ||
| - `PROD_DB_SSH_HOST_KEY_ED25519`: DB EC2의 ED25519 host key SHA-256 fingerprint | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| GitHub Environment: | ||
|
|
||
| - `prod-db`: required reviewer, self-review 차단, `main` 배포 브랜치 제한을 적용합니다. | ||
|
|
||
| > `MySQL Backup Deploy` 워크플로우는 `main` 브랜치에서만 실행되며, `prod-db` 승인을 통과해야 합니다. 기본값인 `validate`는 변경 없이 사전 조건 또는 설치 상태만 검사하고, 실제 설치는 `install`을 명시적으로 선택해야 합니다. | ||
|
|
||
| `MySQL Backup Test` 워크플로우는 AWS 권한이나 운영 환경 접근 없이 백업 스크립트 단위 테스트를 수동으로 실행합니다. | ||
|
|
||
| ## dump 실패 처리 | ||
|
|
||
| - dump 실행 직전에 예상 dump 크기의 2배와 256MiB의 여유 공간을 확인합니다. | ||
| - 실패한 작업은 10분 간격으로 최대 3회 다시 시도합니다. | ||
| - 6시간을 초과한 작업은 staging과 포인터를 폐기하고 새 dump를 생성합니다. | ||
| - 이미 완성된 dump가 남아 있으면 재생성하지 않고 S3 업로드만 다시 시도합니다. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
workflow_dispatch는 이 파일이 존재하는 아무 브랜치에서나 실행됩니다. 지금은environment:도 브랜치 조건도 없어서, write 권한이 있으면 feature 브랜치에서install.sh를 수정한 뒤 dispatch → prod DB에 root로 임의 코드 실행이 가능합니다. 리뷰·승인 경로를 전혀 거치지 않습니다.추가로
secrets.AWS_ROLE_ARN역할의 trust policysub조건이repo:...:ref:refs/heads/main으로 제한돼 있는지 확인 부탁드립니다. IAM은 코드로 확인이 안 돼서요. 제한돼 있지 않다면 위 게이트가 실질적인 유일한 방어선입니다.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지적해 주신 feature branch 기반 운영 실행 문제는 반영하겠습니다. 다만 validate 모드도 선택한 브랜치의 스크립트를 운영 서버에서 sudo로 실행하므로 main || validate 조건만으로는 충분하지 않은 것으로 확인했습니다. 두 모드 모두 main 브랜치에서만 실행되도록 제한하고, prod-db Environment의 승인 및 배포 브랜치 정책을 함께 적용하겠습니다. Environment 적용에 따라 IAM OIDC 신뢰 정책의 subject도 수동으로 점검하겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OIDC와 연결된 Role의 trust policy 확인 결과,
repo:...:ref:refs/heads/main로 정상적으로 반영되어 있습니다!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다! validate, install 모두 main 브랜치에서만 실행되도록 제한했고, prod-db Environment의 승인 절차를 거치도록 설정했습니다. Environment에는 required reviewer, self-review 차단, main 배포 브랜치 제한이 적용되어 있습니다.