使用CDK构建Golang托管应用程序跑步者
#aws #go #cdk #apprunner

概述

在10/28/2022,PHP, GO ,.net和Ruby最终在 aws App App Runner的托管运行时得到支持(可以直接从Github部署源代码,而无需创建一个创建一个容器图像),请参阅this page

因此,如果您正在使用GO语言开发后端,我相信您想使用AWS App Runner用GO语言作为托管运行时

我认为,如果我可以在使用AWS CDK的GO语言的GO中构建应用程序和基础架构,那将是很棒的

此外,截至2023年6月,App Runner的L2 Construct在CDK上的Alpha版本中,但我尝试将其构建 ,“ L1构造”和“ alpha版本中的L2构造” 。


假设

App Runner支持的GO版本来自1.18。

因此,此处介绍的代码使用1.18.7

另外,AWS CDK版本是2.52.0


github

所有代码都可以在GitHub

上找到

目录结构

使用以下目录结构。 (省略了与主要主题无关的文件。)

  • 应用
    • 应用程序由App Runner发布的应用程序
  • CDK
    • CDK目录
  • 自定义
    • 此CDK中使用的自定义资源lambda目录
  • create_connection.sh
    • 创建GitHub连接的脚本
  • go.work,go.work.sum
    • GO Workspaces模式的文件
├── app
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── cdk
│   ├── Makefile
│   ├── cdk.context.json
│   ├── cdk.json
│   ├── go-cdk-go-managed-apprunner.go
│   ├── go-cdk-go-managed-apprunner_test.go
│   ├── go.mod
│   ├── go.sum
│   └── input
│       └── input.go
├── create_connection.sh
├── custom
│   ├── custom.go
│   ├── go.mod
│   └── go.sum
├── go.work
└── go.work.sum

工作区模式

这次,如上所述,将出现三组应用程序代码:“应用程序跑步者代码”,“ CDK代码”和“自定义资源lambda的代码”。

可以在一个存储库中准备这些目录,并通过共享go.mod在根目录中构建它们,但是我想准备一个go.mod。 。

因此,我使用了一个名为Workspaces mode的功能,该功能在GO 1.18中引入。

这使您可以在子目录中准备go.mod,并使用多模型开发和构建。

您可以通过点击以下命令进入工作空间模式。

go work init app cdk custom

然后将创建以下go.work文件。

go 1.18

use (
    ./app
    ./cdk
    ./custom
)

CDK代码

使用AWS CDK构建App Runner(批准服务,VPC连接器等)时, l2构造尚未“提供”

但是,在Alpha版本中提供了

因此,在本文中,我将向您展示如何使用“ L1”和“ L2 Alpha版本”构建应用程序跑步者。

go-cdk-go-go-managed-apprunner.go(stack定义)

整个堆栈

为外部参数创建结构称为AppRunnerStackProps,并使用NewAppRunnerStack函数创建堆栈。

对于AppRunnerStackPropsAppRunnerStackInputProps,一个名为input.go的文件管理类型和设置值传递给堆栈的参数。

type AppRunnerStackProps struct {
    awscdk.StackProps
    AppRunnerStackInputProps *input.AppRunnerStackInputProps
}

func NewAppRunnerStack(scope constructs.Construct, id string, props *AppRunnerStackProps) awscdk.Stack {
    var sprops awscdk.StackProps
    if props != nil {
        sprops = props.StackProps
    }
    stack := awscdk.NewStack(scope, &id, &sprops)

    // Resource definition from here

请参见下文的input.go(Adrunnerstackinputprops)。

自定义资源lambda(用于自动configuration)

这突然成为自定义资源的lambda。

为什么突然如此?实际上, App Runner的某些资源还不是“” cloudformation兼容

由于CDK无法创建不支持云形式支持的资源,因此我使用自定义资源在CDK中构建它们。

此外,还有一个用于使用CDK构建自定义资源的模块,它允许您构建简单的事物,而无需自己构建lambda,可以单独构建AWS API(请参阅doc)。

这次,我将创建一个称为“ AutoscalingConfiguration”的自定义资源,但是如果我只想创建它(ongreate),我可以通过在上述模块中调用Create API来做到这一点,但是为了删除,我需要它自己的arn。 ARN本身是在资源创建时创建的随机字符。

创建资源时,ARN本身是作为随机字符串生成的,因此在定义自定义资源时,我还不知道它将是什么字符串,并且我无法在OnDelete定义中指定ARN。

由于仅定义创建不是一个好主意,因此我使用lambda不仅启用创建,而且可以通过“动态列表,检索和指定ARN”时的删除” 在删除时。 (也可以更新。

长话短说,这是自定义资源的lambda创建部分的CDK定义。

    customResourceLambda := awslambda.NewFunction(stack, jsii.String("CustomResourceLambda"), &awslambda.FunctionProps{
        Runtime: awslambda.Runtime_GO_1_X(),
        Handler: jsii.String("main"),
        Code: awslambda.AssetCode_FromAsset(jsii.String("../"), &awss3assets.AssetOptions{
            Bundling: &awscdk.BundlingOptions{
                Image:   awslambda.Runtime_GO_1_X().BundlingImage(),
                Command: jsii.Strings("bash", "-c", "GOOS=linux GOARCH=amd64 go build -o /asset-output/main custom/custom.go"),
                User:    jsii.String("root"),
            },
        }),
        Timeout: awscdk.Duration_Seconds(jsii.Number(900)),
        InitialPolicy: &[]awsiam.PolicyStatement{
            awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
                Actions: &[]*string{
                    jsii.String("apprunner:*AutoScalingConfiguration*"),
                    jsii.String("apprunner:UpdateService"),
                    jsii.String("apprunner:ListOperations"),
                },
                Resources: &[]*string{
                    jsii.String("*"),
                },
            }),
            awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
                Actions: &[]*string{
                    jsii.String("cloudformation:DescribeStacks"),
                },
                Resources: &[]*string{
                    stack.StackId(),
                },
            }),
        },
    })

这是由Docker捆绑的,但是您可以在Code.Bundling.Command中指定构建命令。

从CDK目录开始,通过AssetCode_FromAsset(jsii.String("../"),指定父目录路径以指定父目录路径,而构建命令(Command)则指定custom/custom.go以指定自定义资源lambda的文件,然后Handler: jsii.String("main")指定函数(handler)(handler)(handler) )名称。

jsii.Xxx是一个仅返回指针的函数,原因是省略的,但是由于CDK的GO版本要求将参数指定为指针,因此指定它有点麻烦。

另外,使用InitialPolicy,您可以授予自定义资源Lambda许可来操纵App Runner的自动configuration。

实际创建此自动configuration的自定义资源lambda也写在GO中,但是此代码也很长,并且与主要主题不直接相关。

如果您有兴趣,可以找到here

(我分别使用创建和删除分支编写了创建,更新和删除过程,并且使用创建和更新时返回创建资源的ARN。)

AutoscalingConfiguration

然后使用上面的自定义资源lambda创建自动configuration本身。

这就是名称所暗示的,是自动化的阈值设置资源。

    autoScalingConfiguration := awscdk.NewCustomResource(stack, jsii.String("AutoScalingConfiguration"), &awscdk.CustomResourceProps{
        ResourceType: jsii.String("Custom::AutoScalingConfiguration"),
        Properties: &map[string]interface{}{
            "AutoScalingConfigurationName": *stack.StackName(),
            "MaxConcurrency":               strconv.Itoa(props.AppRunnerStackInputProps.AutoScalingConfigurationArnProps.MaxConcurrency),
            "MaxSize":                      strconv.Itoa(props.AppRunnerStackInputProps.AutoScalingConfigurationArnProps.MaxSize),
            "MinSize":                      strconv.Itoa(props.AppRunnerStackInputProps.AutoScalingConfigurationArnProps.MinSize),
            "StackName":                    *stack.StackName(),
        },
        ServiceToken: customResourceLambda.FunctionArn(),
    })
    autoScalingConfigurationArn := autoScalingConfiguration.GetAttString(jsii.String("AutoScalingConfigurationArn"))

AutoScalingConfigurationName是配置名称,MaxConcurrency是与fire缩放的并发连接的数量,而MaxSizeMinSize分别是最小和最大数字。

所有值均在input.go中指定,并通过props传递。

autoScalingConfigurationArn由以后定义的批准服务使用,因此我通过GetAttString从自定义资源lambda中收到ARN并将其存储在变量中。

GitHub连接

如果您正在使用托管运行时构建App Runner,则需要设置与源代码所在的GitHub的连接。

此需要使用GitHub进行身份验证,并且在创建连接(一种称为资源)并手动按控制台的“完整握手”按钮之前,该构建将无法完成。

因此,由于无法通过整个过程自动构建(手动操作是不可避免的),“如果尚未创建与CreateConnection API创建连接在此期间操作,完成握手的握手,并在完成握手时恢复CDK建筑过程。如果完成握手,则重新启动CDK施工过程。

    connectionArn, err := createConnection(props.AppRunnerStackInputProps.SourceConfigurationProps.ConnectionName, *props.Env.Region)
    if err != nil {
        panic(err)
    }

这里的功能本身也有点长,所以请参阅the link

这将等待第一次输入,但是创建后,CDK将完全自动运行

另外,github连接本身可能会按帐户为基础设置,而不是与应用程序一对一设置,因此,如果您已经具有GitHub连接,它将使用连接您指定而不是创建一个新的。因此,如果尚不存在连接,并且要创建一个连接,则可以使用API​​在堆栈管理之外创建它,而不是使用自定义资源,以更改应用程序的生命周期。

  • “我不希望第一个CDK部署完全自动化。”
  • “我宁愿先手工制作,然后将CDK部署完全自动化。

然而,基于这样的声音(虚拟),我创建了一个“ shell脚本只是为了创建github连接” ,因此,如果我先扔这个连接以创建连接,那么 CDK将是完全自动的,请参见this page

上面的解释是针对CDK和AWS侧的,但是它假设“ GitHub的AWS连接器”安装在GitHub侧。 (创建GitHub连接并在控制台中完成握手时,您可以跳到GitHub页面。)

Instancerole

IAM角色“实例角色”,用于App Runner发布的应用程序,以访问AWS Resources。

指定它,以便可以将tasks.apprunner.amazonaws.com弄脏。

在这种情况下,我没有编写任何程序以访问样本的AWS,因此我没有指定任何许可(操作)。

    appRunnerInstanceRole := awsiam.NewRole(stack, jsii.String("AppRunnerInstanceRole"), &awsiam.RoleProps{
        AssumedBy: awsiam.NewServicePrincipal(jsii.String("tasks.apprunner.amazonaws.com"), nil),
    })

还有一个与实例角色不同的IAM角色,即访问ECR的“访问角色”,该角色在App Runner中可用,但对于托管运行时并不是必需的。

VPC连接器(L2 Alpha版本)

这是允许访问VPC资源的功能。

App Runner当前具有L1构造和L2构造的alpha版本。

因此,我分别在 l1和l2 alpha版本中撰写

请注意,VPC连接器还具有一个安全组,该组被定义在一起,VPC和子网ID通过Props传递为外部参数。

首先是 l2

    securityGroupForVpcConnectorL2 := awsec2.NewSecurityGroup(stack, jsii.String("SecurityGroupForVpcConnectorL2"), &awsec2.SecurityGroupProps{
        Vpc: awsec2.Vpc_FromLookup(stack, jsii.String("VPCForSecurityGroupForVpcConnectorL2"), &awsec2.VpcLookupOptions{
            VpcId: jsii.String(props.AppRunnerStackInputProps.VpcConnectorProps.VpcID),
        }),
        Description: jsii.String("for AppRunner VPC Connector L2"),
    })

    vpcConnectorL2 := apprunner.NewVpcConnector(stack, jsii.String("VpcConnectorL2"), &apprunner.VpcConnectorProps{
        Vpc: awsec2.Vpc_FromLookup(stack, jsii.String("VPCForVpcConnectorL2"), &awsec2.VpcLookupOptions{
            VpcId: jsii.String(props.AppRunnerStackInputProps.VpcConnectorProps.VpcID),
        }),
        SecurityGroups: &[]awsec2.ISecurityGroup{securityGroupForVpcConnectorL2},
        VpcSubnets: &awsec2.SubnetSelection{
            Subnets: &[]awsec2.ISubnet{
                awsec2.Subnet_FromSubnetId(stack, jsii.String("Subnet1"), jsii.String(props.AppRunnerStackInputProps.VpcConnectorProps.SubnetID1)),
                awsec2.Subnet_FromSubnetId(stack, jsii.String("Subnet2"), jsii.String(props.AppRunnerStackInputProps.VpcConnectorProps.SubnetID2)),
            },
        },
    })

VPC连接器(L1版本)

这是 l1 One的VPC连接器(和安全组)。

    securityGroupForVpcConnectorL1 := awsec2.NewSecurityGroup(stack, jsii.String("SecurityGroupForVpcConnectorL1"), &awsec2.SecurityGroupProps{
        Vpc: awsec2.Vpc_FromLookup(stack, jsii.String("VPCForVpcConnectorL1"), &awsec2.VpcLookupOptions{
            VpcId: jsii.String(props.AppRunnerStackInputProps.VpcConnectorProps.VpcID),
        }),
        Description: jsii.String("for AppRunner VPC Connector L1"),
    })

    vpcConnectorL1 := awsapprunner.NewCfnVpcConnector(stack, jsii.String("VpcConnectorL1"), &awsapprunner.CfnVpcConnectorProps{
        SecurityGroups: jsii.Strings(*securityGroupForVpcConnectorL1.SecurityGroupId()),
        Subnets: jsii.Strings(
            props.AppRunnerStackInputProps.VpcConnectorProps.SubnetID1,
            props.AppRunnerStackInputProps.VpcConnectorProps.SubnetID2,
        ),
    })

批准服务(L2 Alpha版本)

和主要批准者本身,批准服务。

我在l1和l2 alpha版本中制作了这个。

让我们从L2 alpha版本开始。

    apprunnerServiceL2 := apprunner.NewService(stack, jsii.String("AppRunnerServiceL2"), &apprunner.ServiceProps{
        InstanceRole: appRunnerInstanceRole,
        Source: apprunner.Source_FromGitHub(&apprunner.GithubRepositoryProps{
            RepositoryUrl:       jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.RepositoryUrl),
            Branch:              jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.BranchName),
            ConfigurationSource: apprunner.ConfigurationSourceType_API,
            CodeConfigurationValues: &apprunner.CodeConfigurationValues{
                Runtime:      apprunner.Runtime_GO_1(),
                Port:         jsii.String(strconv.Itoa(props.AppRunnerStackInputProps.SourceConfigurationProps.Port)),
                StartCommand: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.StartCommand),
                BuildCommand: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.BuildCommand),
                Environment: &map[string]*string{
                    "ENV1": jsii.String("L2"),
                },
            },
            Connection: apprunner.GitHubConnection_FromConnectionArn(jsii.String(connectionArn)),
        }),
        Cpu:          apprunner.Cpu_Of(jsii.String(props.AppRunnerStackInputProps.InstanceConfigurationProps.Cpu)),
        Memory:       apprunner.Memory_Of(jsii.String(props.AppRunnerStackInputProps.InstanceConfigurationProps.Memory)),
        VpcConnector: vpcConnectorL2,
        AutoDeploymentsEnabled: jsii.Bool(true),
    })

    var cfnAppRunner awsapprunner.CfnService
    //lint:ignore SA1019 This is deprecated, but Go does not support escape hatches yet.
    jsii.Get(apprunnerServiceL2.Node(), "defaultChild", &cfnAppRunner)
    cfnAppRunner.SetAutoScalingConfigurationArn(autoScalingConfigurationArn)
    cfnAppRunner.SetHealthCheckConfiguration(&awsapprunner.CfnService_HealthCheckConfigurationProperty{
        Path:     jsii.String("/"),
        Protocol: jsii.String("HTTP"),
    })

配置比其他配置更长,但是为了匹配L1配置,也指定了不需要的项目。

再次通过道具获取每个信息。

这里重要的是,由于某些项目无法仅使用L2构造设置,因此L2资源被视为L1资源,然后属性被覆盖 “ Escape Hatch” (某物这样)。

这样做的原因是GO当前不支持Escape Hatch,请参见this page

CDK具有一个名为“逃生舱口”的概念,它允许您修改L2构造的基础CFN资源以访问由云形式支持但尚未得到CDK支持的功能。不幸的是,GO的CDK尚未支持此功能,因此我必须通过L1构造创建资源。有关更多信息,请参见此GitHub问题,并在GO中遵循支持CDK逃生舱口的支持。

因此,我代替了jsii.Get的方法。

    jsii.Get(apprunnerServiceL2.Node(), "defaultChild", &cfnAppRunner)
    cfnAppRunner.SetAutoScalingConfigurationArn(autoScalingConfigurationArn)
    cfnAppRunner.SetHealthCheckConfiguration(&awsapprunner.CfnService_HealthCheckConfigurationProperty{
        Path:     jsii.String("/"),
        Protocol: jsii.String("HTTP"),
    })

请注意,jsii.Get已弃用。 (但是这次我使用了它,因为我忍不住了。)

参见doc

应用服务(L1版)

这是L1版本。

    apprunnerServiceL1 := awsapprunner.NewCfnService(stack, jsii.String("AppRunnerServiceL1"), &awsapprunner.CfnServiceProps{
        SourceConfiguration: &awsapprunner.CfnService_SourceConfigurationProperty{
            AutoDeploymentsEnabled: jsii.Bool(true),
            AuthenticationConfiguration: &awsapprunner.CfnService_AuthenticationConfigurationProperty{
                ConnectionArn: jsii.String(connectionArn),
            },
            CodeRepository: &awsapprunner.CfnService_CodeRepositoryProperty{
                RepositoryUrl: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.RepositoryUrl),
                SourceCodeVersion: &awsapprunner.CfnService_SourceCodeVersionProperty{
                    Type:  jsii.String("BRANCH"),
                    Value: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.BranchName),
                },
                CodeConfiguration: &awsapprunner.CfnService_CodeConfigurationProperty{
                    ConfigurationSource: jsii.String("API"),
                    CodeConfigurationValues: &awsapprunner.CfnService_CodeConfigurationValuesProperty{
                        Runtime:      jsii.String("GO_1"),
                        Port:         jsii.String(strconv.Itoa(props.AppRunnerStackInputProps.SourceConfigurationProps.Port)),
                        StartCommand: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.StartCommand),
                        BuildCommand: jsii.String(props.AppRunnerStackInputProps.SourceConfigurationProps.BuildCommand),
                        RuntimeEnvironmentVariables: []interface{}{
                            &awsapprunner.CfnService_KeyValuePairProperty{
                                Name:  jsii.String("ENV1"),
                                Value: jsii.String("L1"),
                            },
                        },
                    },
                },
            },
        },
        HealthCheckConfiguration: &awsapprunner.CfnService_HealthCheckConfigurationProperty{
            Path:     jsii.String("/"),
            Protocol: jsii.String("HTTP"),
        },
        InstanceConfiguration: &awsapprunner.CfnService_InstanceConfigurationProperty{
            Cpu:             jsii.String(props.AppRunnerStackInputProps.InstanceConfigurationProps.Cpu),
            Memory:          jsii.String(props.AppRunnerStackInputProps.InstanceConfigurationProps.Memory),
            InstanceRoleArn: appRunnerInstanceRole.RoleArn(),
        },
        NetworkConfiguration: &awsapprunner.CfnService_NetworkConfigurationProperty{
            EgressConfiguration: awsapprunner.CfnService_EgressConfigurationProperty{
                EgressType:      jsii.String("VPC"),
                VpcConnectorArn: vpcConnectorL1.AttrVpcConnectorArn(),
            },
        },
        AutoScalingConfigurationArn: autoScalingConfigurationArn,
    })

go-cdk-go-go-managed-apprunner_test.go(单位测试)

我还创建了a simple sample unit test for CDK

我有快照测试,细粒度的主张测试和快照lambda S3资产替换(每次执行都会更改的值掩盖)。


应用程序代码(App/main.go)

这是要在App Runner中部署的示例代码。

这真的很简单,只需以杜松子酒为Web服务器,并显示CDK中传递的环境变量。

package main

import (
    "os"

    "github.com/gin-gonic/gin"
)

func main() {
    outputValue := os.Getenv("ENV1")
    engine := gin.Default()
    engine.GET("/", func(c *gin.Context) {
        c.String(200, outputValue)
    })
    engine.Run(":8080")
}

自定义资源Lambda代码(自定义/custom.go)

这在上面的CDK部分中有点提及,但这是重述。 (这很长,所以请参阅the link。)


补充

顺便说一句,如果您想阅读它,我已经写了有关the features and usage of writing CDK in Go language and the difference from TypeScript的文章。


最后

由于现在在App Runner的托管运行时和AWS CDK中支持GO,所以我认为您中的一些人可能想完全编写您的应用程序和基础架构。

关于GO中CDK的信息仍然很少,但我希望它会稍微传播。