-
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
2
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
2 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,230 @@ | ||
| 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: | ||
| 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[@]}" ubuntu@127.0.0.1 \ | ||
| "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"; "$install_dir/install.sh" "$install_dir/mysql-backup.env"' | ||
| ssh "${SSH_OPTIONS[@]}" ubuntu@127.0.0.1 \ | ||
| "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
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,22 @@ | ||
| # 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 | ||
|
|
||
| > `MySQL Backup Deploy` 워크플로우는 기본값인 `validate`에서 변경 없이 사전 조건 또는 설치 상태만 검사합니다. 실제 설치는 `install`을 명시적으로 선택해야 합니다. | ||
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.
Uh oh!
There was an error while loading. Please reload this page.