추가: 마이그레이션 러너 및 배포 가이드
- MigrationRunner 구현 (자동 DB 마이그레이션) - Program.cs에 마이그레이션 자동 실행 추가 - 마이그레이션 SQL 파일을 임베드 리소스로 설정 - 완전한 배포 가이드 작성 (DEPLOYMENT_GUIDE.md) - E2E 테스트 절차 포함 - 롤백 및 모니터링 가이드 추가 배포 준비 완료: Gitea CI/CD 자동 배포 활성화 가능 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
|||||||
|
# TaxBaik 배포 가이드
|
||||||
|
|
||||||
|
## 서버 초기 설정
|
||||||
|
|
||||||
|
### 1. PostgreSQL 데이터베이스 생성
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh kjh2064@178.104.200.7
|
||||||
|
|
||||||
|
# PostgreSQL 접속
|
||||||
|
sudo -u postgres psql
|
||||||
|
|
||||||
|
# 데이터베이스 및 사용자 생성
|
||||||
|
CREATE USER taxbaik WITH PASSWORD 'secure_password_here';
|
||||||
|
CREATE DATABASE taxbaikdb OWNER taxbaik;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 환경 변수 설정
|
||||||
|
|
||||||
|
**Web 서비스** (`/etc/systemd/system/taxbaik.service`):
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
Environment=ASPNETCORE_URLS=http://127.0.0.1:5001
|
||||||
|
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admin 서비스** (`/etc/systemd/system/taxbaik-admin.service`):
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
Environment=ASPNETCORE_URLS=http://127.0.0.1:5002
|
||||||
|
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. systemd 서비스 파일 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/taxbaik.service /etc/systemd/system/
|
||||||
|
sudo cp deploy/taxbaik-admin.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable taxbaik taxbaik-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Nginx 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 현재 Nginx 설정 확인
|
||||||
|
sudo cat /etc/nginx/sites-available/default | head -30
|
||||||
|
|
||||||
|
# location 블록 추가 (또는 기존 설정에 병합)
|
||||||
|
sudo cp deploy/nginx-taxbaik-locations.conf /etc/nginx/conf.d/taxbaik.conf
|
||||||
|
|
||||||
|
# 테스트 및 재로드
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 배포 프로세스
|
||||||
|
|
||||||
|
### Gitea Actions 준비
|
||||||
|
|
||||||
|
1. Gitea 저장소 Secrets 추가:
|
||||||
|
- `DEPLOY_USER`: `kjh2064`
|
||||||
|
- `DEPLOY_HOST`: `178.104.200.7`
|
||||||
|
- `DEPLOY_SSH_KEY`: SSH 개인키 (줄바꿈 포함)
|
||||||
|
|
||||||
|
2. 배포 워크플로우는 자동으로 실행:
|
||||||
|
```
|
||||||
|
master 브랜치 push → build → publish → rsync → restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 수동 배포 (필요시)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 로컬에서 빌드
|
||||||
|
dotnet publish TaxBaik.sln -c Release -o ./publish
|
||||||
|
|
||||||
|
# 서버에 배포
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
rsync -az ./publish/web/ kjh2064@178.104.200.7:~/deployments/taxbaik_${TIMESTAMP}/
|
||||||
|
rsync -az ./publish/admin/ kjh2064@178.104.200.7:~/deployments/taxbaik_admin_${TIMESTAMP}/
|
||||||
|
|
||||||
|
# 서버에서 심링크 변경 및 재시작
|
||||||
|
ssh kjh2064@178.104.200.7 << EOF
|
||||||
|
ln -sfn ~/deployments/taxbaik_${TIMESTAMP} ~/taxbaik_active
|
||||||
|
ln -sfn ~/deployments/taxbaik_admin_${TIMESTAMP} ~/taxbaik_admin_active
|
||||||
|
sudo systemctl restart taxbaik taxbaik-admin
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## 마이그레이션 자동 실행
|
||||||
|
|
||||||
|
애플리케이션 시작시 자동으로 마이그레이션이 실행됩니다:
|
||||||
|
|
||||||
|
1. `schema_migrations` 테이블 생성 (없으면)
|
||||||
|
2. 실행된 마이그레이션 확인
|
||||||
|
3. 미실행 마이그레이션 순서대로 실행
|
||||||
|
|
||||||
|
로그 확인:
|
||||||
|
```bash
|
||||||
|
journalctl -u taxbaik -n 50
|
||||||
|
journalctl -u taxbaik-admin -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
### E2E 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 공개 사이트 접근
|
||||||
|
curl -I http://178.104.200.7/taxbaik/
|
||||||
|
|
||||||
|
# 관리자 로그인 페이지
|
||||||
|
curl -I http://178.104.200.7/taxbaik/admin/login
|
||||||
|
|
||||||
|
# 문의 폼 제출 테스트
|
||||||
|
curl -X POST http://178.104.200.7/taxbaik/contact \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트"
|
||||||
|
|
||||||
|
# DB에서 확인
|
||||||
|
ssh kjh2064@178.104.200.7
|
||||||
|
psql -U taxbaik -d taxbaikdb -c "SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 블로그 포스트 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 초기 5개 포스트 확인
|
||||||
|
curl http://178.104.200.7/taxbaik/blog
|
||||||
|
|
||||||
|
# 첫 번째 포스트 상세 (slug: accountant-mistakes-5)
|
||||||
|
curl http://178.104.200.7/taxbaik/blog/accountant-mistakes-5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 롤백
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 이전 버전으로 복귀
|
||||||
|
ssh kjh2064@178.104.200.7
|
||||||
|
|
||||||
|
# 이전 배포 디렉토리 확인
|
||||||
|
ls -la ~/deployments/ | grep taxbaik
|
||||||
|
|
||||||
|
# 심링크 변경 (예: 이전 버전이 taxbaik_20260626_140000)
|
||||||
|
ln -sfn ~/deployments/taxbaik_20260626_140000 ~/taxbaik_active
|
||||||
|
sudo systemctl restart taxbaik
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모니터링
|
||||||
|
|
||||||
|
### 서비스 상태 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh kjh2064@178.104.200.7
|
||||||
|
|
||||||
|
# 서비스 상태
|
||||||
|
systemctl status taxbaik taxbaik-admin
|
||||||
|
|
||||||
|
# 포트 확인
|
||||||
|
netstat -tlnp | grep -E '5001|5002'
|
||||||
|
|
||||||
|
# 프로세스 확인
|
||||||
|
ps aux | grep TaxBaik
|
||||||
|
```
|
||||||
|
|
||||||
|
### 성능 모니터링
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nginx 프록시 로그
|
||||||
|
tail -f /var/log/nginx/access.log | grep taxbaik
|
||||||
|
|
||||||
|
# 애플리케이션 로그
|
||||||
|
journalctl -u taxbaik -f
|
||||||
|
journalctl -u taxbaik-admin -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 트러블슈팅
|
||||||
|
|
||||||
|
| 증상 | 원인 | 해결 |
|
||||||
|
|------|------|------|
|
||||||
|
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
|
||||||
|
| DB 연결 오류 | 환경 변수 미설정 | systemd service 파일의 ConnectionStrings__Default 확인 |
|
||||||
|
| 503 Service Unavailable | 앱 미시작 | `sudo systemctl restart taxbaik` |
|
||||||
|
| 마이그레이션 실패 | DB 권한 문제 | `GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;` |
|
||||||
|
|
||||||
|
## 초기 데이터
|
||||||
|
|
||||||
|
### 관리자 계정
|
||||||
|
|
||||||
|
- **username**: `admin`
|
||||||
|
- **password**: `admin123` (bcrypt 해시됨)
|
||||||
|
- 초기 로그인 후 비밀번호 변경 권장
|
||||||
|
|
||||||
|
### 블로그 포스트
|
||||||
|
|
||||||
|
V003 마이그레이션에서 5개 포스트 자동 생성:
|
||||||
|
1. 사업자 기장 시 자주 하는 실수 5가지
|
||||||
|
2. 부동산 양도세 계산하기
|
||||||
|
3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
4. 부가가치세 간이과세 vs 일반과세
|
||||||
|
5. 가족 자산 증여세 절세 방법
|
||||||
|
|
||||||
|
## 차후 작업
|
||||||
|
|
||||||
|
- [ ] SSL 인증서 적용 (Let's Encrypt)
|
||||||
|
- [ ] 도메인 연결 (현재는 IP 기반)
|
||||||
|
- [ ] 관리자 인증 로직 구현 (현재는 플레이스홀더)
|
||||||
|
- [ ] 블로그 포스트 CRUD 기능 완성
|
||||||
|
- [ ] Naver/Google Search Console 등록
|
||||||
@@ -22,6 +22,16 @@ builder.Services.AddApplication();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Run migrations on startup
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
|
||||||
|
var cs = builder.Configuration.GetConnectionString("Default")
|
||||||
|
?? throw new InvalidOperationException("Missing connection string");
|
||||||
|
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(cs, connectionFactory);
|
||||||
|
await migrationRunner.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace TaxBaik.Infrastructure.Data;
|
||||||
|
|
||||||
|
public class MigrationRunner
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public MigrationRunner(string connectionString, IDbConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
_connectionString = connectionString;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync()
|
||||||
|
{
|
||||||
|
await EnsureMigrationTableAsync();
|
||||||
|
await ExecutePendingMigrationsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureMigrationTableAsync()
|
||||||
|
{
|
||||||
|
using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version VARCHAR(50) PRIMARY KEY,
|
||||||
|
description VARCHAR(500),
|
||||||
|
installed_on TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecutePendingMigrationsAsync()
|
||||||
|
{
|
||||||
|
var executedMigrations = await GetExecutedMigrationsAsync();
|
||||||
|
var migrations = GetAvailableMigrations();
|
||||||
|
|
||||||
|
foreach (var migration in migrations.OrderBy(x => x.Version))
|
||||||
|
{
|
||||||
|
if (!executedMigrations.Contains(migration.Version))
|
||||||
|
{
|
||||||
|
await ExecuteMigrationAsync(migration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<string>> GetExecutedMigrationsAsync()
|
||||||
|
{
|
||||||
|
var executed = new HashSet<string>();
|
||||||
|
using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT version FROM schema_migrations ORDER BY version;";
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
executed.Add(reader.GetString(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return executed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Migration> GetAvailableMigrations()
|
||||||
|
{
|
||||||
|
var migrations = new List<Migration>();
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceNames = assembly.GetManifestResourceNames()
|
||||||
|
.Where(x => x.Contains("Migrations") && x.EndsWith(".sql"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var resourceName in resourceNames.OrderBy(x => x))
|
||||||
|
{
|
||||||
|
var parts = resourceName.Split('.');
|
||||||
|
var fileName = parts[parts.Length - 2];
|
||||||
|
|
||||||
|
if (fileName.StartsWith("V"))
|
||||||
|
{
|
||||||
|
var version = fileName.Substring(1, fileName.IndexOf('_') - 1);
|
||||||
|
var description = fileName.Substring(fileName.IndexOf('_') + 2);
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var sql = reader.ReadToEnd();
|
||||||
|
|
||||||
|
migrations.Add(new Migration { Version = version, Description = description, Sql = sql });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteMigrationAsync(Migration migration)
|
||||||
|
{
|
||||||
|
using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = migration.Sql;
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
using var insertCmd = conn.CreateCommand();
|
||||||
|
insertCmd.CommandText =
|
||||||
|
"INSERT INTO schema_migrations (version, description) VALUES (@version, @description);";
|
||||||
|
insertCmd.Parameters.AddWithValue("@version", migration.Version);
|
||||||
|
insertCmd.Parameters.AddWithValue("@description", migration.Description);
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
Console.WriteLine($"✓ Migration {migration.Version} executed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"✗ Migration {migration.Version} failed: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Migration
|
||||||
|
{
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Sql { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="8.0.1" />
|
<PackageReference Include="Npgsql" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="../../db/migrations/*.sql" LinkBase="Migrations" />
|
||||||
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ builder.Services.AddApplication();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Run migrations on startup
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
|
||||||
|
var cs = builder.Configuration.GetConnectionString("Default")
|
||||||
|
?? throw new InvalidOperationException("Missing connection string");
|
||||||
|
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(cs, connectionFactory);
|
||||||
|
await migrationRunner.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
app.UsePathBase("/taxbaik");
|
app.UsePathBase("/taxbaik");
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|||||||
Reference in New Issue
Block a user