GoCasts آموزش Go به زبان ساده

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی

اتوماسیون زیرساخت با گولنگ - SSH، API و Config Management

یکی از کاربردهای عملی Go برای DevOps، ساخت ابزارهای اتوماسیون زیرساخت است. برخلاف اسکریپت‌های Bash یا Python، ابزارهای Go قابل توزیع، سریع و type-safe هستند.

در این آموزش، چندین سناریوی واقعی را پیاده‌سازی می‌کنیم.

۱. SSH Automation

اجرای دستور روی سرور

package main

import (
    "bytes"
    "fmt"
    "log"
    "os"
    "time"

    "golang.org/x/crypto/ssh"
)

func connectSSH(host, user, keyPath string) (*ssh.Client, error) {
    // خواندن private key
    key, err := os.ReadFile(keyPath)
    if err != nil {
        return nil, fmt.Errorf("cannot read key file: %w", err)
    }

    signer, err := ssh.ParsePrivateKey(key)
    if err != nil {
        return nil, fmt.Errorf("cannot parse key: %w", err)
    }

    config := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(), // فقط برای تست!
        Timeout:         10 * time.Second,
    }

    client, err := ssh.Dial("tcp", host+":22", config)
    if err != nil {
        return nil, fmt.Errorf("cannot connect: %w", err)
    }

    return client, nil
}

func runCommand(client *ssh.Client, command string) (string, error) {
    session, err := client.NewSession()
    if err != nil {
        return "", err
    }
    defer session.Close()

    var stdout, stderr bytes.Buffer
    session.Stdout = &stdout
    session.Stderr = &stderr

    if err := session.Run(command); err != nil {
        return "", fmt.Errorf("%s: %w", stderr.String(), err)
    }

    return stdout.String(), nil
}

func main() {
    client, err := connectSSH(
        "192.168.1.100",
        "ubuntu",
        os.Getenv("HOME")+"/.ssh/id_rsa",
    )
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // اجرای دستور
    output, err := runCommand(client, "uptime && df -h")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(output)
}
go get golang.org/x/crypto/ssh

اجرای دستور روی چندین سرور همزمان

package main

import (
    "bytes"
    "fmt"
    "os"
    "sync"
    "time"

    "golang.org/x/crypto/ssh"
)

type Server struct {
    Name    string
    Host    string
    User    string
    KeyPath string
}

type Result struct {
    Server string
    Output string
    Error  error
}

func runOnServer(server Server, command string, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()

    // خواندن key
    key, err := os.ReadFile(server.KeyPath)
    if err != nil {
        results <- Result{Server: server.Name, Error: err}
        return
    }

    signer, err := ssh.ParsePrivateKey(key)
    if err != nil {
        results <- Result{Server: server.Name, Error: err}
        return
    }

    config := &ssh.ClientConfig{
        User: server.User,
        Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         10 * time.Second,
    }

    client, err := ssh.Dial("tcp", server.Host+":22", config)
    if err != nil {
        results <- Result{Server: server.Name, Error: err}
        return
    }
    defer client.Close()

    session, err := client.NewSession()
    if err != nil {
        results <- Result{Server: server.Name, Error: err}
        return
    }
    defer session.Close()

    var stdout bytes.Buffer
    session.Stdout = &stdout

    if err := session.Run(command); err != nil {
        results <- Result{Server: server.Name, Error: err}
        return
    }

    results <- Result{Server: server.Name, Output: stdout.String()}
}

func main() {
    keyPath := os.Getenv("HOME") + "/.ssh/id_rsa"

    servers := []Server{
        {Name: "web-1", Host: "192.168.1.101", User: "ubuntu", KeyPath: keyPath},
        {Name: "web-2", Host: "192.168.1.102", User: "ubuntu", KeyPath: keyPath},
        {Name: "db-1", Host: "192.168.1.103", User: "ubuntu", KeyPath: keyPath},
    }

    command := "hostname && uptime"
    results := make(chan Result, len(servers))
    var wg sync.WaitGroup

    // اجرای همزمان روی همه سرورها
    for _, server := range servers {
        wg.Add(1)
        go runOnServer(server, command, results, &wg)
    }

    // بستن channel بعد از اتمام همه goroutines
    go func() {
        wg.Wait()
        close(results)
    }()

    // جمع‌آوری نتایج
    for result := range results {
        fmt.Printf("\n=== %s ===\n", result.Server)
        if result.Error != nil {
            fmt.Printf("Error: %v\n", result.Error)
        } else {
            fmt.Println(result.Output)
        }
    }
}

کپی فایل با SCP

package main

import (
    "fmt"
    "io"
    "os"
    "path/filepath"

    "github.com/pkg/sftp"
    "golang.org/x/crypto/ssh"
)

func uploadFile(client *ssh.Client, localPath, remotePath string) error {
    // ایجاد SFTP client
    sftpClient, err := sftp.NewClient(client)
    if err != nil {
        return err
    }
    defer sftpClient.Close()

    // باز کردن فایل محلی
    localFile, err := os.Open(localPath)
    if err != nil {
        return err
    }
    defer localFile.Close()

    // ایجاد فایل remote
    remoteFile, err := sftpClient.Create(remotePath)
    if err != nil {
        return err
    }
    defer remoteFile.Close()

    // کپی محتوا
    _, err = io.Copy(remoteFile, localFile)
    return err
}

func downloadFile(client *ssh.Client, remotePath, localPath string) error {
    sftpClient, err := sftp.NewClient(client)
    if err != nil {
        return err
    }
    defer sftpClient.Close()

    remoteFile, err := sftpClient.Open(remotePath)
    if err != nil {
        return err
    }
    defer remoteFile.Close()

    localFile, err := os.Create(localPath)
    if err != nil {
        return err
    }
    defer localFile.Close()

    _, err = io.Copy(localFile, remoteFile)
    return err
}

// Deploy یک دایرکتوری کامل
func deployDirectory(client *ssh.Client, localDir, remoteDir string) error {
    sftpClient, err := sftp.NewClient(client)
    if err != nil {
        return err
    }
    defer sftpClient.Close()

    return filepath.Walk(localDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // مسیر نسبی
        relPath, _ := filepath.Rel(localDir, path)
        remotePath := filepath.Join(remoteDir, relPath)

        if info.IsDir() {
            return sftpClient.MkdirAll(remotePath)
        }

        // کپی فایل
        localFile, err := os.Open(path)
        if err != nil {
            return err
        }
        defer localFile.Close()

        remoteFile, err := sftpClient.Create(remotePath)
        if err != nil {
            return err
        }
        defer remoteFile.Close()

        _, err = io.Copy(remoteFile, localFile)
        fmt.Printf("Uploaded: %s\n", remotePath)
        return err
    })
}
go get github.com/pkg/sftp

۲. کار با API های Cloud

AWS SDK - لیست EC2 Instances

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/ec2"
)

func main() {
    ctx := context.Background()

    // بارگذاری config از environment یا ~/.aws/credentials
    cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
    if err != nil {
        log.Fatal(err)
    }

    client := ec2.NewFromConfig(cfg)

    // لیست instances
    output, err := client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{})
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("EC2 Instances:")
    fmt.Println("─────────────────────────────────────────")

    for _, reservation := range output.Reservations {
        for _, instance := range reservation.Instances {
            name := ""
            for _, tag := range instance.Tags {
                if *tag.Key == "Name" {
                    name = *tag.Value
                    break
                }
            }

            fmt.Printf("ID: %s\n", *instance.InstanceId)
            fmt.Printf("Name: %s\n", name)
            fmt.Printf("Type: %s\n", instance.InstanceType)
            fmt.Printf("State: %s\n", instance.State.Name)
            if instance.PublicIpAddress != nil {
                fmt.Printf("Public IP: %s\n", *instance.PublicIpAddress)
            }
            fmt.Println("─────────────────────────────────────────")
        }
    }
}
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/ec2

HTTP Client برای REST API

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type APIClient struct {
    BaseURL    string
    HTTPClient *http.Client
    Token      string
}

func NewAPIClient(baseURL, token string) *APIClient {
    return &APIClient{
        BaseURL: baseURL,
        Token:   token,
        HTTPClient: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

func (c *APIClient) doRequest(ctx context.Context, method, path string, body any) (*http.Response, error) {
    var bodyReader io.Reader
    if body != nil {
        jsonBody, err := json.Marshal(body)
        if err != nil {
            return nil, err
        }
        bodyReader = bytes.NewBuffer(jsonBody)
    }

    req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, bodyReader)
    if err != nil {
        return nil, err
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+c.Token)

    return c.HTTPClient.Do(req)
}

// مثال: Cloudflare API
type DNSRecord struct {
    ID      string `json:"id,omitempty"`
    Type    string `json:"type"`
    Name    string `json:"name"`
    Content string `json:"content"`
    TTL     int    `json:"ttl"`
}

type CloudflareResponse struct {
    Success bool        `json:"success"`
    Result  []DNSRecord `json:"result"`
}

func (c *APIClient) ListDNSRecords(ctx context.Context, zoneID string) ([]DNSRecord, error) {
    resp, err := c.doRequest(ctx, "GET", "/zones/"+zoneID+"/dns_records", nil)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result CloudflareResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }

    return result.Result, nil
}

func (c *APIClient) CreateDNSRecord(ctx context.Context, zoneID string, record DNSRecord) error {
    resp, err := c.doRequest(ctx, "POST", "/zones/"+zoneID+"/dns_records", record)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("API error: %s", string(body))
    }

    return nil
}

func main() {
    client := NewAPIClient(
        "https://api.cloudflare.com/client/v4",
        "your-api-token",
    )

    ctx := context.Background()
    records, err := client.ListDNSRecords(ctx, "zone-id")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    for _, record := range records {
        fmt.Printf("%s %s -> %s\n", record.Type, record.Name, record.Content)
    }
}

۳. پردازش فایل‌های Config

خواندن و نوشتن YAML

package main

import (
    "fmt"
    "log"
    "os"

    "gopkg.in/yaml.v3"
)

type ServerConfig struct {
    Name        string            `yaml:"name"`
    Host        string            `yaml:"host"`
    Port        int               `yaml:"port"`
    Environment string            `yaml:"environment"`
    Features    []string          `yaml:"features"`
    Database    DatabaseConfig    `yaml:"database"`
    Labels      map[string]string `yaml:"labels"`
}

type DatabaseConfig struct {
    Host     string `yaml:"host"`
    Port     int    `yaml:"port"`
    Name     string `yaml:"name"`
    Username string `yaml:"username"`
    Password string `yaml:"password,omitempty"`
}

type Config struct {
    Version string         `yaml:"version"`
    Servers []ServerConfig `yaml:"servers"`
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    var config Config
    if err := yaml.Unmarshal(data, &config); err != nil {
        return nil, err
    }

    return &config, nil
}

func SaveConfig(path string, config *Config) error {
    data, err := yaml.Marshal(config)
    if err != nil {
        return err
    }

    return os.WriteFile(path, data, 0644)
}

func main() {
    // ایجاد config جدید
    config := &Config{
        Version: "1.0",
        Servers: []ServerConfig{
            {
                Name:        "api-server",
                Host:        "api.example.com",
                Port:        8080,
                Environment: "production",
                Features:    []string{"auth", "logging", "metrics"},
                Database: DatabaseConfig{
                    Host:     "db.example.com",
                    Port:     5432,
                    Name:     "myapp",
                    Username: "admin",
                },
                Labels: map[string]string{
                    "team":    "backend",
                    "version": "v2",
                },
            },
        },
    }

    // ذخیره
    if err := SaveConfig("config.yaml", config); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Config saved to config.yaml")

    // خواندن
    loaded, err := LoadConfig("config.yaml")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Loaded %d servers\n", len(loaded.Servers))
    for _, server := range loaded.Servers {
        fmt.Printf("- %s (%s:%d)\n", server.Name, server.Host, server.Port)
    }
}
go get gopkg.in/yaml.v3

جایگزینی Template در Config

package main

import (
    "bytes"
    "fmt"
    "os"
    "text/template"
)

type DeploymentVars struct {
    AppName     string
    Environment string
    Replicas    int
    Image       string
    Tag         string
    Port        int
    Resources   ResourceVars
}

type ResourceVars struct {
    CPURequest    string
    CPULimit      string
    MemoryRequest string
    MemoryLimit   string
}

const deploymentTemplate = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .AppName }}
  namespace: {{ .Environment }}
  labels:
    app: {{ .AppName }}
    env: {{ .Environment }}
spec:
  replicas: {{ .Replicas }}
  selector:
    matchLabels:
      app: {{ .AppName }}
  template:
    metadata:
      labels:
        app: {{ .AppName }}
    spec:
      containers:
      - name: {{ .AppName }}
        image: {{ .Image }}:{{ .Tag }}
        ports:
        - containerPort: {{ .Port }}
        resources:
          requests:
            cpu: {{ .Resources.CPURequest }}
            memory: {{ .Resources.MemoryRequest }}
          limits:
            cpu: {{ .Resources.CPULimit }}
            memory: {{ .Resources.MemoryLimit }}
`

func GenerateManifest(vars DeploymentVars) (string, error) {
    tmpl, err := template.New("deployment").Parse(deploymentTemplate)
    if err != nil {
        return "", err
    }

    var buf bytes.Buffer
    if err := tmpl.Execute(&buf, vars); err != nil {
        return "", err
    }

    return buf.String(), nil
}

func main() {
    vars := DeploymentVars{
        AppName:     "api-server",
        Environment: "production",
        Replicas:    3,
        Image:       "registry.example.com/api",
        Tag:         "v1.2.3",
        Port:        8080,
        Resources: ResourceVars{
            CPURequest:    "100m",
            CPULimit:      "500m",
            MemoryRequest: "128Mi",
            MemoryLimit:   "512Mi",
        },
    }

    manifest, err := GenerateManifest(vars)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Println(manifest)

    // ذخیره در فایل
    os.WriteFile("deployment.yaml", []byte(manifest), 0644)
}

۴. Health Check Tool

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net"
    "net/http"
    "os"
    "sync"
    "time"
)

type Check struct {
    Name     string `json:"name"`
    Type     string `json:"type"` // http, tcp, dns
    Target   string `json:"target"`
    Expected int    `json:"expected,omitempty"` // for HTTP
    Timeout  string `json:"timeout"`
}

type CheckResult struct {
    Name     string        `json:"name"`
    Status   string        `json:"status"` // ok, fail
    Duration time.Duration `json:"duration"`
    Error    string        `json:"error,omitempty"`
}

type Config struct {
    Checks []Check `json:"checks"`
}

func checkHTTP(ctx context.Context, target string, expected int, timeout time.Duration) error {
    client := &http.Client{Timeout: timeout}

    req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
    if err != nil {
        return err
    }

    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if expected > 0 && resp.StatusCode != expected {
        return fmt.Errorf("expected %d, got %d", expected, resp.StatusCode)
    }

    return nil
}

func checkTCP(target string, timeout time.Duration) error {
    conn, err := net.DialTimeout("tcp", target, timeout)
    if err != nil {
        return err
    }
    conn.Close()
    return nil
}

func checkDNS(target string, timeout time.Duration) error {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    resolver := &net.Resolver{}
    _, err := resolver.LookupHost(ctx, target)
    return err
}

func runCheck(ctx context.Context, check Check) CheckResult {
    start := time.Now()

    timeout, _ := time.ParseDuration(check.Timeout)
    if timeout == 0 {
        timeout = 10 * time.Second
    }

    var err error
    switch check.Type {
    case "http":
        err = checkHTTP(ctx, check.Target, check.Expected, timeout)
    case "tcp":
        err = checkTCP(check.Target, timeout)
    case "dns":
        err = checkDNS(check.Target, timeout)
    default:
        err = fmt.Errorf("unknown check type: %s", check.Type)
    }

    result := CheckResult{
        Name:     check.Name,
        Duration: time.Since(start),
    }

    if err != nil {
        result.Status = "fail"
        result.Error = err.Error()
    } else {
        result.Status = "ok"
    }

    return result
}

func main() {
    config := Config{
        Checks: []Check{
            {Name: "Google", Type: "http", Target: "https://google.com", Expected: 200, Timeout: "5s"},
            {Name: "DNS Google", Type: "dns", Target: "google.com", Timeout: "5s"},
            {Name: "Redis", Type: "tcp", Target: "localhost:6379", Timeout: "3s"},
            {Name: "API", Type: "http", Target: "http://localhost:8080/health", Expected: 200, Timeout: "5s"},
        },
    }

    ctx := context.Background()
    results := make([]CheckResult, len(config.Checks))
    var wg sync.WaitGroup

    for i, check := range config.Checks {
        wg.Add(1)
        go func(idx int, c Check) {
            defer wg.Done()
            results[idx] = runCheck(ctx, c)
        }(i, check)
    }

    wg.Wait()

    // نمایش نتایج
    fmt.Println("Health Check Results")
    fmt.Println("═══════════════════════════════════════")

    allOK := true
    for _, r := range results {
        status := "✓"
        if r.Status == "fail" {
            status = "✗"
            allOK = false
        }

        fmt.Printf("%s %s (%v)", status, r.Name, r.Duration.Round(time.Millisecond))
        if r.Error != "" {
            fmt.Printf(" - %s", r.Error)
        }
        fmt.Println()
    }

    // خروجی JSON
    output, _ := json.MarshalIndent(results, "", "  ")
    os.WriteFile("health-results.json", output, 0644)

    if !allOK {
        os.Exit(1)
    }
}

۵. Log Parser

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
    "regexp"
    "sort"
    "strings"
)

type LogEntry struct {
    IP        string
    Timestamp string
    Method    string
    Path      string
    Status    string
    Size      string
}

type Stats struct {
    TotalRequests int
    ByStatus      map[string]int
    ByPath        map[string]int
    ByIP          map[string]int
}

// Apache/Nginx combined log format
var logPattern = regexp.MustCompile(
    `^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) [^"]*" (\d+) (\d+|-)`,
)

func parseLogLine(line string) (*LogEntry, error) {
    matches := logPattern.FindStringSubmatch(line)
    if matches == nil {
        return nil, fmt.Errorf("invalid log format")
    }

    return &LogEntry{
        IP:        matches[1],
        Timestamp: matches[2],
        Method:    matches[3],
        Path:      matches[4],
        Status:    matches[5],
        Size:      matches[6],
    }, nil
}

func analyzeLog(filePath string) (*Stats, error) {
    file, err := os.Open(filePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    stats := &Stats{
        ByStatus: make(map[string]int),
        ByPath:   make(map[string]int),
        ByIP:     make(map[string]int),
    }

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        entry, err := parseLogLine(scanner.Text())
        if err != nil {
            continue
        }

        stats.TotalRequests++
        stats.ByStatus[entry.Status]++
        stats.ByPath[entry.Path]++
        stats.ByIP[entry.IP]++
    }

    return stats, scanner.Err()
}

func topN(m map[string]int, n int) []struct {
    Key   string
    Count int
} {
    type kv struct {
        Key   string
        Count int
    }

    var sorted []kv
    for k, v := range m {
        sorted = append(sorted, kv{k, v})
    }

    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i].Count > sorted[j].Count
    })

    if len(sorted) > n {
        sorted = sorted[:n]
    }

    var result []struct {
        Key   string
        Count int
    }
    for _, item := range sorted {
        result = append(result, struct {
            Key   string
            Count int
        }{item.Key, item.Count})
    }

    return result
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: logparser <logfile>")
        os.Exit(1)
    }

    stats, err := analyzeLog(os.Args[1])
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Total Requests: %d\n\n", stats.TotalRequests)

    fmt.Println("Status Codes:")
    for status, count := range stats.ByStatus {
        pct := float64(count) / float64(stats.TotalRequests) * 100
        fmt.Printf("  %s: %d (%.1f%%)\n", status, count, pct)
    }

    fmt.Println("\nTop 10 Paths:")
    for _, item := range topN(stats.ByPath, 10) {
        fmt.Printf("  %s: %d\n", item.Key, item.Count)
    }

    fmt.Println("\nTop 10 IPs:")
    for _, item := range topN(stats.ByIP, 10) {
        fmt.Printf("  %s: %d\n", item.Key, item.Count)
    }

    // خروجی JSON
    output, _ := json.MarshalIndent(stats, "", "  ")
    os.WriteFile("log-stats.json", output, 0644)
}

جمع‌بندی

در این آموزش یاد گرفتید:

موضوع کتابخانه
SSH Automation golang.org/x/crypto/ssh
SFTP File Transfer github.com/pkg/sftp
AWS SDK github.com/aws/aws-sdk-go-v2
YAML Config gopkg.in/yaml.v3
Template Processing text/template (built-in)

این ابزارها به شما کمک می‌کنند:

  • اسکریپت‌های Bash پیچیده را به Go تبدیل کنید
  • ابزارهای قابل توزیع بسازید
  • اتوماسیون type-safe داشته باشید

مقالات مرتبط

منابع

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی