Closed1

選択したEC2へSSMアクセスするgolangツール

not75743not75743
main.go
package main

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
	"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/ssm"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// InstanceInfo はEC2インスタンス情報を表す構造体
type InstanceInfo struct {
	InstanceID   string
	InstanceName string
	State        string
	PrivateIP    string
	PublicIP     string
}

// EC2Client はEC2操作を行うためのクライアント
type EC2Client struct {
	client *ec2.Client
}

// NewEC2Client は新しいEC2クライアントを作成する
func NewEC2Client() (*EC2Client, error) {
	// AWS 設定を読み込む
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, fmt.Errorf("AWS設定の読み込みに失敗しました: %w", err)
	}

	// EC2クライアントを作成
	client := ec2.NewFromConfig(cfg)
	return &EC2Client{client: client}, nil
}

// ListInstances はすべてのEC2インスタンスをリストアップする
func (c *EC2Client) ListInstances() ([]InstanceInfo, error) {
	input := &ec2.DescribeInstancesInput{}
	result, err := c.client.DescribeInstances(context.TODO(), input)
	if err != nil {
		return nil, fmt.Errorf("インスタンス情報の取得に失敗しました: %w", err)
	}

	var instances []InstanceInfo
	for _, reservation := range result.Reservations {
		for _, instance := range reservation.Instances {
			instanceInfo := InstanceInfo{
				InstanceID: *instance.InstanceId,
				State:      string(instance.State.Name),
				PrivateIP:  aws.ToString(instance.PrivateIpAddress),
				PublicIP:   aws.ToString(instance.PublicIpAddress),
			}

			// NameタグからインスタンスB名を取得
			for _, tag := range instance.Tags {
				if aws.ToString(tag.Key) == "Name" {
					instanceInfo.InstanceName = aws.ToString(tag.Value)
					break
				}
			}

			instances = append(instances, instanceInfo)
		}
	}

	return instances, nil
}

// FilterInstances はインスタンスリストをフィルタリングする
func FilterInstances(instances []InstanceInfo, query string) []InstanceInfo {
	if query == "" {
		return instances
	}

	lowercaseQuery := strings.ToLower(query)
	var filtered []InstanceInfo

	for _, instance := range instances {
		// インスタンスIDもしくはインスタンス名に検索クエリが含まれるかをチェック
		if strings.Contains(strings.ToLower(instance.InstanceID), lowercaseQuery) ||
			strings.Contains(strings.ToLower(instance.InstanceName), lowercaseQuery) {
			filtered = append(filtered, instance)
		}
	}

	return filtered
}

// GetInstanceByID は指定されたIDのインスタンス情報を取得する
func (c *EC2Client) GetInstanceByID(instanceID string) (*InstanceInfo, error) {
	input := &ec2.DescribeInstancesInput{
		InstanceIds: []string{instanceID},
	}

	result, err := c.client.DescribeInstances(context.TODO(), input)
	if err != nil {
		return nil, fmt.Errorf("インスタンス情報の取得に失敗しました: %w", err)
	}

	if len(result.Reservations) == 0 || len(result.Reservations[0].Instances) == 0 {
		return nil, fmt.Errorf("インスタンスが見つかりませんでした: %s", instanceID)
	}

	instance := result.Reservations[0].Instances[0]
	instanceInfo := &InstanceInfo{
		InstanceID: *instance.InstanceId,
		State:      string(instance.State.Name),
		PrivateIP:  aws.ToString(instance.PrivateIpAddress),
		PublicIP:   aws.ToString(instance.PublicIpAddress),
	}

	// NameタグからインスタンスB名を取得
	for _, tag := range instance.Tags {
		if aws.ToString(tag.Key) == "Name" {
			instanceInfo.InstanceName = aws.ToString(tag.Value)
			break
		}
	}

	return instanceInfo, nil
}

// SSMClient はSSM操作を行うためのクライアント
type SSMClient struct {
	client *ssm.Client
}

// NewSSMClient は新しいSSMクライアントを作成する
func NewSSMClient() (*SSMClient, error) {
	// AWS 設定を読み込む
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, fmt.Errorf("AWS設定の読み込みに失敗しました: %w", err)
	}

	// SSMクライアントを作成
	client := ssm.NewFromConfig(cfg)
	return &SSMClient{client: client}, nil
}

// StartSession はSSMセッションを開始する
func (c *SSMClient) StartSession(instanceID string) error {
	fmt.Printf("インスタンス %s にSSM接続しています...\n", instanceID)

	// AWS CLIを使用してSSMセッションを開始
	cmd := exec.Command("aws", "ssm", "start-session", "--target", instanceID)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	return cmd.Run()
}

// CheckInstanceSSMStatus はインスタンスがSSM接続可能かを確認する
func (c *SSMClient) CheckInstanceSSMStatus(instanceID string) (bool, error) {
	input := &ssm.DescribeInstanceInformationInput{}
	result, err := c.client.DescribeInstanceInformation(context.TODO(), input)
	if err != nil {
		return false, fmt.Errorf("インスタンス情報の取得に失敗しました: %w", err)
	}

	for _, instance := range result.InstanceInformationList {
		if *instance.InstanceId == instanceID && instance.PingStatus == "Online" {
			return true, nil
		}
	}

	return false, nil
}

// LogClient はCloudWatch Logs操作を行うためのクライアント
type LogClient struct {
	client *cloudwatchlogs.Client
}

// LogEntry はログエントリを表す構造体
type LogEntry struct {
	Timestamp int64
	Message   string
}

// NewLogClient は新しいCloudWatch Logsクライアントを作成する
func NewLogClient() (*LogClient, error) {
	// AWS 設定を読み込む
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, fmt.Errorf("AWS設定の読み込みに失敗しました: %w", err)
	}

	// CloudWatch Logsクライアントを作成
	client := cloudwatchlogs.NewFromConfig(cfg)
	return &LogClient{client: client}, nil
}

// GetSystemLogs はEC2インスタンスのシステムログを取得する
func (c *LogClient) GetSystemLogs(instanceID string, startTime, endTime time.Time) ([]LogEntry, error) {
	// EC2のシステムログのロググループ名
	logGroupName := fmt.Sprintf("/aws/ec2/instances/%s/var/log/system.log", instanceID)

	// ロググループが存在するかチェック
	describeInput := &cloudwatchlogs.DescribeLogGroupsInput{
		LogGroupNamePrefix: aws.String(logGroupName),
	}
	
	describeResult, err := c.client.DescribeLogGroups(context.TODO(), describeInput)
	if err != nil {
		return nil, fmt.Errorf("ロググループの確認に失敗しました: %w", err)
	}
	
	groupExists := false
	for _, group := range describeResult.LogGroups {
		if aws.ToString(group.LogGroupName) == logGroupName {
			groupExists = true
			break
		}
	}
	
	if !groupExists {
		return nil, fmt.Errorf("インスタンス %s のシステムログが見つかりません", instanceID)
	}

	// ログストリームを取得
	streamsInput := &cloudwatchlogs.DescribeLogStreamsInput{
		LogGroupName: aws.String(logGroupName),
		OrderBy:      types.OrderByLastEventTime,
		Descending:   aws.Bool(true),
		Limit:        aws.Int32(10),
	}
	
	streamsResult, err := c.client.DescribeLogStreams(context.TODO(), streamsInput)
	if err != nil {
		return nil, fmt.Errorf("ログストリームの取得に失敗しました: %w", err)
	}
	
	if len(streamsResult.LogStreams) == 0 {
		return nil, fmt.Errorf("インスタンス %s のログストリームが見つかりません", instanceID)
	}

	// 最新のログストリームからログイベントを取得
	eventsInput := &cloudwatchlogs.GetLogEventsInput{
		LogGroupName:  aws.String(logGroupName),
		LogStreamName: streamsResult.LogStreams[0].LogStreamName,
		StartTime:     aws.Int64(startTime.UnixMilli()),
		EndTime:       aws.Int64(endTime.UnixMilli()),
		Limit:         aws.Int32(1000),
	}
	
	eventsResult, err := c.client.GetLogEvents(context.TODO(), eventsInput)
	if err != nil {
		return nil, fmt.Errorf("ログイベントの取得に失敗しました: %w", err)
	}

	var logEntries []LogEntry
	for _, event := range eventsResult.Events {
		logEntries = append(logEntries, LogEntry{
			Timestamp: aws.ToInt64(event.Timestamp),
			Message:   aws.ToString(event.Message),
		})
	}

	return logEntries, nil
}

// FilterLogs はログエントリをフィルタリングする
func FilterLogs(logs []LogEntry, query string) []LogEntry {
	if query == "" {
		return logs
	}

	lowercaseQuery := strings.ToLower(query)
	var filtered []LogEntry

	for _, log := range logs {
		if strings.Contains(strings.ToLower(log.Message), lowercaseQuery) {
			filtered = append(filtered, log)
		}
	}

	return filtered
}

// FormatLogTime はUNIXタイムスタンプをフォーマットする
func FormatLogTime(timestamp int64) string {
	t := time.Unix(timestamp/1000, 0)
	return t.Format("2006-01-02 15:04:05")
}

// スタイル定義
var (
	titleStyle        = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FAFAFA")).Background(lipgloss.Color("#7D56F4")).Padding(0, 1)
	selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Background(lipgloss.Color("#2D7D9A"))
	itemStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("#CCCCCC"))
	errorStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
	infoStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
	helpStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666"))
)

// モデルの状態
const (
	stateList         = iota // インスタンス一覧表示
	stateSearch              // 検索モード
	stateInstanceMenu        // インスタンスメニュー表示
	stateViewLogs            // ログ表示
	stateSearchLogs          // ログ検索
)

// メインモデル
type model struct {
	state            int
	instances        []InstanceInfo
	filteredInstance []InstanceInfo
	cursor           int
	searchQuery      string
	viewMode         string
	error            string
	info             string
	selectedInstance InstanceInfo
	logs             []LogEntry
	filteredLogs     []LogEntry
	logSearchQuery   string
}

func initialModel() model {
	return model{
		state:       stateList,
		searchQuery: "",
		viewMode:    "all",
	}
}

// Init はアプリケーションの初期化を行う
func (m model) Init() tea.Cmd {
	return tea.Batch(
		loadInstancesCmd(),
	)
}

// インスタンス情報読み込みコマンド
func loadInstancesCmd() tea.Cmd {
	return func() tea.Msg {
		client, err := NewEC2Client()
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		instances, err := client.ListInstances()
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		return instancesLoadedMsg{instances: instances}
	}
}

// SSM接続チェックコマンド
func checkSSMStatusCmd(instanceID string) tea.Cmd {
	return func() tea.Msg {
		client, err := NewSSMClient()
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		status, err := client.CheckInstanceSSMStatus(instanceID)
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		return ssmStatusMsg{
			instanceID: instanceID,
			ssmEnabled: status,
		}
	}
}

// ログ取得コマンド
func getLogsCmd(instanceID string) tea.Cmd {
	return func() tea.Msg {
		client, err := NewLogClient()
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		// 過去24時間のログを取得
		endTime := time.Now()
		startTime := endTime.Add(-24 * time.Hour)

		logEntries, err := client.GetSystemLogs(instanceID, startTime, endTime)
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		return logsLoadedMsg{logs: logEntries}
	}
}

// SSM接続コマンド
func connectSSMCmd(instanceID string) tea.Cmd {
	return func() tea.Msg {
		client, err := NewSSMClient()
		if err != nil {
			return errorMsg{err: err.Error()}
		}

		if err := client.StartSession(instanceID); err != nil {
			return errorMsg{err: err.Error()}
		}

		return ssmConnectedMsg{}
	}
}

// メッセージ型定義
type errorMsg struct {
	err string
}

type instancesLoadedMsg struct {
	instances []InstanceInfo
}

type ssmStatusMsg struct {
	instanceID string
	ssmEnabled bool
}

type ssmConnectedMsg struct{}

type logsLoadedMsg struct {
	logs []LogEntry
}

// Update はモデルの状態を更新する
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch m.state {
		case stateList, stateSearch:
			return m.updateListState(msg)
		case stateInstanceMenu:
			return m.updateInstanceMenuState(msg)
		case stateViewLogs, stateSearchLogs:
			return m.updateLogsState(msg)
		}

	case instancesLoadedMsg:
		m.instances = msg.instances
		m.filteredInstance = msg.instances
		m.error = ""
		return m, nil

	case errorMsg:
		m.error = msg.err
		return m, nil

	case ssmStatusMsg:
		if msg.ssmEnabled {
			m.info = fmt.Sprintf("インスタンス %s はSSM接続可能です。接続しています...", msg.instanceID)
			return m, connectSSMCmd(msg.instanceID)
		} else {
			m.error = fmt.Sprintf("インスタンス %s はSSM接続が利用できません", msg.instanceID)
			return m, nil
		}

	case ssmConnectedMsg:
		// SSM接続後はインスタンス一覧に戻る
		m.state = stateList
		m.info = "SSMセッションが終了しました"
		return m, loadInstancesCmd()

	case logsLoadedMsg:
		m.logs = msg.logs
		m.filteredLogs = msg.logs
		m.state = stateViewLogs
		return m, nil
	}

	return m, nil
}

// インスタンス一覧・検索モードの状態更新
func (m model) updateListState(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "ctrl+c", "q":
		return m, tea.Quit

	case "up", "k":
		if m.cursor > 0 {
			m.cursor--
		}

	case "down", "j":
		if m.cursor < len(m.filteredInstance)-1 {
			m.cursor++
		}

	case "/":
		// 検索モードに切り替え
		m.state = stateSearch

	case "enter":
		if len(m.filteredInstance) > 0 && m.cursor < len(m.filteredInstance) {
			// インスタンスを選択してメニューモードへ
			m.selectedInstance = m.filteredInstance[m.cursor]
			m.state = stateInstanceMenu
			m.error = ""
			m.info = ""
		}

	case "esc":
		if m.state == stateSearch {
			// 検索モードを終了
			m.state = stateList
			m.searchQuery = ""
			m.filteredInstance = m.instances
		}

	default:
		if m.state == stateSearch {
			// 検索クエリを処理
			switch msg.String() {
			case "backspace":
				if len(m.searchQuery) > 0 {
					m.searchQuery = m.searchQuery[:len(m.searchQuery)-1]
				}
			case "enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down":
				// これらのキーは検索クエリに追加しない
			default:
				m.searchQuery += msg.String()
			}

			// 検索クエリに基づいてインスタンスをフィルタリング
			m.filteredInstance = FilterInstances(m.instances, m.searchQuery)
			if m.cursor >= len(m.filteredInstance) {
				m.cursor = len(m.filteredInstance) - 1
			}
			if m.cursor < 0 {
				m.cursor = 0
			}
		}
	}

	return m, nil
}

// インスタンスメニューモードの状態更新
func (m model) updateInstanceMenuState(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "esc":
		// インスタンス一覧に戻る
		m.state = stateList
		m.error = ""
		m.info = ""

	case "s":
		// SSM接続
		m.info = fmt.Sprintf("インスタンス %s のSSM接続状態を確認中...", m.selectedInstance.InstanceID)
		return m, checkSSMStatusCmd(m.selectedInstance.InstanceID)

	case "l":
		// ログ表示
		m.info = fmt.Sprintf("インスタンス %s のシステムログを取得中...", m.selectedInstance.InstanceID)
		return m, getLogsCmd(m.selectedInstance.InstanceID)
	}

	return m, nil
}

// ログ表示・検索モードの状態更新
func (m model) updateLogsState(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "esc":
		if m.state == stateSearchLogs {
			// ログ検索モードからログ表示モードに戻る
			m.state = stateViewLogs
			m.logSearchQuery = ""
			m.filteredLogs = m.logs
		} else {
			// インスタンスメニューに戻る
			m.state = stateInstanceMenu
		}

	case "/":
		// ログ検索モードに切り替え
		m.state = stateSearchLogs

	default:
		if m.state == stateSearchLogs {
			// ログ検索クエリを処理
			switch msg.String() {
			case "backspace":
				if len(m.logSearchQuery) > 0 {
					m.logSearchQuery = m.logSearchQuery[:len(m.logSearchQuery)-1]
				}
			case "enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down":
				// これらのキーは検索クエリに追加しない
			default:
				m.logSearchQuery += msg.String()
			}

			// 検索クエリに基づいてログをフィルタリング
			m.filteredLogs = FilterLogs(m.logs, m.logSearchQuery)
		}
	}

	return m, nil
}

// View はUIをレンダリングする
func (m model) View() string {
	switch m.state {
	case stateList, stateSearch:
		return m.renderInstanceList()
	case stateInstanceMenu:
		return m.renderInstanceMenu()
	case stateViewLogs, stateSearchLogs:
		return m.renderLogs()
	default:
		return "Unknown state"
	}
}

// インスタンス一覧表示
func (m model) renderInstanceList() string {
	var s strings.Builder

	title := titleStyle.Render("EC2インスタンス管理ツール")
	s.WriteString(title + "\n\n")

	if m.state == stateSearch {
		s.WriteString(fmt.Sprintf("検索: %s\n\n", m.searchQuery))
	}

	if len(m.filteredInstance) == 0 {
		s.WriteString("表示するインスタンスがありません\n")
	} else {
		for i, instance := range m.filteredInstance {
			item := fmt.Sprintf("%s %s (%s)", instance.InstanceID, instance.InstanceName, instance.State)
			
			if i == m.cursor {
				s.WriteString(selectedItemStyle.Render(item) + "\n")
			} else {
				s.WriteString(itemStyle.Render(item) + "\n")
			}
		}
	}

	s.WriteString("\n")
	
	// エラーまたは情報メッセージを表示
	if m.error != "" {
		s.WriteString(errorStyle.Render(m.error) + "\n")
	}
	if m.info != "" {
		s.WriteString(infoStyle.Render(m.info) + "\n")
	}

	// ヘルプ表示
	s.WriteString("\n")
	s.WriteString(helpStyle.Render("↑/↓: 移動, /: 検索, Enter: 選択, q: 終了") + "\n")

	return s.String()
}

// インスタンスメニュー表示
func (m model) renderInstanceMenu() string {
	var s strings.Builder

	title := titleStyle.Render("インスタンス操作")
	s.WriteString(title + "\n\n")

	s.WriteString(fmt.Sprintf("選択中のインスタンス: %s %s (%s)\n\n", 
		m.selectedInstance.InstanceID, 
		m.selectedInstance.InstanceName,
		m.selectedInstance.State))

	s.WriteString("以下の操作が可能です:\n\n")
	s.WriteString("s: SSM接続\n")
	s.WriteString("l: システムログを表示\n")
	s.WriteString("q/Esc: 戻る\n\n")

	// エラーまたは情報メッセージを表示
	if m.error != "" {
		s.WriteString(errorStyle.Render(m.error) + "\n")
	}
	if m.info != "" {
		s.WriteString(infoStyle.Render(m.info) + "\n")
	}

	return s.String()
}

// ログ表示
func (m model) renderLogs() string {
	var s strings.Builder

	title := titleStyle.Render(fmt.Sprintf("インスタンス %s のシステムログ", m.selectedInstance.InstanceID))
	s.WriteString(title + "\n\n")

	if m.state == stateSearchLogs {
		s.WriteString(fmt.Sprintf("検索: %s\n\n", m.logSearchQuery))
	}

	if len(m.filteredLogs) == 0 {
		s.WriteString("表示するログがありません\n")
	} else {
		// 最新のログを最大20件表示
		maxLogs := 20
		startIdx := 0
		if len(m.filteredLogs) > maxLogs {
			startIdx = len(m.filteredLogs) - maxLogs
		}

		for i := startIdx; i < len(m.filteredLogs); i++ {
			log := m.filteredLogs[i]
			timeStr := FormatLogTime(log.Timestamp)
			s.WriteString(fmt.Sprintf("[%s] %s\n", timeStr, log.Message))
		}
	}

	s.WriteString("\n")
	
	// エラーまたは情報メッセージを表示
	if m.error != "" {
		s.WriteString(errorStyle.Render(m.error) + "\n")
	}

	// ヘルプ表示
	s.WriteString("\n")
	s.WriteString(helpStyle.Render("/: ログ検索, q/Esc: 戻る") + "\n")

	return s.String()
}

// AWS認証情報を設定する関数
func setupAWSCredentials() error {
	// AWS認証情報ファイルへのパス
	credPath := filepath.Join("/path/to/aws", ".credentials")
	configPath := filepath.Join("/path/to/aws", "config")

	// 認証情報ファイルの存在確認
	if _, err := os.Stat(credPath); err != nil {
		return fmt.Errorf("AWS認証情報ファイルが見つかりません: %w", err)
	}
	if _, err := os.Stat(configPath); err != nil {
		return fmt.Errorf("AWS設定ファイルが見つかりません: %w", err)
	}

	// AWS_SHARED_CREDENTIALS_FILE環境変数を設定
	os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credPath)
	os.Setenv("AWS_CONFIG_FILE", configPath)

	return nil
}

func main() {
	// AWS認証情報の設定
	if err := setupAWSCredentials(); err != nil {
		fmt.Printf("AWS認証情報の設定に失敗しました: %v\n", err)
		os.Exit(1)
	}

	p := tea.NewProgram(initialModel())
	if _, err := p.Run(); err != nil {
		fmt.Printf("エラーが発生しました: %v\n", err)
		os.Exit(1)
	}
}
このスクラップは5ヶ月前にクローズされました