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

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ヶ月前にクローズされました