+ 本文思路基于echo框架,将pulumi和Automation机制写法进行封装 [参考地址](https://www.pulumi.com/docs/iac/using-pulumi/automation-api/getting-started-automation-api/) ### 集成自动化平台思路 + 整体思路由自动化平台扩容后存储DB,pulumi服务基于id取对应参数反序列化json解析拿到并填充  ### 环境变量 ```bash # ALICLOUD_ACCESS_KEY和AWS_ACCESS_KEY_ID可以一样,需要执行下面login将pulumi状态存储到支持s3协议的OSS对象存储中 # pulumi login s3://xxx?endpoint=oss-cn-beijing.aliyuncs.com export ALICLOUD_ACCESS_KEY="xxxxxxxxxx" export ALICLOUD_SECRET_KEY="xxxxxxxxxx" export AWS_ACCESS_KEY_ID="xxxxxxxxxx" export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxx" export AWS_REGION=oss-cn-beijing export PULUMI_CONFIG_PASSPHRASE="xxxxxxxxxxxx" ``` ### 目录结构 ``` ├── go.mod ├── go.sum ├── main.go ├── README.md └── utils ├── factory.go ├── logger │ └── logger.go └── provider └── aliyun └── aliyun.go ``` ### 核心代码 > `main.go` ```golang package main import ( "errors" "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "iss-pulumi/utils" ) type CloudRequest struct { Provider string `json:"provider"` Config utils.CloudConfig `json:"config"` } type CloudResponse struct { Success bool `json:"success"` Message string `json:"message"` Outputs map[string]interface{} `json:"outputs,omitempty"` } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) e.POST("/setup", setupInfra) e.POST("/destroy", destroyInfra) e.Logger.Fatal(e.Start(":8333")) } func setupInfra(c echo.Context) error { req := new(CloudRequest) if err := c.Bind(req); err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: "请求参数无效: " + err.Error(), }) } err := filterReqParams(req) if err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: err.Error(), }) } config := utils.DefaultConfig() if req.Config.Name != "" { config.Name = req.Config.Name } if req.Config.AvailabilityZone != "" { config.AvailabilityZone = req.Config.AvailabilityZone } if req.Config.Region != "" { config.Region = req.Config.Region } if req.Config.ProjectName != "" { config.ProjectName = req.Config.ProjectName } if req.Config.StackName != "" { config.StackName = req.Config.StackName } if req.Config.InstanceType != "" { config.InstanceType = req.Config.InstanceType } if req.Config.SystemDiskSize != 0 { config.SystemDiskSize = req.Config.SystemDiskSize } if req.Config.DataDiskSize != 0 { config.DataDiskSize = req.Config.DataDiskSize } if req.Config.BandwidthOut != 0 { config.BandwidthOut = req.Config.BandwidthOut } if len(req.Config.Tags) > 0 { config.Tags = req.Config.Tags } factory := utils.NewCloudFactory() provider, err := factory.CreateProvider(req.Provider) if err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: err.Error(), }) } result, err := provider.SetupInfra(config) if err != nil { return c.JSON(http.StatusInternalServerError, CloudResponse{ Success: false, Message: "创建基础设施失败: " + err.Error(), }) } outputs := make(map[string]interface{}) for k, v := range result.Outputs { outputs[k] = v.Value } return c.JSON(http.StatusOK, CloudResponse{ Success: true, Message: "基础设施创建成功", Outputs: outputs, }) } func filterReqParams(req *CloudRequest) error { var notNullList = [3]string{req.Provider, req.Config.Name, req.Config.ProjectName} for _, notNull := range notNullList { if notNull != "" { continue } return errors.New("请求参数无效: " + "provider, name, projectName" + "不能为空") } return nil } func destroyInfra(c echo.Context) error { req := new(CloudRequest) if err := c.Bind(req); err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: "请求参数无效: " + err.Error(), }) } err := filterReqParams(req) if err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: err.Error(), }) } config := utils.DefaultConfig() if req.Config.Name != "" { config.Name = req.Config.Name } if req.Config.ProjectName != "" { config.ProjectName = req.Config.ProjectName } if req.Config.StackName != "" { config.StackName = req.Config.StackName } factory := utils.NewCloudFactory() provider, err := factory.CreateProvider(req.Provider) if err != nil { return c.JSON(http.StatusBadRequest, CloudResponse{ Success: false, Message: err.Error(), }) } err = provider.DestroyInfra(config) if err != nil { return c.JSON(http.StatusInternalServerError, CloudResponse{ Success: false, Message: "销毁基础设施失败: " + err.Error(), }) } return c.JSON(http.StatusOK, CloudResponse{ Success: true, Message: "基础设施销毁成功", }) } ``` > `utils/provider/factory` ```golang package utils import ( "errors" "iss-pulumi/utils/provider/aliyun" "github.com/pulumi/pulumi/sdk/v3/go/auto" ) type CloudConfig struct { Name string `json:"name"` AvailabilityZone string `json:"availabilityZone"` Region string `json:"region"` ProjectName string `json:"projectName"` StackName string `json:"stackName"` Tags map[string]string `json:"tags"` InstanceType string `json:"instanceType"` SystemDiskSize int `json:"systemDiskSize"` DataDiskSize int `json:"dataDiskSize"` BandwidthOut int `json:"bandwidthOut"` } func DefaultConfig() CloudConfig { return CloudConfig{ AvailabilityZone: "cn-beijing-f", Region: "cn-beijing", StackName: "dev", InstanceType: "ecs.c7.large", SystemDiskSize: 40, DataDiskSize: 400, BandwidthOut: 10, } } type CloudProvider interface { SetupInfra(config CloudConfig) (auto.UpResult, error) DestroyInfra(config CloudConfig) error } type CloudProviderFactory interface { CreateProvider(providerType string) (CloudProvider, error) } type CloudFactory struct{} // NewCloudFactory 创建工厂实例 func NewCloudFactory() *CloudFactory { return &CloudFactory{} } // CreateProvider 根据类型创建对应的云厂商提供者 func (f *CloudFactory) CreateProvider(providerType string) (CloudProvider, error) { switch providerType { case "aliyun": provider := aliyun.NewALiYunProvider() return &aliYunProviderAdapter{provider: provider}, nil // 可以在此添加其他云厂商支持 // case "aws": // return NewAWSProvider(), nil default: return nil, errors.New("不支持的云厂商类型") } } type aliYunProviderAdapter struct { provider *aliyun.ALiYunProvider } func (a *aliYunProviderAdapter) SetupInfra(config CloudConfig) (auto.UpResult, error) { aliConfig := aliyun.CloudConfig{ StackName: config.StackName, ProjectName: config.ProjectName, Region: config.Region, Name: config.Name, AvailabilityZone: config.AvailabilityZone, InstanceType: config.InstanceType, SystemDiskSize: config.SystemDiskSize, DataDiskSize: config.DataDiskSize, BandwidthOut: config.BandwidthOut, Tags: config.Tags, } result, err := a.provider.SetupInfra(aliConfig) if err != nil { return auto.UpResult{}, err } return result, nil } func (a *aliYunProviderAdapter) DestroyInfra(config CloudConfig) error { aliConfig := aliyun.CloudConfig{ StackName: config.StackName, ProjectName: config.ProjectName, Region: config.Region, Name: config.Name, AvailabilityZone: config.AvailabilityZone, InstanceType: config.InstanceType, SystemDiskSize: config.SystemDiskSize, DataDiskSize: config.DataDiskSize, BandwidthOut: config.BandwidthOut, Tags: config.Tags, } return a.provider.DestroyInfra(aliConfig) } ``` > `utils/provider/aliyun/aliyun.go` ```golang package aliyun import ( "context" "fmt" "os" "github.com/pulumi/pulumi-alicloud/sdk/v3/go/alicloud" "github.com/pulumi/pulumi-alicloud/sdk/v3/go/alicloud/ecs" "github.com/pulumi/pulumi-alicloud/sdk/v3/go/alicloud/vpc" auto "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "iss-pulumi/utils/logger" ) type ALiYunProvider struct{} func NewALiYunProvider() *ALiYunProvider { return &ALiYunProvider{} } // 定义自己的配置结构体,而不是使用 utils.CloudConfig type CloudConfig struct { StackName string ProjectName string Region string Name string AvailabilityZone string InstanceType string SystemDiskSize int DataDiskSize int BandwidthOut int Tags map[string]string } func (a *ALiYunProvider) SetupInfra(config CloudConfig) (auto.UpResult, error) { ctx := context.Background() stack, err := auto.UpsertStackInlineSource(ctx, config.StackName, config.ProjectName, func(ctx *pulumi.Context) error { return aLiYunPulumiProgram(ctx, config) }) if err != nil { return auto.UpResult{}, fmt.Errorf("stack 创建失败: %w", err) } logger.Info("stack 创建成功:", config.StackName) err = stack.SetConfig(ctx, "alicloud:region", auto.ConfigValue{Value: config.Region}) if err != nil { return auto.UpResult{}, fmt.Errorf("设置 region 失败: %w", err) } err = stack.Workspace().InstallPlugin(ctx, "alicloud", "v3.49.0") if err != nil { return auto.UpResult{}, fmt.Errorf("安装插件失败: %w", err) } logger.Info("正在执行 pulumi up ...") res, err := stack.Up(ctx, optup.ProgressStreams(os.Stdout)) if err != nil { return auto.UpResult{}, fmt.Errorf("部署失败: %w", err) } logger.Info("资源创建完成,输出如下:") for k, v := range res.Outputs { logger.Infof("%s = %v", k, v.Value) } return res, nil } func (a *ALiYunProvider) DestroyInfra(config CloudConfig) error { ctx := context.Background() stack, err := auto.SelectStackInlineSource(ctx, config.StackName, config.ProjectName, func(ctx *pulumi.Context) error { return aLiYunPulumiProgram(ctx, config) }) if err != nil { return fmt.Errorf("选择 stack 失败: %w", err) } logger.Info("开始销毁资源 ...") _, err = stack.Destroy(ctx, optdestroy.ProgressStreams(os.Stdout)) if err != nil { return fmt.Errorf("销毁失败: %w", err) } logger.Info("资源销毁完成") return nil } // 定义要创建的阿里云资源 func aLiYunPulumiProgram(ctx *pulumi.Context, config CloudConfig) error { name := config.Name availabilityZone := config.AvailabilityZone if availabilityZone == "" { _default, err := alicloud.GetZones(ctx, &alicloud.GetZonesArgs{ AvailableDiskCategory: pulumi.StringRef("cloud_essd"), AvailableResourceCreation: pulumi.StringRef("VSwitch"), }, nil) if err != nil { return fmt.Errorf("获取可用区失败: %w", err) } availabilityZone = _default.Zones[0].Id } // 创建一个 VPC vpcRes, err := vpc.NewNetwork(ctx, "vpc", &vpc.NetworkArgs{ VpcName: pulumi.String(name), CidrBlock: pulumi.String("172.16.0.0/16"), }) if err != nil { return err } // 创建一个 VSwitch(交换机) vswitch, err := vpc.NewSwitch(ctx, "vswitch", &vpc.SwitchArgs{ VpcId: vpcRes.ID(), CidrBlock: pulumi.String("172.16.1.0/24"), ZoneId: pulumi.String(availabilityZone), }) if err != nil { return err } group, err := ecs.NewSecurityGroup(ctx, "group", &ecs.SecurityGroupArgs{ SecurityGroupName: pulumi.String(name), Description: pulumi.String("foo"), VpcId: vpcRes.ID(), }) if err != nil { return err } var splat0 pulumi.StringArray splat0 = append(splat0, group.ID()) _, err = ecs.NewInstance(ctx, "instance", &ecs.InstanceArgs{ AvailabilityZone: pulumi.String(availabilityZone), SecurityGroups: splat0, InstanceType: pulumi.String(config.InstanceType), SystemDiskCategory: pulumi.String("cloud_essd"), SystemDiskSize: pulumi.Int(config.SystemDiskSize), SystemDiskName: pulumi.String(name), SystemDiskDescription: pulumi.String("system_disk_description"), ImageId: pulumi.String("ubuntu_22_04_x64_20G_alibase_20250324.vhd"), InstanceName: pulumi.String(name), HostName: pulumi.String(name), VswitchId: vswitch.ID(), InternetMaxBandwidthOut: pulumi.Int(config.BandwidthOut), InstanceChargeType: pulumi.String("PostPaid"), SpotStrategy: pulumi.String("NoSpot"), Tags: pulumi.ToStringMap(config.Tags), DataDisks: ecs.InstanceDataDiskArray{ &ecs.InstanceDataDiskArgs{ Name: pulumi.String("data-disk"), Size: pulumi.Int(config.DataDiskSize), Category: pulumi.String("cloud_essd"), Description: pulumi.String("disk-description"), Encrypted: pulumi.Bool(false), DeleteWithInstance: pulumi.Bool(true), }, }, }) if err != nil { return err } return nil } ```