Redmine

プロジェクト管理チケット管理オープンソースRuby on Railsイシュー追跡Wiki

チケット管理ツール

Redmine

概要

Redmineは、Ruby on Railsで開発されたオープンソースのプロジェクト管理・イシュー追跡ツールです。GPL v2ライセンスの下で無料提供され、Web ベースの直感的なインターフェースを通じて、複数プロジェクトの管理、タスク追跡、時間管理、Wiki、フォーラム機能を統合的に提供します。高いカスタマイズ性と豊富なプラグインエコシステムにより、様々な業界・規模の組織で活用されています。

詳細

Redmineは、オープンソースプロジェクト管理ツールとして10年以上の実績を持ち、シンプルながら強力な機能を提供する設計が特徴です。

主要な特徴

  • 柔軟なイシュー管理: Bug、Feature、Supportの標準タイプに加え、カスタムタイプ作成可能
  • プロジェクト階層管理: 親子関係のある複雑なプロジェクト構造をサポート
  • 統合Wiki機能: プロジェクト毎のWikiによる文書管理とナレッジ共有
  • 時間追跡: 詳細な工数管理とレポート生成機能
  • ロールベースアクセス制御: 細かな権限設定による柔軟なアクセス管理

技術仕様

  • アーキテクチャ: Ruby on Rails フレームワーク、MySQL/PostgreSQL/SQLite対応
  • 拡張性: 豊富なプラグインエコシステム、REST API提供
  • デプロイメント: オンプレミス・クラウド両対応、Docker対応
  • 国際化: 多言語対応(日本語含む)

メリット・デメリット

メリット

  1. 完全無料のオープンソース

    • ライセンス費用不要、チーム規模に関係なく利用可能
    • 豊富なカスタマイズオプションによる組織要件への適応
  2. 高いカスタマイズ性

    • カスタムフィールド、ワークフロー、UI拡張が可能
    • プラグインによる機能拡張(TestRail統合、モバイルアプリ対応等)
  3. シンプルで直感的な操作性

    • 基本的なWebインターフェースで学習コストが低い
    • 複雑な設定なしで即座に利用開始可能
  4. 統合機能の充実

    • Wiki、フォーラム、時間管理、ガントチャートを一元管理
    • 多数の外部ツールとの連携機能

デメリット

  1. ユーザーインターフェースの古さ

    • 基本的すぎるデザインと機能性の制約
    • モダンなUX/UIに慣れたユーザーには使いにくさ
  2. パフォーマンスの制約

    • Ruby on Railsの特性上、大規模データでの応答速度低下
    • 同時アクセス数増加時のスケーラビリティ課題
  3. 高度な機能の不足

    • アジャイル開発支援機能の限界
    • リアルタイムコラボレーション機能の不備

参考ページ

基本的な使い方

1. プロジェクト設定と初期構成

# Redmine REST API を使用したプロジェクト作成(Ruby例)
require 'net/http'
require 'json'

class RedmineAPI
  def initialize(redmine_url, api_key)
    @base_url = redmine_url
    @api_key = api_key
  end

  def create_project(project_data)
    uri = URI("#{@base_url}/projects.json")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true if uri.scheme == 'https'
    
    request = Net::HTTP::Post.new(uri)
    request['X-Redmine-API-Key'] = @api_key
    request['Content-Type'] = 'application/json'
    
    project_params = {
      project: {
        name: project_data[:name],
        identifier: project_data[:identifier],
        description: project_data[:description],
        homepage: project_data[:homepage],
        is_public: project_data[:is_public] || false,
        parent_id: project_data[:parent_id],
        inherit_members: project_data[:inherit_members] || false,
        enabled_modules: project_data[:modules] || ['issue_tracking', 'time_tracking', 'wiki', 'repository'],
        trackers: project_data[:trackers] || [1, 2, 3], # Bug, Feature, Support
        custom_fields: project_data[:custom_fields] || []
      }
    }
    
    request.body = project_params.to_json
    response = http.request(request)
    
    if response.code == '201'
      project = JSON.parse(response.body)
      setup_project_configuration(project['project']['id'], project_data)
      project
    else
      raise "Project creation failed: #{response.body}"
    end
  end

  private

  def setup_project_configuration(project_id, config)
    # プロジェクト固有の設定
    setup_project_members(project_id, config[:members]) if config[:members]
    setup_issue_categories(project_id, config[:categories]) if config[:categories]
    setup_versions(project_id, config[:versions]) if config[:versions]
    create_wiki_structure(project_id, config[:wiki_pages]) if config[:wiki_pages]
  end
end

2. イシュー管理とワークフロー

// Redmine REST API を使用したイシュー管理(JavaScript例)
class RedmineIssueManager {
  constructor(redmineUrl, apiKey) {
    this.baseUrl = redmineUrl;
    this.apiKey = apiKey;
  }

  async createIssue(projectId, issueData) {
    const issue = {
      project_id: projectId,
      tracker_id: issueData.tracker_id || 1, // Bug=1, Feature=2, Support=3
      status_id: issueData.status_id || 1, // New
      priority_id: issueData.priority_id || 2, // Normal
      subject: issueData.subject,
      description: issueData.description,
      category_id: issueData.category_id,
      fixed_version_id: issueData.fixed_version_id,
      assigned_to_id: issueData.assigned_to_id,
      parent_issue_id: issueData.parent_issue_id,
      start_date: issueData.start_date,
      due_date: issueData.due_date,
      estimated_hours: issueData.estimated_hours,
      done_ratio: issueData.done_ratio || 0,
      custom_fields: issueData.custom_fields || [],
      watcher_user_ids: issueData.watchers || []
    };

    const response = await fetch(`${this.baseUrl}/issues.json`, {
      method: 'POST',
      headers: {
        'X-Redmine-API-Key': this.apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ issue })
    });

    if (!response.ok) {
      throw new Error(`Issue creation failed: ${response.statusText}`);
    }

    const createdIssue = await response.json();
    
    // 添付ファイル処理
    if (issueData.attachments) {
      await this.uploadAttachments(createdIssue.issue.id, issueData.attachments);
    }

    // 関連づけ設定
    if (issueData.relations) {
      await this.createIssueRelations(createdIssue.issue.id, issueData.relations);
    }

    return createdIssue;
  }

  async updateIssueStatus(issueId, statusId, notes = '') {
    const issue = {
      status_id: statusId,
      notes: notes
    };

    const response = await fetch(`${this.baseUrl}/issues/${issueId}.json`, {
      method: 'PUT',
      headers: {
        'X-Redmine-API-Key': this.apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ issue })
    });

    return response.ok;
  }

  async createIssueRelations(issueId, relations) {
    for (const relation of relations) {
      await fetch(`${this.baseUrl}/issues/${issueId}/relations.json`, {
        method: 'POST',
        headers: {
          'X-Redmine-API-Key': this.apiKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          relation: {
            issue_to_id: relation.issue_to_id,
            relation_type: relation.type, // relates, duplicates, blocks, precedes
            delay: relation.delay || null
          }
        })
      });
    }
  }
}

3. 時間管理とレポート機能

# Redmine 時間追跡とレポート生成(Python例)
import requests
import json
from datetime import datetime, timedelta
import pandas as pd

class RedmineTimeTracking:
    def __init__(self, redmine_url, api_key):
        self.base_url = redmine_url
        self.api_key = api_key
        self.headers = {
            'X-Redmine-API-Key': api_key,
            'Content-Type': 'application/json'
        }

    def log_time_entry(self, issue_id, hours, activity_id, comments='', spent_on=None):
        """工数記録"""
        time_entry = {
            'issue_id': issue_id,
            'hours': hours,
            'activity_id': activity_id,  # Development=9, Testing=10, Documentation=11
            'comments': comments,
            'spent_on': spent_on or datetime.now().strftime('%Y-%m-%d')
        }
        
        response = requests.post(
            f'{self.base_url}/time_entries.json',
            headers=self.headers,
            data=json.dumps({'time_entry': time_entry})
        )
        
        return response.status_code == 201

    def get_project_time_report(self, project_id, from_date, to_date):
        """プロジェクト工数レポート"""
        params = {
            'project_id': project_id,
            'from': from_date,
            'to': to_date,
            'limit': 100
        }
        
        response = requests.get(
            f'{self.base_url}/time_entries.json',
            headers=self.headers,
            params=params
        )
        
        if response.status_code == 200:
            time_entries = response.json()['time_entries']
            return self.analyze_time_data(time_entries)
        
        return None

    def analyze_time_data(self, time_entries):
        """工数データ分析"""
        df = pd.DataFrame(time_entries)
        
        analysis = {
            'total_hours': df['hours'].sum(),
            'entries_count': len(df),
            'by_user': df.groupby('user')['hours'].sum().to_dict(),
            'by_activity': df.groupby('activity')['hours'].sum().to_dict(),
            'by_issue': df.groupby('issue')['hours'].sum().to_dict(),
            'daily_breakdown': df.groupby('spent_on')['hours'].sum().to_dict()
        }
        
        return analysis

    def generate_burndown_data(self, project_id, version_id):
        """バーンダウンチャートデータ生成"""
        # バージョン情報取得
        version_response = requests.get(
            f'{self.base_url}/versions/{version_id}.json',
            headers=self.headers
        )
        
        if version_response.status_code != 200:
            return None
            
        version = version_response.json()['version']
        
        # イシュー一覧取得
        issues_response = requests.get(
            f'{self.base_url}/issues.json',
            headers=self.headers,
            params={
                'project_id': project_id,
                'fixed_version_id': version_id,
                'limit': 100
            }
        )
        
        if issues_response.status_code != 200:
            return None
            
        issues = issues_response.json()['issues']
        
        # バーンダウンデータ計算
        total_estimated = sum(issue.get('estimated_hours', 0) for issue in issues)
        completed_hours = sum(
            issue.get('estimated_hours', 0) 
            for issue in issues 
            if issue['status']['id'] in [5, 6]  # Closed, Rejected
        )
        
        remaining_hours = total_estimated - completed_hours
        
        return {
            'version_name': version['name'],
            'due_date': version.get('due_date'),
            'total_estimated': total_estimated,
            'completed_hours': completed_hours,
            'remaining_hours': remaining_hours,
            'progress_percentage': (completed_hours / total_estimated * 100) if total_estimated > 0 else 0
        }

4. Wiki管理とナレッジベース

# Redmine Wiki管理(Ruby例)
class RedmineWikiManager
  def initialize(redmine_url, api_key)
    @base_url = redmine_url
    @api_key = api_key
  end

  def create_wiki_page(project_id, page_title, content, parent_title = nil)
    uri = URI("#{@base_url}/projects/#{project_id}/wiki/#{page_title}.json")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true if uri.scheme == 'https'
    
    request = Net::HTTP::Put.new(uri)
    request['X-Redmine-API-Key'] = @api_key
    request['Content-Type'] = 'application/json'
    
    wiki_page = {
      text: content,
      comments: "Page created via API",
      parent_title: parent_title
    }
    
    request.body = { wiki_page: wiki_page }.to_json
    response = http.request(request)
    
    response.code == '200'
  end

  def setup_project_documentation(project_id)
    # 基本ドキュメント構造作成
    documentation_structure = [
      {
        title: 'ProjectOverview',
        content: generate_project_overview_content,
        children: [
          { title: 'Requirements', content: '# 要件定義\n\nプロジェクトの要件を記載します。' },
          { title: 'Architecture', content: '# システム構成\n\nアーキテクチャ図と説明。' }
        ]
      },
      {
        title: 'DevelopmentGuide',
        content: generate_development_guide_content,
        children: [
          { title: 'SetupInstructions', content: '# 開発環境構築\n\n環境構築手順。' },
          { title: 'CodingStandards', content: '# コーディング規約\n\nコーディングルール。' },
          { title: 'TestingGuidelines', content: '# テストガイドライン\n\nテスト方針と手順。' }
        ]
      },
      {
        title: 'UserManual',
        content: generate_user_manual_content,
        children: [
          { title: 'GettingStarted', content: '# はじめに\n\n利用開始方法。' },
          { title: 'FeatureGuide', content: '# 機能説明\n\n各機能の使い方。' }
        ]
      }
    ]

    # ドキュメント作成
    documentation_structure.each do |section|
      create_wiki_page(project_id, section[:title], section[:content])
      
      section[:children]&.each do |child|
        create_wiki_page(project_id, child[:title], child[:content], section[:title])
      end
    end
  end

  private

  def generate_project_overview_content
    <<~CONTENT
      # プロジェクト概要

      ## 目的
      このプロジェクトの目的と背景を記載します。

      ## スコープ
      プロジェクトの範囲と制約事項。

      ## ステークホルダー
      | 役割 | 担当者 | 連絡先 |
      |------|--------|--------|
      | プロジェクトマネージャー | | |
      | 開発リーダー | | |
      | 品質管理責任者 | | |

      ## スケジュール
      主要マイルストーンと期日。
    CONTENT
  end

  def generate_development_guide_content
    <<~CONTENT
      # 開発ガイド

      ## 開発プロセス
      本プロジェクトで採用する開発プロセスの説明。

      ## ブランチ戦略
      Git ブランチ運用ルール。

      ## レビュープロセス
      コードレビューとプルリクエストのガイドライン。

      ## デプロイメント
      各環境へのデプロイ手順。
    CONTENT
  end

  def generate_user_manual_content
    <<~CONTENT
      # ユーザーマニュアル

      ## システム概要
      システムの全体像と主要機能。

      ## ログイン方法
      システムアクセス手順。

      ## 機能別操作説明
      各機能の詳細な操作方法。

      ## トラブルシューティング
      よくある問題と解決方法。
    CONTENT
  end
end

5. プラグイン活用とカスタマイズ

# Redmine プラグイン開発とカスタマイズ例
class RedmineCustomizations
  def self.install_useful_plugins
    plugins = [
      {
        name: 'redmine_agile',
        description: 'アジャイル開発支援(Scrumボード、バーンダウンチャート)',
        installation: 'gem install redmine_agile'
      },
      {
        name: 'redmine_dmsf',
        description: 'ドキュメント管理システム',
        installation: 'git clone https://github.com/danmunn/redmine_dmsf.git'
      },
      {
        name: 'redmine_contacts',
        description: '顧客・連絡先管理',
        installation: 'gem install redmine_contacts'
      },
      {
        name: 'redmine_spent_time',
        description: '高度な時間管理とレポート',
        installation: 'git clone https://github.com/eyestreet/redmine_spent_time.git'
      }
    ]
    
    plugins
  end

  def self.create_custom_field(field_config)
    # カスタムフィールド作成例
    {
      name: field_config[:name],
      field_format: field_config[:type], # string, text, int, float, date, bool, list
      possible_values: field_config[:options],
      is_required: field_config[:required] || false,
      is_for_all: field_config[:global] || false,
      trackers: field_config[:trackers] || [1, 2, 3], # Bug, Feature, Support
      projects: field_config[:projects] || []
    }
  end

  def self.setup_email_notifications(project_id, notification_rules)
    # メール通知設定
    notification_rules.each do |rule|
      # イシュー作成時
      setup_notification_rule(project_id, 'issue_add', rule[:on_create])
      # イシュー更新時
      setup_notification_rule(project_id, 'issue_edit', rule[:on_update])
      # コメント追加時
      setup_notification_rule(project_id, 'issue_note_added', rule[:on_comment])
    end
  end

  def self.integrate_with_external_tools(tool_configs)
    integrations = {}
    
    # Git連携設定
    if tool_configs[:git]
      integrations[:git] = setup_git_integration(tool_configs[:git])
    end
    
    # CI/CD連携設定
    if tool_configs[:ci_cd]
      integrations[:ci_cd] = setup_ci_cd_integration(tool_configs[:ci_cd])
    end
    
    # Slack連携設定
    if tool_configs[:slack]
      integrations[:slack] = setup_slack_integration(tool_configs[:slack])
    end
    
    integrations
  end

  private

  def self.setup_git_integration(git_config)
    {
      repository_type: git_config[:type], # Git, SVN, Mercurial
      url: git_config[:url],
      login: git_config[:username],
      password: git_config[:password_or_token],
      path_encoding: 'UTF-8',
      log_encoding: 'UTF-8',
      extra_info: git_config[:extra_options] || {}
    }
  end

  def self.setup_ci_cd_integration(ci_config)
    # Jenkins, GitHub Actions等との連携設定
    webhook_config = {
      url: ci_config[:webhook_url],
      events: ['issue_updated', 'issue_closed'],
      secret_token: ci_config[:secret],
      verify_ssl: ci_config[:verify_ssl] || true
    }
    
    webhook_config
  end

  def self.setup_slack_integration(slack_config)
    {
      webhook_url: slack_config[:webhook_url],
      channel: slack_config[:channel],
      username: 'Redmine Bot',
      icon_emoji: ':redmine:',
      events: slack_config[:events] || ['issue_add', 'issue_edit', 'issue_closed']
    }
  end
end

6. バックアップとメンテナンス

#!/bin/bash
# Redmine バックアップとメンテナンススクリプト

# データベースバックアップ
backup_database() {
    BACKUP_DIR="/backup/redmine/$(date +%Y%m%d)"
    mkdir -p $BACKUP_DIR
    
    # MySQLの場合
    mysqldump -u redmine_user -p redmine_production > $BACKUP_DIR/redmine_db_$(date +%Y%m%d_%H%M%S).sql
    
    # PostgreSQLの場合
    # pg_dump -U redmine_user redmine_production > $BACKUP_DIR/redmine_db_$(date +%Y%m%d_%H%M%S).sql
}

# ファイルバックアップ
backup_files() {
    BACKUP_DIR="/backup/redmine/$(date +%Y%m%d)"
    REDMINE_ROOT="/var/www/redmine"
    
    # 設定ファイル
    tar -czf $BACKUP_DIR/redmine_config_$(date +%Y%m%d_%H%M%S).tar.gz \
        $REDMINE_ROOT/config/database.yml \
        $REDMINE_ROOT/config/configuration.yml \
        $REDMINE_ROOT/config/additional_environment.rb
    
    # 添付ファイル
    tar -czf $BACKUP_DIR/redmine_files_$(date +%Y%m%d_%H%M%S).tar.gz \
        $REDMINE_ROOT/files
    
    # プラグイン
    tar -czf $BACKUP_DIR/redmine_plugins_$(date +%Y%m%d_%H%M%S).tar.gz \
        $REDMINE_ROOT/plugins
}

# システムメンテナンス
system_maintenance() {
    cd /var/www/redmine
    
    # データベースクリーンアップ
    bundle exec rake redmine:cleanup RAILS_ENV=production
    
    # セッションクリーンアップ
    bundle exec rake redmine:sessions:clear RAILS_ENV=production
    
    # ログローテーション
    find log/ -name "*.log" -type f -mtime +30 -delete
    
    # 一時ファイルクリーンアップ
    bundle exec rake tmp:clear RAILS_ENV=production
}

# パフォーマンス最適化
optimize_performance() {
    cd /var/www/redmine
    
    # データベース最適化
    bundle exec rake db:optimize RAILS_ENV=production
    
    # キャッシュクリア
    bundle exec rake tmp:cache:clear RAILS_ENV=production
    
    # アセット再生成
    bundle exec rake assets:precompile RAILS_ENV=production
}

# メイン実行
main() {
    echo "Starting Redmine maintenance..."
    
    backup_database
    backup_files
    system_maintenance
    optimize_performance
    
    echo "Maintenance completed successfully."
}

main "$@"

Redmineは、オープンソースの強力なプロジェクト管理ツールとして、多くの組織で信頼され活用されています。コスト効率と高いカスタマイズ性により、組織の特定ニーズに合わせた柔軟なプロジェクト管理環境を構築できます。