Octopus-博客中文翻译-二-

Octopus 博客中文翻译(二)

原文:Octopus Blog

协议:CC BY-NC-SA 4.0

为 CloudFormation - Octopus Deploy 生成实例类型列表

原文:https://octopus.com/blog/cloudformation-instance-types

构建 EC2 实例时,实例类型是必需的参数。虽然 AWS 文档提供了一个允许值的列表,但它并不是以一种您自己的 CloudFormation 模板可以轻松使用的格式呈现的。

在这篇文章中,您将学习如何使用实例类型的完整列表作为允许值来构建参数,并将其复制和粘贴到您自己的模板中。

入门指南

脚本需要jq

jq 下载页面包含为主要 Linux 发行版安装工具的说明。

查找脚本

下面的脚本返回了一个定义了 CloudFormation 参数的 YAML 块,其中包含所有可用实例类型的排序列表:

echo "Parameters:"
echo "  InstanceTypeParameter:"
echo "    Type: String"
echo "    Default: t3.micro"
echo "    AllowedValues:"
instances=$(aws ec2 describe-instance-types | jq -r '.InstanceTypes |= sort_by(.InstanceType) | .InstanceTypes[].InstanceType')
for row in $instances; do
  echo "    - $row"
done 

将脚本保存到名为instancetypes.sh的文件中,并使用以下命令使其可执行:

chmod +x instancetypes.sh 

使用以下命令运行脚本:

./instancetypes.sh 

输出如下所示:

Parameters:
  InstanceTypeParameter:
    Type: String
    Default: t3.micro
    AllowedValues:
    - c1.medium
    - c1.xlarge
    - c3.2xlarge
    - c3.4xlarge
    - c3.8xlarge
    - c3.large
    - c3.xlarge
    - c4.2xlarge
    - c4.4xlarge
    - c4.8xlarge
    - c4.large
    - c4.xlarge
    - c5.12xlarge
    - c5.18xlarge
    - c5.24xlarge
    - c5.2xlarge
    - c5.4xlarge
    - c5.9xlarge
    - c5.large
    - c5.metal
    - c5.xlarge
    - c5a.12xlarge
    - c5a.16xlarge
    - c5a.24xlarge
    - c5a.2xlarge
    - c5a.4xlarge
    - c5a.8xlarge
    - c5a.large
    - c5a.xlarge
    - c5d.12xlarge
    - c5d.18xlarge
    - c5d.24xlarge
    - c5d.2xlarge
    - c5d.4xlarge
    - c5d.9xlarge
    - c5d.large
    - c5d.metal
    - c5d.xlarge
    - c5n.18xlarge
    - c5n.2xlarge
    - c5n.4xlarge
    - c5n.9xlarge
    - c5n.large
    - c5n.metal
    - c5n.xlarge
    - c6g.12xlarge
    - c6g.16xlarge
    - c6g.2xlarge
    - c6g.4xlarge
    - c6g.8xlarge
    - c6g.large
    - c6g.medium
    - c6g.metal
    - c6g.xlarge
    - c6gd.12xlarge
    - c6gd.16xlarge
    - c6gd.2xlarge
    - c6gd.4xlarge
    - c6gd.8xlarge
    - c6gd.large
    - c6gd.medium
    - c6gd.metal
    - c6gd.xlarge
    - d2.2xlarge
    - d2.4xlarge
    - d2.8xlarge
    - d2.xlarge
    - g2.2xlarge
    - g2.8xlarge
    - g3.16xlarge
    - g3.4xlarge
    - g3.8xlarge
    - g4dn.12xlarge
    - g4dn.16xlarge
    - g4dn.2xlarge
    - g4dn.4xlarge
    - g4dn.8xlarge
    - g4dn.metal
    - g4dn.xlarge
    - i2.2xlarge
    - i2.4xlarge
    - i2.8xlarge
    - i2.xlarge
    - i3.16xlarge
    - i3.2xlarge
    - i3.4xlarge
    - i3.8xlarge
    - i3.large
    - i3.metal
    - i3.xlarge
    - i3en.12xlarge
    - i3en.24xlarge
    - i3en.2xlarge
    - i3en.3xlarge
    - i3en.6xlarge
    - i3en.large
    - i3en.metal
    - i3en.xlarge
    - inf1.24xlarge
    - inf1.2xlarge
    - inf1.6xlarge
    - inf1.xlarge
    - m1.large
    - m1.medium
    - m1.small
    - m1.xlarge
    - m2.2xlarge
    - m2.4xlarge
    - m2.xlarge
    - m3.2xlarge
    - m3.large
    - m3.medium
    - m3.xlarge
    - m4.10xlarge
    - m4.16xlarge
    - m4.2xlarge
    - m4.4xlarge
    - m4.large
    - m4.xlarge
    - m5.12xlarge
    - m5.16xlarge
    - m5.24xlarge
    - m5.2xlarge
    - m5.4xlarge
    - m5.8xlarge
    - m5.large
    - m5.metal
    - m5.xlarge
    - m5a.12xlarge
    - m5a.16xlarge
    - m5a.24xlarge
    - m5a.2xlarge
    - m5a.4xlarge
    - m5a.8xlarge
    - m5a.large
    - m5a.xlarge
    - m5ad.12xlarge
    - m5ad.16xlarge
    - m5ad.24xlarge
    - m5ad.2xlarge
    - m5ad.4xlarge
    - m5ad.8xlarge
    - m5ad.large
    - m5ad.xlarge
    - m5d.12xlarge
    - m5d.16xlarge
    - m5d.24xlarge
    - m5d.2xlarge
    - m5d.4xlarge
    - m5d.8xlarge
    - m5d.large
    - m5d.metal
    - m5d.xlarge
    - m5zn.12xlarge
    - m5zn.2xlarge
    - m5zn.3xlarge
    - m5zn.6xlarge
    - m5zn.large
    - m5zn.metal
    - m5zn.xlarge
    - m6g.12xlarge
    - m6g.16xlarge
    - m6g.2xlarge
    - m6g.4xlarge
    - m6g.8xlarge
    - m6g.large
    - m6g.medium
    - m6g.metal
    - m6g.xlarge
    - m6gd.12xlarge
    - m6gd.16xlarge
    - m6gd.2xlarge
    - m6gd.4xlarge
    - m6gd.8xlarge
    - m6gd.large
    - m6gd.medium
    - m6gd.metal
    - m6gd.xlarge
    - m6i.12xlarge
    - m6i.16xlarge
    - m6i.24xlarge
    - m6i.2xlarge
    - m6i.32xlarge
    - m6i.4xlarge
    - m6i.8xlarge
    - m6i.large
    - m6i.metal
    - m6i.xlarge
    - r3.2xlarge
    - r3.4xlarge
    - r3.8xlarge
    - r3.large
    - r3.xlarge
    - r4.16xlarge
    - r4.2xlarge
    - r4.4xlarge
    - r4.8xlarge
    - r4.large
    - r4.xlarge
    - r5.12xlarge
    - r5.16xlarge
    - r5.24xlarge
    - r5.2xlarge
    - r5.4xlarge
    - r5.8xlarge
    - r5.large
    - r5.metal
    - r5.xlarge
    - r5a.12xlarge
    - r5a.16xlarge
    - r5a.24xlarge
    - r5a.2xlarge
    - r5a.4xlarge
    - r5a.8xlarge
    - r5a.large
    - r5a.xlarge
    - r5ad.12xlarge
    - r5ad.16xlarge
    - r5ad.24xlarge
    - r5ad.2xlarge
    - r5ad.4xlarge
    - r5ad.8xlarge
    - r5ad.large
    - r5ad.xlarge
    - r5d.12xlarge
    - r5d.16xlarge
    - r5d.24xlarge
    - r5d.2xlarge
    - r5d.4xlarge
    - r5d.8xlarge
    - r5d.large
    - r5d.metal
    - r5d.xlarge
    - r5n.12xlarge
    - r5n.16xlarge
    - r5n.24xlarge
    - r5n.2xlarge
    - r5n.4xlarge
    - r5n.8xlarge
    - r5n.large
    - r5n.metal
    - r5n.xlarge
    - r6g.12xlarge
    - r6g.16xlarge
    - r6g.2xlarge
    - r6g.4xlarge
    - r6g.8xlarge
    - r6g.large
    - r6g.medium
    - r6g.metal
    - r6g.xlarge
    - r6gd.12xlarge
    - r6gd.16xlarge
    - r6gd.2xlarge
    - r6gd.4xlarge
    - r6gd.8xlarge
    - r6gd.large
    - r6gd.medium
    - r6gd.metal
    - r6gd.xlarge
    - t1.micro
    - t2.2xlarge
    - t2.large
    - t2.medium
    - t2.micro
    - t2.nano
    - t2.small
    - t2.xlarge
    - t3.2xlarge
    - t3.large
    - t3.medium
    - t3.micro
    - t3.nano
    - t3.small
    - t3.xlarge
    - t3a.2xlarge
    - t3a.large
    - t3a.medium
    - t3a.micro
    - t3a.nano
    - t3a.small
    - t3a.xlarge
    - t4g.2xlarge
    - t4g.large
    - t4g.medium
    - t4g.micro
    - t4g.nano
    - t4g.small
    - t4g.xlarge
    - z1d.12xlarge
    - z1d.2xlarge
    - z1d.3xlarge
    - z1d.6xlarge
    - z1d.large
    - z1d.metal
    - z1d.xlarge 

当此参数包含在 Octopus 部署的模板中时,可用选项列表将显示在下拉列表中:

Available values in a drop down list

结论

ec2 有几十种实例类型,向部署 CloudFormation 模板的人提供一个选项列表就不需要参考外部文档了。

在这篇文章中,您了解了如何在 YAML 中使用实例类型的完整列表构建 CloudFormation 参数,以便复制到 CloudFormation 模板中。

我们还有其他关于云形成的帖子,你可能也会觉得有帮助。

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

林挺您的 Kubernetes 集群与 Clusterlint 和 runbooks - Octopus 部署

原文:https://octopus.com/blog/clusterlint-with-runbooks

Octopus 中的操作手册将 Ops 置于 DevOps 中。这篇文章是一系列文章的一部分:


Octopus 2021 Q3 包括对 Kubernetes 部署的更新支持,以及针对 Google Cloud、AWS 和 Azure 用户的 runbooks。在我们的发布公告中了解更多信息。

Kubernetes 让简单的事情变得困难,让困难的事情变得可能。这是一个贴切的说法。你只需要看看 Kubernetes 和周围生态系统的最佳实践指南的数量,就可以理解,即使是一个单独的 pod 正确运行也是一项艰巨的任务。

这就是林挺工具可以帮忙的地方。通过将最佳实践封装到对集群的自动检查中,林挺工具可以突出您可能没有意识到的改进,并为改进基础架构创建反馈循环。

一个这样的林挺工具是 Clusterlint 。它由 DigitalOcean 开发,并集成到其托管的 Kubernetes 产品中,通过在集群更新等操作之前识别问题来减少支持负载。但是,大多数检查通常适用于任何集群。

lint 反馈回路

在实现林挺工具时要问的一个问题是,它是应该以一个子集为目标,例如,仅仅是一个部署,还是整个集群。如果您的目标是单个部署的结果,那么将 lint 检查作为部署过程的一部分是有意义的。

然而,我要告诫不要过早地将林挺推入部署过程。作为一名开发人员,我见过全球代码林挺的实现每次都失败,因为它们产生了太多的误报,有着不被开发团队分享的观点,并且最终被忽略或者以一种特别的方式实现,因为它们碍事。

更好的解决方案是在部署工作流之外实现林挺,至少在最初是这样。这提供了生成具有最大价值的集中 lint 规则集的能力,并识别出没有人可能触及的配置问题,以及仅针对活动部署运行的检查所遗漏的问题。

那么,如何使用 Octopus 实现部署之外的工作流呢?直到最近,Octopus 中的每个自动化流程都被认为是一次部署。现在,随着 Operations Runbooks 的推出,Octopus 内置了对无需部署即可运行管理和维护任务的支持。

林挺运行手册示例

在下面的截图中,你可以看到一个调用clusterlint可执行文件的 runbook。

Octopus dashboard open on Projects tab and Operations Runbooks page showing ClusterLint Step Editor

这个 runbook 例子值得注意的是它有多简单。只需一行代码就可以自动检查您的 Kubernetes 集群。

runbook 很简单,因为它利用了 Octopus 中对 Kubernetes 的现有支持。运行一个 kubectl 脚本步骤用于使用从 Kubernetes 目标生成的kubectl配置文件执行clusterlint。如果您使用 Octopus 执行 Kubernetes 部署,那么这些目标已经配置好了。

超越概念验证的扩展

Runbooks 的真正好处是它们为超越概念验证的工作流提供了基础。

林挺应该自动按固定时间表运行。Runbooks 通过自定义触发器支持这一点:

Octopus dashboard open on Projects tab and Operations Triggers page showing Daily check

Lint 结果没有任何意义,除非它们被共享和执行。通过一些脚本,我们可以生成一个总结报告,并将其捕获到一个名为Report的 Octopus 变量中:

$emailReport = clusterlint run -g basic -o json |
  ConvertFrom-Json |
  Select -ExpandProperty Diagnostics |
  Group-Object -Property Check -NoElement |
  % {$report="Clusterlint report`n----"} {$report += "`n$($_.Name): $($_.Count)"} {$report}

Write-Host $emailReport

Set-OctopusVariable -name "Report" -value $emailReport 
 emailReport=`clusterlint run -g basic -o json | jq -r '.Diagnostics | group_by(.Property)[]| group_by(.Check)      | map({Check: .[0].Check, count: length}) | "Clusterlint Report", "---------", ( .[] | "\(.Check):\(.count)" )'`

echo "$emailReport"

set_octopusvariable "Report" "$emailReport" 

Octopus 提供了通过电子邮件、Slack、HipChat 和团队等渠道发送报告的步骤。在这里,我配置了一个发送包含报告摘要的电子邮件的步骤:

Octopus dashboard open on Projects tab and Operations Runbooks page showing Email report

当您的 lint 规则集被锁定时,如果有任何规则被破坏,您可以使 runbook 失败。然后,审计日志将为您提供集群状态的历史记录:

Octopus dashboard open on Tasks tab showing audit log

而这些例子只是冰山一角。您可以使用:

结论

从概念上讲,runbooks 是一个简单的想法。它们允许您运行支持部署的相同自动化流程,而不需要部署。

但是可重复部署比部署软件的实际行为要多得多,runbooks 继承了所有这些跨领域的功能。借助 runbook automation,您可以获得内置的安全性、日志记录、审计、报告、仪表板和计划。

正如我们在这篇文章中看到的,即使是最简单的一行脚本也可以利用这些特性来扩展为一个健壮的、生产就绪的解决方案。

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

八达通将在代码会议 2019 -八达通部署

原文:https://octopus.com/blog/code-conf-2019

在 Octopus Deploy,我们热衷于走出去与客户交谈并回馈社区,这就是为什么我们很高兴地宣布,我们将参加并赞助 2019 年 10 月 28 日在丹麦哥本哈根 H.C. Andersens Boulevard 18 举行的 QDI -丹麦工业大会。

CoDe-Conf 将在第六年重返哥本哈根,它承诺会比以往更大更好。CoDe-Conf 2019 将汇集 DevOps 生态系统中的所有人,从高管和经理到工程师和程序员。

持续交付和 DevOps 是关于文化和工具的,所以今年的 CoDe-Conf 将有两个主题。一条轨道将关注文化和变革,另一条轨道将关注技术和工具。每个人都会有所收获,你可以在这里看到完整的发言人名单,在这里看到关于活动的全面信息。

现在注册还来得及!我们有一些打折机票与客户分享,可以为您节省 25%的费用。要获取折扣代码,请发送电子邮件至Advice@Octopus.com并提及代码 Conf。不要犹豫,因为我们只有一些折扣票,一旦他们走了,他们走了!您可以在这里注册

如果你已经来了,一定要和我们展位的团队打招呼,拿一些贴纸。我们也有一些八达通 T 恤衫代金券可以赠送,所以早点参加吧!

常见的部署模式以及如何在 Octopus - Octopus 部署中使用它们

原文:https://octopus.com/blog/common-deployment-patterns-and-how-to-set-them-up-in-octopus

部署模式对于任何开发管道都很重要,有助于减少您的团队和客户的停机时间和其他问题。然而,部署模式有许多方法,其中一种可能比其他方法更适合您的需求。

这篇文章着眼于一些最常见的部署模式,并解释了如何在 Octopus 中设置它们。

滚动部署

滚动模式一次向一个部署目标(或一批目标)交付版本。这减少了部署期间您环境中的停机时间和流量拥塞。

作为更简单的选择之一,滚动部署可能很慢,但是可靠、低风险且易于回滚。

在 Octopus 中设置滚动部署模式

默认情况下,Octopus 中的部署流程只有在前一个步骤结束时才开始新的步骤。然而,如果一个步骤被部署到一个具有多个目标的目标角色(例如,一个服务器场),那么您可以同时命中多达 10 个目标。

相反,您可以在定义部署流程时强制实施滚动模式:

  1. 在流程编辑器中部分的 On Targets in Roles 下,点击配置滚动部署

  2. 滚动部署部分,使用窗口大小字段设置您想要一次部署多少台机器。例如:

    • 窗口大小为“1”将一次部署到 1 台机器
    • 窗口大小为“3”将同时部署到 3 台机器

The

如果在部署到另一个目标之前需要在目标上运行一系列步骤,请使用子步骤。您还可以在流程编辑器中添加子步骤:

  1. 点击预期“父”步骤旁边的 3 个垂直点,并选择添加子步骤
  2. 像处理父步骤一样完成子步骤(选择步骤类型并完成字段),然后单击保存。根据需要重复任意多的步骤。

【T2 The

有关滚动部署的更多信息

看一看 Octopus 示例实例中的滚动部署设置示例

此外,查看我们的滚动部署文档了解更多信息,包括如何使用引导故障和可变运行条件。更多关于滚动部署的阅读,请看我们的其他博客帖子:

蓝色/绿色部署

蓝/绿模式使用两个生产环境,在每个版本中在“实时”和“暂存”之间交换角色。

例如,如果应用程序的实时版本在您的蓝色服务器上,您可以在将流量重定向到绿色服务器之前使用绿色服务器进行暂存和测试。切换后,蓝色服务器将充当新的临时区域。

蓝/绿模式在这个列表中有最简单的回滚解决方案——只需将您的流量重定向回原始服务器。鉴于需要克隆您的整个生产环境,蓝/绿技术可能会非常昂贵和复杂。

在 Octopus 中设置蓝/绿图案

您可以使用环境在 Octopus 中设置蓝/绿图案。

创建两个带有清晰标签的生产环境,并为它们分配所需的部署目标。

要创建环境:

  1. 点击顶部菜单中的基础设施,然后从左侧选择环境
  2. 点击添加环境
  3. 输入环境名称(例如Production – Blue,点击保存

我们还建议创建一个新的生命周期(或者改变现有的生命周期),这样蓝色和绿色环境就处于一个共享阶段。要创建这样的生命周期:

  1. 点击顶部菜单中的,然后从左侧选择生命周期
  2. 点击右上角的添加生命周期
  3. 输入新生命周期的名称和描述,然后单击添加阶段
  4. 如果添加完整的开发管道(推荐):
    • 输入您最早阶段的名称(例如,Development)。
    • 点击添加环境按钮,从下拉菜单中选择相关环境。决定是否要自动部署并点击确定
    • 点击添加阶段添加另一个并重复这些步骤。
  5. 当添加生产阶段时,添加蓝色和绿色环境。
  6. 对您的生命周期感到满意时,单击保存

【T2 An example of a blue/green lifecycle in Octopus, with both production environments in the same phase

要分配生命周期,请执行以下操作:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的流程
  2. 点击生命周期标题下右侧的变更
  3. 从下拉列表中选择您的生命周期,然后单击保存

当准备好一个新的发布进行试运行时,检查您的 Octopus 仪表板并部署到不作为您的实时服务的环境中。

A blue/green setup on the Octopus dashboard

有关蓝/绿部署的更多信息

看看我们的 Octopus 示例实例中的示例蓝/绿部署设置

有关蓝/绿部署的更多信息,请查看我们的其他博客帖子:

金丝雀部署

canary 部署模式在推广到其他地方之前,会发布对少数生产目标的更新以供测试。以旧的矿工早期预警系统命名,它有助于及早发现问题,而不会影响你的整个服务。

在 Octopus 中设置金丝雀模式

您可以在 Octopus 中执行金丝雀模式,而无需任何特殊设置。只需手动部署到您选择的 canary 目标上,进行测试,然后在满意时继续其余的目标。

也就是说,Octopus 完全是自动化的,您可以在部署过程中构建一个金丝雀模式:

  1. 部署到您的“金丝雀”目标
  2. 当您测试或邀请用户反馈时,通过手动干预步骤等待手动批准
  3. 一旦您满意了,就可以部署到其余的生产目标

首先,您应该创建目标角色,以确保在正确的阶段达到正确的部署目标:

  1. 点击顶部菜单中的基础设施,然后从左侧选择部署目标
  2. 单击您打算充当金丝雀的部署目标。
  3. 点击部署标题下的目标角色部分展开。
  4. 角色字段输入一个名称(例如canary,点击保存

重复这些步骤,并为剩余的部署目标创建目标角色(如果它们还不存在)。创建目标角色后,可以在其他部署目标中重用它。

现在,您可以创建部署流程:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的流程
  2. 点击添加步骤,选择步骤类型,完成部署细节。在角色部分的目标上选择您的金丝雀目标角色。完成后点击保存
  3. 再次点击添加步骤并使用选择步骤模板字段搜索“手动干预”。将光标悬停在需要手动干预框上,点击添加
  4. 填写以下字段并点击保存:
    • 步骤名称
    • 注释–描述手动步骤的目的
    • 说明–输入需要发生的事情,例如测试或等待用户反馈
    • 负责团队–选择负责测试或监控反馈的团队
    • 阻止部署–选择在等待干预时阻止其他部署
    • 必需的–使这成为必需的步骤
    • 根据需要完成其他选项,点击保存
  5. 单击添加步骤并重新创建第一个步骤,但是这一次部署到剩余生产目标的目标角色。

T52

关于金丝雀部署的更多信息

看看我们的 Octopus 示例实例中的示例 canary 部署设置

多区域部署

多区域模式是指将一个版本部署到多个海外目标,比如服务器或数据中心。虽然我们认为它本身是一个部署模式,但它是一个离群值,因为它使用其他模式作为过程的一部分。

在 Octopus 中设置多区域部署模式

在 Octopus 中设置多区域部署有三种方式:

环境和生命周期

您可以使用此解决方案来强制您所在区域的部署顺序。

要创建环境:

  1. 点击顶部菜单中的基础设施,然后从左侧选择环境。
  2. 点击添加环境
  3. 输入环境名称(例如US-West,点击保存

对您的每台全球服务器重复上述步骤。

要创建合适的生命周期:

  1. 点击顶部菜单中的,然后从左侧选择生命周期
  2. 点击右上角的添加生命周期
  3. 输入新生命周期的名称和描述,然后单击添加阶段
  4. 如果添加完整的开发管道(推荐):
    • 输入您最早阶段的名称(例如,Development)。
    • 点击添加环境按钮,从下拉列表中选择相关环境。决定是否要自动部署,然后单击确定
    • 点击添加阶段添加另一个并重复这些步骤。
  5. 当添加生产阶段时,添加您所有的区域环境。在这里,您的生命周期可以:
    • 连续部署到您的所有环境中
    • 部署到一个环境,然后在您准备好的时候部署到其他环境
  6. 对您的生命周期感到满意时,单击保存

Examples of multi-region setups with Octopus lifecycles

要分配生命周期:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的流程
  2. 点击生命周期标题下右侧的变更
  3. 从下拉列表中选择您的生命周期,然后单击保存

您还可以使用计划部署在低使用率时间段进行部署:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的发布
  2. 点击创建发布
  3. 点击部署到...按钮
  4. 在时点击展开菜单,之后选择。****
    *** 选择最适合该地区的日期和时间,然后单击部署。**

**Examples of a multi-region deployment using environments

云区域和变量

如果您不在乎部署到您的区域的顺序,云区域是完美的。

要设置您的云区域:

  1. 点击顶部菜单中的基础设施,然后从左侧选择部署目标
  2. 点击添加部署目标,选择云区域
  3. 将光标悬停在云区域框上,点击添加
  4. 填写以下字段并点击保存:
    • 显示名称
    • 环境
    • 目标角色

重复你所有的云区域。

要使用云区域,您必须使用特定于区域的变量。要设置这些:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的变量
  2. 完成以下各列,输入第一个变量:
    • 名称–您只需要将它用于第一个变量
    • 值–输入定义变量的值
    • 范围–点击该字段,并使用选择目标选项选择您的云区域。
  3. 点击添加另一个值添加更多,为每个云区域创建一个值。
  4. 准备好后点击保存

T34

房客

虽然我们为那些提供软件即服务(SaaS)的公司开发了租户,但是您也可以使用它来管理多区域部署。对于那些希望在 Octopus 仪表盘上获得更多控制和清晰度的人来说,这是一个很好的选择。

要创建您的租户:

  1. 点击顶部菜单中的租户,然后点击右上角的添加租户
  2. 为您的租户提供一个名称和描述,然后单击保存
  3. 点击连接项目,从下拉框中选择您的项目,点击添加连接
  4. 如果您还没有为您的项目启用租用部署,Octopus 会提示您。点击启用租用部署,用下拉菜单选择您的环境,然后点击添加连接
  5. 我们建议为租户添加一个徽标或图标,使他们的目的更加明确。点击左侧菜单中的设置,上传 Logo 部分的图像,点击保存
  6. 点击顶部菜单中的租户,重复上述步骤创建您的其他租户。

您还应该使用通用变量模板来提示您每个地区所需的数据(例如,存储帐户细节)。与项目变量不同,您可以在所有租户之间重用公共变量。另外,它们不局限于特定的环境。

  1. 点击顶部菜单中的,从左侧选择变量集
  2. 点击添加变量集,输入名称和描述,点击保存
  3. 点击新集合上的变量模板选项卡,然后点击添加模板
  4. 填写以下字段并点击添加(根据您选择的控制类型,可能会有其他选项):
    • 变量名–输入一个名称,如‘租户’。'别名'
    • 标签–提示输入数据时显示的内容
    • 帮助文本–描述所需的操作
    • 控制类型–变量将提示什么类型的选项
    • 默认值(可选)
  5. 回到变量集屏幕时,点击保存

现在,您可以将变量连接到您的项目:

  1. 点击顶部菜单中的项目,从列表中选择您的项目,然后点击左侧的变量
  2. 点击左边的库集合,点击右上角的包含库变量集合
  3. 选中新变量集的复选框,点击保存

现在,如果您的租户丢失了信息,他们会提醒您。要设置所需的值:

  1. 点击顶部菜单中的租户,从列表中选择您的租户,然后点击左侧的变量
  2. 点击常用变量选项卡,填写所需信息,点击保存

现在,您可以在 Octopus 仪表板上看到一个项目部署了多少租户。

T57

要了解更多信息,您可以观看我们关于多租户部署的网络研讨会

结论

如您所见,Octopus 可以帮助管理一系列部署模式,以适应您的团队、项目和客户。

请务必查看我们的文档,获取更多关于部署模式的帮助,以及 Octopus 的其他内容。

愉快的部署!**

DevOps 指标中的常见错误- Octopus 部署

原文:https://octopus.com/blog/common-mistakes-devops-metrics

作为持续改进过程的一部分,度量对于开发运维及持续交付至关重要。但是,您必须在收集和显示数据与信息泛滥之间取得平衡。你需要决定收集哪些数据,以及在任何时候关注哪些更小的数据集。

如果你的汽车有一个仪表板,显示它通过引擎管理系统收集的每一个指标,那就没有挡风玻璃的空间了。

早期汽车的仪表板上只有一个安培计,用来测量电池和电压调节器之间的电流。这很重要,因为它告诉你充电系统正在工作。没有速度计。汽车的最高时速只能达到 35 英里,而悬挂系统不鼓励以这种速度行驶。没有必要测量速度。

在现代汽车中,仪表板上没有给电流表留出空间(尽管如果出现问题,电池灯会亮起)。但是,你会发现几乎每辆车上都有速度计。当前的仪表板设计反映了汽车的发展和它们所处的更广泛的系统。发动机更强劲,悬挂系统更好,道路普遍更顺畅,更多的汽车上路。对安全的态度也发生了变化。

同样,当您对团队和组织中的差异做出反应时,您收集和显示的指标会随着时间而变化。

当您创建和发展您的测量系统时,有一些您应该避免的常见错误。

忽略数据

度量的第一个问题是,您花费了很大的精力来收集它们,但是它们并不总是被使用。这种情况甚至发生在定期审查数字的组织中。

你的数据需要一些能产生洞察力的过程。你可以将你从信息中发现的东西转化为你用于实验的理论。然后,实验应该提供新的数据,重新开始循环。

The cycle of Data, Insight, Theory, Experiment

收集 DevOps 度量标准的唯一好的理由是更多地了解您的工作,并找到改进它的方法。如果你收集数据只是为了以防万一,这些数据很可能会被误用或者根本不会被使用。

活动偏差

有 4 个级别的测量,活动通常是最早和最容易收集的。您通常可以近乎实时地跟踪活动。活动的结果通常不会作为输出或基于结果的指标提供,直到以后的某个日期。

Measurement level Software example 加热示例
活动 代码行 功率消耗
输出 每周功能 加热元件温度
系统输出 研制周期 室温
结果 用户价值 人们很舒服

活动度量通常是您现有工具的内置特性,因此它们已经可用。问题是并不是所有的活动都代表进步。有些活动甚至会减少产出,导致更糟糕的结果。

您可以使用活动度量来预测输出和结果的未来变化。要做到这一点,您必须不断地测试您的活动度量和您的相关输出或结果度量之间的关系。

如果你只测量活动,你会得到很多运动,但没有进展。

一次跟踪太多

您收集和显示的指标数量可能会增加,通常会很快增加。不久,你的仪表板上堆满了图表,你不知道什么是重要的,什么是不重要的。

您需要让您跟踪的指标保持精简、最新和相关。当图表不再有用时,您应该将其从仪表板中删除。您还应该考虑是否仍然需要收集该指标,如果您没有很好的理由来跟踪它,就让它退役。

如果你已经有了一个仪表板,打开它,对每个图表问:“如果这个数字上升或下降,我会有什么不同?”。经常重温这个问题,删除任何你没有答案的图表。

您的指标集应侧重于关键的长期产出和结果指标,仪表板显示您当前改进工作中所有类别的短期指标。

像 Microsoft Power BI、Tableau 或 Google Data Studio 这样的数据可视化工具是您组织中最有用的软件产品。许多业务工具都有一个基于网格或文本的界面,但是数据工具有丰富多彩的动画图表。

创建一个引人入胜的仪表板很容易让人分心。如果你不从度量设计开始,你最终会得到许多对你的日常工作没有影响的令人愉快的仪表板。您需要仪表板和图表工具来帮助您理解信息,但是首先要设计指标。

最好从低保真度开始,以收集有意义的指标。可以从简单的电子表格甚至白板开始。在你确定了哪些测量对你的团队和组织有帮助之后,开始自动化收集和创建光滑的显示。

如果你花了太多时间来创建一个令人惊叹的仪表板,你会发现当不再需要图表时,很难删除它们。

标准化

一些组织试图通过让其他团队遵循相同的过程来复制高绩效团队的成功。这很少成功,因为每个团队处理不同的问题,并且具有不同的技能水平。正如过程和实践需要特定于上下文一样,度量标准也是如此。

您应该将指标作为持续改进活动的一部分。对于一个有长交付周期的团队,您测量批量大小、排队时间和工作在每个状态花费的时间。这些度量标准不适合主要问题是质量的团队。

这需要数据、洞察力、理论、实验周期,在这个过程中,你要查看你所拥有的关于你想要解决的问题的信息,形成一个关于什么可以改善这个问题的理论,然后进行一个实验来测试你的想法。

您收集的指标也向团队传达了在当前时间什么是重要的。您经常看到改进,仅仅是因为您的度量传达了您关心软件交付的某些方面。

依靠眼球

您应该创建一个简单的仪表板来显示您正在跟踪的实验指标。这应该显示在信息辐射器上,这样团队中的每个人都可以看到数据。

然而,如果您只在有人查看仪表板时对数据做出反应,您会让仪表板塞满太多的信息而错过关键事件。随着时间的推移,你所保持的跟踪进展的长期度量标准将会和你用来改进你的软件交付的短期度量标准一样重要。

解决保留长期指标而不使您的仪表板混乱的问题的关键是为数据建立一个监控和警报流程。自动化警报应该告诉您指标是否超过阈值,并且您可以使用异常检测来告诉您是否发生了有趣的事情。

通过自动跟踪指标,您可以将它们从仪表板中删除以释放空间。

奖励表现

如果您的团队正在努力提高他们的部署率,那么如果他们实现了日常部署,就可以通过奖励来激励他们。这种激励方式会导致糟糕的结果。一个团队可能会为了达到目标而放弃其他重要的工作——不是为了欺骗系统,而是因为你把日常部署看得比什么都重要。

在里程碑式的著作《T2》中,阿尔菲·科恩解释说,试图用激励来管理员工会给你的组织带来长期的伤害。数百项研究发现,当给予奖励时,人们会做得更差。

使用指标来营造竞争氛围,无论是为了个人表现、不同团队的比较,还是为了将工作场所游戏化(在这里,你将游戏元素作为一种“有趣”的竞争形式引入),都会带来麻烦。

竞争与你在组织中真正需要的东西相冲突:协作。如果你假设人们想做好工作,你会发现你不需要用奖励或惩罚来让他们提高表现。

结论

5 DORA 度量和空间框架提供了预构建的、平衡的方法来度量软件交付性能。(以前有 4 个 DORA 指标,但增加了一个额外的可靠性指标。)

一套好的指标将把预测业绩的领先指标与检查预测准确性的落后指标结合起来。测量应该跨越活动、输出、系统输出和结果类别。

我们在关于测量持续交付的白皮书中详细介绍了 DevOps 和持续交付指标。

我们的 DevOps Insights in Octopus 通过展示基于 4 个关键 DORA 指标的见解,让您更好地了解贵公司的 DevOps 绩效。这些指标有助于您确定开发运维绩效的结果,并深入了解需要改进的地方。

无论您度量什么,您都需要不断地改进您的度量标准,以确保它们对您的团队和组织仍然有用。理想情况下,您收集的指标与您正在运行的测试理论的特定实验相关。

如果您很好地使用了度量标准,那么当您试图成为软件交付中的精英之一时,您就放大了绩效和学习。

进一步阅读

愉快的部署!

Octopus 部署配置代码:早期访问预览- Octopus 部署

原文:https://octopus.com/blog/config-as-code-eap

我们的代码早期访问预览(EAP)配置现在可用。

Octopus 中的 configuration as Code(Config as Code)提供了 Git 的全部功能和 Octopus 的可用性。除了现有的数据库实现,我们还构建了一个健壮的 Git-persistent 层。这意味着您可以在应用程序代码旁边的 Git repo 中看到您的部署过程,并一起发展它们。

Octopus Configuration as Code - Early Access Preview

我们的配置代码解决方案允许您:

  • 通过分支支持部署流程的多个版本
  • 将对部署流程的更改与拉请求合并
  • 查看部署流程的更改历史,包括谁在何时执行了更改
  • 恢复到先前版本的部署流程

重要的是,Octopus UI 仍然有效,因此您不需要学习一种全新的语言来编辑您的流程。但是,如果您愿意,您仍然可以使用您最喜欢的文本编辑器来编辑您的配置。

在这篇文章中,我将介绍我们的 Config as Code 解决方案,并解释如何开始。

Octopus 中的 Config as 代码是一个早期的访问预览,所以可能会有一些粗糙的边缘。我们建议创建新的项目或克隆现有的项目来进行试验,而不是在生产中进行测试。

由于我们仍在开发这一功能,我们非常感谢您愿意分享的任何反馈。我们还建议在#config-as-code频道的社区 Slack 中加入讨论。

为什么将配置作为代码?

许多现代 IT 和 DevOps 系统提供“as code”实现。常见的例子是基础设施即代码(IaC)解决方案,如 HashiCorp 的 TerraformPulumi 到 CI 服务器,如 JenkinsTeamCity ,它们允许您将构建配置指定为代码。

这些解决方案将纯文本代码/配置细节存储在源代码存储库中。这允许您发展您的系统配置并享受 Git 的好处,包括分支和历史。

在 Octopus UserVoice 网站上,以及在会议和活动中,Config As Code 一直是我们的客户提出的最高 要求之一。

我们使用您的反馈来指导我们的“配置为代码”解决方案的构建。

Octopus 配置代码-两全其美

当为项目启用 Config as Code 时,您可以像往常一样继续使用 Octopus UI,或者可以在您喜欢的编辑器中编辑文本文件。你可以在效率最高的地方工作。

我们的配置文件格式是章鱼配置语言(OCL),基于 HashiCorp 的 HCL 。我们希望我们的配置文件是人类可读的,并支持复杂的文档,如部署和操作手册流程。我们喜欢 HCL,并认为它是这项工作的正确工具,但我们已经编写了自己的解析器/串行化器。这意味着我们没有义务遵循哈希公司的任何方向,也没有什么可以阻止我们做出改变。

参见 Michael Richardson 关于将配置塑造成代码的帖子,了解更多关于将配置塑造成代码解决方案的因素。

Git 的所有功能

分支是 Git 的超能力,我们想要展示这种能力。您可以在 Octopus UI 中切换分支(并很快创建它们),允许您对分支上的部署过程进行更改。这实际上允许一个草稿模式,因为您可以从您的分支中创建一个发布并测试您的变更。当与 Git 提供者的特性结合起来时,比如 pull 请求、受保护的分支和代码所有者,这可以实现一组全新的工作流。

保存对部署流程的更改时,可以添加提交消息。您还可以在没有消息的情况下提交更改(这是默认设置),这在迭代以使复杂的部署过程工作时非常方便。

Octopus 用户界面仍然有效

我们希望你有一个伟大的配置代码使用八达通门户网站的经验。您可以配置您的 Git 存储库、选择您的分支、提交更改等等,所有这些都可以从 Octopus 用户界面中完成。

如果您喜欢完全控制,您可以编辑底层配置文件,但这不是必需的。丰富的双向同步意味着您可以混合搭配使用 web UI 和直接编辑。

使用 Visual Studio 代码编辑配置

The VS Code extension for Octopus includes OCL syntax highlighting and an OCL tree outline

有些人喜欢 web 界面,而有些人喜欢在他们喜欢的文本编辑器中编辑文本文件。为了使与 OCL 的合作更容易,我们为 Visual Studio 代码构建了一个 Octopus Deploy 扩展来补充 Octopus UI。

OCL 的编辑经验包括:

  • 语法突出显示
  • 步骤和操作的代码片段
  • 在文件中导航节点的集成树视图

我们的 VS 代码扩展也是一个早期访问预览版,但是我们迭代得很快。我们建议安装并试用它。

转换现有项目

可以在现有项目上启用“配置为代码”。这意味着您不需要创建一个新项目并重新开始。大多数 Octopus 特性与 Config as 代码完全兼容(例外情况见我们的文档)。

注意:这是一个单向过程,您无法将项目恢复为存储在数据库中。鉴于这是早期 access 预览版,我们建议您在测试此功能时克隆现有项目或导出它们。

什么过程是受版本控制的?

对于这个第一版,只有部署过程是受版本控制的(还有一些与部署过程密切相关的设置)。

我们打算将操作手册和变量作为以下工作来实现。

以代码形式配置入门

要开始使用 Octopus 中的 Config as Code EAP,您需要启用特性标志。

导航至配置,然后功能,并开启该功能。

由于这是早期的 access 预览,请记住创建新项目或克隆现有项目来测试它。

或者,您可以在我们的网上技术交流讲座以代码的形式深入了解配置中看到配置代码。

https://www.youtube.com/embed/oZfxlbpSP14

VIDEO

配置您的 Git 存储库

Octopus Config as Code settings

导航到设置然后版本控制来指定您的 Git 存储库细节。

在上面的例子中,我选择了一个公共存储库,并输入了我的 GitHub 用户名和一个个人访问令牌。

选择一个分支并提交更改

Octopus Config as Code branch selector

部署流程编辑器最明显的变化是我们新的分支选择器。

要开始使用:

  1. 给你的回购推一个新的分支
  2. 对您的部署过程进行更改(例如,添加一个新的 Hello world!脚本步骤)
  3. 提交带有描述的更改
  4. 导航到您的 Git repo 并检查更改

Octopus Config as Code commit description

Octopus Config as Code pull request diff

创建版本

Octopus Config as Code: release creation

创建发布时,选择包含您的部署过程的分支。Octopus 会自动选择所选分支的负责人。您的部署一如既往地以同样可重复且可靠的方式执行。

结论

我们的配置代码早期访问预览现在可用。它提供了 Git 的强大功能和人类可读的文本格式,与 Octopus 的可用性相平衡。

这只是该功能的第一步。我们有一个即将推出的功能路线图,我们希望您能帮助指导我们。

我们希望您能尝试将 Config as Code 用于您的工作流程,并让我们知道如何改进它

观看我们的网络研讨会:在 Octopus 中将配置作为代码引入

德里克·坎贝尔和皮特·加拉格尔将带你了解在 Octopus 中配置为代码的入门知识,以及在大规模使用配置为代码时的最佳实践。

https://www.youtube.com/embed/Z4DgiJ630FU

VIDEO

我们定期举办网络研讨会。请参见网络研讨会第页,了解有关即将举行的活动和实时流媒体录制的详细信息。

愉快的(版本控制的)部署!

配置为代码和持久性无知- Octopus 部署

原文:https://octopus.com/blog/config-as-code-persistence-ignorance

当将配置为代码(Config as Code)特性引入我们的代码库时,我们遇到了一些忽略持久性的机会——让我们的代码忽略任何持久性/存储逻辑。

这篇文章探讨了我们的核心平台团队是如何对持久性无知做出决策的。我们希望其他人能从我们的经验中学习。

Octopus 和 SQL

2014 年,在 Octopus 3.0 时代,我们决定切换到 SQL 作为我们的存储/持久层。这意味着当您在 Octopus 中保存数据时,它会存储/持久化到 SQL 数据库表中,正如您所期望的那样。

a deployment process stored in SQL

在 Octopus 代码库中,我们选择保持简单。我们直接与那些有永不超生事务的表进行对话。永不超生是一个微型 ORM,它将 SQL Server 视为一个文档存储库。

在这个例子中,我们的类:

  • 对一个store产生了依赖
  • 在必要的地方开始查询我们需要什么
  • 用我们的using块完成并处理我们的transaction

任务完成。

class Thinger : IDoThings
{
    readonly IOctopusRelationalStore store;

    public Thinger(IOctopusRelationalStore store)
    {
        this.store = store;
    }

    public DeploymentProcess GetDeploymentProcessForProject(Project project)
    {
        using (var transaction = store.BeginTransaction())
        {
            var deploymentProcess = transaction
                .LoadRequired<DeploymentProcess>(project.DeploymentProcessId);
            return deploymentProcess;
        }
    }

    ...
} 

配置为代码

当我们开始将 Config 塑造成代码时,我们突然有了多个需要考虑的持久层。

对于受版本控制的项目,部署过程的数据现在作为 Octopus 配置语言(OCL)文件存储在 Git 存储库中,而不是存储在 SQL 中。

a deployment process stored in Git

从 API 消费者的角度来看,变化并不大。您请求一个部署过程,并告诉我们您想要查看哪个分支。我们从 Git 分支读取,将 OCL 反序列化为我们数据模型,并将结果作为 JSON 返回给您。

例如,设想一个常规项目的部署过程可以被请求如下:

/api/Spaces-1/projects/Projects-1/deploymentprocesses 

受版本控制的项目将包含分支信息。在这种情况下,我们看到一个beta分支被请求:

/api/Spaces-1/projects/Projects-1/beta/deploymentprocesses 

作为一个 API 消费者,你不知道(或者不关心)这些数据来自哪里。你只是想要一个部署过程。您依赖 API 来获得答案应该是简单和直观的。

作为一名 API 开发者,你支持用户体验的旅程也应该简单直观。

我只想要一个 x,我不在乎它是怎么储存的,也不在乎它从哪里来。

如果我们的开发人员在考虑数据模型时不得不在头脑中管理太多的切换逻辑,就会有这样的风险,混乱会以 bug、不一致的行为或特性/修复的缓慢变动的形式扩展到客户

再考虑一下这种带有版本控制的方法:

public DeploymentProcess GetDeploymentProcessForProject(Project project)
{
    // Wait, we better check if this thing is version-controlled!
    if (project.PersistenceSettings is VersionControlSettings)
    {
        throw new NotImplementedException(); //TODO: Lookup deployment process from Git.
    }
    else
    {
        using (var transaction = store.BeginTransaction())
        {
            var deploymentProcess = transaction
                .LoadRequired<DeploymentProcess>(project.DeploymentProcessId);
            return deploymentProcess;
        }
    }
} 

如果您正在进行原型开发以证明 Git 的可能性,这并不是一个不合理的起点(并且这个是我们在探索可能性时开始的)。

但是从长远来看,随着更多的特性可能转移到 Git,以及更多的工程师需要查询/改变数据,这是一个架构和可伸缩性的机会。

文档存储简介

为了解决这个问题,我们引入了一个文档存储抽象层(参见存储库模式)。

public interface IDocumentStore<TDocument> where TDocument : class, IDocument
{
    Task Add(TDocument document, ...);
    Task Update(TDocument document, ...);
    Task<TDocument> Get(string id, ...);
    Task<TDocument?> GetOrNull(string id, ...);
    Task<IReadOnlyList<TDocument>> GetMany(IReadOnlyCollection<string> ids, ...);
    Task<IReadOnlyList<TDocument>> All(...);
    IQueryable<TDocument> Query();
    ... 

我们的愿景是开发人员可以依赖于一个IDocumentStore<T>,通用接口将允许他们以类似于我们现有的(永不超生)接口的方式查询数据模型。

我们的新抽象需要两个具体的实现:

  • DatabaseDocumentStore<T>
  • GitDocumentStore<T>

当开发人员要求一个IDocumentStore<T>时,我们的基本文档库使用一个策略模式来确定你是在处理一个版本控制项目还是一个常规(SQL)项目。它的工作是为您提供正确类型的文档存储。作为一个开发者,你不用考虑这个问题。

我们之前的代码示例变得更容易推理:

class Thinger : IDoThings
{
    readonly IDocumentStore<DeploymentProcess> deploymentProcessesStore;

    public Thinger(IDocumentStore<DeploymentProcess> deploymentProcessesStore)
    {
        this.deploymentProcessesStore = deploymentProcessesStore;
    }

    public DeploymentProcess GetDeploymentProcessForProject(Project project)
    {
        return deploymentProcessesStore.Get(project.DeploymentProcessId);
    }
    ...
} 

事务性呢?

事务性是一个有趣的挑战和观察。

当我们与 SQL 数据库对话时,我们打开一个事务,对该事务执行操作,然后关闭并处理它。

您可能会问上面代码中的事务发生了什么?

从 2014 年开始,我们在整个代码库(包括我们的 API 和任务系统)中选择了短期事务。

在许多方面,这是理想的。我们可以保证短期事务,并孤立地推理事务性代码。例如,无论您在哪里看到这样的代码,您都可以很容易地推断出事务的生存期:

using (var transaction = store.BeginTransaction())
{
    var foo = transaction.LoadRequired<Foo>("Foo-123");
    foo.Bar = "testing";
    transaction.Update(foo);
    transaction.Commit();
} 

在其他方面,我们看到了两个长期的不利因素。

首先,当我们考虑给定请求/命令的生命周期时,我们无法保证的单一事务性。因为我们在代码库的不同层次对事务进行微观管理,所以在一个给定的请求/命令中可能会涉及到几个事务

有可能几个事务会成功完成并提交,但最后有一个会失败。在这种情况下,异常将被正确地返回给用户,但是来自相同失败请求的早期事务将不会回滚,给你留下数据不一致的问题(在这项工作中发现并修复了其中的几个问题)。

为了避免这种情况,我们发现在许多情况下,我们会在调用链的早期创建一个事务,然后将该事务作为方法参数传递(有时通过深度超过 10 个方法的大规模调用链,包括许多静态调用链)。在这些情况下,一些中间方法可能会调用.Commit(),你只能希望在同一个事务中没有其他方法会调用.Commit()(因为你只能调用 commit 一次)。

其次,当我们开始讨论文档存储抽象时,我们开始问:

开发人员必须考虑他们要求的东西(部署过程),以及如何围绕这个东西进行事务处理。

当我们在这种情况下谈论单一责任时,交易是额外的和不必要的上下文。这与我们要求的部署过程无关。

明确地说,像我们一直做的那样,拥有事务控制的选项是非常强大的。但是对于大多数情况,我们不应该去想它。

我们发现,如果你只依赖于一个像 SQL 这样的持久层,你可以在很多年内不直接使用事务。但是一旦你有了多个持久层(不是永不超生/EF/SQL,而是 T12),你就有问题了。

此外,事务是一个高级主题。我们日益壮大的团队中的开发人员很容易以可疑的方式持有事务,或者完全忘记提交。像这样的事情很可能在测试中被发现,但是这说明了不必要的变动开始悄悄出现。

我们在架构上的工作是帮助我们的开发人员落入成功的陷阱。当我们观察到这种复杂性时(特别是当我们开始扩展时),这是一个很好的机会来考虑一个新的抽象,并尝试在让我们走到这一步的惊人基础上进行改进。

介绍工作单元

为了帮助我们管理跨请求/命令的事务,我们引入了一个工作单元,这样开发者就少了一件需要考虑的事情。消费者只需要一个给定类型的文档存储。围绕他们操作的工作单元将为他们管理,而不是他们所关心的。

可以肯定地说,消费者只是想要一个X,所以让他们要求正是他们需要的

例如,当你想要的只是一把画笔时,你不应该要求整个邦宁商店。就要画笔!画笔是否来自邦宁商店与你无关。

随着事务性被抽象成一个工作单元,这就导致了分支的问题。

引入项目范围

还记得我们前面提到的关于文档存储的策略模式吗(它帮助我们确定当您请求一个IDocumentStore<T>时,您应该接收什么类型的文档存储)?

我们知道会有一些细节(比如项目和/或 Git 分支)对层级较低的消费者有用。我们希望将这些信息封装到一个“项目范围”中。

我们希望不要将这些信息作为方法参数到处传递。风险是在我们的后端有相当于道具爆炸的东西(传递一个东西超过 10 层,以便最底层的消费者可以使用它)。

想象一下,必须将 Git 分支名称和/或提交信息作为可选参数传递给我们可能需要用于版本控制文档的所有IDocumentStore方法。我们的文档存储抽象会突然变成...不是很抽象。

依靠我们的依赖倒置 (DI)原则,我们可以获取对一个IProjectScope的依赖,并告诉它尽可能在调用链的最高层(如 ASP.NET 中间件或动作过滤器)对每个请求封装一次分支或项目信息

那么我们在同一个调用链中最底层的代码也可以把IProjectScope作为一个依赖项并安全地查询那个信息,而不需要我们把所有东西都作为方法参数传递。

当我们以这种方式使用 DI 时,我们很快发现了所有的拦路虎,比如不能利用构造函数依赖的public static方法。我们以非静态的方式重写了这些方法,去掉了(现在)不必要的transaction方法参数,并包含了更多的单一责任。

最后,我们发现脑子里一下子没什么可记的了;更少的理由;改变的理由更少。

缓存呢?

在以前的许多案例中,我们必须依赖缓存抽象并直接使用它,以便为不同的客户提供性能改进。

这些年来,这项工作做得很好。但是随着我们的成长,我们的代码变得越来越复杂。

考虑这个例子,消费者正在为一个给定的项目寻找Foo文档:

using (var transaction = store.BeginTransaction())
{
    var foos = cache.Get<Foo>(transaction, x => x.ProjectId == projectId).ToList();
    ... 

只有几行字,却提供了重要的价值。它提供了一个Foo的缓存版本,避免我们不必要地访问数据库。

但是消费者必须停下来思考一下这个问题。

消费者必须:

  1. 旋起一个transaction
  2. 意识到他们可以利用一个cache依赖
  3. 弄清楚他们给定的类型(Foo)是否支持缓存(不是我们所有的类型都支持缓存)
  4. 确定如何直接调用缓存方法,传递Get<T>方法 a transaction和 Lambda

开发人员在编写代码时经常没有考虑到我们的缓存。正因为如此,我们对一些性能问题做出了反应,并在出现问题时使用缓存来修复问题。

但是,消费者为什么要考虑缓存呢?这不是他们的责任。

然而,这是某人的责任。我们仍然想要它所提供的价值。它只需要移到别的地方。

有了IDocumentStore<T>抽象,我们可以编写装饰器并透明地引入缓存层。

例如,类似这样的事情:

public class CachingDocumentStoreDecorator<TDocument> : IDocumentStore<TDocument> where TDocument : class
{
    readonly IDocumentStore<TDocument> inner;
    readonly ICache cache;

    public CachingDocumentStoreDecorator(IDocumentStore<TDocument> inner, ICache cache)
    {
        this.inner = inner;
        this.cache = cache;
    }

    public async Task<TDocument> GetAsync(string id, CancellationToken cancellationToken)
    {
        return TryGetFromCache(id).SingleOrDefault()
            ?? await inner.GetAsync(id, cancellationToken);
    }
    ... 

当您请求一个带有给定id的文档时,TryGetFromCache方法会判断该类型是否支持缓存,并在缓存可用时返回结果。

消费者对这一层一无所知。我们修饰了我们的抽象,并围绕这一层放置了强大的测试覆盖,给了我们信心,它做了我们期望的事情。任何IDocumentStore<T>的消费者都会得到这些开箱即用的装饰品。

受到惯例的鼓励

我们的开发者现在被鼓励依赖IDocumentStore<T>,我们有惯例测试来帮助他们停止直接依赖商店/交易。

我们核心平台的消费者请求他们需要的东西,在他们认为合适的时候改变它,知道(或者不知道)每个请求都有一个IUnitOfWork,它将在需要时负责建立一个transaction,并在请求完成时提交该事务(对于 Git 或 SQL)。

所有独立的问题都得到透明的处理,因此您可以专注于向客户提供出色功能的工作。

结论

我们希望这篇文章提供了一些关于架构变化的见解,当我们支持像代码配置这样的特性时,可以帮助我们建立长期的信心和稳定性。

例如:

public class CreateChannelController : BaseApiController
{
    readonly IResourceMapper resourceMapper;
    readonly IDocumentStore<Channel> channelStore;
    readonly IDocumentStore<Project> projectStore;

    public CreateChannelController(
        IResourceMapper resourceMapper,
        IDocumentStore<Channel> channelStore,
        IDocumentStore<Project> projectStore)
    {
        this.resourceMapper = resourceMapper;
        this.channelStore = channelStore;
        this.projectStore = projectStore;
    }

    // I don't think about DB vs Git
    // I don't think about transactions
    // I don't think about caching
    // I don't think about entity tracking
    // I don't think about deletion rules
    // I don't think about veto rules
    // ...
    // I just use my document stores
    ... 

我们相信,这将为使用我们核心平台的开发人员带来更简单、更快速、更直观的未来,从而为您(客户)带来简单、快速、直观的特性。

愉快的部署!

配置为代码策略- Octopus 部署

原文:https://octopus.com/blog/config-as-code-strategies

自从我们去年以代码形式发布 Octopus 配置的早期访问预览版以来,就有许多关于如何使用该功能以获得最佳效果的问题。

这篇文章解释了使用 Config 作为代码的一些好的实践,以及如何在不同的情况下调整你的策略。

你也可以在我们的 2022 年 Q1 公告帖子中阅读更多关于 Octopus 中的 Config as 代码。

为什么使用配置作为代码?

Git 是对代码进行版本控制和跟踪随时间变化的完美解决方案。它已经建立了代码分支以及发布和批准变更的模式。如果需要的话,它还允许你比较版本和回到过去。

Octopus Config as Code 特性允许您将部署过程作为配置文件存储在 Git 存储库中,而不是 Octopus 数据库中。您可以使用 Config as Code 来:

  • 将您的配置分支,并在合并之前测试分支中的更改
  • 使用拉式请求对变更进行审核和协作
  • 克隆现有项目以用作未来项目的模板
  • 使用已经用于应用程序代码的相同工具来跟踪对部署配置的更改
  • 在您喜欢的文本编辑器或 Octopus 应用程序中编辑您的部署配置

配置存储为使用 Octopus 配置语言(OCL)的可读文件。我们设计 OCL 是为了使阅读和编辑部署过程以及审查任何变更更加容易。有一个 Visual Studio 代码扩展,使得处理 OCL 文件更加容易。

当您启用版本控制时,并不是所有东西都被移动到存储库中。在我们的配置中有一个版本控制资源列表,作为代码引用

打开项目的版本控制是单向的。一旦项目进入存储库,就不能再将它移回 Octopus 数据库。您可以克隆一个现有的项目来尝试将 Config 作为代码,并在为您的生产项目启用它之前确认它满足您的需求。

在哪里存储您的配置

您需要做出的第一个决定是在哪里存储您的部署配置文件。您可以保留您的配置:

  • 除了您的应用程序代码
  • 在单独的部署存储库中

请继续阅读,了解每个选项什么时候最有效,什么时候应该避免。

您可能已经注意到,您可以在一定范围内安排这些可能性,从与应用程序的一对一关系到单个大型存储库。我们建议将您的部署配置保存在与应用程序代码相同的存储库中,但是在某些特定的情况下,其他选项可能也适用。

将 Config 设置为代码后,如果您改变主意,您可以移动您的部署配置文件

除了应用程序代码

将部署配置与应用程序代码放在一起是我们推荐的模式。最好是与您的应用程序代码一起发展您的部署过程。将配置放在与应用程序相同的位置符合开发运维实践,在开发运维实践中,工程师对他们的应用程序承担端到端的责任。

如果您选择将您的配置存储在应用程序存储库中,每个应用程序都有自己的.octopus目录和配置文件。这种安排便于为每个应用程序找到合适的部署过程。它还确保对应用程序和部署过程的更改被一起版本化,这消除了应用程序和部署过程的特定版本之间的任何依赖性。

如果您不希望对部署过程的更改触发应用程序的构建,您可以在构建服务器中屏蔽掉.octopus文件夹。

当团队同时负责应用程序及其部署时,这种模式是理想的。

特定于部署的存储库

如果您需要将部署配置从应用程序存储库中分离出来,您可以创建一个特定于部署的存储库。您应该只使用一个单独的存储库来在应用程序和它的部署过程之间创建一个明确的划分。例如,如果您需要限制谁可以访问部署存储库,或者如果您需要对变更实施不同的策略。

您的组织设计可以告知您是否使用:

  • 每个应用程序有单独的部署存储库
  • 每个 Octopus 空间一个部署存储库
  • 中央部署存储库

在为您的存储库选择设计时,考虑您可能会创建多少个分支。Octopus 应用程序将在分支切换器中显示所有分支。如果您将多个项目分组到一个单一的存储库中,您还应该将每个项目放在.octopus文件夹的子目录中。

将配置作为代码有效地用于分支和拉取请求

切换到版本控制后,您可以使用一些熟悉的工具来帮助您成功部署。其中两个是分支和拉请求。

当更改部署过程时,您可以使用分支来控制风险。您可以在 Octopus 应用程序中为受版本控制的项目创建和切换分支。您也可以在 Octopus 应用程序之外创建和编辑分支。无论您是选择使用 Octopus 应用程序、文本编辑器,还是两者都使用,流程将始终与分行保持同步。

The branch switcher in Octopus Deploy

当配置为代码是错误的选项时

许多部署资源不属于一个项目,例如,空间、租户、环境和帐户。我们并不打算用 Config as 代码来处理这些项目的版本控制。

你可以使用章鱼部署平台提供商来处理这些资源。您可以在我们的博客上找到如何开始使用面向 Octopus Deploy 的 Terraform provider。

您还可以考虑在多个项目之间共享一个配置,以保持过程同步。然而,这需要许多非项目资源在项目之间保持一致,这很快变得难以管理。

不要在多个项目之间共享相同的 OCL 文件,您应该创建一个定制工具来与 Octopus Deploy REST API 进行交互,以执行所需的流程配置。你可以在我们的文档中读到更多关于同步多个实例的信息。

结论

现在,您可以放心地为代码为的配置选择一个合适的策略。这篇文章解释了如何在版本控制中为您的配置文件选择一个合适的位置,如何使用分支和拉请求来管理您的部署过程,以及当 Config as Code 不是合适的工具时。

您可以在我们的路线图上保持对未来新增内容的更新,例如将 Config 作为 Runbooks 的代码。

观看我们的网络研讨会:在 Octopus 中将配置作为代码引入

德里克·坎贝尔和皮特·加拉格尔将带你了解在 Octopus 中配置为代码的入门知识,以及在大规模使用配置为代码时的最佳实践。

https://www.youtube.com/embed/Z4DgiJ630FU

VIDEO

我们定期举办网络研讨会。请参见网络研讨会第页,了解有关即将举行的活动和实时流媒体录制的详细信息。

愉快的部署!

将配置整形为变量代码- Octopus Deploy

原文:https://octopus.com/blog/config-as-code-variables

我们在 2022 年 3 月推出了配置即代码(Config as Code) ,并根据客户反馈继续增加新功能。最受欢迎的特性之一是将变量配置为代码。

Octopus 2022.3 开始,您可以在 Git 中存储您的非敏感变量以及您的部署流程和部署设置。

在这篇文章中,我将介绍我们所做的改变,并深入探讨变量章鱼配置语言(OCL)模式的设计。

为什么变量是 Config as Code 的第一个主要升级

自 2022 年 3 月发布 Config as Code 以来,我们一直在听取每个人的反馈,以继续改进我们的支持。

虽然有许多特性请求,但有一个明确的主题: Config,因为在 Git 存储库中没有变量,代码会感觉不完整。

变量是第一位的,但是我们将继续听取您的反馈,并将配置作为代码进行改进,以满足您的需求。

随着代码变得更加强大

Config as Code 的第一个版本很好地支持了版本控制的部署过程和设置,但是需要补充变量变化的过程变化需要仔细协调。如果您更改了错误的值,或者删除了错误的变量,可能会中断其他部署。

Git 存储库中现在有了项目变量,Config as Code 特性和分支功能更加强大。您可以在一个特性分支上对您的部署过程和变量一起进行重大更改,而不会影响到其他人,直到您准备好合并您的更改。

Octopus 现在在项目变量页面上有了分支上下文。当设置操作范围时,它从选定的分支中提取操作列表(而不是只显示默认分支中的操作)。

Screenshot of Octopus project variables page showing variable action scopes expanded showing

这使得动作范围在 Git 项目中更加有用。您可以在您的特性分支上添加一个新的步骤,将一个变量作用于这个步骤,并在准备好的时候将它们合并回您的默认分支,作为一个单独的配置块。

支持敏感值

目前,敏感变量将保留在 Octopus 数据库中。在“项目变量”页面上,我们通过在所有敏感值旁边显示一个数据库图标来明确这一点。

【T2 Screenshot of Octopus project variables page showing database icon to the left of masked sensitive variable placeholder

不管选择了哪个分支、提交或标记,您总是要查看和修改一组共享的敏感变量。我们正在研究在 Git 中安全存储敏感变量的最佳方式。我们希望在将来的某个时候增加对它的支持,但是现在敏感的值仍然保留在数据库中。

入门指南

在您升级到受支持的版本之后,当将项目转换为 Git 时,项目变量将会随着您的部署过程和部署设置自动迁移。您不需要完成任何额外的步骤。

对于现有的 Git 项目,您需要手动将变量迁移到 Git。

将变量迁移到 Git

我们最初计划在下次提交时为每个升级的人自动迁移变量。然而,在对这种方法进行了一些测试之后,很明显这对于许多团队来说是行不通的。中断的可能性太大了,尤其是对于经常收到实例更新的云客户。

相反,我们选择了一个产品化阶段,这样每个人都可以在适合自己的时间执行迁移。如果您有一个现有的 Git 项目,其中的变量还没有被迁移,那么您会在项目页面上看到一个横幅。

Screenshot of Octopus project variables page showing database icon to the left of masked sensitive variable placeholder

单击 MIGRATE VARIABLES TO GIT 按钮会显示一个向导,引导您完成迁移。

过渡到自动迁移

这个产品化阶段导致了 3 种可能的项目状态:

  • 带有 Git 变量的 Git 项目
  • 带有数据库变量的 Git 项目
  • 数据库项目

这给代码库增加了额外的复杂性,并且需要可以用于新功能的时间和精力。考虑到这一点,我们不会永远支持这个过渡状态。我们最终会取消对数据库中有变量的 Git 项目的支持,所以我们建议尽快迁移您的变量。

路线变更

随着项目变量集现在在 Git 存储库和 Octopus 数据库之间分离,Git 项目有 2 个项目变量路径:

  • /api/{spaceId}/projects/{projectId}/{gitRef}/variables:对所有不敏感的变量类型使用此路径。每个 Git 引用都有一组独立的变量,并在{gitRef}中指定这个引用。虽然您可以读取任何类型的引用,但提交和标记是不可变的,因此您只能写入分支。
  • /api/{spaceId}/projects/{projectId}/variables:对你的敏感变量使用这条路线。这些都保留在数据库中,永远不会写入您的 Git 存储库。

最初的变量 route ( /api/{spaceId}/variables/variableset-{projectId})将继续为 Git 中有变量的项目工作,但是它将只返回敏感值。我们建议始终使用新项目范围内的路线。

如果你只通过 UI 使用 Octopus,这些都不适用。“项目变量”页面会将值写入正确的位置。

可变 OCL 模式

Config as Code 的一个持续目标是保持 Octopus UI 对 Git 项目的完全功能性。我们希望为那些直接在文本文件中编辑 OCL 的人提供良好的体验。

在 Config as Code 的第一个版本中,我们采用现有的部署流程和部署设置资源模型,并将它们直接转换到 OCL 来创建模式。这对于这些资源来说很好,但是我们必须对变量采取不同的方法。

变量通过 API 传递,并作为单级变量数组保存。多值变量作为具有相同名称的完全独立的变量来保存。如果我们只是将它直接写入 Git 存储库,那么 OCL 应该是这样的。

variable "DatabaseName" {
    value = "AU-BNE-TST-001"
    scope = {
        environment = ["test"]
    }
}

variable "DatabaseName" {
    value = "AU-BNE-001"
    scope = {
        environment = ["production"]
    }
}

variable "DeploymentPool" {
    type = "WorkerPool"
    value = "production-pool"
    scope = {
        environment = ["production"]
    }
}

variable "DeploymentPool" {
    type = "WorkerPool"
    value = "non-production-pool"
}

variable "DatabaseName" {
    value = "AU-BNE-DEV-001"
    scope = { 
        environment = ["production"]
    }
} 

这是功能性的,用户界面工作正常,但是 OCL 的编辑体验并不理想。存在重复的变量名和类型,同一个变量的值很容易被分开,并且产生了不必要的嵌套。相反,当序列化为 OCL 时,我们将同一个变量的值合并在一起,缩小范围,一切都变得更加清晰。

variable "DatabaseName" {
    value "AU-BNE-TST-001" {
        environment = ["test"]
    }

    value "AU-BNE-DEV-001" {
        environment = ["development"]
    }

    value "AU-BNE-001" {
        environment = ["production"]
    }
}

variable "DeploymentPool" {
    type = "WorkerPool"

    value "non-production-pool" {}

    value "production-pool" {
        environment = ["production"]
    }
} 

我们采用了完全不同的 OCL 序列化方法来实现这一点,但我们对结果很满意。这让 OCL 更容易理解,并提供了很好的编辑体验。这为我们将来如何定义 OCL 模式提供了选择,并将为我们计划对持久性和 API 层进行的增强提供支持。

下一步是什么?

当您下载 2022.3 版本时,最新版本的 Config as 代码和 Git 变量将被部署到云中,并在本地可用。在实例更新之后,只要准备好了,就可以将变量迁移到 Git。

我们希望听到您的反馈,并通过我们的配置代码反馈表了解您下一步想要什么功能。

提供反馈

愉快的部署!

配置为代码:它是什么?它有什么好处?-章鱼部署

原文:https://octopus.com/blog/config-as-code-what-is-it-how-is-it-beneficial

Config as Code: What is it and how is it beneficial?

管理应用程序配置设置是现代应用程序开发中越来越重要的一个方面。通常,配置与它们相关的应用程序代码存储库一起存储,任何更改都需要部署新版本的代码。即使只更改了一个配置设置,也是如此。

Config as Code (CaC)将配置与应用程序代码分开。通常,应用程序设置存储在它们自己的存储库中,并在不同于主要代码库的过程中进行管理。

在本文中,我们将探讨 Config as Code 的含义以及版本控制对您的配置的好处。

配置为代码的好处

如上所述,Config as Code 是不同环境之间配置的版本化迁移。除了增加现有构建和部署流程的复杂性之外,这种方法还能为组织带来什么好处?

  • 安全性:用户访问分离提升了最低特权权限和更高级别的访问可审计性。
  • 可追溯性:使用一个独立的版本控制库,特定于配置,使得跟踪变更和更新变得更加容易。
  • 可管理性:特定于配置的构建和部署流程可以包括额外的批准、验证和测试步骤,以确保不间断的更改。

将配置管理转移到它自己独特的过程中有很多好处。尽管引入了复杂性,并且需要一些设置和配置来从代码库中移除配置,但是长期的好处是值得努力的。

花时间规划“配置即代码”方法可以成功地管理配置部署流程。这包括确保:

  • 配置已版本化。
  • 只有需要的人才能得到它。
  • 秘密是加密的。
  • 适当的审批关口已经到位。

基础架构作为代码与配置作为代码

基础设施即代码(IaC)关注支持应用程序环境所必需的底层基础设施的部署。这可能会造成混乱,因为它看起来类似于管理配置。

Config as Code 是关于管理特定的应用程序配置设置本身的,它与您的基础结构代码相分离,并在其自己独特的过程中进行管理。

基础设施即代码和配置即代码仍然是相辅相成的,而且经常一起使用。例如,某些配置可以而且应该在配置过程中进行管理,稍后将由基础设施过程使用。通过以这种方式使用每种方法,组织可以快速掌握复杂的配置以及如何管理它们。

配置作为代码的实际使用

在实践中使用 Config 作为代码实际上意味着什么?在实践中,有几种方法可以实现它,但并不是所有的方法对每个组织都有意义。阅读以下概述如何满足您的独特需求:

  • 使用独特的配置源代码管理存储库。
  • 开发定制的构建和部署流程。
  • 创建配置特定的测试环境。
  • 确保存在批准和质量保证流程。
  • 配置中的秘密管理。

使用独特的配置源代码管理存储库

通常,应用程序配置与部署的代码打包在一起。当配置值发生更改时,必须部署整个代码库才能使更改生效。尽管这意味着存在版本化的更改,但配置设置通常存储在平面文件中,并且可能很难跟踪所做的确切更改,尤其是在较大的代码提交中。

通过将配置存储在其自己的存储库中并对其进行版本控制,您将获得:

  • 独立配置更改的好处。
  • 量身定制的部署流程。
  • 变更审计的简易性。
  • 附加安全控件,通常用于开发人员可能不需要访问的应用程序机密。

开发定制的构建和部署流程

应用程序的部署过程可能不适合简单配置更改的需要。配置构建和部署过程通常会得到简化。配置更改的验证和测试可能不会像代码更改那样触及应用程序的许多不同方面。

对于特定的配置部署流程,可以在流程中加入额外的验证步骤。这可以消除许多对于配置特定的部署没有意义的流程步骤。例如,可能需要验证配置项,例如格式和内容,然后可以在特定于配置的构建和部署过程中处理这些内容。

创建特定于配置的测试环境

也许对于一个简单的配置更改,不需要构建一个完整的应用程序代码测试环境。根据配置部署过程的需要来确定测试环境的范围可以为组织节省时间和金钱。

这也可能意味着独立的变化可以同时发生。应用程序开发人员可以在测试配置更改的同时测试他们的代码。有了这种并行测试的能力,您可以在环境的操作和管理中获得效率。

确保存在批准和质量保证流程

也许更改 API 键或端点 URI 需要不同团队的输入和签署。如果配置本身隐藏在应用程序代码库中,那么这个批准过程通常不会存在。通过衔接定制的构建和部署流程,可以集成审批和质量保证,以确保将正确的配置部署到它们应该部署的位置。

配置中的机密管理

大多数配置将包括访问数据库、密钥存储或不应该广泛共享的文件位置所必需的秘密。由于配置只存在于存储在应用程序代码旁边的平面文件中,因此很难将这些秘密保护给那些应该有权访问的人。使用 Config as Code,您可以将这些受保护的秘密分离为加密的值,这些值将接受不同的批准和验证过程。这增加了应用程序的安全性,并大大降低了代码或基础结构被破坏的可能性。

结论

尽管有许多不同的方法可以将 DevOps 及其所有相关流程集成到您的环境中,但 Config as Code 为更好地管理和控制应用程序环境提供了一个很好的起点。

将配置作为代码集成到工作流中的组织在安全性、可审核性、可管理性和控制方面的优势,使得管理复杂的配置比在应用程序代码库中捆绑配置的典型方法更容易、更安全。

观看我们的网络研讨会:在 Octopus 中将配置作为代码引入

德里克·坎贝尔和皮特·加拉格尔将带你了解在 Octopus 中配置为代码的入门知识,以及在大规模使用配置为代码时的最佳实践。

https://www.youtube.com/embed/Z4DgiJ630FU

VIDEO

我们定期举办网络研讨会。请参见网络研讨会第页,了解有关即将举办的活动和实时流媒体录制的详细信息。


Adam Bertram 拥有 20 多年的 IT 经验,是一名经验丰富的在线商务专家。他是多家科技公司的顾问、微软 MVP、博客作者、培训师、出版作家和内容营销人员。在 adamtheautomator.com上关注亚当的文章,在的 LinkedIn 上联系,或者在推特 @adbertram 上关注他。

在 Azure - Octopus 部署中配置 Octopus 高可用性

原文:https://octopus.com/blog/configure-octopus-high-availability-in-azure

Octopus High-Availability on Azure

我们最近更新了我们的高可用性文档,为托管 Octopus 高可用性提供更多信息和选项。

在这篇博客中,我在 Azure 上设置了 Octopus 高可用性,评估了您可以使用的不同选项,并带您了解了在微软 Azure 上高可用性 Octopus Deploy 设置的不同组件。

Octopus 高可用性的好处

高可用性允许你运行多个 Octopus 服务器,在它们之间分配负载和任务。高可用性有几个好处:

  • 业务关键型工作负载的弹性更高。
  • 更简单的维护任务,如服务器补丁
  • 性能和可扩展性。
  • 更少的停机时间。
  • 使用八达通高可用性不收取额外费用。

Octopus 高可用性组件

Octopus HA 配置需要四个主要组件:

  • 负载平衡器:负载平衡器在不同的 Octopus 服务器节点之间引导去往 Octopus web 接口的用户流量。
  • Octopus 服务器节点:运行 Octopus 服务器 windows 服务。它们服务于用户流量并协调部署。
  • 一个数据库:Octopus 服务器节点使用的大部分数据都存储在这个数据库中。
  • 共享存储:一些较大的文件(如 NuGet 包,工件,部署任务日志)不适合存储在数据库中,必须存储在所有节点都可用的共享文件夹中。

章鱼虚拟机

当创建高可用性配置时,您需要在 Azure 中提供至少两个虚拟机来托管 Octopus。我们没有一个适用于所有章鱼的标准,因为它取决于:

如果您在 Octopus 中的工作负载相当小,您可以选择较小的虚拟机。不过,Azure D 系列虚拟机是一个很好的起点,因为它们是通用的,非常适合大多数场景。

我们的建议是考虑您的工作负载,然后使用其中一个 D 系列虚拟机,看看它在满足您的要求方面表现如何。

在这种情况下,我使用 D2s V2 启动了两个虚拟机,分别名为 Octo1Octo2 ,它们使用 Server 2019 拥有两个 vCPU 和 8GB 内存。这个规格是一个很好的起点,您可以将 D 系列更改为其他尺寸。

理论上,您可以根据需要增加资源和减少资源。您可以在这里使用某种形式的自动化来水平或垂直扩展。

虚拟机磁盘

您需要考虑您的 Octopus 虚拟机需要什么类型的存储,并且您可以看到可用性磁盘类型的完整列表。有几个问题需要考虑:

请务必记住,这仅适用于虚拟机,我选择了标准固态硬盘,因为其成本和性能符合我的要求。Octopus 对磁盘的占用不是很大,这意味着使用 Ultra Disks 不太可能获得很多好处。如果你在数千个项目中使用 Octopus,你应该考虑高级 SSD,因为这可能是有益的。

Azure 可用性集与 Azure 可用性区域

请查看 Azure 文档以获得 Azure 虚拟机可用性选项的完整列表,因为我们不会涵盖所有这些。

  • Azure 可用性区域是 Azure 区域内独立的数据中心,具有专用的电力、冷却和网络。通过这个选项,当使用可用性区域时,您可以确保 Octopus 在您的主要 Azure 区域中保持对故障的弹性。对于弹性,所有启用区域至少有三个独立的区域。Azure 为这个选项提供了 99.99%的正常运行时间 SLA。
  • Azure 可用性集是虚拟机的逻辑分组,提供冗余和可用性。Azure 为可用性集提供了 99.95%的正常运行时间 SLA ,除了虚拟机成本之外,没有任何成本。

在微软 Azure 上设计和配置 Octopus 时,我选择了 Azure Availability Zones 选项,纯粹是为了提高 SLA。这也是微软通常推荐的高可用性。

我在可用性区域 1可用性区域 2 中设置了我的两个虚拟机 Octo1Octo2 。这让我对 Octopus HA 有了容忍度,因为它使用了不同的逻辑数据中心,并且具有对存储和 SQL 数据库进行低延迟访问的优势。

大多数 Octopus 高可用性配置将包含两个虚拟机。如果您使用三个或更多,您需要将它们放入各自的区域。

多虚拟机 Octopus 高可用性的配置示例如下:

  • AZ1 的 10 月 1 日,AZ2 的 10 月 2 日
  • AZ1 的 10 月 1 日,AZ2 的 10 月 2 日,AZ3 的 10 月 3 日
  • AZ1 中的十月一日、AZ2 中的十月二日、AZ3 中的十月三日、AZ1 中的十月四日

我们只在八个节点上测试 Octopus,但是您可以根据需要将这些节点划分到多个可用区域。

Octopus SQL 数据库

Octopus 由一个存储环境、项目、变量、版本和部署历史的 SQL 数据库支撑。你需要在 Azure 中启动一个 SQL 服务器。有两种选择可以考虑,而 Octopus 天生就支持这两种选择:

如果您可以联系数据库管理员,您应该寻求他们的专业知识,因为他们可能会提供进一步的见解。

SQL 虚拟机与 Azure SQL

大多数组织在云中调配 SQL 工作负载时都使用虚拟机。在这一节中,我将介绍选择 SQL 虚拟机的好处和一些缺点。

因为我们需要 Octopus 的高可用性配置,所以我们需要考虑 SQL 级别的高可用性。这意味着你最少需要三个 SQL 服务器,最好是在 Azure 的一个 SQL 集群中,或者在 Azure 的一个永远可用组中。

大多数情况下,我将保持这一部分的高水平,因为在这些主题上有很好的内容,您可能有一个数据库管理员会为您执行此操作。如果你已经在 Azure 上安装了这个,我推荐使用这个设置来托管 Octopus,最好是在一个专用的 SQL 实例上。

在 Azure SQL 上使用 SQL 虚拟机的优势:

  • 更大的灵活性。
  • 更多的控制。
  • 托管多个数据库,无需额外成本。

在 Azure SQL 上使用 SQL 虚拟机的缺点:

  • 更高的总拥有成本。
  • 增加了设置时间。
  • 维护基础设施和数据库。

我做了很多概念验证,我是任何 PaaS 的忠实粉丝。我特别喜欢 Azure SQL 数据库服务,因为我不需要投入太多时间来启动虚拟机、网络安全组、配置 SQL、防火墙规则、维护计划等。

我可以登录到 Azure 门户网站,在几分钟内就可以构建一个新的 SQL Server、数据库和连接字符串。如果你已经准备好了手臂模板,这可能需要一些时间。基础设施作为代码可以节省大量时间,但我意识到这可能还不是每个人的偏好。

当在 Azure SQL 上构建数据库时,我可以在几分钟内创建一个地理复制或本地复制的数据库,这就是我的高可用性数据库。然后我简单地为 Octopus 配置连接字符串,就可以开始了。

在 SQL 虚拟机上使用 Azure SQL 有很多好处:

  • 更容易配置。
  • 在几秒钟/几分钟内根据需要旋转和拆卸。
  • 只需几个命令或几下鼠标,就能使数据库高度可用。
  • 管理备份和维护任务。
  • 伟大的 Azure AQL 分析和监测内置。

在 SQL 虚拟机上使用 Azure SQL 的一些缺点:

  • 更少的控制。
  • 当出现问题时,恢复备份需要相当长的时间。
  • 重构 SQL 脚本。
  • 试图理解什么是 DTU。

两种选择各有利弊。在为您和您的组织需求选择正确的解决方案时,您应该考虑这一点。

SQL 数据库选择

下面是我在我的例子中所做的:

  • 选择西欧的 Azure SQL Server,因为这是我的主要地区。
  • 为我的地理复制 Octopus 数据库选择了位于北欧的 Azure SQL Server,以满足灾难恢复需求。
  • 在主区域和 SQL Server 中创建了一个名为 Octo-HA 的数据库。
  • 浏览到主 Azure SQL 数据库上的地理复制并启用复制。
  • 让它复制到辅助数据库服务器和区域。

此时,您有一个主数据库服务器和数据库,它跨区域同步到一个辅助服务器和数据库。

区域冗余数据库目前正在 Azure 中预览。如果这是可用的,我会使用区域冗余数据库,因为这与我为 Azure 虚拟机设置的冗余级别相同。

SQL 数据库规范

博客的这一部分涵盖了 SQL Server 性能的选项。我建议查看高可用性 SLA 文档,以选择正确的数据库和服务器规模。

在我的例子中,我选择了:

  • 一般用途。
  • Provisioned,提供预先分配并按小时计费的计算机资源。
  • 2 个 vCores。
  • 最大数据大小为 30GB。
  • 区域冗余开启(区域冗余增加约 20%的成本)。

在高可用性配置中,这些规范是一个很好的起点。不过,您可能需要考虑您的工作量,因为如果您拥有一个小的 Octopus 实例,这对于您的要求来说可能太大了。

如果你拥有一个大型实例,我会考虑微软 Azure 中的超大规模和业务关键负载。找到适合您的 SQL 需求的方法,因为您不想要一个性能缓慢的 Octopus 实例,但您可能也不想让它太大,并为您的数据库托管支付太多费用。

八达通存储

在单节点设置中,你通常将 Octopus 托管在本地存储上C:\OctopusD:\Octopus.你需要 Octopus 的一些本地存储,除非你决定将 Azure 文件共享作为映射驱动器或作为到服务器的符号链接。

我们建议在服务器上本地托管您的 Octopus 日志和配置。这避免了可能导致 Octopus 停止响应的潜在文件锁定问题。

工件、包和任务日志

Octopus 存储了几个不适合存储在数据库中的文件。其中包括:

  • Octopus 内部的内置 NuGet 库使用的 NuGet 包。这些包裹通常很大。
  • 部署期间收集的工件。使用 Octopus 的团队有时会在部署过程中使用这个特性从机器上收集大型日志文件和其他文件。
  • 任务日志是存储部署和其他任务的所有日志输出的文本文件。

与数据库一样,从 Octopus 的角度来看,您告诉 Octopus 服务器将它们作为文件路径存储在操作系统中的什么位置。Octopus 不在乎你用什么技术来呈现共享存储;它可以是映射的网络驱动器或文件共享的 UNC 路径。这三种类型的数据都存储在不同的位置。

无论您以何种方式提供共享存储,都有一些注意事项:

  • 对 Octopus 来说,它需要显示为映射的网络驱动器(例如,D:\)或文件共享的 UNC 路径(例如,\\server\path)。
  • Octopus 运行的服务帐户需要完全控制目录。
  • 驱动器是按用户映射的,所以您应该使用运行 Octopus 的同一服务帐户来分配驱动器。

Azure 文件

如果你的 Octopus 服务器运行在微软 Azure 中,只有一个解决方案(除非你在 Azure 中有一个 DFS 副本)。这个解决方案就是 Azure 文件存储,它通过 SMB 3.0 提供一个文件共享,在你所有的 Octopus 服务器上共享。

一旦你创建了你的文件共享,添加 Azure 文件共享作为一个符号链接,然后将它添加到C:\Octopus\用于工件、包和任务日志,它们需要对所有节点可用。

安装 Octopus 前运行以下:

# Add the Authentication for the symbolic links. You can get this from the Azure Portal.

cmdkey /add:octostorage.file.core.windows.net /user:Azure\octostorage /pass:XXXXXXXXXXXXXX

# Add Octopus folder to add symbolic links

New-Item -ItemType directory -Path C:\Octopus
New-Item -ItemType directory -Path C:\Octopus\Artifacts
New-Item -ItemType directory -Path C:\Octopus\Packages
New-Item -ItemType directory -Path C:\Octopus\TaskLogs

# Add the Symbolic Links. Do this before installing Octopus.

mklink /D C:\Octopus\TaskLogs \\octostorage.file.core.windows.net\octoha\TaskLogs
mklink /D C:\Octopus\Artifacts \\octostorage.file.core.windows.net\octoha\Artifacts
mklink /D C:\Octopus\Packages \\octostorage.file.core.windows.net\octoha\Packages 

安装 Octopus ,然后运行以下程序:

# Set the path
& 'C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.exe' path --artifacts "C:\Octopus\Artifacts"
& 'C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.exe' path --taskLogs "C:\Octopus\TaskLogs"
& 'C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.exe' path --nugetRepository "C:\Octopus\Packages" 

负载平衡

当您配置第一个 Octopus 服务器节点和每个后续节点时,您配置了 Octopus Web 门户可用的 HTTP 端点。

最后一步是配置一个负载平衡器,在每个 Octopus 服务器节点之间引导用户流量。

Octopus 可以与任何负载平衡器技术一起工作,包括硬件和软件负载平衡器。Azure 为我们提供了以下负载平衡器:

在评估了 Azure 中的选项后,我的首选是 Azure 负载平衡器选项,因为这是一个功能丰富的负载平衡器,符合我的要求。如果你想比较负载平衡的所有 Azure 选项,请查看选择负载平衡服务

在创建虚拟机之前创建 Azure 负载平衡器,因为您可以在配置期间选择负载平衡器。

负载平衡器会话持久性

我们通常建议使用循环(或类似的)方法在集群中的节点之间共享流量,因为 Octopus Web 门户是无状态的。

但是,群集中的每个节点都保留了数据的本地缓存,包括用户权限。当用户权限更改时,会出现一个已知问题。本地缓存仅在进行更改的节点上无效。

为了同时解决这个问题,您可以用会话持久性配置您的负载平衡器。这将确保用户会话被路由到同一个节点。

身份验证提供商

如果你从内部迁移到 Azure,你需要考虑你的身份验证提供者。你可能在本地使用活动目录,通常,这是 Azure 中的 Octopus 不支持的,因为你需要在 Octopus 安装的同一网络中有一个域控制器,以允许对你的活动目录用户进行身份验证。

如果您在 Azure 中有可联系的域控制器,您可以继续使用 Active Directory 身份验证。

如果您没有可在 Azure 中联系的域控制器,您需要考虑切换到:

移动身份验证提供程序

如果您已经在内部使用了 Active Directory,并且正在迁移到 Azure,并且无法继续使用 Active Directory,则可以在 Octopus 中将多个外部身份与单个用户相关联。最常见的迁移可能是从 Active Directory 到 Azure Active Directory。在这个例子中,假设我有一个名为 Derek 的用户。坎贝尔在一个名为 work.local 的域名和一个 worklocal.onmicrosoft.com的活动目录租户上,我会:

  • 设置 Azure 活动目录
  • 将我的Derek.Campbell@Worklocal.OnMicrosoft.com帐户添加到 Octopus Deploy 用户,该用户也有我的德里克。坎贝尔@Work.local 用户。
  • 拆下德里克。来自用户的 Campbell@work.local
  • 测试身份验证。
  • 冲洗,并为所有用户重复。

请查看该脚本,因为如果您要切换到 Azure Active Directory,它可以帮助您从本地迁移到 Azure。

建立工作关系网

即使在最好的情况下,网络也是一个有争议的问题,您的配置将在很大程度上取决于您现有的网络拓扑和标准。我会实现下面的一个或几个来帮助保护你的 Azure 工作负载:

  • Azure Bastion :一个完全平台管理的 PaaS 服务,可以用来为您的服务器提供安全无缝的 RDP/SSH 连接。
  • VPN 网关:将你的本地网络连接到 Azure 服务的服务。
  • ExpressRoute :一种通过私人链接直接连接 Azure 的专线方式。
  • 跳转框:一种从安全服务器启用到 Azure 服务的单一路由的方式。

考虑使用现有的方法连接到 Azure,如果你已经有了它们。如果您有快速路线,那么这是最好的方法,但它是最昂贵的。

如果你有一个 VPN 网关,一个跳转框,或者甚至 Azure Bastion 服务,我推荐你利用这些优势。

最重要的建议是减少您的攻击面,同时保持您的网络尽可能简单明了,不会导致任何潜在的安全问题。

  • 在可能的情况下,使用内部 IP 和网络而不是公共 IP,尤其是针对您的 SQL 配置。
  • 使用 VPN 或跳转/堡垒盒。最好两者都有。
  • 安全八达通使用 HTTPS 只有一个有效的证书。

投票触角

监听触角不需要特殊的配置来实现高可用性。然而,轮询触须定期轮询服务器,以检查触须是否需要执行任何任务。

在高可用性场景中,轮询触角必须轮询配置中的所有 Octopus 服务器。您可以轮询一个负载平衡器,但是根据您的负载平衡器配置,存在一个风险,即触手不会立即轮询所有服务器。

您还可以将触手配置为轮询每个服务器,方法是将它注册到您的一个 Octopus 服务器,然后将每个 Octopus 服务器添加到触手. config 文件中。添加 Octopus 服务器有两种选择,通过命令行或直接编辑触手. config 文件:

触手. config

通过命令行配置触手是首选选项,每个服务器只执行一次命令。下面是使用默认实例的命令示例:

C:\Program Files\Octopus Deploy\Tentacle>Tentacle poll-server --server=http://my.Octopus.server --apikey=API-77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6 

有关该命令的更多信息,请参考触手轮询服务器选项文档

或者,可以直接编辑 Tentacle.config 来添加每个 Octopus 服务器(这被解释为服务器的 JSON 数组)。不建议使用此方法,因为每台服务器的 Octopus 服务都需要重新启动,以通过此方法接受传入连接:

<set key="Tentacle.Communication.TrustedOctopusServers">
[
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://10.0.255.160:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://10.0.255.161:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://10.0.255.162:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"}
]
</set> 

请注意,在高可用性配置中,每个 Octopus 服务器都有一个地址条目。根据您的配置和网络拓扑,您可以为每个 Octopus 节点使用私有 IP 或公共 IP 和/或 FQDN。以下示例显示了在端口 10943 上注册到octo1.domain.comocto2.domain.comocto3.domain.com的情况:

<set key="Tentacle.Communication.TrustedOctopusServers">
[
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://octo1.domain.com:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://octo2.domain.com:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://octo3.domain.com:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"}
]
</set> 

在以下示例中,我使用公共 IP,而不是 FQDN 或私有 IP:

<set key="Tentacle.Communication.TrustedOctopusServers">
[
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://1.2.3.4:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://1.2.3.5:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"},
  {"Thumbprint":"77751F90F9EEDCEE0C0CD84F7A3CC726AD123FA6","CommunicationStyle":2,"Address":"https://1.2.3.6:10943","Squid":null,"SubscriptionId":"poll://g3662re9njtelsyfhm7t/"}
]
</set> 

移民

如果您正在将 Octopus 的一个实例迁移到 Azure,我们推荐的方法是:

  • 建立基础设施。
  • 设置 Octopus 文件夹和存储位置。
  • 运行高可用性 Octopus 安装。
  • 对新实例进行概念验证。
  • 测试身份验证和部署。
  • 确认后计划生产移动。
  • 计划停机时间。
  • 使用移动 Octopus 服务器和数据库进行迁移。
  • 切换到新的 Azure 设置。

结论

尽管 Azure 中的高可用性非常简单,但是在从内部部署迁移到 Azure 托管 Octopus Deploy 时,需要考虑很多问题。在这篇博客中,我解释并推荐了一些可以使用的技术,以帮助您在 Microsoft Azure 上配置 Octopus 高可用性。

我希望这些技巧能让在微软 Azure 上设置 Octopus 高可用性变得更容易。

愉快的部署!

在 Azure 中配置 Jenkins 并使用 Octopus - Octopus Deploy 部署

原文:https://octopus.com/blog/configuring-jenkins-azure-deploying-octopus

Jenkins 是市场上最受欢迎的持续集成(CI)平台。它是开源和免费的,让你自动构建和测试你的代码。

您可以将它与 Octopus Deploy 一起使用来自动管理发布和部署。

在这篇文章中,我将向您展示如何配置一个 Jenkins 实例,将一个包推送到 Octopus Deploy 实例,以及将一个 web 应用程序部署到 Azure。

开始之前

要跟进这篇文章,你需要:

为 Octopus 部署设置 Jenkins

设置完 Jenkins 后,转到 Jenkins 实例的 URL 来访问 UI。

在 UI 中,进入管理 Jenkins ,然后进入管理插件,在可用下搜索 Octopus Deploy 插件,安装插件。

Octopus Plugin

现在需要在 Octopus 部署实例中生成一个 API 键。

在 Octopus 中,进入你的用户名,然后个人资料,然后我的 API 密匙,创建一个密匙。Jenkins 使用这个值。

Octopus API Key

接下来,进入管理詹金斯,然后配置系统

在 Octopus Deploy 插件设置下,添加 Octopus Deploy 实例的 URL,并添加 API 密钥。

Octopus URL

Jenkins 让你的编译包在 Octopus 中可用,随时可以部署。

进入 Jenkins 主页,点击新项目,然后点击自由式项目,并分配以下设置,创建一个新作业:

源代码管理

Git: https://github.com/OctopusSamples/RandomQuotes-JS.git构建说明符:*/master

构建触发器

轮询供应链:H/5 * * * *

构建步骤-执行 shell

您必须在虚拟机上安装 npm 和 Node.js。

npm install
npm tests 

构建步骤 Octopus 部署:打包应用程序

  • Octopus 部署 CLI:默认
  • 包 ID: RandomQuotes
  • 版本号:1.0.${BUILD_NUMBER}
  • 包格式:zip
  • 包包含路径:${WORKSPACE}/**
  • 包输出文件夹:${WORKSPACE}

构建步骤 Octopus 部署:推送包

  • Octopus 部署 CLI:默认
  • 章鱼部署连接:奥克托-詹金斯
  • 包路径:${WORKSPACE}/RandomQuotes.1.0.${BUILD_NUMBER}.zip

点击保存

回到仪表板并点击立即构建开始工作。

Jenkins Build Now

构建开始后,导航到构建号并检查其进度。如果每个步骤都通过了,您会看到一个成功状态。

Jenkins Success

Jenkins 将包上传到 Octopus Deploy 实例,您可以在下找到该实例,然后在下找到该实例。软件包版本对应于 Jenkins 中的最新内部版本号。

Octopus Package

配置 Azure 帐户

您需要配置一个 Azure 帐户和 web 应用程序作为从 Octopus 部署的目标。也可以使用其他目标,比如 AWS 或者 Linux 和 Windows 服务器。

通过导航到 Azure 门户在 Azure 中创建一个帐户。

使用 Azure 门户创建 Azure 服务主体

https://www.youtube.com/embed/QDwDi17Dkfs

VIDEO

  1. 在 Azure 门户中,打开菜单,导航到 Azure Active Directory ,然后导航到属性,并复制来自租户 ID 字段的值。这是您的租户 ID
  2. 接下来你需要你的应用 ID :
  • 如果你创建了一个 AAD 注册的应用,导航到 Azure Active Directory ,然后应用注册,点击查看所有应用。选择应用程序并复制应用程序 ID 。请注意,Azure UI 默认为拥有的应用标签。点击所有应用选项卡查看所有应用注册。
  • 如果您尚未创建已注册的应用程序,请导航至 Azure Active Directory ,然后导航至应用程序注册,点击新注册,并为您的应用程序添加详细信息,然后点击保存。记下应用 ID
  1. 通过导航到证书&机密,然后导航到新客户端机密,生成一次性密码。添加新密码,输入描述,点击保存。记下显示的应用程序密码,以便在 Octopus 中使用。如果您不想接受默认的密码一年到期,您可以更改到期日期。

您现在拥有以下内容:

  • 租户 ID
  • 应用程序 ID
  • 应用程序密码/机密

这意味着您可以在 Octopus 中添加服务主账户。

接下来,您需要配置您的资源权限

资源权限

资源权限确保你的注册应用可以使用你的 Azure 资源。

  1. 在 Azure 门户中,导航到资源组并选择您希望注册的应用程序访问的资源组。如果资源组不存在,通过转到主页,然后资源组,然后创建来创建一个资源组。创建之后,记下资源组的 Azure 订阅 ID。
  2. 点击访问控制(IAM) 选项。在角色分配下,如果您的应用未列出,请点击添加角色分配。选择适当的角色( Contributor 是一个常见选项),并搜索您的新应用程序名称。从搜索结果中选择它,然后点击保存

下一步是设置一个 Azure web 应用程序并配置其属性。

Web 应用程序设置

  1. 在你的资源组中点击创建,然后 Web App
  2. 在运行时堆栈和操作系统下创建 Windows 节点应用程序。
  3. 记下你的 Azure 应用名称,因为这将是你的 web 应用的地址:[your-site].azurewebsites.net

在八达通上加入服务主账户

您可以使用以下值将您的帐户添加到 Octopus:

  • 应用程序 ID
  • 租户 ID
  • 应用程序密码/密钥
  1. 导航至基础设施,然后导航至账户
  2. 选择添加账户,然后选择 Azure 订阅
  3. 在 Octopus 中给帐户起一个你想要的名字
  4. 给账户一个描述
  5. 添加您的 Azure 订阅 ID -这可以在 Azure 门户的订阅下找到
  6. 添加应用 ID租户 ID应用密码/关键字

点击保存并测试以确认帐户可以与 Azure 交互。Octopus 尝试使用帐户凭证来访问 Azure 资源管理(ARM) API,并列出该订阅中的资源组。

您可能需要将目标 Azure 数据中心的 IP 地址列入白名单。请参见通过防火墙部署到 Azure了解更多详细信息。

新创建的服务主体可能需要几分钟才能通过凭据测试。如果您已经仔细检查了您的凭据值,请等待 15 分钟,然后重试。

配置 Octopus 以部署到 Azure

在您的 Octopus 实例中,通过转到基础设施,然后转到环境,再转到添加环境来添加生产环境。

转到基础设施,然后部署目标并添加一个 Azure Web 应用。分配生产环境,并为目标设置一个角色(例如,azure)。

选择您之前设置的 Azure 帐户,并选择您的 Azure Web 应用程序。点击保存

进入项目创建一个项目,然后添加项目

转到工序部分。添加一个部署 Azure App 服务步骤

代表

  1. 选择角色(例如,azure)

部署

  1. 选择从 zip、Java WAR 或 NuGet 包部署
  2. 从内置库中选择包

其他一切使用默认设置。

转到您的项目并创建一个发布。点击保存,然后部署到生产,然后部署,等待部署完成。

Octopus Success

转到您的站点 URL [your-site].azurewebsites.net来查看部署的 web 应用程序。

T45

后期制作

Octopus Deploy Jenkins 插件也可以用于在 Jenkins 中创建发布和部署。

在 Jenkins 作业的仪表板中,转到配置,并添加以下步骤:

后期生成操作:创建发布

  • Octopus 部署 CLI:默认
  • 八达通服务器:Octo-Jenkins
  • 项目名称:jenkins
  • 发布版本:0.0.i
  • 是否在创建后部署此版本?选中该框

点击保存,返回作业仪表盘,点击立即构建。Jenkins 触发包的构建,并在 Octopus Deploy 中开始构建后的发布和部署步骤。

结论

在本文中,您设置并使用了一个 Jenkins 实例来构建并推送一个包到 Octopus Deploy。您使用这个包将 web 应用程序部署到 Azure Web 应用程序。

这篇文章向你展示了 Jenkins 如何集成 Octopus Deploy 来自动管理发布和部署。

尝试我们免费的 Jenkins 管道生成器工具来用 Groovy 语法创建一个管道文件。这是您启动管道项目所需的一切。

有关持续集成(CI)和构建服务器的更多信息,请查看我们的 CI 博客系列

愉快的部署!

用 Octopus Runbooks 配置 Linux 服务器- Octopus Deploy

原文:https://octopus.com/blog/configuring-linux-servers-with-runbooks

当设置 Linux 服务器时,您必须根据您的需要配置系统。您可以将服务器用于 web 开发、系统管理、数据科学等等。每个用例都有不同的配置要求,手动配置这些服务器可能会很繁琐。

使用 Octopus Runbooks,您可以创建一个可重复的、自动的过程来配置您的 Linux 服务器,并且它可以适应不同的配置需求。

在这篇文章中,我使用 runbook 配置了一个 Linux 服务器,以指定特定任务所需的所有依赖项。可以保存并导出该操作手册,以供未来具有相同要求的服务器使用。

先决条件

要跟进,您需要:

在 Azure 中创建 Linux 服务器

Microsoft Azure 是一个云计算平台,用于创建虚拟机、web 应用程序和其他基于云的资源。我们使用 Azure 为我们的 runbook 示例创建一个 Linux 服务器。

在 Azure 主页上,导航到创建资源,然后 Ubuntu Server 20.04 LTS ,然后创建

选择链接订阅并创建新的资源组。给服务器一个名称。在 Administrator account 下,生成一个 SSH 公钥或密码,供以后使用。选择查看+创建接受默认设置。

完成后,选择转到资源。点击连接,堡垒连接到服务器。您需要输入之前的 SSH 密钥或密码。如果这是你第一次设置堡垒,点击部署堡垒,等待它完成设置。

在 Linux 服务器上安装 Octopus 触手

Bastion 连接到 Linux 服务器后,您会看到一个 Bash shell。您设置了一个 Octopus 触手来与运行 runbook 的 Octopus 实例通信。

运行以下命令安装触手:

 sudo apt-key adv --fetch-keys https://apt.octopus.com/public.key

sudo add-apt-repository "deb https://apt.octopus.com/ stretch main"

# for Raspbian use

# sh -c "echo 'deb https://apt.octopus.com/ buster main' >> /etc/apt/sources.list"

sudo apt-get update

sudo apt-get install tentacle 

触手需要与您的 Octopus 实例通信,因此您需要提供触手的密钥进行身份验证。

在你的 Octopus 实例中,点击你的个人资料,然后个人资料,然后我的 API 密匙,以及新 API 密匙。给密钥命名并确保保存它,因为它只出现一次。

通过运行以下命令,在 Linux 服务器上配置触手:

 /opt/octopus/tentacle/configure-tentacle.sh 

安装脚本会询问您一系列问题。请确保指定以下参数:

  1. 触手实例的名称(默认触手):按回车键接受默认值
  2. 你想配置哪种触手: 1)监听或 2)轮询(默认 1): 2
  3. 您希望触手在哪里存储日志文件?(/etc/octopus): 按回车键接受默认值
  4. 您希望触手将应用程序安装到哪里?(/home/Octopus/Applications):按回车键接受默认值
  5. Octopus 服务器 URL(例如 https://octopus-server): 您的 Octopus 实例的 URL
  6. 选择验证方法:1) API-Key 或 2)用户名和密码(默认为 1): 1
  7. API-Key: 输入之前配置的 API key
  8. 选择您想要设置的触手类型:1)部署目标或 2)工作器(默认为 1): 1
  9. 你想在哪个空间注册这个触手?(默认值):按回车键接受默认值
  10. 你想用什么名字注册这个触手?按回车键接受默认值
  11. 输入该触手的环境(用逗号分隔):指定 Octopus 实例的环境
  12. 输入该触手的角色(用逗号分隔):为触手指定一个角色,例如 Linux(稍后将在操作手册中使用)
  13. 按下回车继续

确认触手连接

你通过导航到基础设施,然后部署目标来确认你的触手已经连接到 Octopus。这里您可以看到您连接的部署目标。

Linux Deployment Target

在 Octopus 中设置操作手册

创建 Linux 服务器,安装并连接触手之后,就可以创建 runbook 了。该操作手册为 web 开发用例建立了一个开发环境。其他用例需要不同的配置设置。当 web 开发人员需要设置服务器时,操作人员可以运行 runbook。

创建操作手册

  1. 通过转到项目,然后添加项目,创建一个项目来托管运行手册。
  2. 在您的项目中,转到运行手册,然后添加运行手册,然后处理,然后添加步骤,然后脚本,然后运行脚本,最后添加
  3. 在角色中目标的下,添加您在触手设置脚本中指定的角色。
  4. 指定一个 Bash 脚本并添加以下代码:
 apt update

apt upgrade

apt install -y build-essential

apt install -y curl

apt-get install -y git-core

apt-get install -y nodejs

apt-get install -y npm 
  1. 点击保存运行

然后你会看到一个成功的结果。

【T2 Runbook success web developer

转到 Linux 服务器并运行npm命令来确认安装。

npm command

创建不同的配置

让我们使用一个简单的。网络配置。

创建另一个 Linux 服务器,按照上面的步骤创建另一个带有 Octopus 触手的 runbook。

这一次,在步骤 4 中,添加以下代码:

 wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb

sudo dpkg -i packages-microsoft-prod.deb

rm packages-microsoft-prod.deb

sudo apt-get update; \

sudo apt-get install -y apt-transport-https && \

sudo apt-get update && \

sudo apt-get install -y dotnet-sdk-6.0 

运行操作手册来配置服务器

Runbook success

转到 Linux 服务器并运行dotnet命令来确认安装。

dotnet command

这个工作流程演示了 runbooks 可以配置 Linux 服务器。您创建了两个操作手册,一个将 web 开发配置应用到服务器,另一个使用. NET 配置来配置服务器。

您可以根据需要为任意多的配置创建操作手册,确保每次创建时每个配置都是一致和自动的。

结论

配置服务器可能是一个繁琐的手动过程。通常,一台服务器有多种配置要求。为了帮助解决这个问题,Octopus Runbooks 提供了一种可重复的自动化方式来配置服务器。

Runbooks 可以满足特定的配置需求,并在需要时触发。操作手册的可重复特性在您的组织基础设施中引入了一致性。Runbooks 还减少了系统管理员的工作量,使他们可以专注于其他任务。

要进一步了解 Octopus Runbooks 如何帮助您满足部署需求,请联系我们的客户成功团队

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

部署期间配置 web.xml 文件- Octopus Deploy

原文:https://octopus.com/blog/configuring-web-xml

使用 Octopus 的包的典型部署使您能够替换模板文件中的标记。当您控制源包并且可以将所需的标记语句嵌入到配置文件中时,这是非常好的,但是当您正在部署您不控制的包时会发生什么呢?

在这篇博文中,我们将看看如何使用一些简单的脚本来更新来自 Maven central 的 Java web 应用程序中的web.xml文件,我们对此没有任何控制。

下载八达通部署 4.1

首先从下载页面获取 Octopus Deploy 4.1 的副本。版本 4.1 包括与 Maven repos 集成的能力。你可以从文档中找到更多关于安装 Octopus 的信息。

Octopus 4.1 目前处于测试阶段,所以如果现在还不能从下载页面获得,那也很快了。看好这个空间!

安装 WildFly

在这个例子中,我们将为 WildFly 安装 Hawtio 管理应用程序。特别是,我们将安装 io.hawt:hawtio-no-slf4j 包。

所以下载一个 WildFly 11 的副本,用一个可以被 Octopus 使用的 admin 用户配置它。

配置 Maven 中央存储库

下一步是将 Maven central 设置为外部提要。更多细节可以参考文档,或者这个的博文

添加应用程序用户

我们将在部署期间更新 Hawtio WEB-INF/web.xml文件,以启用身份验证。但是在我们这样做之前,WildFly 需要配置 Hawtio 用户登录的凭证。

运行bin/add-user.sh(适用于 Linux 和 Mac)或bin\add-user.bat(适用于 Windows)脚本,将新的Application User添加到admin组。在下面的例子中,我们创建了一个名为monitor的用户。

PS C:\wildfly11_standalone\wildfly-11.0.0.Beta1\bin> .\add-user.bat
JAVA_HOME is not set. Unexpected results may occur.
Set JAVA_HOME to the directory of your local JDK to avoid this message.

What type of user do you wish to add?
 a) Management User (mgmt-users.properties)
 b) Application User (application-users.properties)
(a): b

Enter the details of the new user to add.
Using realm 'ApplicationRealm' as discovered from the existing property files.
Username : monitor
Password recommendations are listed below. To modify these restrictions edit the add-user.properties configuration file.
 - The password should be different from the username
 - The password should not be one of the following restricted values {root, admin, administrator}
 - The password should contain at least 8 characters, 1 alphabetic character(s), 1 digit(s), 1 non-alphanumeric symbol(s)
Password :
Re-enter Password :
What groups do you want this user to belong to? (Please enter a comma separated list, or leave blank for none)[  ]: admin
About to add user 'monitor' for realm 'ApplicationRealm'
Is this correct yes/no? yes
Added user 'monitor' to file 'C:\wildfly11_standalone\wildfly-11.0.0.Beta1\standalone\configuration\application-users.properties'
Added user 'monitor' to file 'C:\wildfly11_standalone\wildfly-11.0.0.Beta1\domain\configuration\application-users.properties'
Added user 'monitor' with groups admin to file 'C:\wildfly11_standalone\wildfly-11.0.0.Beta1\standalone\configuration\application-roles.properties'
Added user 'monitor' with groups admin to file 'C:\wildfly11_standalone\wildfly-11.0.0.Beta1\domain\configuration\application-roles.properties'
Is this new user going to be used for one AS process to connect to another AS process?
e.g. for a slave host controller connecting to the master or for a Remoting connection for server to server EJB calls.
yes/no? no
Press any key to continue . . . 

部署 Hawtio

我们可以使用Deploy to WildFly or EAP步骤部署 Hawtio,选择外部 Maven 提要,然后部署包io.hawt:hawtio-no-slf4j

在部署过程中,我们希望更新web.xml文件中的三个<env-entry-value>元素。这些值配置了hawtio/authenticationEnabledhawtio/rolehawtio/realmT5 的设置。默认值如下所示。

<web-app 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
          http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
          version="2.4">

  ...

  <env-entry>
    <description>Enable/disable hawtio's authentication filter, value is really a boolean</description>
    <env-entry-name>hawtio/authenticationEnabled</env-entry-name>
    <env-entry-type>java.lang.String</env-entry-type>
    <env-entry-value>false</env-entry-value>
  </env-entry>

  <env-entry>
    <description>Authorized user role, empty string disables authorization</description>
    <env-entry-name>hawtio/role</env-entry-name>
    <env-entry-type>java.lang.String</env-entry-type>
    <env-entry-value></env-entry-value>
  </env-entry>

  <env-entry>
    <description>JAAS realm used to authenticate users</description>
    <env-entry-name>hawtio/realm</env-entry-name>
    <env-entry-type>java.lang.String</env-entry-type>
    <env-entry-value>*</env-entry-value>
  </env-entry>

  ...

</web-app> 

我们希望hawtio/authenticationEnabled<env-entry-value>truehawtio/roleadmin(我们用add-user脚本将用户添加到的同一个组),以及hawtio/realmjboss-web-policy

jboss-web-policy是 WildFly 和 JBoss EAP 中配置的默认领域,它遵从由add-user脚本配置的Application User凭证。你可以阅读文档了解更多细节。

要进行这些更改,我们需要启用Custom deployment scripts功能。

Custom Deployment Scripts

我们将使用一个 C#预部署脚本来更新web.xml文件,然后将其重新打包并部署到 WildFly。

Pre-Deployment Script

该脚本加载了web.xml文件,并使用一些 XPath 语句来查找需要更新的元素。然后保存 XML 文件,更新后的文件将被重新打包和部署。

XPath 语句通过引用元素*[local-name()='env-entry']来避免与名称空间相关的问题。

using System.Xml;
using System.IO;

/*
    Get the location of the web.xml fie
*/
var installation = Octopus.Parameters["Octopus.Action.Package.InstallationDirectoryPath"];
var xmlFile = installation + Path.DirectorySeparatorChar + "WEB-INF" + Path.DirectorySeparatorChar + "web.xml";

/*
    Parse the web.xml file
*/
XmlDocument doc = new XmlDocument();
doc.Load(xmlFile);
XmlNode root = doc.DocumentElement;

/*
    Update the nodes
*/
XmlNode authenticationEnabled = root.SelectSingleNode("*[local-name()='env-entry']/*[local-name()='env-entry-value'][../*[local-name()='env-entry-name'][text()=\"hawtio/authenticationEnabled\"]]");
Console.WriteLine("Existing hawtio/authenticationEnabled InnerText: " + authenticationEnabled?.InnerText);
authenticationEnabled.InnerText = "true";

XmlNode role = root.SelectSingleNode("*[local-name()='env-entry']/*[local-name()='env-entry-value'][../*[local-name()='env-entry-name'][text()=\"hawtio/role\"]]");
Console.WriteLine("Existing hawtio/role InnerText: " + role?.InnerText);
role.InnerText = "admin";

XmlNode realm = root.SelectSingleNode("*[local-name()='env-entry']/*[local-name()='env-entry-value'][../*[local-name()='env-entry-name'][text()=\"hawtio/realm\"]]");
Console.WriteLine("Existing hawtio/realm InnerText: " + realm?.InnerText);
realm.InnerText = "jboss-web-policy";

/*
    Commit the changes
*/
doc.Save(xmlFile); 

定义渠道规则

出于某种原因,Hawtio 的一个旧版本发布到了 Maven central,其版本为 2.0.0。Hawtio 的最新版本其实是 1.5.6。默认情况下,Octopus 会尝试部署最新版本,但在这种情况下,版本号最大的版本不是最新版本。为了解决这个问题,我们可以创建一个通道规则,强制 Octopus 部署版本 1.5.6。

Channel Rule

测试部署

在 WildFly 中配置了 Hawtio 用户并且更新了web.xml文件以要求认证之后,我们可以通过打开http://server:8080/haw TiO来完成部署并检查结果。您将看到登录页面。

Hawtio Login

使用我们之前创建的monitor用户登录后,将显示欢迎页面。

Hawtio Welcome

结论

通过一些简单的 C#脚本,我们可以对 XML 文件进行任何我们无法控制的修改。这是由 Octopus 部署时可以应用于 Java 应用程序的强大定制的一个例子。

如果您对 Java 应用程序的自动化部署感兴趣,下载 Octopus Deploy 的试用版,并查看我们的文档

将 AWS 帐户连接到 Octopus Deploy - Octopus Deploy

原文:https://octopus.com/blog/connect-an-aws-account-to-octopus

Connect an AWS Account to Octopus Deploy

当您与任何云提供商合作时,您都希望有一种连接到云的简单方法。您不希望仅仅为了部署代码或构建持续交付的基础设施而担心创建定制脚本、API 调用和管道胶带解决方案。

Octopus Deploy 有一种干净、直接的方式来连接许多云提供商。在这篇博文中,您将了解如何将 Octopus Deploy 连接到 AWS。

先决条件

要跟进这篇博文,您需要具备:

  • AWS 账户。
  • Octopus 部署服务器,可以是 Octopus 服务器的本地实例,也可以是 Octopus 云实例。

你可以从八达通服务器或八达通云免费开始使用。

创建 IAM 用户

在从 Octopus Deploy 部署到 AWS 之前,您需要一种身份验证方法。因为 Octopus Deploy 将向 AWS 部署基础设施或应用程序,AWS 需要知道Octopus Deploy是谁。AWS 的典型认证方法是身份和访问管理( IAM ),它提供访问密钥和秘密。

在 UI 中创建 IAM 用户

  1. 要在 AWS UI 中创建 IAM 用户,请打开 web 浏览器并进入 AWS 控制台:

AWS console

  1. 在搜索栏的查找服务下,输入 IAM
  2. 点击权限管理下的用户选项。
  3. 要创建可以从 Octopus Deploy 访问 AWS 的新用户,请单击蓝色的添加用户按钮。
  4. 在“设置用户详细信息”下,为用户名创建一个适当的名称。比如OctopusDeployAccount
  5. 选择 AWS 访问类型部分,选择编程访问选项。Octopus Deploy 将在 SDK 级别对 AWS 进行 API 调用。
  6. 点击蓝色下一步:权限按钮。

用户的权限将取决于您希望 Octopus Deploy 拥有哪些 AWS 服务的权限。例如,假设您希望 Octopus Deploy 只部署 EC2 实例,在这种情况下,您可以让 IAM 用户访问类似于AmazonEC2FullAccess的内容。

  1. 出于这篇博文的目的,我们希望 Octopus 与所有 AWS 服务通信,因此我们将选择直接附加现有策略下的AdministratorAccess策略。选择AdministratorAccess选项后,点击蓝色的 Next: Tags 按钮,如下图所示。

  2. 标签对于这篇博文来说并不是必须的,所以你可以点击蓝色的 Next: Review 按钮。

  3. 最后,要创建新的 IAM 用户,单击蓝色的创建用户按钮。

您将看到一个包含访问密钥 ID 和秘密访问密钥的屏幕。将秘密访问密钥保存在安全的位置,因为您将无法再次访问它。但是,如果丢失了这个密钥,您可以创建一个新的秘密访问密钥。访问密钥 ID 和秘密访问密钥将用于 Octopus Deploy 身份验证。

Successfully added an IAM user

CLI 上的 IAM 用户

正如您在上一节中看到的,创建一个 IAM 用户并将其添加到适当的策略可能有点麻烦,并且需要大量的点击操作。如果你使用 AWS CLI,有一个简单得多的方法,只需要几行代码。

第一段代码创建了新的 IAM 用户:

aws iam create-user --user-name OctopusDeployAWSAccount 

输出应该类似于下面的屏幕截图:

AWS CLI create-user output

接下来,您需要创建秘密访问密钥。秘密访问密钥充当各种类型的密码

要创建秘密访问密钥,请运行以下代码:

aws iam create-access-key --user-name OctopusDeployAccount 

输出应该类似于下面的屏幕截图:

AWS CLI create-access-key output

现在,您已经准备好将 AWS IAM 帐户连接到 Octopus Deploy。

将 AWS 连接到 Octopus Deploy

在上一节中,您了解了如何在编程级别创建让 Octopus Deploy 与 AWS 交互的能力。既然您已经理解了访问密钥和秘密密钥的用途,那么是时候在 Octopus Deploy 中设置 AWS 帐户了。

  1. 打开网络浏览器,进入 Octopus Deploy 门户网站:

Octopus Web Portal

  1. 导航至 基础设施➜账户 以设置新的 AWS 账户。
  2. 账户下,点击绿色添加账户按钮,选择 AWS 账户:

  1. 详情下,您可以添加关于您的账户的元数据;名称和描述。
  2. 接下来,在凭证下,您可以添加 AWS 访问密钥和秘密密钥。
  3. 最后,您可以在“限制”部分设置限制。例如,我选择只允许我的开发环境使用这个帐户。
  4. 点击页面顶部的绿色保存按钮。

恭喜你。您已经成功地在八达通部署中设置了一个 AWS 帐户。

结论

与不同的基于云的平台进行交互的需求不会消失。无论您使用什么样的持续交付和部署工具,从 CD 工具到云甚至本地平台的身份验证总是有原因的。

愉快的部署!

构建构建信息- Octopus 部署

原文:https://octopus.com/blog/constructing-build-information

构建服务器传统上是一种内部工具,但是,许多组织决定将他们的构建卸载到云上。

Azure DevOps、Jenkins (CloudBees)以及最近的 TeamCity 等大牌公司都创建了他们流行的构建平台的云版本。像 AppVeyor、Travis CI、Circle CI、GitHub Actions 和 GitLab 这样的纯在线技术也越来越受欢迎。

对于已经开发了插件或集成的技术,推送构建信息就像将任务添加到流程中一样简单。

对于那些唯一的选择是集成 Octopus CLI(运行时或容器)的技术,让提交信息显示出来可能会令人困惑。

在这篇文章中,我演示了如何构建将构建信息推送到 Octopus Deploy 所需的文件。

GitLab

在这篇文章中,我使用 GitLab 作为构建服务器,因为我以前没有使用过它。

这篇文章关注的是生成构建信息并将其上传到 Octopus Deploy 的单一任务。它不包括使用 GitLab 构建应用程序。

变量

在开始构建定义之前,您需要创建在流程中使用的变量:

  • GitLab 个人访问令牌
  • Octopus 部署 API 密钥
  • Octopus 部署服务器 URL
  • Octopus 部署空间名称

GitLab 个人访问令牌

为了收集您正在执行的构建的提交,您需要对 GitLab 进行 API 调用。API 端点受到保护,需要访问令牌才能成功调用它。

创建个人访问令牌

要创建访问令牌,请单击右上角的您的个人资料,然后选择编辑个人资料

GitLab dashboard showing profile menu open and Edit profile highlighted in the drop down.

点击左侧菜单中的访问令牌。给你的令牌起个名字,至少要有 read_api 权限。

截止日期是可选的。将其留空会创建一个永不过期的令牌。

点击创建个人访问令牌。当令牌显示时,将其存储在安全的地方-该值仅显示一次。

【T2 GitLab dashboard open on Access Tokens page with Token name field and read_api fields highlighted.

为个人访问令牌创建变量

获得令牌后,导航回项目并单击设置,然后单击 CI/CD

GitLab dashboard drop down menu open, with Settings highlighted and Settings drop down menu with CI?CD highlighted.

滚动到变量部分,点击展开

GitLab dashboard open with the Expand button highlighted in the Variables section.

点击添加变量并填写详细信息。对于这个例子,我使用GITLAB_PAT作为和我们上面为生成的令牌。

勾选掩码变量选项,确保令牌不会在构建期间显示在任何消息中。

GitLab dashboard open on the Add variable section with Mask variable tick box selected and highlighted.

章鱼变量

对 Octopus 变量重复上面的添加变量过程。这篇文章假设你熟悉创建一个 API 键:

  • Octopus 部署 API 密钥
  • Octopus 部署服务器 URL
  • Octopus 部署空间名称

建设 YAML

对于 GitLab,构建是使用 YAML 在一个特殊的文件中定义的,.gitlab-ci.yml位于您的库的根目录中。您的流程将包括两个阶段:

  • 建筑信息
  • 推送-构建-信息

建筑信息

构建信息阶段包括构建用于上传构建信息的文件。我用 PowerShell Core 来构建文件,Ubuntu 默认没有。我没有安装 PowerShell Core,而是使用了 GitLab Docker runner 功能。镜像mcr.microsoft.com/dotnet/core/sdk:3.1是安装了 PowerShell 核心的。

build-information:
    stage: build-information
    image: mcr.microsoft.com/dotnet/core/sdk:3.1 
GitLab 提交 API

提交 API 需要您之前作为变量创建的访问令牌。这个令牌需要作为流程中 API 调用的头提供。这些变量可以在代码中以环境变量的形式访问。变量GITLAB_PAT是我们创建的变量,而CI_PROJECT_ID是 GitLab 预定义的

$headers = @{ "PRIVATE-TOKEN" = $env:GITLAB_PAT}

# Get commits from GitLab
$commits = (Invoke-RestMethod -Method Get -Uri "https://gitlab.com/api/v4/projects/$($env:CI_PROJECT_ID)/repository/commits?first_parent=true" -Headers $headers) 
构建信息对象

为了存储构建信息,创建一个 PowerShell 哈希表对象。Commits部分被定义为哈希表中的一个数组:

$jsonPayload = @{
    PackageId = "OctoPetShop.Web"
    Version = "1.0.21132.111113"
    Branch = $env:CI_COMMIT_BRANCH
    BuildUrl = $env:CI_JOB_URL
    BuildNumber = $env:CI_JOB_ID
    BuildEnvironment = "GitLabCI"
    VcsCommitNumber = $env:CI_COMMIT_SHA
    VcsType = "Git"
    VcsRoot = $env:CI_PROJECT_URL
    Commits = @()
} 

接下来,遍历 commits API 调用的结果,并将它们添加到数组中:

foreach ($commit in $commits)
{
    $commitInfo = @{
        Id = $commit.id
        LinkUrl = $commit.web_url
        Comment = $commit.message
    }
    $jsonPayload.Commits += $commitInfo
} 

最后,将 PowerShell 哈希表转换为 JSON 字符串,并将其写入文件:

Add-Content -Path "BuildInformation.json" -Value "$($jsonPayload | ConvertTo-JSON -Depth 10)" 

您需要将该文件作为一个工件包含进来,以便在以后的过程中使用。为此,在舞台 YAML 中加入一个artifacts组件:

artifacts:
    paths: [ BuildInformation.json ] 

推送-构建-信息

将构建信息推送到 Octopus 由脚本中的一个命令组成,并使用前一阶段创建的BuildInformation.json:

push-build-information:
    stage: push-build-information
    image: octopuslabs/gitlab-octocli
    script:
        - octo build-information --package-id=OctoPetShop.Web --version=1.0.21132.111113 --file=BuildInformation.json --server="$OCTOPUS_SERVER_URL" --apiKey="$OCTOPUS_API_KEY" --space="$OCTOPUS_SPACE_NAME" 

。gitlab-ci.yml 文件

完成后,你的 YAML 应该看起来像这样:

image: ubuntu:latest

stages:
    - build-information
    - push-build-information

build-information:
    stage: build-information
    image: mcr.microsoft.com/dotnet/core/sdk:3.1
    script:
        - |
          pwsh -c '$headers = @{ "PRIVATE-TOKEN" = $env:GITLAB_PAT}

          # Get commits from GitLab
          $commits = (Invoke-RestMethod -Method Get -Uri "https://gitlab.com/api/v4/projects/$($env:CI_PROJECT_ID)/repository/commits?first_parent=true" -Headers $headers)

          # Create payload
          $jsonPayload = @{
            PackageId = "OctoPetShop.Web"
            Version = "1.0.21132.111113"
            Branch = $env:CI_COMMIT_BRANCH
            BuildUrl = $env:CI_PIPELINE_URL
            BuildNumber = $env:CI_PIPELINE_ID
            BuildEnvironment = "GitLabCI"
            VcsCommitNumber = $env:CI_COMMIT_SHA
            VcsType = "Git"
            VcsRoot = $env:CI_PROJECT_URL
            Commits = @()
          }

          # Loop through commits and add to collection
          foreach ($commit in $commits)
          {
            $commitInfo = @{
              Id = $commit.id
              LinkUrl = $commit.web_url
              Comment = $commit.message
            }
            $jsonPayload.Commits += $commitInfo
          }

          # Write information to file
          Add-Content -Path "BuildInformation.json" -Value "$($jsonPayload | ConvertTo-JSON -Depth 10)"'
    artifacts:
      paths: [ BuildInformation.json ]

push-build-information:
    stage: push-build-information
    image: octopuslabs/gitlab-octocli
    script:
        - octo build-information --package-id=OctoPetShop.Web --version=1.0.21132.111113 --file=BuildInformation.json --server="$OCTOPUS_SERVER_URL" --apiKey="$OCTOPUS_API_KEY" --space="$OCTOPUS_SPACE_NAME" 

执行构建

在构建被触发后,您将看到类似这样的内容(为了简洁起见,图片显示了日志的最后一部分):

构建-信息 GitLab build information log

推-建-信息 GitLab build information log

导航到 OctoPetShop 的构建信息。Web 在 Octopus Deploy 中,您可以看到您的构建信息已经上传。

Octopus Build Information Repository dashboard showing build information for OctoPetShop.Web with list of commits.

结论

在这篇文章中,我演示了如何为构建信息构建文件,以及如何使用 Octopus Deploy CLI 上传它。

愉快的部署!

超越 Hello World:容器化现实世界的 web 应用程序——Octopus Deploy

原文:https://octopus.com/blog/containerize-a-real-world-web-app

Containerize a real-world web application

Docker 容器和 Kubernetes 是您 DevOps 工具箱中的优秀技术。这个超越 Hello World 博客系列涵盖了如何在现实应用中使用它们。


设计在容器中运行的应用程序已经变得非常流行,但是从哪里开始呢,如何使现有的应用程序与容器兼容呢?

在这篇文章中,我的目标是揭开容器化应用程序的含义。

什么是集装箱,什么是集装箱化?

与虚拟机(VM)类似,容器有自己的 RAM、CPU 和文件系统。然而,容器的许多基本功能依赖于主机操作系统(OS ),这使它们变得轻量级和可移植。当虚拟机需要安装自己的操作系统和应用程序的所有专用组件时,容器会将应用程序运行所需的组件捆绑到所谓的image中。这些图像是完全独立且不可变的,这意味着在它们的生命周期中不能被修改。如果需要对容器进行更新,任何正在运行的实例都必须在被容器的新版本替换之前销毁。如果容器需要保留任何数据(如数据库),这可能是一个问题;但是,有一些方法可以在容器被销毁时保存数据。

Docker 是什么,它与虚拟机相比如何?

最流行的容器技术是 Docker。Docker 是安装在 Windows 或 Linux 上的引擎,它使用操作系统级虚拟化来运行容器。在撰写本文时,容器是为 Windows 或 Linux 构建的,并且不是跨平台的。

为了说明虚拟机和容器之间的区别,请考虑下面的图表。首先,让我们看看虚拟机(管理程序)架构。

注意:虚拟机管理程序是一种运行 Windows Hyper-V 或 VMWare ESXi 等虚拟机的技术。

Core concepts of running an application in with virtual machines

在上图中,每个虚拟机和虚拟机管理程序都有自己的操作系统,在某种程度上独立工作(除了虚拟机需要虚拟机管理程序才能运行之外)。)然后将应用程序部署到虚拟机,并使用虚拟硬件(网络、RAM、CPU 等)提供服务...

有了 Docker,就不再需要虚拟机管理程序,容器通过 Docker 引擎直接在主机操作系统之外运行:

Core concepts of running an application with Docker

Docker 桌面

Docker 桌面是一个免费的工具,你可以从 Docker 下载,用于本地开发。Docker Desktop 在您的本地机器上创建一个 VM,您可以使用它从您的主机(即 Docker 主机)与 Docker 引擎进行交互。除了 Docker 引擎,Docker 桌面还允许您:

  • 在 Windows 和 Linux 容器之间切换。
  • 运行 Docker 编写。
  • 运行本地版本的 Kubernetes。

创建 Dockerfile 文件

Docker 文件包含 Docker 构建容器映像所需的指令。在大多数情况下,映像是从基本映像构建的,包含运行容器所需的最少组件,如。NET Core SDK。

让我们以 OctoPetShop 为例。OctoPetShop 是一个用。NET 核心,包含三个主要组件:

  • 一个 web 前端。
  • 产品网络服务。
  • 购物车 web 服务。

它也使用一个数据库,但是我们将在这篇文章的后面讨论。

为了将 OctoPetShop 前端构建为一个容器,我们定义了一个 docker 文件,如下所示:

FROM mcr.microsoft.com/dotnet/core/sdk:2.1

RUN mkdir /src
WORKDIR /src
ADD . /src
RUN dotnet restore
RUN ["dotnet", "build", "--configuration", "release"]

EXPOSE 5000
EXPOSE 5001

ENV ASPNETCORE_URLS="http://+:5000;https://+:5001"
ENV ASPNETCORE_ENVIRONMENT="Production"

ENTRYPOINT [ "dotnet", "run", "--no-launch-profile" ] 

值得注意的是,docker 文件中的每一行都构建了一个新的映像,使用前一个命令的映像作为基础。

让我们仔细看看 dockerfile 示例中的每一行。

FROM mcr.microsoft.com/dotnet/core/sdk:2.1 

dockerfile 示例的FROM部分告诉 Docker 什么是基本图像。对于 OctoPetShop 前端(以及产品服务、购物车服务和数据库),基本映像是包含。NET Core SDK。这些基本映像是从公共存储库 Docker Hub 下载的。在构建 Docker 映像时,Docker 首先将基础映像下载到磁盘,然后缓存它。

图像名称的第一部分mcr.microsoft.com,是图像所属的存储库的用户名。下一部分/dotnet/core/是存储库内的文件夹路径,SDK 映像驻留在这里。最后一部分,:2.1,是图像 SDK 的标签名。这个标签是 SDK 图像与其他具有相同名称和位置的图像的区别。

RUN mkdir /src 

RUN是我们告诉 Docker 执行的指令。对于这一行,我们告诉 Docker 创建一个名为 src 的新目录(mkdir)。

WORKDIR 

RUN一样,WORKDIR是另一个指令。WORKDIR设置运行其他命令的工作目录。

ADD . /SRC 

ADD指令将文件和文件夹复制到容器映像中。在这一行,我们指示 Docker 将当前目录中的所有文件和文件夹复制到我们之前创建的/src 目录中。

RUN dotnet restore 

这条指令运行dotnet restore命令,它将下载我们的应用程序构建所需的任何缺失的 NuGet 引用。

RUN ["dotnet", "build", "--configuration", "release"] 

任何需要多个参数的指令都要求将参数放在数组中。这里我们运行dotnet build命令,它在图像本身中编译我们的应用程序。

EXPOSE 5000 and EXPOSE 5001 

EXPOSE指令用于打开集装箱的端口。对于 OctoPetShop web 前端,我们开放端口 5000 和 5001。

ENV ASPNETCORE_URLS="http://+:5000;https://+:5001" and ENV ASPNETCORE_ENVIRONMENT="Production" 

ENV环境变量的简称。我们需要告诉我们的 Kestrel 服务器监听哪个地址/端口。使用环境变量ASPNETCORE_URLS可以覆盖地址/端口。我们也可以用ASPNETCORE_ENVIRONMENT覆盖环境名。

ENTRYPOINT [ "dotnet", "run", "--no-launch-profile" ] 

ENTRYPOINT命令在容器启动时运行。就像我们的RUN命令一样,如果命令需要多个参数,它们需要封装在一个数组中。

entry point vs CMD
Docker 还有一个类似于ENTRYPOINT的命令叫做CMD经常会引起混淆。ENTRYPOINT配置一个容器作为可执行文件运行,而CMD设置默认命令和/或参数,当容器运行时,可以从命令行覆盖这些命令和/或参数。

使用 Docker build 将您的 web 应用程序构建为映像

将应用程序构建成映像的命令是docker build。当您运行 Docker 构建时,您需要告诉 Docker Docker 文件在哪里。如果 dockerfile 存在于当前目录中,那么运行构建命令docker build .。用 Docker 用户名/应用程序名来标记您的构建是一种常见的做法。要标记您的构建,只需将-t username/application name添加到构建命令中:

docker build . -t octopussamples/octopetshop-web 

当对 OctoPetShop 前端发出 Docker build 命令时,您将收到以下输出(GUIDs 会有所不同):

Sending build context to Docker daemon  4.439MB
Step 1/11 : FROM mcr.microsoft.com/dotnet/core/sdk:2.1
 ---> bf77a711b92c
Step 2/11 : RUN mkdir /src    
 ---> Using cache
 ---> e590281bfd90
Step 3/11 : WORKDIR /src      
 ---> Using cache
 ---> 3a5646783c8c
Step 4/11 : ADD . /src        
 ---> Using cache
 ---> 5ca49131b227
Step 5/11 : RUN dotnet restore
 ---> Using cache
 ---> 2c231113eef0
Step 6/11 : RUN ["dotnet", "build", "--configuration", "release"]
 ---> Using cache
 ---> a2bbe5911620
Step 7/11 : EXPOSE 5000
 ---> Using cache
 ---> 759d97686c25
Step 8/11 : EXPOSE 5001
 ---> Using cache
 ---> d973bb954156
Step 9/11 : ENV ASPNETCORE_URLS="http://+:5000;https://+:5001"
 ---> Using cache
 ---> f32adcc6f8a1
Step 10/11 : ENV ASPNETCORE_ENVIRONMENT="Production"
 ---> Using cache
 ---> 3168d6f82375
Step 11/11 : ENTRYPOINT [ "dotnet", "run", "--no-launch-profile" ]
 ---> Using cache
 ---> fc176971f626
Successfully built fc176971f626
Successfully tagged octopussamples/octopetshop-web:latest 

我们刚刚成功地集装箱化了 OctoPetShop 前端!

使用 Docker build 将 web 服务和数据库项目构建为 Docker 映像

OctoPetShop 包含两个 web 服务和一个数据库项目,我们需要将它们打包成 Docker 图像。封装这些组件的过程反映了前端的过程,您可以在 OctoPetShop repo 中找到 dockerfile 示例文件。

数据库项目使用 DbUp 并包含脚本(数据库迁移)来创建我们的数据库并为其播种数据。唯一缺少的是数据库服务器。幸运的是,微软为 SQL Server 2017 制作了一个容器映像:microsoft/mssql-server-linux:2017-latest

使用 Docker run 运行您的容器化应用程序

现在我们已经将所有组件整齐地放入容器中,我们需要让它们启动并运行。为了启动我们的容器,我们使用了docker run <image>命令。使用EXPOSE指令打开的任何端口都需要映射到主机端口,以便容器可以访问。这是使用 Docker run 命令的-p开关完成的,如果需要为一个容器映射多个端口,可以多次指定。-e开关将把环境变量传递给容器。

我们的 OctoPetShop web 前端需要知道产品服务和购物车服务的后端服务的地址。这些值存储在应用程序的 appsettings.json 文件中;但是,我们已经对应用程序进行了编码,以便在环境变量存在时覆盖这些变量。

为了让我们的整个解决方案正常工作(包括将数据库服务器作为容器运行),我们运行以下命令:

docker run -p 1433:1433 -e SA_PASSWORD="SomeGoodPassword" -e ACCEPT_EULA="Y" -d microsoft/mssql-server-linux:2017-latest

docker run -p 5010:5000 -p 5001:5001 -d -e ProductServiceBaseUrl=http://localhost:5011 -e ShoppingCartServiceBaseUrl=http://localhost:5012 -d octopussamples/octopetshop-web

docker run -p 5011:5011 -e OPSConnectionString="Data Source=172.17.0.2;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword" -d octopussamples/octopetshop-productservice

docker run -p 5012:5012 -e OPSConnectionString="Data Source=172.17.0.2;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword" -d octopussamples/octopetshop-shoppingcartservice

docker run -e DbUpConnectionString="Data Source=172.17.0.2;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword" -d octopussamples/octopetshop-database 

172.17.0.2的 IP 地址是 SQL Server 容器被分配的地址。

随着容器的运行,我们可以导航到 http://localhost:5000。OctoPetShop 自动重定向到 https 地址(使用端口 5001),并使用自签名证书。你很可能会得到一个警告,说它不安全。在这种情况下,我们可以安全地忽略警告。当页面加载时,您应该会看到:

【T2

使用 Docker compose 运行您的容器化应用程序

一个接一个地运行 Docker 命令会变得非常乏味。为了解决这个问题,Docker 创建了 Docker compose。通过一个 Docker compose YAML 文件,您可以构建所有的容器,设置它们的端口,创建一个供它们使用的本地网络,并为每个容器定义环境变量。在下面的 YAML 代码中,我们设置了所有的容器,类似于上面的 Docker run 命令。我们没有将主机端口映射到容器端口,而是创建了一个名为container_net的 Docker 网络。对于 container_net 网络,需要映射到主机的唯一端口是 web 前端端口(5000 和 5001),其余端口只能由其他容器访问:

version: '3'
services:
  sql-server:
    container_name: sql-server-db
    image: mcr.microsoft.com/mssql/server:2019-latest
    ports:
      - "1433:1433"
    environment:
      SA_PASSWORD: "SomeGoodPassword"
      ACCEPT_EULA: "Y"
  productservice:
    environment:
      - OPSConnectionString=Data Source=sql-server;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword
    build:
      dockerfile: dockerfile
      context: ./OctopusSamples.OctoPetshop.Productservice
    ports:
      - '5011:5011'
      - '5014:5014'
    depends_on: 
      - "database"
  octopetshop:
    environment:
      - ProductServiceBaseUrl=http://productservice:5011/
      - ShoppingCartServiceBaseUrl=http:/shoppingcartservice:5012
    build:
      dockerfile: dockerfile
      context: ./OctopusSamples.OctoPetShop.Web
    ports:
      - '5000:5000'
      - '5001:5001'
    depends_on: 
      - "shoppingcartservice"
      - "productservice"
  shoppingcartservice:
    environment:
      - OPSConnectionString=Data Source=sql-server;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword
    build:
      dockerfile: dockerfile
      context: ./OctopusSamples.OctoPetShop.ShoppingCartService
    ports:
      - '5012:5012'
      - '5013:5013'
    depends_on: 
      - "database"
  database:
    environment:
      - DbUpConnectionString=Data Source=sql-server;Initial Catalog=OctoPetShop; User ID=sa; Password=SomeGoodPassword
    build:
      dockerfile: dockerfile
      context: ./OctopusSamples.OctoPetShop.Database
    depends_on: 
      - "sql-server" 

与之前对每个容器使用 Docker run 的方法不同,我们可以通过运行docker-compose up来启动我们的整个解决方案,这需要更少的输入。运行 Docker compose 还会向我们显示容器运行时的输出:

Starting octopetshop_database_1            ... done                                                                     Starting octopetshop_octopetshop_1         ... done                                                                     Recreating sql-server-db                   ... done                                                                     Starting octopetshop_shoppingcartservice_1 ... done                                                                     Starting octopetshop_productservice_1      ... done                                                                     Attaching to octopetshop_octopetshop_1, octopetshop_shoppingcartservice_1, octopetshop_productservice_1, sql-server-db, octopetshop_database_1
database_1             | Master ConnectionString => Data Source=192.168.1.4;Initial Catalog=master;User ID=sa;Password=********************
shoppingcartservice_1  | Hosting environment: Production
shoppingcartservice_1  | Content root path: /src
shoppingcartservice_1  | Now listening on: http://[::]:5012
shoppingcartservice_1  | Now listening on: https://[::]:5013
shoppingcartservice_1  | Application started. Press Ctrl+C to shut down.
octopetshop_1          | Hosting environment: Production
octopetshop_1          | Content root path: /src
octopetshop_1          | Now listening on: http://[::]:5000
octopetshop_1          | Now listening on: https://[::]:5001
octopetshop_1          | Application started. Press Ctrl+C to shut down.
productservice_1       | Hosting environment: Production
productservice_1       | Content root path: /src
productservice_1       | Now listening on: http://[::]:5011
productservice_1       | Now listening on: https://[::]:5014
productservice_1       | Application started. Press Ctrl+C to shut down.
sql-server-db          | 2019-11-07 11:44:15.07 Server      Setup step is copying system data file 'C:\templatedata\master.mdf' to '/var/opt/mssql/data/master.mdf'.
2019-11-07 11:44:15.14 Server      Did not find an existing master data file /var/opt/mssql/data/master.mdf, copying the missing default master and other system database files. If you have moved the database location, but not moved the database files, startup may fail. To repair: shutdown SQL Server, move the master database to configured location, and restart.
2019-11-07 11:44:15.14 Server      Setup step is copying system data file 'C:\templatedata\mastlog.ldf' to '/var/opt/mssql/data/mastlog.ldf'.
2019-11-07 11:44:15.15 Server      Setup step is copying system data file 'C:\templatedata\model.mdf' to '/var/opt/mssql/data/model.mdf'.
2019-11-07 11:44:15.16 Server      Setup step is copying system data file 'C:\templatedata\modellog.ldf' to '/var/opt/mssql/data/modellog.ldf'.
2019-11-07 11:44:15.17 Server      Setup step is copying system data file 'C:\templatedata\msdbdata.mdf' to '/var/opt/mssql/data/msdbdata.mdf'.
2019-11-07 11:44:15.21 Server      Setup step is copying system data file 'C:\templatedata\msdblog.ldf' to '/var/opt/mssql/data/msdblog.ldf'.
2019-11-07 11:44:15.29 Server      Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64)
        Nov 30 2018 12:57:58
        Copyright (C) 2017 Microsoft Corporation
        Developer Edition (64-bit) on Linux (Ubuntu 16.04.5 LTS)
2019-11-07 11:44:15.29 Server      UTC adjustment: 0:00
2019-11-07 11:44:15.29 Server      (c) Microsoft Corporation.
2019-11-07 11:44:15.29 Server      All rights reserved.
2019-11-07 11:44:15.29 Server      Server process ID is 4120.
2019-11-07 11:44:15.30 Server      Logging SQL Server messages in file '/var/opt/mssql/log/errorlog'.
2019-11-07 11:44:15.30 Server      Registry startup parameters:
         -d /var/opt/mssql/data/master.mdf
         -l /var/opt/mssql/data/mastlog.ldf
         -e /var/opt/mssql/log/errorlog
2019-11-07 11:44:15.31 Server      SQL Server detected 1 sockets with 1 cores per socket and 2 logical processors per socket, 2 total logical processors; using 2 logical processors based on SQL Server licensing. This is an informational message; no user action is required.
2019-11-07 11:44:15.31 Server      SQL Server is starting at normal priority base (=7). This is an informational message only. No user action is required.
2019-11-07 11:44:15.31 Server      Detected 1600 MB of RAM. This is an informational message; no user action is required.
2019-11-07 11:44:15.31 Server      Using conventional memory in the memory manager.
2019-11-07 11:44:15.46 Server      Buffer pool extension is already disabled. No action is necessary.
2019-11-07 11:44:15.61 Server      InitializeExternalUserGroupSid failed. Implied authentication will be disabled.
2019-11-07 11:44:15.62 Server      Implied authentication manager initialization failed. Implied authentication will be disabled.
2019-11-07 11:44:15.63 Server      Successfully initialized the TLS configuration. Allowed TLS protocol versions are ['1.0 1.1 1.2']. Allowed TLS ciphers are ['ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!DHE-RSA-AES256-GCM-SHA384:!DHE-RSA-AES128-GCM-SHA256:!DHE-RSA-AES256-SHA:!DHE-RSA-AES128-SHA'].
2019-11-07 11:44:15.66 Server      The maximum number of dedicated administrator connections for this instance is '1'
2019-11-07 11:44:15.66 Server      Node configuration: node 0: CPU mask: 0x0000000000000003:0 Active CPU mask: 0x0000000000000003:0\. This message provides a description of the NUMA configuration for this computer. This is an informational message only. No user action is required.
2019-11-07 11:44:15.67 Server      Using dynamic lock allocation.  Initial allocation of 2500 Lock blocks and 5000 Lock Owner blocks per node.  This is an informational message only.  No user action is required.
2019-11-07 11:44:15.68 Server      In-Memory OLTP initialized on lowend machine.
2019-11-07 11:44:15.75 Server      Database Instant File Initialization: enabled. For security and performance considerations see the topic 'Database Instant File Initialization' in SQL Server Books Online. This is an informational message only. No user action is required.
ForceFlush is enabled for this instance.
2019-11-07 11:44:15.76 Server      Query Store settings initialized with enabled = 1,
2019-11-07 11:44:15.77 Server      Software Usage Metrics is disabled.
2019-11-07 11:44:15.78 spid7s      Starting up database 'master'.
ForceFlush feature is enabled for log durability.
2019-11-07 11:44:15.95 spid7s      The tail of the log for database master is being rewritten to match the new sector size of 4096 bytes.  3072 bytes at offset 418816 in file /var/opt/mssql/data/mastlog.ldf will be written.
2019-11-07 11:44:16.13 spid7s      Converting database 'master' from version 862 to the current version 869.
2019-11-07 11:44:16.15 spid7s      Database 'master' running the upgrade step from version 862 to version 863.
2019-11-07 11:44:16.20 spid7s      Database 'master' running the upgrade step from version 863 to version 864.
2019-11-07 11:44:16.28 spid7s      Database 'master' running the upgrade step from version 864 to version 865.
2019-11-07 11:44:16.31 spid7s      Database 'master' running the upgrade step from version 865 to version 866.
2019-11-07 11:44:16.34 spid7s      Database 'master' running the upgrade step from version 866 to version 867.
2019-11-07 11:44:16.36 spid7s      Database 'master' running the upgrade step from version 867 to version 868.
2019-11-07 11:44:16.39 spid7s      Database 'master' running the upgrade step from version 868 to version 869.
2019-11-07 11:44:16.74 spid7s      Resource governor reconfiguration succeeded.
2019-11-07 11:44:16.75 spid7s      SQL Server Audit is starting the audits. This is an informational message. No user action is required.
2019-11-07 11:44:16.77 spid7s      SQL Server Audit has started the audits. This is an informational message. No user action is required.
2019-11-07 11:44:16.84 spid7s      SQL Trace ID 1 was started by login "sa".
2019-11-07 11:44:16.87 spid19s     Password policy update was successful.
2019-11-07 11:44:16.88 spid7s      Server name is '88ca920a03ae'. This is an informational message only. No user action is required.
2019-11-07 11:44:16.89 spid22s     Always On: The availability replica manager is starting. This is an informational message only. No user action is required.
2019-11-07 11:44:16.90 spid7s      Starting up database 'msdb'.
2019-11-07 11:44:16.90 spid22s     Always On: The availability replica manager is waiting for the instance of SQL Server to allow client connections. This is an informational message only. No user action is required.
2019-11-07 11:44:16.91 spid11s     Starting up database 'mssqlsystemresource'.
2019-11-07 11:44:16.91 spid11s     The resource database build version is 14.00.3048\. This is an informational message only. No user action is required.
2019-11-07 11:44:16.93 spid11s     Starting up database 'model'.
2019-11-07 11:44:17.12 spid7s      The tail of the log for database msdb is being rewritten to match the new sector size of 4096 bytes.  512 bytes at offset 306688 in file /var/opt/mssql/data/MSDBLog.ldf will be written.
2019-11-07 11:44:17.20 spid7s      Converting database 'msdb' from version 862 to the current version 869.
2019-11-07 11:44:17.21 spid7s      Database 'msdb' running the upgrade step from version 862 to version 863.
2019-11-07 11:44:17.21 spid19s     A self-generated certificate was successfully loaded for encryption.
2019-11-07 11:44:17.22 spid11s     The tail of the log for database model is being rewritten to match the new sector size of 4096 bytes.  2048 bytes at offset 75776 in file /var/opt/mssql/data/modellog.ldf will be written.
2019-11-07 11:44:17.23 spid19s     Server is listening on [ 'any' <ipv4> 1433].
2019-11-07 11:44:17.24 Server      Server is listening on [ 127.0.0.1 <ipv4> 1434].
2019-11-07 11:44:17.24 Server      Dedicated admin connection support was established for listening locally on port 1434.
2019-11-07 11:44:17.26 spid19s     SQL Server is now ready for client connections. This is an informational message; no user action is required.
2019-11-07 11:44:17.28 spid11s     Converting database 'model' from version 862 to the current version 869.
2019-11-07 11:44:17.29 spid11s     Database 'model' running the upgrade step from version 862 to version 863.
2019-11-07 11:44:17.33 spid7s      Database 'msdb' running the upgrade step from version 863 to version 864.
2019-11-07 11:44:17.38 spid11s     Database 'model' running the upgrade step from version 863 to version 864.
2019-11-07 11:44:17.39 spid7s      Database 'msdb' running the upgrade step from version 864 to version 865.
2019-11-07 11:44:17.41 spid11s     Database 'model' running the upgrade step from version 864 to version 865.
2019-11-07 11:44:17.42 spid7s      Database 'msdb' running the upgrade step from version 865 to version 866.
2019-11-07 11:44:17.44 spid11s     Database 'model' running the upgrade step from version 865 to version 866.
2019-11-07 11:44:17.44 spid7s      Database 'msdb' running the upgrade step from version 866 to version 867.
2019-11-07 11:44:17.47 spid11s     Database 'model' running the upgrade step from version 866 to version 867.
2019-11-07 11:44:17.48 spid7s      Database 'msdb' running the upgrade step from version 867 to version 868.
2019-11-07 11:44:17.52 spid11s     Database 'model' running the upgrade step from version 867 to version 868.
2019-11-07 11:44:17.53 spid7s      Database 'msdb' running the upgrade step from version 868 to version 869.
2019-11-07 11:44:17.59 spid11s     Database 'model' running the upgrade step from version 868 to version 869.
2019-11-07 11:44:17.75 spid11s     Polybase feature disabled.
2019-11-07 11:44:17.75 spid11s     Clearing tempdb database.
2019-11-07 11:44:18.16 spid11s     Starting up database 'tempdb'.
2019-11-07 11:44:18.35 spid11s     The tempdb database has 1 data file(s).
2019-11-07 11:44:18.38 spid22s     The Service Broker endpoint is in disabled or stopped state.
2019-11-07 11:44:18.38 spid22s     The Database Mirroring endpoint is in disabled or stopped state.
2019-11-07 11:44:18.39 spid22s     Service Broker manager has started.
2019-11-07 11:44:18.77 spid51      Starting up database 'OctoPetShop'.
2019-11-07 11:44:18.92 spid51      Parallel redo is started for database 'OctoPetShop' with worker pool size [1].
2019-11-07 11:44:18.94 spid51      Parallel redo is shutdown for database 'OctoPetShop' with worker pool size [1].
database_1             | Created database OctoPetShop
2019-11-07 11:44:19.12 spid7s      Recovery is complete. This is an informational message only. No user action is required.
2019-11-07 11:44:19.14 spid17s     The default language (LCID 0) has been set for engine and full-text services.
database_1             | Beginning database upgrade
database_1             | Checking whether journal table exists..
database_1             | Journal table does not exist
database_1             | Executing Database Server script 'OctopusSamples.OctoPetShop.Database.scripts.0001-create-tables.sql'
database_1             | Checking whether journal table exists..
database_1             | Creating the [SchemaVersions] table
database_1             | The [SchemaVersions] table has been created
database_1             | Executing Database Server script 'OctopusSamples.OctoPetShop.Database.scripts.0002-seed-data.sql'
database_1             | Upgrade successful
database_1             | Success!
octopetshop_database_1 exited with code 0 

将容器并入 CI/CD 管道

到目前为止,我们已经在命令行上做了所有的事情,而不是以任何自动化的方式(除了 Docker compose)。下一个合乎逻辑的步骤是将容器映像的构建和上传交给构建服务器。诸如微软 Azure DevOps 、JetBrains TeamCityJenkins 和 Atlassian Bamboo 等流行的构建服务器都有内置的或可下载插件提供的步骤,这些步骤将构建您的 Docker 映像并将它们推送到存储库。在存储库中,您可以使用 Azure DevOps Pipelines 或 Octopus Deploy 等连续交付软件来自动将映像部署到运行 Docker 引擎或 Kubernetes 集群的机器上。

结论

在我完成为 OctoPetShop 创建容器的练习之前,将应用程序作为容器运行对我来说是非常神奇的。这一经历让我有信心在 Kubernetes 集群中继续运行 OctoPetShop!请继续关注那篇文章。

连续交货。使用 Octopus Deploy 和 Bitbucket 管道的 NET Core-Octopus Deploy

原文:https://octopus.com/blog/continuous-delivery-bitbucket-pipelines

自从这篇文章首次发表以来,我们已经重新命名了 Octo.exe,现在它是 Octopus CLI,更多信息,请参见这篇文章: Octopus release 2020.1

Bitbucket pipelinse and Octopus Deploy

上周,我热爱 PowerShell 的同事 Jason Brown 写了一篇关于使用 Octopus 和 T2 TakoFukku 为 PowerShell 模块建立连续交付渠道的精彩文章。在晚上的时间里,我一直在摆弄 Bitbucket 管道来自动化一些个人项目,所以我想我应该写一篇关于为。净核心项目。

人物简介

Bitbucket 是一个基于 git 的源代码控制平台,由 Atlassian 开发,是 GitHub 的替代产品,提供免费无限的私有回购,因此它非常适合你不想在野外进行的个人项目。

bit bucket Pipelines是 Atlassian 的新产品,它是一个轻量级的云持续集成服务器,使用预配置的 docker 容器。每月有 50 分钟的构建时间是免费的,额外时间的费用非常实惠(在撰写本文时是正确的)。设置一个 CI 服务器并构建在云中运行的代理可能是一个漫长而昂贵的过程,因此这是一个很好的入门方式。

序言:计划

该解决方案有几个移动部分,因此该过程将按如下方式工作:

  1. 新代码被提交到 Bitbucket 上一个被祝福的分支。
  2. 当提交被推送到远程存储库时,位桶管道启动构建脚本。
  3. 构建脚本构建、测试、打包应用程序代码,并将其推送到 Octopus Deploy 服务器。
  4. Octopus 会自动创建一个版本,并将其部署到 Azure(或任何您想部署代码的地方)。

第一幕:设置好一切

我们需要创建一些帐户和服务,设置它们的说明超出了本文的范围。您需要做的关键事情是:

创建一个比特币账户

如果你还没有一个 Bitbucket 账户,那就去 https://bitbucket.org/account/signup/建立一个吧。这是您将设置远程存储库和保存源代码的地方。

创建新的存储库

如果您刚刚创建了一个新的 Bitbucket 帐户,您还应该设置一个可以将源代码放入其中的存储库。Bitbucket UI 相当直观,(但是如果你喜欢具体的说明,你可以阅读这个有用的教程)。一旦你建立了回购,做一个初始提交,从你的本地源代码(或者任何你正在测试的玩具项目)推它,这样你就有一些代码提交了。

在本文中,我将使用一个名为“问候”的玩具项目。Web”,它在一个名为“Greetings”的测试项目中有一个单元测试。测试”。

建立一个八达通服务器

如果你还没有运行的 Octopus 服务器,开始试用并按照我们的安装指南立即开始运行。我正在使用我们全新的章鱼云预览服务器;如果您有兴趣了解我们的最新进展,您可以注册您的兴趣并在我们发布到 RTM 时得到通知。

动作 2:配置管道以在提交时运行构建

CI(持续集成)通常通过将您的构建系统集成到您的源代码控制仓库中来工作,因此当新代码被推送到您的远程仓库时,新的构建会自动启动。我们将使用管道(在 Bitbucket 中)来完成这些构建,所以我们需要创建一个名为bitbucket-pipelines.yml的特殊文件来让 Bitbucket 知道做什么和如何做。

在浏览器中打开您的 Bitbucket 存储库,并选择管道菜单选项;这将把你带到管道界面,有一个有用的图形用户界面来设置你的管道 YAML 文件。因为我将部署一个. Net 核心应用程序,所以我将使用。NET Core 模板(在 More 菜单中)作为起点。有许多模板可以作为其他语言类型的起点,所以请随意选择对您有意义的模板。

如果你想知道为什么没有。NET 模板可用,原因是因为 Pipelines 使用 Linux docker 容器来运行构建步骤。作为。NET 核心是跨平台的,有一个模板供微软使用。网络核心 docker 镜像;在撰写本文时,不支持 Windows 和 Mac/iOS 版本。

在生成的模板中,我们可以调整命令以适应我们的构建环境;每一行都是一个可以在终端运行的命令行。如果您选择了。NET 核心模板,您的文件将如下所示:

.NET Core Pipelines YAML template file

命令

每个关键词在链接到该模板的帮助材料中都有详细的解释,所以请阅读这些文章,以便更广泛地了解 YAML 文件应该如何构建。出于我们的目的,我们正在查看特定的命令本身:

  • export <name>=<value>是设置一个名为<name>(值为<value>)的环境变量的命令,我们可以在后面的文件中引用它。这是一种指定像PROJECT_NAME这样的变量的简便方法,并在构建和测试的多个地方使用该值。
  • dotnet restore是一个. NET 核心命令,用于将 NuGet 包恢复到您的项目中。这确保了您所引用的任何包都可以在本地使用,因此您的生成可以运行(通常包不会提交给源代码管理,因此需要在您的生成代理上还原)。
  • dotnet build <ProjectName>将构建指定的项目;这是使用PROJECT_NAME变量的好地方。关于该命令选项的更多信息,见点网构建参考资料
  • dotnet test <TestProjectName>会使用。NET 测试运行器来运行指定测试项目中的任何单元测试。这也是放置变量的好地方(例如TEST_NAME)。关于该命令选项的更多信息,参见点网测试参考资料

简而言之,这个模板文件将恢复您的 NuGet 包,构建您的项目并运行单元测试。现在,只需修改export命令分别指向您的项目和测试项目名称,并使用#注释掉任何不适用的行。

我已经用一个项目名变量设置了我的,由build命令使用,但是我已经移除了TEST_NAME变量来显示你如何注释掉你不需要的行,并且如果你不需要的话,你不需要使用变量。我完全配置的文件如下所示:

Configured Pipelines YAML file

然后我们只需要点击Commit file,pipelines 文件就会在正确的位置(项目的根目录)提交给你的 repo,你的第一次构建就拉开了序幕。如果一切配置正确,您应该会看到如下屏幕:

A successfully completed Pipelines build

第一次建不起来也不用担心;阅读输出,进行任何修改,并推送您的更改——每次推送都会自动启动一个新的构建。

扩展脚本

现在,您已经愉快地运行了一个基本的管道构建,是时候扩展脚本来打包您构建的代码,并将其推送到 Octopus 进行部署了。

更新 2018/06:自从写了这篇文章后,我们开始将octo.exe发布到一个容器中,以使这个过程更加简单。查看我们的更新博客文章了解详情。

Octopus API 密钥

首先,我们需要为我们的 Octopus 登录创建一个 API 键,这样管道就可以使用 Octopus API 来推送我们的代码包。本文将带您创建一个 API 密匙;请确保您将它保存在某个地方,以便以后检索。

管道敏感变量

API 密匙(以及其他类似的认证令牌)非常强大,因为它们授予持有者做任何你可以做的事情的权利,而不需要密码——所以我们需要保证它们的安全。Pipelines 通过“安全变量”来管理这一点,这些变量就像 Octopus 中的敏感变量一样——一旦你设置了它们,它们就是安全的,没有人可以再把它们复制回来。这对我们的 API 键来说是完美的。

  1. 在 Bitbucket 上打开你的库,打开设置页面,然后在管道部分选择环境变量
  2. 在这个页面上,输入Octopus.ApiKey作为变量名,并将您的 API 键粘贴到 value 框中。勾选Secured,点击Add保存变量。我们现在可以在构建脚本中使用它,就像之前的PROJECT_NAME一样。

扩展构建脚本

现在我们需要调整我们的构建脚本来添加额外的命令——您可以在本地进行这些更改并提交更新后的 YAML 文件,或者您可以使用 web GUI 找到该文件并在浏览器中编辑它。

由于我们正在推进 Octopus 中的内置提要,而不是 NuGet 提要,我们可以只压缩我们发布的项目输出——不需要花哨的 NuGet 包或 nuspec 文件。但是,如果您愿意,您可以修改这些步骤,使用octo.exe pack来生成 NuGet 包。

经过编辑,我们的管道文件的script部分现在看起来如下,并带有解释新命令的注释:

# zip is not installed in the docker image, so we need to update apt-get and then install it so we can create our package later on
- apt-get update
- apt-get install zip -qq

# we use the PROJECT_NAME variable to generate our package name, as well as the in-built BITBUCKET_BUILD_NUMBER, which is the incrementing number of the build. This ensures we get incrementing unique package versions
- export PROJECT_NAME=Greetings.Web
- export VERSION=1.$BITBUCKET_BUILD_NUMBER
- export ZIP_FILE=$PROJECT_NAME.$VERSION.zip

- dotnet restore
- dotnet build $PROJECT_NAME
- dotnet test Greetings.Tests
# this will publish our app output into a subfolder so that we can package it up
- dotnet publish $PROJECT_NAME --output ../publish
# we need to make sure our app is at the root level of the zip file, or Azure will complain about it when we deploy. So we change into that directory
- cd ./publish
# we use the ZIP_FILE variable from above to generate the package
- zip -r $ZIP_FILE * -q

# curl comes installed in the image, so we just need to post the contents of the zip file to our Octopus instance, making sure we add an HTTP Header for our API key. This will be expanded by Pipelines from the environment variable we created previously
- curl -X POST https://cloud-perm.octopus.com/api/packages/raw -H "X-Octopus-ApiKey:$Octopus.ApiKey" -F "data=@$ZIP_FILE" 

确保适当地更改任何变量名/值,并为 Octopus 实例使用正确的 HTTP 端点。

运行时,如果。NET 命令正确运行(即,只要我们的代码编译和测试成功运行),我们的应用程序将被发布、打包并推送到内置的 Octopus feed。通过向你的回购协议提交一份承诺,并观察这一过程的展开,来测试一下。如果一切正常,您应该在 Library -> Packages 下看到一个包,它的版本与成功构建的版本相匹配。

第 3 步:设置 Octopus 自动部署

现在我们有了构建和打包代码的管道,我们有很大的信心没有引入任何错误,所以我们想把它推到一个部署目标——也许是一个可以测试您的更改的阶段环境。然而,我们不希望每次构建完成时都必须手动部署——这简直是胡说八道!

用 Octopus 实现这一自动化的关键包括两个伟大的(但经常被忽视的)特性:

  1. 自动释放创建会按照盒子上说的去做;每当在内置的包提要中检测到与项目名称相匹配的新包时,就会自动创建一个新的版本,并且
  2. 自动部署,当一个版本到达部署的这个阶段时,它将自动部署到一个环境中。

有了这两个便利的特性,我们所需要做的就是向 Octopus 推送一个新的包,它将自动地被包装在一个新的版本中,并被部署到您已经设置好的环境中。多棒啊。!

1.为自动部署创建环境和生命周期

如果我们部署到一个试运行环境来持续测试我们的工作,我们需要一个称为“试运行”的环境——如果您还没有设置,现在就通过 infra structure-> Environments-> Add Environment 创建它。

接下来,我们需要一个我们的项目可以使用的特殊生命周期,通过 Library-> life cycles-> Add life cycle,将我们的新版本自动部署到这个阶段环境中(您也可以在其他项目中使用这个生命周期)。称之为“自动部署到登台”,添加一个称为“登台”的阶段,并将您的Staging环境添加到其中。在弹出窗口中,确保选择了“自动部署到该环境”选项;这就是让一切运转的魔力。

通过环境名称旁边的橙色闪电,您可以知道我们已经正确设置了自动部署:

Lifecycle is set up to auto-deploy

现在,任何使用这个生命周期的项目都会自动部署到Staging中,无论何时创建一个新的版本。在离开此页面之前,请确保保存生命周期。

2.设置您的项目

接下来,我们需要一个新的 Octopus 项目来执行我们需要的部署步骤。项目的进程将取决于您正在部署什么,以及您需要将它部署到哪里,我们的文档中有一个很棒的章节是关于用 Octopus 部署的。对于我的玩具项目,我想将其部署到现有的 Azure web 应用程序,因此我有一个项目,只需一步即可部署到 Azure。

创建项目

通过“项目”->“添加项目”创建一个新项目,为其命名,并确保您展开了高级设置部分,并选择我们在上面创建的新生命周期(请记住,我的生命周期称为“自动部署到暂存”)。如果您正在使用现有项目,您可以在项目设置的“流程”页面下更改生命周期。

设置流程

为了让自动发布创建工作,我们至少需要一个部署包的步骤;这就是 Octopus 如何知道在内置提要中应该关注哪些包。如果你已经设置了一个 Azure 账户,请阅读本文以获取关于设置 Azure 部署步骤的信息。否则,请查阅 Octopus 文档以获得更多关于部署到您的环境的信息。

设置自动发放创建

现在我们已经有了一个建立了流程的项目,并且处于正确的生命周期中,我们需要打开自动发布创建——这是每当检测到一个新包时创建一个新发布的魔法。打开项目的“触发器”页面,点击自动创建发布下右侧的“设置”;您将需要选择它应该使用哪个步骤来评估一个新的包是否应该触发一个发布。选择您在上面创建的步骤,并保存。

创建一个发布并测试流程

最后,使用在您第一次成功构建之后出现的包创建一个发布,一旦您保存了这个发布,由于您之前配置的生命周期设置,它应该会自动部署到 Staging。如果工作正常,那么一切都准备好了!如果有任何错误,请阅读输出日志,查看部署中的错误并修复任何问题。

收场白

是时候尝试一下了!对您的代码进行更改,使其可在您的暂存主机上识别,并将此更改推送到您的 Bitbucket repo。您应该看到 Pipelines 开始构建,一旦成功,Octopus 应该创建一个发布并自动将其部署到您的登台环境中——一切都像变魔术一样!

希望这篇文章能让你为你的小型个人项目采用 CD 管道,以一种成本有效且轻量级的方式扩展到其他项目。如果没有别的,也许它已经激励你采用这个过程的一部分,并给你一些关于未来可能发生的事情的想法。

愉快的部署!

了解更多信息

持续运送八达通-八达通展开

原文:https://octopus.com/blog/continuous-delivery-of-octopus

Versions

每夜构建和持续部署

一段时间以来,我们一直在执行一项长期任务,以增加工作流程,减少我们所有团队的反馈时间。在 2021 年初,我们仍然面临一些关键的制约因素:

  • 我们的部署管道经常遭受“比特腐蚀”。
  • 我们的代码变更经常停留在比我们想要的时间更长的分支上。

截至本周,我们已经交付了一些关键的更改,以帮助我们发布更快、更高质量的版本。

持续部署

我们现在正在练习将 Octopus 服务器、触手和 Octopus CLI 持续部署到内部客户环境,以及将 T2 持续交付到外部客户环境。

这对你意味着什么?

好吧,我们现在从每个提交到通过所有测试的可发布分支中自动创建一个发布,而不是必须做出部署一个发布的深思熟虑的决定。成功的构建部署到我们的内部环境,然后经过一段合适的“烘焙时间”,部署到 Octopus Cloud,然后部署到网站。这意味着我们在追求交付高质量产品的过程中不断“喝自己的香槟”。

您可以阅读更多关于我们如何看待持续交付和持续部署之间差异的信息。

每夜构建

对于我们旧的 LTS 版本,我们发现“比特腐烂”导致我们的管道失败,因为我们只在需要向旧版本发布补丁时才使用它们。现在,除了我们的提交触发构建之外,我们每晚为每个潜在可发布的分支触发一次构建,以清理所有管道,在出现问题时及早提醒我们,这反过来使问题更容易诊断,修复成本更低。

主要版本.次要版本.内部版本

不过,夜间构建确实给我们带来了有趣的挑战。使用我们之前的编号方案(major.minor.patch),每天晚上的重新构建将导致每次 Git 提交的多次构建,并且我们的构建编号只有在提交后才会改变。这意味着我们最终得到了具有相同版本号的多个版本。许多下游系统(如 nuget.org、registry.npmjs.org)不喜欢不同的包有相同的版本号,这意味着我们需要一个新的计划。

我们现在使用一种major.minor.build编号策略。这意味着补丁数量比我们习惯的要大得多,但实际上,它只是一个数字。因为这是一个内部版本号,所以版本号之间会有差距,但是你总是想得到最新的版本号。

顺便说一句,我们沿着这条道路的旅程意味着我们已经偏离了 GitVersion 的设计目标很远,我们最终编写了自己的版本计算器 OctoVersion 。这更好地处理了我们的多个发布流,并且因为它是以我们的用例为中心的,所以它也更快。

使用 Git 修订图代替 GitHub 里程碑,更准确地计算从版本X到版本Y的发行说明

另一个有趣的变化是我们如何生成发行说明。我们以前使用 GitHub 里程碑,并在构建发布之前分配它们。现在,当我们为每一个提交构建一个版本时,我们使用 Git 修订图来计算哪个修正进入了哪个版本。这意味着更准确的发行说明。

更好地处理有效/无效升级路径

现在我们使用 Git 修订图来计算这些发行说明,我们也获得了一些关于可行升级路径的知识。以前,2020.4.13➜2020.5.0看起来是有效的升级路径,但实际上会回到以前,因为 2020 . 5 . 0 是在 2020 . 4 . 13 创建之前分支的。这偶尔会导致一些错误,比如数据库发生了意料之外的结构变化。现在,我们显示一个警告,这不是一个可行的升级路径,这意味着整个类的错误被避免。

结论

我们对这些变化感到非常兴奋!它们将帮助我们将更多的精力放在改进 Octopus 上,让 Octopus 云客户更快地获得这些改进,并在升级他们的自托管安装时为我们的客户提供更好的体验。

使用 Octopus Deploy 和 TakoFukku - Octopus Deploy 连续交付 PowerShell 模块

原文:https://octopus.com/blog/continuous-delivery-powershell-octopus-takofukku

传统上,Octopus Deploy 是一个推出应用程序的引擎——从历史的角度来说。NET 应用程序-到服务器。但如今,远不止如此。

因为 Octopus 是一个优秀的分布式任务运行器,具有丰富的预滚动模板集,所以它可以用于驱动许多工作负载和流程,否则这些工作负载和流程可能需要手动完成,或者使用在 CI 服务器中运行的精心手工滚动的脚本来完成。事实上,对于 PowerShell 模块来说,它们通常是小而分散的功能块,没有长时间的编译阶段,Octopus 是一个完美的选择。

我有几个开源项目,我使用 Octopus 来驱动对它们的测试和发布,所以今天我将使用 StatusCakeDSC 作为一个例子,向您介绍这是如何完成的。StatusCakeDSC 是一个用于设置状态监控的状态配置模块。我们在八达通这里用它,我自己也用它。它可以在 PowerShell Gallery 上获得,当我想发布新版本时,Octopus 会为我完成这项工作。

The StatuscakeDSC Deployment process

项目设置

这只是一个标准的 Octopus 项目,但是你会注意到这里的一切都运行在 Octopus 服务器上。这并不一定;只是在基础设施方面更便宜。这些步骤基本上可以在任何安装了 PowerShell 的目标上运行,因此我可以针对一个 VM、一个容器,甚至是我的家庭网络上的一个轮询触角。

我在项目中存储了一些重要的、敏感的变量。我的 PowerShell Gallery 的 NuGet API 密钥,我的 StatusCake 凭据,这样我就可以运行测试和我的 Slack webhook 端点。这使得我可以将这些敏感的字符串远离 GitHub,所以我很少有机会不小心发布它们。我发现对于这样的项目来说,使用八达通是保护我的秘密安全和远离公众视线的好方法。

然后我有一套相当简单的 Octopus 步骤来完成这项工作,稍后我将概述一下。

对于这个项目,我有两个环境,DevTestProduction。当部署到Production时,所有的步骤都被配置为运行。但在 DevTest 中,我明确排除了“发布到 PS Gallery”这一步。这是为生产保留的,因为我不希望预发布代码错误地进入画廊。

触发项目

该项目使用 Webhooks 基于 Github 中的提交触发,使用我的一个名为 Takofukku 的小项目。Takofukku 是一个面向 Octopus deploy 的轻量级、无服务器的 webhook 解决方案,开放给任何人使用。您只需将一个名为 takofile 的 YAML 文档放入 GitHub repo 中,在 push 事件上配置一个 webhook ,就可以开始了。

The GitHub webhook

每次 push 事件发生时,GitHub 都会向端点发送 POST 请求。Takofukku 接收这个钩子,然后从指定的 repo 中获取 takofile,如果找到有效的映射,就在您的 Octopus 服务器上触发 Octopus 部署。GitHub 上有完整的文档,当然也接受拉取请求。

在这个具体的例子中,我的 takofile 将 GitHub master分支映射到 Octopus production环境,将 GitHub develop分支映射到 Octopus DevTest环境。每当我们在这些分支上进行推送/合并时,Takofukku 将创建一个新的发布,将最后一次推送的提交消息作为发布说明,并将该发布部署到指定的环境中。

获取代码

The Git Pull Step

它做的第一件事是 git pull,使用来自章鱼库的社区步骤模板。如果我们在Production部署,我们克隆master分支。如果在DevTest中,我们克隆develop,使用一个简单的作用域变量。

在这个步骤之后,您可以看到有一个编写. creds 文件的步骤。这是特定于模块的,因为 StatusCakeDSC 允许你在磁盘上存储凭证,使测试变得更容易——这在模块的库的中有解释,所以我在这里不赘述。

运行测试

The Run Tests Step

StatusCakeDSC 使用 Pester 来运行测试,这个 PowerShell 脚本步骤非常简单:

$error.Clear()             # make sure errors are empty
pushd c:\StatusCakeDSC     # make sure we're in the right working path
# invoke pester
$failcount = Invoke-Pester -EnableExit -Verbose
if($failcount -gt 0 -or $error.count -gt 0)  # if tests have failed _or_ the step has thrown errors, exit
{
    Fail-Step "Pester returned $failcount failed tests"
}
popd 

我在 Pester 测试中发现的一个小问题是,测试直接范围之外的错误并不总是如预期的那样失败。因此,这一步检查步骤中的纠缠故障和一般错误,然后如果出现问题,使用 Octopus 的Fail-Step cmdlet 使部署失败。

发布到 PowerShell 画廊

The Publish Step

我尊敬的同事 Chris van Dal 不久前请求将publish.ps1添加到这个 repo 中,以便我可以轻松地部署到 PS Gallery 中。你可以在 GitHub repo 上看到这个脚本,它几乎是大多数 PowerShell 模块作者用来推出他们的模块的。我只是从我的章鱼步骤如下驱动它:

Set-Location c:\StatuscakeDSC
.\publish.ps1 -nugetapikey $psgalleryapikey 

此步骤的运行条件设置为仅在前面所有步骤都成功运行时运行,并且仅在生产环境中运行。

为了把这个放到 Octopus 中,我对原始脚本做了一些简单的调整。它现在:

  • 将 API 键作为参数引入,这样我可以将它安全地存储在 Octopus 中。
  • 检查模块清单以查找版本号。
  • 检查 PowerShell 库以查看该版本是否已经发布。
  • 如果成功,并且版本已经增加,它发布新的模块。
  • 在最近的代码中,它将一个 git 标签推回到 GitHub 中,用图库中的版本号标记最新的代码。

完成这一切

The Slack Notification Step

最后一步是一个空闲通知,运行条件为“总是运行”。这一步足够智能,可以知道部署何时失败,并相应地调整其消息。我喜欢这个步骤模板,因为它意味着我甚至不需要登录我的 Octopus 服务器就可以知道在我提交一些提交之后部署是否正常。无论我身在何处,我的手机都会收到一个延迟通知。

因此,所有这一切的实际结果是,每当我想对 StatusCakeDSC 进行更改时,我真正需要做的就是合并到 git 中正确的分支,Octopus 将负责运行我的测试并发布到 Gallery。这是 PowerShell 模块的持续交付,我很喜欢。

请随意复制或增强这个过程,如果您添加了任何增强功能,请让我们知道。分散在 step 库中的这一步有很多改进的可能性,事实上,您可以编写的任何脚本都可以容纳在 Octopus 中。

愉快的部署!

为 Octopus Deploy 社区库- Octopus Deploy 提供一个步骤模板

原文:https://octopus.com/blog/contributing-a-step-template-to-the-octopus-deploy-community-library

Contributing a step template to the Octopus Deploy Community Library

我最近为 Firebase CLI deploy 命令创建了一个 Octopus Deploy 步骤模板。

在这篇文章中,我将介绍 Octopus 社区库,并介绍提交新模板的过程。

社区库是 step 模板和其他社区贡献的 Octopus Deploy 扩展的存储库。

库中的步骤由 Octopus、其他供应商和 Octopus 用户提供。

贡献的

如果您已经创作了一个步骤模板,您可以考虑将其贡献给库。

非常适合该库的步骤示例包括:

这不是一份详尽的清单。如果你有一个步骤模板的想法,并且你想要反馈,在 GitHub 上打开一个问题。

也有投稿指南发布在知识库中。

该指南包括提交步骤模板的说明,以及步骤模板上提供的常见审核反馈的清单。

我将遵循这篇文章中的提交说明。

派生并克隆存储库

我分叉了 GitHub 中的存储库,这样我就有了一个可以工作的副本。

然后,我跳进终端窗口克隆存储库并开始一个分支。当我与 Git 交互时,我通常在终端中,但是您也可以使用您喜欢的 Git GUI 来完成这些步骤。

git clone https://github.com/ryanrousseau/Library.git rr_library
cd rr_library
git checkout -b firebase 

随着我的存储库的分叉和克隆,是时候导出我的模板了。

导出步骤模板

查看步骤模板时,您可以从操作菜单中选择导出将其导出。导出生成的 JSON 将进入存储库。

在一些老版本的 Octopus 中,作者需要更新 ID 和版本。在更新的版本中,Octopus 为作者设置了这些。

我的 Firebase 部署步骤的导出 JSON 是:

{
  "Id": "ac0dee2d-dcbe-42aa-96c6-bb6c644183b4",
  "Name": "Firebase - Deploy",
  "Description": "Deploys the contents of a package to a Firebase project using the [Firebase CLI deploy command](https://firebase.google.com/docs/cli/#deployment).",
  "ActionType": "Octopus.Script",
  "Version": 1,
  "CommunityActionTemplateId": null,
  "Packages": [
    {
      "Id": "343306b7-6997-429f-9ed5-4214ca4d32ac",
      "Name": "FirebaseDeploy.Package",
      "PackageId": null,
      "FeedId": "feeds-builtin",
      "AcquisitionLocation": "Server",
      "Properties": {
        "Extract": "True",
        "SelectionMode": "deferred",
        "PackageParameterName": "FirebaseDeploy.Package"
      }
    }
  ],
  "Properties": {
    "Octopus.Action.Script.ScriptSource": "Inline",
    "Octopus.Action.Script.Syntax": "Bash",
    "Octopus.Action.Script.ScriptBody": "packagePath=$(get_octopusvariable \"Octopus.Action.Package[FirebaseDeploy.Package].ExtractedPath\")\ntoken=$(get_octopusvariable \"FirebaseDeploy.CIToken\")\npublic=$(get_octopusvariable \"FirebaseDeploy.Public\")\nmessage=$(get_octopusvariable \"FirebaseDeploy.Message\")\nforce=$(get_octopusvariable \"FirebaseDeploy.Force\")\nonly=$(get_octopusvariable \"FirebaseDeploy.Only\")\nexcept=$(get_octopusvariable \"FirebaseDeploy.Except\")\nprintCommand=$(get_octopusvariable \"FirebaseDeploy.PrintCommand\")\nfirebasePath=$(get_octopusvariable \"FirebaseDeploy.FirebasePath\")\n\nif [ ! -z \"$firebasePath\" ] ; then\n   \tPATH=$firebasePath:$PATH\nfi\n\nif [ \"$force\" = \"True\" ] ; then\n    force=true\nelse\n    force=\nfi\n\nif [ \"$printCommand\" = \"True\" ] ; then\n    set -x\nfi\n\ncd $packagePath\n\nfirebase deploy ${public:+ -p \"$public\"} ${message:+ -m \"$message\"} ${force:+ -f} ${only:+ --only \"$only\"} ${except:+ --except \"$except\"} --token $token"
  },
  "Parameters": [
    {
      "Id": "55ddf9fd-bf2f-4148-912b-bc599c5f6ec6",
      "Name": "FirebaseDeploy.Package",
      "Label": "Package",
      "HelpText": "The package containing the Firebase project being deployed.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Package"
      }
    },
    {
      "Id": "46874eaf-7632-40d1-bd46-4627bd0f2d0c",
      "Name": "FirebaseDeploy.FirebasePath",
      "Label": "Firebase Path",
      "HelpText": "The path to the directory containing the Firebase CLI, if not in $PATH.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "c982c1f3-a91e-4dd4-89a6-db5d99b08347",
      "Name": "FirebaseDeploy.CIToken",
      "Label": "CI Token",
      "HelpText": "A CI token generated by the [Firebase CLI](https://firebase.google.com/docs/cli/#cli-ci-systems)",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      }
    },
    {
      "Id": "56628161-6b99-4ca3-9c4a-1234117a0018",
      "Name": "FirebaseDeploy.Public",
      "Label": "Public Path",
      "HelpText": "Override the Hosting public directory specified in firebase.json.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "e7c41fcb-dd74-4ba2-9671-fa7313d632b8",
      "Name": "FirebaseDeploy.Message",
      "Label": "Message",
      "HelpText": "An optional message describing this deploy.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "6a88a428-a538-4292-b6ee-b843c28887f3",
      "Name": "FirebaseDeploy.Force",
      "Label": "Force?",
      "HelpText": "Delete Cloud Functions missing from the current working directory without confirmation.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      }
    },
    {
      "Id": "c0debcc3-6708-4d3c-977b-880811b48594",
      "Name": "FirebaseDeploy.Only",
      "Label": "Only Targets",
      "HelpText": "Only deploy to specified, comma-separated targets (e.g. \"hosting,storage\"). For functions, can specify filters with colons to scope function deploys to only those functions (e.g. \"--only functions:func1,functions:func2\"). When filtering based on export groups (the exported module object keys), use dots to specify group names (e.g. \"--only functions:group1.subgroup1,functions:group2)\".",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "e62a6b0f-6331-4a63-a908-c759798ccd1c",
      "Name": "FirebaseDeploy.Except",
      "Label": "Except Targets",
      "HelpText": "Deploy to all targets except specified (e.g. \"database\").",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "e2e0ac14-e5e9-4b3c-bdc1-b1da3d7be184",
      "Name": "FirebaseDeploy.PrintCommand",
      "Label": "Print Command?",
      "HelpText": "Prints the command in the logs using `set -x`. This will cause a warning when the step runs.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      }
    }
  ],
  "$Meta": {
    "ExportedAt": "2020-06-08T19:44:37.662Z",
    "OctopusVersion": "2020.2.11",
    "Type": "ActionTemplate"
  },
  "LastModifiedBy": "Your GitHub Username",
  "Category": "other"
} 

它几乎准备好提交。我只需要将LastModifiedBy设置为我的 GitHub 用户名,并将类别设置为“firebase”

幸运的是,命名进入存储库的文件并不困难。我将 JSON 保存在step-templates\firebase-deploy.json中。

添加 Firebase 类别

Firebase - Deployfirebase类别中的第一步库。我需要添加一个标志,并更新网站来处理类别。

我追踪到一个标志,并将其保存为step-templates\logos\firebase.png

我打开.gulpfile.babel.js,找到humanize功能。我为 Firebase 添加了一个案例。

 case 'firebase': return 'Firebase'; 

测试

我可以在本地运行图书馆网站,以确保我没有错过任何东西。

我需要安装gulp来构建和测试网站:

npm install -g gulp 

然后,我安装依赖项,构建并启动站点:

npm install
gulp
node build/server.js 

我导航到http://localhost:9000并搜索firebase:

Screenshot of the step found in the library

看起来一切正常。是时候创建一个拉取请求了。

提交更改

我将我的更改存放并提交到库中,然后将它们推送到我的存储库分支:

git add ./gulpfile.babel.js
git add ./step-templates/firebase-deploy.json
git add ./step-templates/logos/firebase.png
git commit -m "Add Firebase - Deploy template"
git push -u origin firebase 

提交拉取请求

我在 GitHub 中导航到我的分支。我看到 GitHub 已经选择了新的分支,并提供给我一个比较&拉取请求按钮。

GitHub offers an option to compare and create a pull request

我单击按钮启动拉取请求流程。GitHub 默认拉请求目标为原始存储库。这是正确的,所以我不改变它。

拉请求模板包括两个部分。第一部分是指南的副本。我确认我的模板符合指导原则。然后我按照说明删除了第一部分。

第二部分是步骤模板需要完成的项目清单。我检查列表,确认我的步骤遵循了这些规则。

我提交了拉取请求。在这篇文章发表时,它可能会被关闭,但任何评论和更改的历史记录都将可用。

后续步骤

在合并拉取请求之前,需要签署一份贡献者许可协议。还有一个自动构建来确保站点的构建。拼图的最后一块是 Octopus 团队的评论。

Octopod 的一个同伴将检查这些变化,以确保我遵循了指南,并且这一步是对库的一个有价值的补充。

如果有任何更改,我会进行更改,并将其添加到 pull 请求中,以便再次审核。

请求获得批准后,审阅者会将步骤合并到库中。将开始构建和部署库网站。几分钟后,其他八达通用户将可以使用该步骤。

结论

八达通社区图书馆举办了各种各样的社区贡献的步骤。为了他人的利益,您可以向库提交模板。

转换现有应用程序以使用滚动部署- Octopus Deploy

原文:https://octopus.com/blog/convert-to-rolling-deployments

Rolling deployments

在之前的一篇文章中,我写了滚动部署模式的好处,这是一种在部署时减少应用程序停机时间的方法。当您第一次创建应用程序时,设计一个适合这种部署模式的应用程序可能要容易得多,但是从现有的应用程序开始,如何将应用程序转换成使用滚动部署模式呢?

在这篇文章中,我将向您展示如何在 Octopus 的子步骤的帮助下转换现有的应用程序以使用滚动部署模式。

在这篇文章中

应用程序

我将以 PetClinic 为例,将应用程序的部署过程从在 Octopus 中顺序运行部署步骤的过程转换为滚动部署过程。PetClinic 是一个用 Java 编写的示例 Spring Boot 应用程序,它有两个主要组件:

  • 一个 web 前端。
  • 一个数据库。

在本文中,我不会解释如何构建 PetClinic 应用程序。如果您是构建 Java 应用程序的新手,我们有许多指南,其中包括为各种工具设置 CI/CD 管道的逐步说明。

对于顺序和滚动部署流程,PetClinic 应用程序和 MySQL 数据库都托管在 Google Cloud 中。这些示例中使用的所有基础设施,包括服务器、负载平衡器和数据库,都使用运营手册定期重新创建。

一些警告

需要强调的是,这篇文章不会涵盖零停机部署所需的所有元素。它对应用程序的设置做了一些假设:

  1. 数据库已经部署在高可用性配置中。有关 MySQL 高可用性的更多信息,请参考文档
  2. 使用 Flyway 以向后和向前兼容的方式对数据库进行更改。
  3. 将单个服务器部署到时,任何必需的会话状态都将保持。

顺序部署流程

对于不担心应用程序停机的部署,默认情况下,Octopus 通过依次运行各个步骤来满足这一需求。

Start trigger
也可以配置您的部署流程,以并行的方式运行步骤。但是,应该注意避免并行运行的步骤相互依赖的情况。

现有的 PetClinic 应用程序使用这种顺序部署技术进行建模。部署流程由许多关键步骤组成:

  • 仅用于生产环境的手动干预批准步骤。
  • Flyway DB 迁移社区步骤模板显示迁移状态和应用任何新数据库更改的步骤。
  • PetClinic web 前端的部署到 WildFly 步骤。

此外,部署流程包括将消息发布到具有部署进度更新的 Slack 通道的步骤。

完整的部署流程如下所示:

Project sequential deployment process

在 Octopus 中创建了一个版本之后,您可以看到一个在开发环境中的顺序部署实例:

Project sequential deployment run

一次运行一个步骤,直到部署完成。当 Deploy PetClinic web app 步骤运行时,应用程序变得不可用于向用户提供请求。

也许不足为奇的是,这些部署(每个环境)中使用的基础架构如下所示:

Project sequential infrastructure

它包括:

  • 一台 Ubuntu 虚拟机托管 Wildfly 应用服务器。
  • 一个托管在谷歌云 SQL 服务中的 MySQL 数据库。

样本 Octopus 项目
您可以在我们的样本实例中看到转换为滚动部署流程之前的 PetClinic 顺序部署流程

转换为滚动部署流程

既然我们已经看到了现有应用程序的部署过程,我们首先需要决定滚动部署过程中我们的基础设施将会是什么样子。

纵向扩展服务器

为了减少停机时间并满足用户的请求,我们需要增加我们使用的服务器数量。我们还需要一个负载平衡器来控制哪些服务器可用。

在前面的顺序部署示例中,我们在每个环境中有一台虚拟机。为了简单起见,我们将保持开发环境的基础设施和以前一样。然而,对于测试生产环境,基础架构将如下所示:

Project rolling infrastructure

这包括一个共享的负载平衡器,这一次,每个环境中有两个应用服务器,像以前一样连接到 MySQL 数据库。

在您创建了新的服务器之后,您还需要将它们作为新的部署目标添加到 Octopus 中,并用任何适当的目标角色标记它们。

选择负载平衡器

有许多不同类型的负载平衡器,但这个滚动部署示例的一个关键要求是能够控制哪些服务器可用于服务流量。由于这个原因,并且这个例子是从 Google Cloud 运行的,我们将使用一个网络负载平衡器。作为部署过程的一部分,这提供了一种从负载均衡器中添加和删除服务器的方法,稍后我们将看到这一点。有关设置网络负载平衡器的更多信息,请参考谷歌文档

在本例中,负载平衡器在测试生产环境之间共享。为了将流量路由到正确的位置,负载平衡器使用不同的 TCP 端口来识别预期的环境。

  • 端口8080用于去往测试环境的流量。
  • 端口80用于目的地为生产环境的流量。

负载平衡器目标池

以前,用户直接在单个虚拟机上访问 PetClinic web 前端。这里,我们将专用的目标池用于测试环境和生产环境。目标池是一组托管在 Google Cloud 中的虚拟机实例的名称。

创建新项目

为了改变我们的部署过程,并保持顺序部署 PetClinic 的能力,我们需要创建一个新项目。实现这一点的方法之一是克隆现有的项目。

在现有项目中,在设置下,使用溢出菜单(...)并选择克隆:

Project clone menu option

  • 为您正在从原始项目克隆的新项目命名,检查设置,当您满意时,单击保存:

T32

转换 PetClinic 部署流程

接下来,我们将转换项目本身的部署过程。并不是所有的 PetClinic 部署过程都适合滚动部署模式,因此,我们将重点放在 PetClinic 的 web 前端。

选择要转换的内容
自己决定项目部署过程中的哪些元素应该转换为使用滚动部署模式是很重要的。在某些情况下,这可能会使事情更难部署

配置滚动部署

为了将部署 PetCinic web app 步骤转换为滚动部署,我们执行以下操作:

  • 打开部署流程编辑器中的步骤,展开角色中目标的部分,点击配置滚动部署:

PetClinic step configure rolling deployment

  • 在出现的滚动展开选项中,选择一个窗口大小。我选择了1的窗口大小,因为我们将在每个环境中最多部署两台服务器:

PetClinic step configure rolling deployment window size

  • 点击保存更新部署步骤。

添加子步骤

我们已经配置了滚动部署,但它还不是很智能。目前,该流程一次一个地将 PetClinic 部署到每个服务器,在部署时使应用程序的每个实例脱机。

我们需要向我们的滚动部署添加新的步骤,以便将 PetClinic 应用程序的新版本安全地部署到每个虚拟机,并继续向其他服务器上的用户提供流量。

这些步骤将:

  1. 检索虚拟机名称。
  2. 在 pet clinic 部署之前,从负载均衡器中移除虚拟机。
  3. 在 pet clinic 部署之后,将虚拟机添加到负载均衡器中。
  4. 测试 PetClinic web 前端是否可用。

在 Octopus 中,向滚动部署流程添加多个步骤是通过子步骤完成的。

gcloud CLI 和授权
下一节中用于与 Google 交互的大多数命令都利用了 Google Cloud CLI 。要使用 g cloud CLI,你通常需要授权。有关 gcloud 授权的更多信息,请参考文档

添加新的子步骤

要添加子步骤,我们打开溢出菜单(...)对于现有的部署 PetClinic web app 步骤,选择添加子步骤:

Project rolling deployment add new child step

我们看到了选择步骤模板选择器,在这里我们选择所需的步骤类型。

接下来,我将介绍完成滚动部署过程所需的新的子步骤。一些脚本示例已经减少到突出关键部分所需的最少数量。

检索实例名称

这个脚本步骤是必需的,以便我们可以识别 Google Cloud 中托管的虚拟机的名称,以便在删除和添加负载平衡器时使用。我们通过使用 gcloud instances describe命令查询 Google 来做到这一点:

$machineName = $OctopusParameters["Octopus.Machine.Name"]

$instanceName=(& gcloud compute instances describe $machineName --project=$projectName --zone=$zone --format="get(name)" --quiet) -join ", " 

如果使用由 Octopus 系统变量Octopus.Machine.Name标识的机器找到匹配,脚本将设置一个输出变量,其名称记录在 Google Cloud 中:

Set-OctopusVariable -name "InstanceName" -value $instanceName 
从负载平衡器上拆下机器

当我们知道要部署到的机器的名称时,我们需要将它从负载平衡器目标池中删除。然而,为了防止在虚拟机不存在的情况下尝试从目标池中删除虚拟机,我们可以运行 gcloud target-pools describe命令来检查:

$instances=(& gcloud compute target-pools describe $targetPoolName --format="flattened(instances[])" --region=$region --project=$projectName --quiet) 

如果在目标池中找到实例,我们运行 gcloud target-pools remove-instances命令,为实例名提供--instances参数:

$instanceName = $OctopusParameters["Octopus.Action[Retrieve machine instance name].Output.InstanceName"]

$response=(& gcloud compute target-pools remove-instances $targetPoolName --instances=$instanceName --instances-zone=$zone --project=$projectName --quiet) 

从负载均衡器中移除虚拟机后,我们可以继续部署 PetClinic 应用程序。

我们不需要添加新的子步骤来部署 PetClinic 应用程序,因为它已经存在。相反,我们将在添加必要的子步骤后,将该步骤放在正确的位置。

测试 PetClinic 应用程序

部署 PetClinic 前端后,我们可以通过添加一个名为 HTTP - Test URL 的社区步骤模板作为子步骤来测试它是否响应请求。

我们使用变量#{Project.Wildfly.Url}来测试它是否返回 HTTP 200 OK 响应。这将告诉我们应用程序是否正在运行。任何其他 HTTP 响应都将导致失败。

将机器添加到负载平衡器

最后,当 PetClinic 应用程序被验证为在线时,我们可以将其添加回负载平衡器目标池。我们通过运行 gcloud target-pools add-instances命令并为实例名(和以前一样)提供--instances参数来实现这一点:

$instanceName = $OctopusParameters["Octopus.Action[Retrieve machine instance name].Output.InstanceName"]

$response=(& gcloud compute target-pools add-instances $targetPoolName --instances=$instanceName --instances-zone=$zone --project=$projectName --quiet) 
重新排列子步骤

添加完所有子步骤后,如果需要,可以对它们重新排序。在我们的例子中,我们需要将最初的部署 PetClinic web 应用程序步骤移到中间,这样我们就不会将应用程序部署到虚拟机,直到从负载均衡器中移除。

要重新排序子步骤:

  • 使用溢出菜单(...)并选择重新排序子步骤
  • 按照所需的顺序重新排列这些步骤。
  • 完成后点击保存

滚动部署流程

滚动部署过程中的一些步骤对于开发环境是不需要的。这是因为我们在那个环境中没有使用负载平衡器。为了跳过不需要运行的步骤,我们使用环境运行条件。这将跳过在部署到开发时适用于负载平衡环境的步骤。

完整的滚动部署流程如下所示:

Project rolling deployment run

您可以看到使用新的滚动部署流程部署到生产的示例:

Project rolling deployment run

就是这样!我们已经成功地将我们的部署流程从顺序部署流程转换为滚动部署流程。

样本 Octopus 项目
您可以在我们的样本实例中看到转换为滚动部署流程后的完整 PetClinic 部署流程

切换到新的基础架构

为了使用我们的新基础设施,我们需要通过负载平衡器将用户引导到我们的应用程序。最简单的方法就是调整你的 DNS 记录。在 PetClinic 的情况下,我只需要调整 DNS A 记录指向负载平衡器,并等待 DNS 更改更新。

更改任何 DNS 记录可能会导致用户在一段时间内仍然直接连接到虚拟机。这通常不会超过 24 小时,但这将取决于您的 DNS 提供商以及 DNS 更改传播所需的时间。

清理

此时,您不再需要以前的 Octopus 项目。您可以通过禁用它(并有效地将其归档)或删除它(如果您不再需要它来满足任何审计需求)来清理它。

结论

正如您从这篇文章中所看到的,通过几个步骤,您可以从 Octopus 中的顺序部署过程切换到使用滚动部署特性的过程。这使您能够从减少的停机时间中获益,因为您知道您的应用程序可以保持在线以满足用户的请求。

下次再见,愉快的部署!

了解更多信息

通过 Octopus API - Octopus Deploy 将许多环境转换为租户

原文:https://octopus.com/blog/converting-to-tenants-via-api

在之前的帖子中,Mark Harrison 在 Octopus 中写了关于多租户的好处。从一开始就设计一个适合多租户的应用程序比改造一个更容易,但是您应该如何处理现有的应用程序呢?我经常看到人们在为他们的每个客户部署同一应用程序的多个实例时创建特定于客户的环境。这可能导致复杂的生命周期、不清晰的发布仪表板以及复杂的项目变量范围。

幸运的是,从 Octopus 2.0 开始,Octopus REST API 已经可以用来帮助管理多租户。

使用一个名为 Vet Clinic 的示例项目,我演示了如何利用 Octopus REST API 并开始自动转换到多租户。

初始项目状态

Vet Clinic 项目的概览屏幕显示了您可以部署项目的各种环境。有些环境是特定于客户的,包括客户名称。这些是您转换为租户的环境。

Vet Clinic project overview

该项目针对每个环境设定了多个变量,以便为客户指定价值。您将客户环境范围内的变量转换为项目模板,以便为您创建的每个租户提供用于租赁部署的特定变量值。

通用环境范围内的变量,例如开发和测试,作为未租用部署的项目特定变量保存。

Vet Clinic project variables

最后一部分是项目的生命周期。每个客户都有他们特定的环境,生命周期中的每个阶段都有多个环境,您可以将项目部署到这些环境中。

您为项目可以部署到的每个阶段创建一个具有单一环境的新生命周期。旧的生命周期决定了每个新租户可以将项目部署到哪些环境中。

Vet Clinic lifecycle

入门指南

要开始转换过程,您需要在现有的 Vet Clinic 项目旁边创建一个新项目。通过克隆现有项目来实现这一点。克隆允许您并行测试变更,而不用担心会中断您当前项目的部署。

当您对克隆项目的结果满意后,删除它并在原始项目上运行转换脚本。或者,您可以将所有内容移动到新的、克隆的项目中,并最终淘汰原始项目。

Vet Clinic tenanted project overview

接下来,您将为项目创建一个新的简化生命周期,以便在转换完成后使用。生命周期中的每个阶段都有一个单一的部署环境,这提供了一个更全面的视图,可以了解哪个版本部署到了哪个环境和租户。

Vet Clinic tenanted lifecycle

创建脚本

提供的示例仅供参考,在用于生产 Octopus 实例之前,应该进行修改和测试。

有了这两个部分,您就可以开始创建脚本了。对于第一个脚本,您从旧环境中创建租户。

您需要指定以下输入:

  • $projectnames:您希望您的租户加入的项目
  • $tenantTags:租户标签应用于所有新创建的租户
  • $spaceName:你的项目所在的 Octopus 空间名
  • $lifecycleName:您的未承租项目正在使用的旧生命周期的名称
  • $newEnvironmentNames:不转换为租户的环境列表

设置好这些输入后,脚本得到一个要转换的旧环境的列表。

当遍历旧环境列表时,会发生一些事情:

  • 因为环境名称遵循EnvironmentName-CustomerName的约定,所以它解析出所需的租户名称。
  • 从新租户名称开始,检查该租户是否已经存在。
  • 为了确定每个租户应该部署到哪个新环境,该脚本循环遍历旧生命周期中的每个阶段。
  • 输入的项目与租户应该部署到的环境相结合。
  • 如果租户不存在,脚本将调用 API 用收集的数据创建一个新的租户。
  • 如果租户不存在,脚本将发送一个请求,为租户提供它应该为项目部署的环境。
完整脚本创建*租户*from _ environments . PS1
$ErrorActionPreference = "Stop";

# Define working variables
$octopusURL = "https://youroctopus.octopus.app"
$octopusAPIKey = "API-YOURAPIKEY"
$header = @{ "X-Octopus-ApiKey" = $octopusAPIKey }

# Provide project names to attach new tenants to.
$projectNames = @("VetClinic - Tenanted")

# Optionally, provide existing tenant tagsets you wish to apply.
$tenantTags = @() # Format: TagSet/Tag

# Provide the space name
$spaceName = "Sandbox"
# Get space
$space = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/spaces/all" -Headers $header) | Where-Object {$_.Name -eq $spaceName}

# Provide the old lifecycle name
$lifecycleName = "Vet Clinic"
# Get lifecycle
$lifecycle = (Invoke-RestMethod -Uri "$octopusURL/api/$($space.Id)/lifecycles/all" -Headers $header) | Where-Object {$_.Name -eq $lifecycleName}

# List of environments to not convert to tenants
$newEnvironmentNames = @("Development","Test","Staging","Production")

# List of environment names and ids being converted to Tenants
$oldEnvionments = (Invoke-RestMethod -Uri "$octopusURL/api/$($space.Id)/environments/all" -Headers $header) | Where-Object {$_.Name -notin $newEnvironmentNames} | Select-Object -Property Id,Name

# Get the environment ids to attach the tenant to from the phase in the old lifecycle that the environment was apart of
function Build-EnvironmentIds {
    param ($oldEnvId)

    $environmentIds = @()

    foreach ($phase in $lifecycle.Phases) {
        if ($phase.AutomaticDeploymentTargets -contains $oldEnvId -or $phase.OptionalDeploymentTargets -contains $oldEnvId)
        {
            $newEnvId = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/environments/all" -Headers $header) | Where-Object {$_.Name -eq $phase.Name} | ForEach-Object {$_.Id}
            $environmentIds += $newEnvId
        }
    }

    ,$environmentIds
}

# Parse the old environment name for the new tenants name
function Edit-EnvironmentName {
    param ($oldEnvName)

    $start = $oldEnvName.IndexOf("-")+1
    $end = $oldEnvName.Length
    $newName = $oldEnvName.Substring($start, ($end-$start))
    $newName
}

# Get projects to attach tenants to
$projectIds = @()
foreach ($projectName in $projectNames)
{
    $projectIds += ((Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/projects/all" -Headers $header) | Where-Object {$_.Name -eq $projectName}).Id
}

# Loop though the old environment and create tenants
foreach($oldEnv in $oldEnvionments) {
    $tenantName = Edit-EnvironmentName($oldEnv.Name)

    # Check if tenant already exists
    $existingTenant = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/tenants/all" -Headers $header) | Where-Object {$_.Name -eq $tenantName}

    # Get environment ids to attach the tenants to
    $envIds = Build-EnvironmentIds($oldEnv.Id)

    # New tenant creation
    if ($null -eq $existingTenant) {        
        # Build project/environments
        $projectEnvironments = @{}
        foreach ($projectId in $projectIds)
        {
            $projectEnvironments.Add($projectId, $envIds)
        }

        # Build json payload
        $jsonPayload = @{
            Name = $tenantName
            TenantTags = $tenantTags
            SpaceId = $space.Id
            ProjectEnvironments = $projectEnvironments
        } | ConvertTo-Json

        # Create tenant
        Invoke-RestMethod -Method Post -Uri "$octopusURL/api/$($space.Id)/tenants" -Body $jsonPayload -Headers $header -ContentType "application/json"
    }
    else {

        foreach ($projectId in $projectIds) {
            $existingTenant.ProjectEnvironments.($projectId) += $envIds
        }

        $jsonPayload = $existingTenant | ConvertTo-Json

        # Update tenant
        Invoke-RestMethod -Method Put -Uri "$octopusURL/api/$($space.Id)/tenants/$($existingTenant.Id)" -Body $jsonPayload -Headers $header -ContentType "application/json"
    }
} 

创建租户后,您需要编写以下脚本,将旧的项目变量转换成变量模板,为每个租户提供一个值。

该脚本需要以下输入:

  • $projectNames:包含当前项目变量的项目名称,以及您想要为其创建变量模板的项目的名称
  • $$newEnvironmentNames:环境名称列表,用于确定每个租户的项目变量和模板变量的范围
  • $spaceName:包含项目的 Octopus 空间名称

从指定的旧项目中检索项目变量列表。然后,该脚本遍历每个变量以及该变量的每个环境范围。

如果作用域环境是您将用于租用部署的新环境之一,那么将创建一个项目变量。否则,脚本将创建一个变量模板。

在这两种情况下,都会检查项目变量或变量模板是否已经存在。当它们不存在时,旧变量的细节被用来创建新的变量类型,它们被添加到各自的集合中,在脚本开始时被检索。

循环结束后,有一个对 API 的调用来更新项目的变量集合,还有一个调用来更新项目变量模板。

在为项目创建变量模板的 API 调用之后,脚本循环遍历每个变量,为每个租户的模板提供值。旧变量的环境范围被循环,以解析出该值所属的环境和租户的名称。解析的租户名称检索租户数据。

从租户数据开始,脚本循环遍历附加到租户的每个项目环境,然后遍历每个模板,直到旧变量的名称与模板的名称匹配,并且项目环境与环境范围匹配。进行 API 调用,用新值更新租户的变量。

完整脚本创建*项目*变量*和*模板. ps1
$ErrorActionPreference = "Stop";

# Define working variables
$octopusURL = "https://youroctopus.octopus.app"
$octopusAPIKey = "API-YOURAPIKEY"
$header = @{ "X-Octopus-ApiKey" = $octopusAPIKey }

# Provide the current project name and the new project name.
$projectNames = @{
    old = "Vet Clinic" 
    new = "VetClinic - Tenanted"
}

# Names of new environments
$newEnvironmentNames = @("Development","Test","Staging","Production")
$newEnvironmentIds = (Invoke-RestMethod -Uri "$octopusURL/api/$($space.Id)/environments/all" -Headers $header) | Where-Object {$_.Name -in $newEnvironmentNames} | ForEach-Object {$_.Id}

# Provide the space name
$spaceName = "Sandbox"
# Get space
$space = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/spaces/all" -Headers $header) | Where-Object {$_.Name -eq $spaceName}

# Get old project
$oldProject = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/projects/all" -Headers $header) | Where-Object {$_.Name -eq $projectNames['old']}

# Get new project
$newProject = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/projects/all" -Headers $header) | Where-Object {$_.Name -eq $projectNames['new']}

# Get old variable set
$oldVariableSet = Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/variables/$($oldProject.VariableSetId)" -Headers $header

# Get the new projects variables
$newProjectVariables = Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/variables/$($newProject.VariableSetId)" -Headers $header

# Get the tenants attached to the new project
$tenants = Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/tenants/all?projectId=$($newProject.Id)" -Headers $header 

# Create a new project variable
function New-Project-Variable {
    param (
        $oldVariable
    )
    # Check to see if variable is already present
    $variableToUpdate = $newProjectVariables.Variables | Where-Object {$_.Name -eq $oldVariable.Name -and $_.Scope -eq $oldVariable.Scope} 

    # If the variable does not exist create it
    if ($null -eq $variableToUpdate)
    {
        # Create new object
        $variableToUpdate = New-Object -TypeName PSObject
        $variableToUpdate | Add-Member -MemberType NoteProperty -Name "Name" -Value $oldVariable.Name
        $variableToUpdate | Add-Member -MemberType NoteProperty -Name "Value" -Value $oldVariable.Value
        $variableToUpdate | Add-Member -MemberType NoteProperty -Name "Type" -Value $oldVariable.Type
        $variableToUpdate | Add-Member -MemberType NoteProperty -Name "IsSensitive" -Value $oldVariable.IsSensitive
        $variableToUpdate | Add-Member -MemberType NoteProperty -Name "Scope" -Value $oldVariable.Scope

        # Add to collection
        $newProjectVariables.Variables += $variableToUpdate

        $newProjectVariables.Variables
    } 
}

# Create a new project template variable
function New-Project-Template {
    param (
        $oldVariable
    )

    # Check to see if the template already exists
    $templateToUpdate = $newProject.Templates | Where-Object {$_.Name -eq $oldVariable.Name}

    # If the template does not exist, create it
    if ($null -eq $templateToUpdate) {
        $templateToUpdate = New-Object -TypeName PSObject
        $templateToUpdate | Add-Member -MemberType NoteProperty -Name "Name" -Value $oldVariable.Name
        $templateToUpdate | Add-Member -MemberType NoteProperty -Name "DisplaySettings" -Value @{'Octopus.ControlType' = 'SingleLineText'}

        # Add to collection
        $newProject.Templates += $templateToUpdate

        $newProject.Templates
    }
}

# Create project template values for tenants
function New-Template-Values {
    param (
        $oldVariable
    )

    foreach ($envScope in $oldVariable.Scope.Environment) {

        # Get environment name from the old environment scope
        $environmentName = ($oldVariableSet.ScopeValues.Environments | Where-Object {$_.Id -eq $envScope}).Name

        if ($environmentName -notin $newEnvironmentNames)
        {
            $tenantNameAndEnvironment = $environmentName.Split("-")
            $tenantName = $tenantNameAndEnvironment[1]
            $newEnvironment = $tenantNameAndEnvironment[0]
            $environmentId = (Invoke-RestMethod -Uri "$octopusURL/api/$($space.Id)/environments/all" -Headers $header) | Where-Object {$_.Name -eq $newEnvironment} | ForEach-Object {$_.Id}

            # Get the tenant with that name
            $tenant = $tenants | Where-Object {$_.Name -eq $tenantName}
            # Get tenants variable set
            $tenantVariables = (Invoke-RestMethod -Method Get -Uri "$octopusURL/api/$($space.Id)/tenants/$($tenant.Id)/variables" -Headers $header)
            $newProjectId = $newProject.Id

            # Go through each tenant project environment and create a tenant template value for the corresponding template 
            foreach ($projectEnv in $tenant.ProjectEnvironments.$newProjectId) {
                foreach ($template in $tenantVariables.ProjectVariables.$newProjectId.Templates) {
                    if ($oldVariable.Name -eq $template.Name -and $projectEnv -eq $environmentId) {
                        $tenantVariables.ProjectVariables.$newProjectId.Variables.$projectEnv | Add-Member -MemberType NoteProperty -Name $template.Id -Value $oldVariable.Value
                    }
                }
            }

            # Update the variables with the new value
            Invoke-RestMethod -Method Put -Uri "$octopusURL/api/$($space.Id)/tenants/$($tenant.Id)/variables" -Headers $header -Body ($tenantVariables | ConvertTo-Json -Depth 10)
        }
    }
}

# Go through old project variables to create new project variables and template variables
foreach ($oldVariable in $oldVariableSet.Variables) {

    foreach ($environment in $oldVariable.Scope.Environment)
    {
        # Create project variables to old variables scoped to non tenant environments
        if ($environment -in $newEnvironmentIds) {
            # Create new project variable
            New-Project-Variable($oldVariable)
        }
        else {
            # Create new project template
            New-Project-Template($oldVariable)
        }   
    }  
}

# Update the new projects variable collection
Invoke-RestMethod -Method Put -Uri "$octopusURL/api/$($space.Id)/variables/$($newProject.VariableSetId)" -Headers $header -Body ($newProjectVariables | ConvertTo-Json -Depth 10)

# Update the new projects templates
Invoke-RestMethod -Method Put -Uri "$octopusURL/api/$($space.Id)/projects/$($newProject.Id)" -Headers $header -Body ($newProject | ConvertTo-Json -Depth 10)

# Loop through old variables again to update tenant variable values after the project templates have been created
foreach ($oldVariable in $oldVariableSet.Variables) {
    New-Template-Values($oldVariable)
} 

最终结果

对兽医诊所项目运行这两个脚本后,您可以看到脚本创建的新项目。

在租户控制面板上,您现在可以看到三个不同的租户:

Vet Clinic tenants

兽医诊所租赁项目现在只有新环境范围内的项目变量:

Vet Clinic Tenanted project variables

该项目还包括新的变量模板,为每个租户提供一个值:

Vet Clinic Tenanted project variable templates

在每个新租户中,变量值已经使用旧项目变量的范围和值针对每个环境进行了更新:

Vet Clinic tenant variable values

要开始在部署中使用新租户,您需要设置您的基础设施和任何您想要用来管理租户组的租户标签

结论

Octopus 中的多租户是一个强大的特性,您可以利用它来创建可伸缩的、可重用的、简化的部署。然而,将一个相当大的现有项目转换成多租户似乎是一项艰巨的任务。

这篇文章展示了将现有项目转换为多租户的几个步骤。我希望它演示了 Octopus API 如何帮助您自动化这个过程中的步骤,节省您的时间和在 UI 中单击数百次鼠标的可能性。

了解更多信息

观看网络研讨会:使用 Octopus Deploy 实现更好的多租户部署

https://www.youtube.com/embed/dD8psiK1wL4

VIDEO

我们定期举办网络研讨会。请参见网络研讨会页面了解过去的网络研讨会以及即将举办的网络研讨会的详细信息。

愉快的部署!

Selenium 系列:创建 UberJAR - Octopus 部署

原文:https://octopus.com/blog/selenium/30-create-an-uberjar/create-an-uberjar

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

我们用来构建代码的 Maven 项目会将我们的类打包到一个 JAR 文件中。我们可以通过运行 Maven 包生命周期来创建这个 JAR 文件。为此,打开Maven Projects工具窗口,双击生命周期➜包选项。

C:\b4da756b8e562f2449a92910d5712a96

打包应用程序将首先运行所有的测试,然后在target目录下构建 JAR 文件。

C:\eb232509f6285bcc27ff6f6904d0429f

然而,这个文件本身不足以运行应用程序,因为它不包括我们所依赖的所有附加库,如 WebDriver 和 Cucumber。我们可以通过在任何查看 ZIP 文件的应用程序中打开 JAR 文件来确认这一点,因为 JAR 文件只是具有不同扩展名的 ZIP 文件。你可以看到这个档案中唯一的类是我们自己写的。这个档案中没有来自 WebDriver 或 Cucumber 库的类。

C:\c8e655e8f7f7ad56dc24ed87b905be9d

这带来了一些问题,因为组成 Lambda 函数的代码必须打包到一个单独的归档文件中。一种解决方案是构建一个完全自包含的 JAR 文件,也称为 UberJAR。

UberJAR 是一个 JAR 文件,它包含运行应用程序所需的所有类,这意味着它包含我们编写的代码的所有类,以及我们的代码所依赖的库中的所有类。Maven 使得用 Shade 插件构建 UberJAR 变得很容易。

要配置 Shade 插件,我们需要将其配置添加到<build><plugins>元素下。

这个配置指定插件的shade目标应该在package阶段运行。这意味着当我们用 Maven 打包代码时,Shade 插件将自动运行以生成 UberJAR:

<project xmlns=\"http://maven.apache.org/POM/4.0.0\"
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd\">
  <modelVersion>4.0.0</modelVersion>
  <!-- ... -->
  <properties>
    <!-- ... -->
    <shade.version>3.1.0</shade.version>
    <!-- ... -->
  </properties>
    <build>
      <plugins>
      <!-- ... -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>${shade.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- ... -->
    </plugins>
  </build>
  <!-- ... -->
</project> 

现在,当我们打包应用程序时,创建了两个 JAR 文件。

original-webdrivertraining-1.0-SNAPSHOT.jar文件是常规的 JAR 文件,只包含我们自己代码中的类。你可以看到这个文件只有几千字节大小。

webdrivertraining-1.0-SNAPSHOT.jar是包含运行这个应用程序所需的所有类的 UberJAR 文件。它要大得多,有几兆字节的大小。

C:\651a01a2c62fa638e66ab24197a7af10

查看 UberJAR 文件的内容,我们可以看到它有更多的类。这些类来源于我们在pom.xml文件中定义的所有依赖项,以及它们所有的依赖项。

C:\2f1e567c929f42473046a8ad3d364a97

您会注意到,运行package Maven 生命周期会导致所有的测试都在运行。假设我们编写的测试涉及启动 web 浏览器,如果您只想生成 JAR 文件,这些测试可能会碍事。为了防止测试运行,右键单击 package lifecycle 菜单项并选择Create 'webdrivertraining...'...选项。

C:\c00ed64c68f139e3bbf3bf836c200a12

然后选择Runner选项卡。取消选择Use project settings选项,然后选择Skip tests选项。点击OK按钮保存更改后的内容。

C:\f3de0341a982365112d4ac0fb3ff17d8

这将创建一个名为webdrivertraining [package]的新配置。在选中这个配置的情况下,单击绿色箭头将运行 Maven 包的生命周期,但是跳过测试,允许您快速构建 JAR 文件。

C:\1eaaa83ad11078700649ee9e0810bd14

由此产生的 UberJAR 文件提供了一个方便的、自包含的包,我们可以轻松地分发和运行它,这就是我们将作为 AWS Lambda 部署的内容。在下一篇文章中,我们将添加在 Lambda 中运行 WebDriver 测试所需的代码,并使用无服务器应用程序发布 UberJAR 文件。

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

创建 Octopus 部署步骤模板- Octopus 部署

原文:https://octopus.com/blog/creating-an-octopus-deploy-step-template

Creating an Octopus Deploy step template

今天,我将在 Octopus Deploy 中创建一个自定义步骤模板

自定义步骤模板对于扩展 Octopus 的功能非常有用。他们还可以在您的项目中标准化操作。

我在 Firebase 上主持了一些项目,所以我选择为 Firebase CLI deploy 命令创建一个模板。

我们开始吧!

选择基础模板

我需要做出的第一个决定是,我将在哪个现有的步骤模板上进行构建。

我将一个包的内容部署到 Firebase,所以部署一个包看起来是一个合理的选择。部署包步骤用于将包的内容部署到它将运行的机器或 PaaS 目标。

对于我的 Firebase 部署来说,情况并非如此。

我只需要对我的文件运行 Firebase CLI。运行命令的机器并不重要。运行脚本步骤非常适合于此。

我点击运行脚本模板上的添加,这将我带到步骤模板编辑器。

设置

Octopus 让我进入设置选项卡。在这里,我可以提供名称、徽标和描述。

选择一个好的、描述性的步骤名称至关重要。这个名字对我来说很容易。因为我包装了 Firebase deploy 命令,所以我将我的步骤称为 Firebase Deploy

接下来是 logo。徽标不是必需的,但我喜欢添加它们。看到与技术相关的技术的标志为这一步提供了一点天赋。

最后,我给这个步骤一个描述。描述字段支持降价。我利用这一点链接回 Firebase CLI 文档:

Firebase Deploy Step Settings

下一步做什么?

接下来我应该向我的步骤添加参数,还是添加步骤细节和逻辑?

看情况。我通常从我知道我会需要的参数开始。然后,我开始处理步骤细节,并在进行过程中添加新发现的参数。

在这篇文章中,我描述了所有的参数,然后才进入步骤细节。

因素

我为包含我的 Firebase 资产的包添加了一个参数。打包参数是 Octopus Deploy 中相对较新的特性。包参数允许我的步骤的使用者提供一个包供该步骤使用。

当添加参数时,我提供名称、标签、帮助文本和控件类型。

我将我的包参数命名为FirebaseDeploy.Package。参数的前缀将防止与其他 Octopus 变量的名称冲突。

我给它标签和控件类型Package。我将帮助文本设置为“包含正在部署的 Firebase 项目的包”

一个参数下降:

Firebase package parameter settings

其余的参数将遵循类似的命名约定,只有类型不同。

Firebase Path:包含 Firebase CLI 的目录的路径,如果不在$PATH 中。

CI Token:Octopus 需要 CI 令牌才能代表我使用 Firebase CI。我把这个参数设为敏感参数。敏感参数值将在数据库中加密,并在日志中屏蔽。

Public Path:覆盖 firebase.json 中指定的托管公共目录。

Message:描述此部署的可选消息。

Force?:删除当前工作目录中缺失的云功能,无需确认。Force?是一个复选框参数。

Only Targets:仅部署到指定的逗号分隔目标(例如hosting,storage)。

Except Targets:部署到指定目标以外的所有目标(如database)。

Print Command?:我添加这个参数是为了在命令运行时使用set -x打印命令。在调试和测试该步骤时,这很方便。

Parameters for the Firebase Deploy step

步骤

参数排序后,我切换到步骤选项卡。我的脚本源代码将保持设置为内联源代码包中的脚本文件如果您的团队在包中存储了标准脚本,那么它会非常有用。它不太适合您计划提供给公众的模板(伏笔)。

步骤选项卡下有内联源代码部分。我将跳过这一步,创建一个引用包,因为我的脚本需要它。

参考包

引用的包是额外的包,您可以在运行脚本步骤中使用。

在这一步中,引用的包是包含我们的 Firebase 资产的包。我将把包参数作为引用包连接到脚本。

我可以选择直接选择一个包,但我将设置更改为Let the project select the package。然后我从列表中选择我的包参数。我可以通过使用参数名FirebaseDeploy.Package在我的脚本中使用这个包。

我确保去查Extract package during deployment

Referenced package

剧本

我选择用 Bash 编写我的脚本,因为我预计大多数消费者会在 Linux Worker 上运行它。

我的脚本的第一部分是获取参数值并将它们存储在变量中。

第一个变量是提取包的路径。变量名与其他变量名略有不同。Octopus.Action.Package变量是一个集合。我使用参考包的名称作为索引来访问包信息:

packagePath=$(get_octopusvariable "Octopus.Action.Package[FirebaseDeploy.Package].ExtractedPath") 

其余的变量都很标准,我用它们的名字来引用它们:

token=$(get_octopusvariable "FirebaseDeploy.CIToken")
public=$(get_octopusvariable "FirebaseDeploy.Public")
message=$(get_octopusvariable "FirebaseDeploy.Message")
force=$(get_octopusvariable "FirebaseDeploy.Force")
only=$(get_octopusvariable "FirebaseDeploy.Only")
except=$(get_octopusvariable "FirebaseDeploy.Except")
printCommand=$(get_octopusvariable "FirebaseDeploy.PrintCommand")
firebasePath=$(get_octopusvariable "FirebaseDeploy.FirebasePath") 

如果步骤消费者为 Firebase 提供了一个路径,我将它添加到 path 变量中:

if [ ! -z "$firebasePath" ] ; then
    PATH=$firebasePath:$PATH
fi 

如果Force?被选中,我将该值设为 true。如果不是,我就取消设置:

if [ "$force" = "True" ] ; then
    force=true
else
    force=
fi 

如果Print Command?被选中,我打开命令跟踪:

if [ "$printCommand" = "True" ] ; then
    set -x
fi 

最后,我切换到包目录并调用 Firebase deploy 命令:

cd $packagePath

firebase deploy ${public:+ -p "$public"} ${message:+ -m "$message"} ${force:+ -f} ${only:+ --only "$only"} ${except:+ --except "$except"} --token $token 

对于可选参数,我用了最近学的一个招数

在下面的示例中,如果public为空或未设置,则命令中不会添加任何内容。如果public不为空或未置位,则将-p "$public"添加到命令中。

${public:+ -p "$public"} 

瞧,我已经完成了剧本。

测试

将步骤模板保存在我的库中后,我可以将其添加到部署流程中。自定义步骤模板在库步骤模板类别中:

Adding the Firebase step to a deployment process

我将该步骤配置为在我的 Worker 上运行,并在安装了 Firebase CLI 的容器中运行:

Firebase step configured to run on a worker

我将参数设置为我在部署中使用的值。

首先,我从内置提要中选择了包。

我将 CI 令牌设置为存储令牌的敏感变量。

基于我的 firebase.json 设置,我只设置了目标hosting:blog

最后,我检查了Print Command?,这样我就可以看到该步骤构建的命令:

Set parameters for Firebase step

我创建了一个版本,并将其部署到我的环境中。一切看起来都很好!

Firebase deploy logs

结论

自定义步骤模板对于扩展 Octopus 的功能非常有用。他们还可以在您的项目中标准化操作。

我为 Firebase CLI deploy 命令创建了一个自定义模板,可以在我的任何项目中使用。

下次

我提到过,我希望这一步涵盖所有的部署参数,并可供其他人使用。阅读我的下一篇文章,向 Octopus Deploy 社区库贡献一个步骤模板,了解我如何向 Octopus Deploy 库提交这个步骤。

使用 CloudFormation - Octopus Deploy 创建 EC2 Octopus Worker

原文:https://octopus.com/blog/creating-aws-cf-octopus-worker-cloudformation

Workers 允许您将部署的执行委托给一台机器,该机器具有对正在修改的资源的特权访问,安装了专门的工具,或者只是为了消除从 Octopus 服务器执行部署的负担。

EC2 实例为托管 Octopus 工人提供了一个逻辑解决方案。

在本文中,您将学习如何使用 CloudFormation 将 Octopus Worker 部署到新的 EC2 实例上。

完整的模板

下面的 CloudFormation 模板在新 VPC 的公共子网中部署了一个 EC2 实例,并作为 VMs 初始化的一部分安装了一个 Octopus 触手作为 Worker:

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  InstanceTypeParameter:
    Type: String
    Default: t3a.medium
    AllowedValues:
    - c1.medium
    - c1.xlarge
    - c3.2xlarge
    - c3.4xlarge
    - c3.8xlarge
    - c3.large
    - c3.xlarge
    - c4.2xlarge
    - c4.4xlarge
    - c4.8xlarge
    - c4.large
    - c4.xlarge
    - c5.12xlarge
    - c5.18xlarge
    - c5.24xlarge
    - c5.2xlarge
    - c5.4xlarge
    - c5.9xlarge
    - c5.large
    - c5.metal
    - c5.xlarge
    - c5a.12xlarge
    - c5a.16xlarge
    - c5a.24xlarge
    - c5a.2xlarge
    - c5a.4xlarge
    - c5a.8xlarge
    - c5a.large
    - c5a.xlarge
    - c5d.12xlarge
    - c5d.18xlarge
    - c5d.24xlarge
    - c5d.2xlarge
    - c5d.4xlarge
    - c5d.9xlarge
    - c5d.large
    - c5d.metal
    - c5d.xlarge
    - c5n.18xlarge
    - c5n.2xlarge
    - c5n.4xlarge
    - c5n.9xlarge
    - c5n.large
    - c5n.metal
    - c5n.xlarge
    - c6g.12xlarge
    - c6g.16xlarge
    - c6g.2xlarge
    - c6g.4xlarge
    - c6g.8xlarge
    - c6g.large
    - c6g.medium
    - c6g.metal
    - c6g.xlarge
    - c6gd.12xlarge
    - c6gd.16xlarge
    - c6gd.2xlarge
    - c6gd.4xlarge
    - c6gd.8xlarge
    - c6gd.large
    - c6gd.medium
    - c6gd.metal
    - c6gd.xlarge
    - d2.2xlarge
    - d2.4xlarge
    - d2.8xlarge
    - d2.xlarge
    - g2.2xlarge
    - g2.8xlarge
    - g3.16xlarge
    - g3.4xlarge
    - g3.8xlarge
    - g4dn.12xlarge
    - g4dn.16xlarge
    - g4dn.2xlarge
    - g4dn.4xlarge
    - g4dn.8xlarge
    - g4dn.metal
    - g4dn.xlarge
    - i2.2xlarge
    - i2.4xlarge
    - i2.8xlarge
    - i2.xlarge
    - i3.16xlarge
    - i3.2xlarge
    - i3.4xlarge
    - i3.8xlarge
    - i3.large
    - i3.metal
    - i3.xlarge
    - i3en.12xlarge
    - i3en.24xlarge
    - i3en.2xlarge
    - i3en.3xlarge
    - i3en.6xlarge
    - i3en.large
    - i3en.metal
    - i3en.xlarge
    - inf1.24xlarge
    - inf1.2xlarge
    - inf1.6xlarge
    - inf1.xlarge
    - m1.large
    - m1.medium
    - m1.small
    - m1.xlarge
    - m2.2xlarge
    - m2.4xlarge
    - m2.xlarge
    - m3.2xlarge
    - m3.large
    - m3.medium
    - m3.xlarge
    - m4.10xlarge
    - m4.16xlarge
    - m4.2xlarge
    - m4.4xlarge
    - m4.large
    - m4.xlarge
    - m5.12xlarge
    - m5.16xlarge
    - m5.24xlarge
    - m5.2xlarge
    - m5.4xlarge
    - m5.8xlarge
    - m5.large
    - m5.metal
    - m5.xlarge
    - m5a.12xlarge
    - m5a.16xlarge
    - m5a.24xlarge
    - m5a.2xlarge
    - m5a.4xlarge
    - m5a.8xlarge
    - m5a.large
    - m5a.xlarge
    - m5ad.12xlarge
    - m5ad.16xlarge
    - m5ad.24xlarge
    - m5ad.2xlarge
    - m5ad.4xlarge
    - m5ad.8xlarge
    - m5ad.large
    - m5ad.xlarge
    - m5d.12xlarge
    - m5d.16xlarge
    - m5d.24xlarge
    - m5d.2xlarge
    - m5d.4xlarge
    - m5d.8xlarge
    - m5d.large
    - m5d.metal
    - m5d.xlarge
    - m5zn.12xlarge
    - m5zn.2xlarge
    - m5zn.3xlarge
    - m5zn.6xlarge
    - m5zn.large
    - m5zn.metal
    - m5zn.xlarge
    - m6g.12xlarge
    - m6g.16xlarge
    - m6g.2xlarge
    - m6g.4xlarge
    - m6g.8xlarge
    - m6g.large
    - m6g.medium
    - m6g.metal
    - m6g.xlarge
    - m6gd.12xlarge
    - m6gd.16xlarge
    - m6gd.2xlarge
    - m6gd.4xlarge
    - m6gd.8xlarge
    - m6gd.large
    - m6gd.medium
    - m6gd.metal
    - m6gd.xlarge
    - m6i.12xlarge
    - m6i.16xlarge
    - m6i.24xlarge
    - m6i.2xlarge
    - m6i.32xlarge
    - m6i.4xlarge
    - m6i.8xlarge
    - m6i.large
    - m6i.metal
    - m6i.xlarge
    - r3.2xlarge
    - r3.4xlarge
    - r3.8xlarge
    - r3.large
    - r3.xlarge
    - r4.16xlarge
    - r4.2xlarge
    - r4.4xlarge
    - r4.8xlarge
    - r4.large
    - r4.xlarge
    - r5.12xlarge
    - r5.16xlarge
    - r5.24xlarge
    - r5.2xlarge
    - r5.4xlarge
    - r5.8xlarge
    - r5.large
    - r5.metal
    - r5.xlarge
    - r5a.12xlarge
    - r5a.16xlarge
    - r5a.24xlarge
    - r5a.2xlarge
    - r5a.4xlarge
    - r5a.8xlarge
    - r5a.large
    - r5a.xlarge
    - r5ad.12xlarge
    - r5ad.16xlarge
    - r5ad.24xlarge
    - r5ad.2xlarge
    - r5ad.4xlarge
    - r5ad.8xlarge
    - r5ad.large
    - r5ad.xlarge
    - r5d.12xlarge
    - r5d.16xlarge
    - r5d.24xlarge
    - r5d.2xlarge
    - r5d.4xlarge
    - r5d.8xlarge
    - r5d.large
    - r5d.metal
    - r5d.xlarge
    - r5n.12xlarge
    - r5n.16xlarge
    - r5n.24xlarge
    - r5n.2xlarge
    - r5n.4xlarge
    - r5n.8xlarge
    - r5n.large
    - r5n.metal
    - r5n.xlarge
    - r6g.12xlarge
    - r6g.16xlarge
    - r6g.2xlarge
    - r6g.4xlarge
    - r6g.8xlarge
    - r6g.large
    - r6g.medium
    - r6g.metal
    - r6g.xlarge
    - r6gd.12xlarge
    - r6gd.16xlarge
    - r6gd.2xlarge
    - r6gd.4xlarge
    - r6gd.8xlarge
    - r6gd.large
    - r6gd.medium
    - r6gd.metal
    - r6gd.xlarge
    - t1.micro
    - t2.2xlarge
    - t2.large
    - t2.medium
    - t2.micro
    - t2.nano
    - t2.small
    - t2.xlarge
    - t3.2xlarge
    - t3.large
    - t3.medium
    - t3.micro
    - t3.nano
    - t3.small
    - t3.xlarge
    - t3a.2xlarge
    - t3a.large
    - t3a.medium
    - t3a.micro
    - t3a.nano
    - t3a.small
    - t3a.xlarge
    - t4g.2xlarge
    - t4g.large
    - t4g.medium
    - t4g.micro
    - t4g.nano
    - t4g.small
    - t4g.xlarge
    - z1d.12xlarge
    - z1d.2xlarge
    - z1d.3xlarge
    - z1d.6xlarge
    - z1d.large
    - z1d.metal
    - z1d.xlarge
  WorkstationIp:
    Type: String
    Description: The IP address of the workstation that can RDP into the instance.
  Key:
    Type: String
    Description: The key used to access the instance.
  OctopusURL:
    Type: String
    Description: The URL of the Octopus instance to connect to.
  OctopusAPI:
    Type: String
    Description: The Octopus API key.
  OctopusSpace:
    Type: String
    Description: The Octopus space.
  OctopusWorkerPool:
    Type: String
    Description: The Octopus worker pool.
Mappings:
  RegionMap:
    eu-north-1:
      ami: ami-0f541966b45340fce
    ap-south-1:
      ami: ami-00c7dbcc1310fd066
    eu-west-3:
      ami: ami-0bce8e5f8fd912af2
    eu-west-2:
      ami: ami-02a7ca2a9d03676bf
    eu-west-1:
      ami: ami-02c55114d1d2a8201
    ap-northeast-3:
      ami: ami-02d358004635cea15
    ap-northeast-2:
      ami: ami-0dcf592770858a733
    ap-northeast-1:
      ami: ami-0a428a8bcfce0f804
    sa-east-1:
      ami: ami-035b4cb75ab88f259
    ca-central-1:
      ami: ami-0583af09af7e435f3
    ap-southeast-1:
      ami: ami-09df7bed19956d10b
    ap-southeast-2:
      ami: ami-0666dd0a9eccbab7d
    eu-central-1:
      ami: ami-0e8f6957a4eb67446
    us-east-1:
      ami: ami-0ba45cd7fcf163404
    us-east-2:
      ami: ami-086e001f1a73d208c
    us-west-1:
      ami: ami-0bd3976c0dbacc605
    us-west-2:
      ami: ami-04e6179c63d17513d
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: Linux VPC
  InternetGateway:
    Type: AWS::EC2::InternetGateway
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 0
        - !GetAZs 
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      MapPublicIpOnLaunch: true
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  InternetRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGateway
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTable
  SubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetA
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "Internet Group"
      GroupDescription: "SSH in, all traffic out."
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp:  !Sub ${WorkstationIp}/32
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
  ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref Linux
  Linux:
    Type: 'AWS::EC2::Instance'
    Properties:
      SubnetId: !Ref SubnetA
      ImageId: !FindInMap
        - RegionMap
        - !Ref 'AWS::Region'
        - ami
      InstanceType:
        Ref: InstanceTypeParameter
      KeyName: !Ref Key
      SecurityGroupIds:
        - Ref: InstanceSecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeSize: 250
      Tags:
        - Key: Name
          Value: Linux Server

      UserData:
        Fn::Base64:
          Fn::Sub: |
            #cloud-boothook
            #!/bin/bash
            # Wait for network connectivity
            until ping -c1 www.google.com &>/dev/null; do
                echo "Waiting for network ..."
                sleep 1
            done
            # Update all packages
            sudo yum update -y
            # Install useful tools
            sudo yum install jq wget curl awscli -y
            # Install Kubernetes cli tools
            curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl
            chmod +x ./kubectl
            sudo mv kubectl /usr/local/bin
            curl -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/aws-iam-authenticator
            chmod +x ./aws-iam-authenticator
            sudo mv aws-iam-authenticator /usr/local/bin
            curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
            sudo mv /tmp/eksctl /usr/local/bin
            # Install Linux Tentacle
            sudo wget https://rpm.octopus.com/tentacle.repo -O /etc/yum.repos.d/tentacle.repo
            sudo yum install tentacle -y
            sudo /opt/octopus/tentacle/Tentacle create-instance --instance "Tentacle" --config "/etc/octopus/Tentacle/tentacle-Tentacle.config"
            sudo /opt/octopus/tentacle/Tentacle new-certificate --instance "Tentacle" --if-blank
            sudo /opt/octopus/tentacle/Tentacle configure --instance "Tentacle" --app "/home/Octopus/Applications" --noListen "True" --reset-trust
            sudo /opt/octopus/tentacle/Tentacle register-worker --instance "Tentacle" --server "${OctopusURL}" --name "$(hostname)" --comms-style "TentacleActive" --server-comms-port "10943" --apiKey "${OctopusAPI}" --space "${OctopusSpace}" --workerpool "${OctopusWorkerPool}"
            sudo /opt/octopus/tentacle/Tentacle service --install --start --instance "Tentacle"
Outputs:
  PublicIp:
    Value:
      Fn::GetAtt:                          
        - Linux
        - PublicIp
    Description: Server's PublicIp Address 

这个模板创建了许多资源,所以让我们来分解一下。

可用的实例类型在名为InstanceType的参数中定义。虽然工作人员通常不需要安装在高性能机器上,但不同 EC2 实例的网络功能可能需要选择特定的实例类型:

Parameters:
  InstanceTypeParameter:
    Type: String
    Default: t3a.medium
    AllowedValues:
    - c1.medium
    - c1.xlarge
    - c3.2xlarge
    # ... 

为了增加安全性,只有您的本地工作站可以 SSH 到 EC2 实例。你可以谷歌你的 IP 地址并在WorkstationIp参数中定义它:

 WorkstationIp:
    Type: String
    Description: The IP address of the workstation that can RDP into the instance. 

名为Key的参数定义了分配给 EC2 实例的 SSH 密钥。此 CloudFormation 模板不创建键,因此必须指定一个现有键:

 Key:
    Type: String
    Description: The key used to access the instance. 

工人连接到的 Octopus 实例的 URL 在OctopusURL参数中定义。注意,这个模板创建了一个轮询工作器,这意味着 Octopus 实例必须是可公开访问的:

 OctopusURL:
    Type: String
    Description: The URL of the Octopus instance to connect to. 

用于验证 Octopus 服务器的 API 密钥在OctopusAPI参数中定义:

 OctopusAPI:
    Type: String
    Description: The Octopus API key. 

用于注册工人的 Octopus 空间在OctopusSpace参数中定义:

 OctopusSpace:
    Type: String
    Description: The Octopus space. 

用于放置工人的工人池的名称在OctopusWorkerPool参数中定义:

 OctopusWorkerPool:
    Type: String
    Description: The Octopus worker pool. 

AWS 中的每个可用性区域都有自己唯一的 AMI IDs。Mappings部分将 Amazon ECS 优化的 AMI 映射到每个地区。

您使用 ECS 优化的 AMI,因为它已经安装了 Docker,这对于在执行容器中运行部署非常有用:

Mappings:
  RegionMap:
    eu-north-1:
      ami: ami-0f541966b45340fce
    ap-south-1:
      ami: ami-00c7dbcc1310fd066
    eu-west-3:
      ami: ami-0bce8e5f8fd912af2
    # ... 

所有 EC2 实例必须放在一个虚拟私有云(VPC)中,用一个 AWS EC2 VPC 资源创建。

您定义了一个无类域间路由(CIDR)块10.0.0.0/16,这意味着分配给 VPC 的所有子网必须具有以10.0开头的 IP 地址:

 VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: Linux VPC 

互联网网关提供与互联网之间的连接。它由AWSEC2internet gateway资源表示:

 InternetGateway:
    Type: AWS::EC2::InternetGateway 

互联网网关使用AWSEC2VPCGatewayAttachment资源连接到 VPC:

 VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway 

使用 AWS EC2 子网资源将子网连接到 VPC。通过使用!Select函数从!GetAZs数组返回第一个项目,可以避免对可用性区域进行硬编码。

CIDR 块被设置为10.0.0.0/24,表示该子网中资源的 IP 地址都以10.0.0开头。将MapPublicIpOnLaunch设置为true意味着放置在该子网中的任何 EC2 实例都会收到一个动态的公共 IP 地址,允许您通过 SSH 访问它们:

 SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 0
        - !GetAZs 
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      MapPublicIpOnLaunch: true 

路由表定义了与该 VPC 相关的流量的网络规则,并由一个AWSEC2route table资源定义:

 RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC 

外部互联网流量通过由 AWS EC2 Route 资源表示的路由被定向到互联网网关。CIDR 块0.0.0.0/0匹配所有 IPv4 地址,这意味着此规则匹配所有未由处理 VPC 内部流量的默认规则配置的流量:

 InternetRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGateway
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTable 

路由表与使用AWSEC2SubnetRouteTableAssociation资源的子网相关联:

 SubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetA 

为了允许您的本地工作站 SSH 到 EC2 实例,一个安全组被配置为向来自您的本地 IP 地址的任何流量开放端口 22。安全组还允许将流量发送到任何目的地。安全组由AWSEC2security group资源表示。

因为您配置了轮询触手,它建立了从 Worker 到 Octopus 服务器的出站连接,所以不需要打开任何端口来允许从 Octopus 到 EC2 实例的流量。安全组允许 Octopus 服务器响应工人提出的请求。

如果您配置了一个监听触手,Octopus 在那里建立了到 Worker 的网络连接,那么您必须向与您的托管实例相关联的静态 IP 列表或者您的自托管 Octopus 实例的 IP 地址打开端口 10933。

不过,轮询触角更容易通过防火墙进行配置,这就是这里显示的解决方案:

 InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "Internet Group"
      GroupDescription: "SSH in, all traffic out."
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp:  !Sub ${WorkstationIp}/32
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0 

为了方便起见,您可以为 EC2 实例分配一个静态(或弹性)IP 地址。如果没有静态 IP 地址,EC2 实例在关闭并再次启动时会收到一个新的随机公共 IP 地址。静态地址消除了在登录 EC2 之前确认 IP 地址的需要。

弹性 IP 地址由 AWS EC2 EIP 资源表示:

 ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref Linux 

前面所有的资源都需要为您提供一个放置 EC2 实例和配置其网络的位置。最后的资源是 EC2 实例本身,由 AWS EC2 实例资源表示。

该资源引用来自Mappings部分的 AMI IDs,加入子网,链接到安全组,并使用 SSH 密钥进行配置。它还定义了一个比默认情况下提供的更大的硬盘:

 Linux:
    Type: 'AWS::EC2::Instance'
    Properties:
      SubnetId: !Ref SubnetA
      ImageId: !FindInMap
        - RegionMap
        - !Ref 'AWS::Region'
        - ami
      InstanceType:
        Ref: InstanceTypeParameter
      KeyName: !Ref Key
      SecurityGroupIds:
        - Ref: InstanceSecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeSize: 250
      Tags:
        - Key: Name
          Value: Linux Server 

用户数据脚本在提供实例后运行。在这里,您可以安装部署通常需要的任何专用工具,安装 Octopus 触手,并将触手配置为工作器。

需要注意的一个问题是,当执行这个脚本时,网络可能不可用。这已经在 StackOverflow 上讨论过了。

为了确保任何后续命令都可以访问网络,您必须进入一个循环,等待 ping 一个已知且可靠的站点(如 Google)成功:

 UserData:
        Fn::Base64:
          Fn::Sub: |
            #cloud-boothook
            #!/bin/bash
            # Wait for network connectivity
            until ping -c1 www.google.com &>/dev/null; do
                echo "Waiting for network ..."
                sleep 1
            done 

应用所有操作系统更新,并安装 AWS CLI、jq、wget 和 curl 等常用工具:

 # Update all packages
            sudo yum update -y
            # Install useful tools
            sudo yum install jq wget curl awscli -y 

部署到 Kubernetes 集群要求kubectlPATH上可用。EKS 集群需要一个名为aws-iam-authenticator的附加二进制文件来执行身份验证。eksctl工具提供了一种创建 EKS 集群的简单方法。这些可执行文件被下载并放置在 path 中,以便 Octopus 部署可以使用它们:

 # Install Kubernetes cli tools
            curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl
            chmod +x ./kubectl
            sudo mv kubectl /usr/local/bin
            curl -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/aws-iam-authenticator
            chmod +x ./aws-iam-authenticator
            sudo mv aws-iam-authenticator /usr/local/bin
            curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
            sudo mv /tmp/eksctl /usr/local/bin 

然后你安装章鱼触手:

 # Install Linux Tentacle
            sudo wget https://rpm.octopus.com/tentacle.repo -O /etc/yum.repos.d/tentacle.repo
            sudo yum install tentacle -y 

然后部署一个 Worker,以轮询模式连接到您的 Octopus 实例。

当您在 Linux 中手动配置一个触手时,显示了下面的脚本。要重新创建这些命令,请运行一个手动的触手安装,并复制根据您的输入生成的脚本输出:

 sudo /opt/octopus/tentacle/Tentacle create-instance --instance "Tentacle" --config "/etc/octopus/Tentacle/tentacle-Tentacle.config"
            sudo /opt/octopus/tentacle/Tentacle new-certificate --instance "Tentacle" --if-blank
            sudo /opt/octopus/tentacle/Tentacle configure --instance "Tentacle" --app "/home/Octopus/Applications" --noListen "True" --reset-trust
            sudo /opt/octopus/tentacle/Tentacle register-worker --instance "Tentacle" --server "${OctopusURL}" --name "$(hostname)" --comms-style "TentacleActive" --server-comms-port "10943" --apiKey "${OctopusAPI}" --space "${OctopusSpace}" --workerpool "${OctopusWorkerPool}"
            sudo /opt/octopus/tentacle/Tentacle service --install --start --instance "Tentacle" 

输出捕获分配给 EC2 实例的公共静态 IP 地址:

Outputs:
  PublicIp:
    Value:
      Fn::GetAtt:                          
        - Linux
        - PublicIp
    Description: Server's PublicIp Address 

在这个模板被部署之后,一个新的 Worker 出现在您的 Octopus 实例中,准备开始处理部署:

Octopus Worker

结论

将 Workers 部署为 EC2 实例允许您将部署任务卸载到专用虚拟机,并且可以通过在更接近被修改的 AWS 资源的地方执行部署来提高部署的效率。

在本文中,您了解了一个 CloudFormation 模板,该模板在一个 VPC 中部署了一个 EC2 实例,该实例具有公共互联网访问权限,并且具有安装和配置 Octopus 触手作为工作器的初始化脚本。

我们还有其他关于云形成的帖子,你可能也会觉得有帮助。

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

我们如何思考创造新的架构-章鱼部署

原文:https://octopus.com/blog/creating-new-architecture

An image of a blueprint

在软件中,架构包括:

  • 系统的结构
  • 其组成部分之间的关系
  • 系统发出的属性(有时称为“特征”)
  • 系统编码的行为

一些系统可能包含一个单一的大型软件。其他的可能被分解成更小的子系统,它们一起工作来完成目标。

Diagram

创建新的架构总是一个有趣的挑战:

  • 你从哪里开始?
  • 需要解决哪些问题?
  • 你如何真正地传递东西而不迷失在无尽的兔子洞里?

在这篇文章中,我们将探索如何定义和构建一个新的软件架构来增加你成功的机会。

为什么建筑很重要

好的架构帮助你快速自信地做出决定和改变。想要添加新行为吗?好的架构会使推理和执行变得容易。糟糕的架构会让你在编写一行代码之前就开始思考世界是如何形成的。

卡尔·萨根曾经说过:“如果你希望从零开始做苹果派,你必须首先创造宇宙。”你不希望在软件中执行那种推理——你只是希望从一套好的苹果派配料中推理出来。

快速而自信地做出改变是八达通必须做的。发布管理的前景正以前所未有的速度增长。正在创建新的技术生态系统,可以在其中开发工作负载。新的云服务不断涌现,提供了新的、新颖的方法来托管工作负载。Octopus 的目标是将这些工作负载部署到世界级的新平台上,我们希望尽快在 Octopus 中实现这些体验。

为了确保我们能够实现这一目标,在 Octopus,我们一直在投入时间和精力,为 Octopus 内部的开发步骤创建一个全新的架构。毕竟,步骤是完成部署工作的东西!

目标

软件的成功来自于知道终点线在哪里。你如何知道自己是否成功了?成功不仅仅是拥有一些新软件。如果您正在定义一个全新的架构,或者只是构建一个小的特性,这是正确的。如果你不知道你要去哪里,任何一条路都可以把你带到那里,我们不想去任何地方,我们想要成功!

在与我们的主要利益相关者以及我们的首席执行官 Paul 进行初步合作后,我们就新 steps 架构的以下目标达成了一致:

  • 我们应该能够在 Octopus 服务器版本的带外发布 steps。
  • 步骤应该简单,易于开发。
  • 应该可以用 C#/.NET 之外的技术开发步骤。

这些目标巩固了 Octopus 的业务目标,即建立一个高绩效的团队来提供新的部署能力,并更快地为 Octopus 的客户实现新的部署方案。

目标是必须具备的,因为它们能帮助你思考决策,避免兔子洞。当你决定是否应该以某种方式做某事时,它们提供了一个很好的试金石。如果某件事让你朝着其中一个目标前进,那可能是件好事。如果它让你远离目标,这可能是一件坏事。如果它对一个目标没有任何贡献,那么它可能是不需要的。

限制

一旦我们对定义成功的目标达成一致,我们就可以开始定义架构本身。再说一次,建筑指的是很多东西:

架构包括系统的结构、组件之间的关系、系统发出的属性(有时称为“能力”),以及系统编码的行为。

对于步骤,我们首先通过将“步骤”分解为子系统来定义结构和关系,这样我们就可以对它们进行推理和决策。

其中包括:

  • 步骤用户界面
  • 步骤执行者
  • 输入和输出
  • 编程模型
  • 服务器集成(打包模型)
  • 部署执行

在定义新体系结构时,最初的对话可能具有挑战性。在对话中很难“让飞机着陆”,因为您会围绕一个子系统中的决策将如何影响其他子系统,探索不同的观点,如用户体验、开发人员体验和每一圈的产品体验。一行对话可以让你在子系统中绕一整圈,直到你再次到达你试图决定的起点。

如果你很难在早期做出决定,并且谈话感觉循环或永无止境,你可能会错过一个关键因素:约束

当定义和开发架构时,约束限制了我们的选择自由。

这正是我们早期想要的。我们希望对我们可以施加的约束做出强有力的决定,因为它限制了我们的选择并使决定更容易。

我们的一些目标已经施加了严格的约束,例如,“应该有可能用 C#/.NET 之外的技术开发步骤。”

其他的没有这么严格。例如,“简单且易于开发”提供了上下文,但不是硬性的约束,它是一个定性的目标。

在设计我们的新架构的早期阶段,我们努力围绕步骤的编程和组合模型获得清晰性和一致性。如果用户希望某个步骤的行为“稍有不同”,并对步骤行为的逻辑内部顺序进行重新排序,或者在步骤中注入一些他们自己独特的行为,这是我们应该支持的吗?

通过对上述潜在需求采取立场,我们可以建立一个明确的约束。这个约束影响了架构的许多子系统,UI、执行器、编程模型。它甚至超越了我们架构的界限,可能会影响 Octopus 的其他领域。

通过合作,我们决定不需要在步骤中实现任意组合。如果您遵循这种思路,最终您将需要开发一种 DSL 或编程语言来构建部署流程,因为这是唯一一种足够灵活来满足所有用例的方法。

我们没有追求这一目标,而是决定专注于为用户提供高杠杆、高价值的步骤,并在用户需要自己独特的行为时,为他们提供一种从固执己见的步骤到更灵活的步骤(例如,运行模板、运行脚本/cli)的顺畅方式。

这个约束产生了直接影响,我们可以更清楚地推理我们的 UI、我们的执行器、我们的编程模型,以及对 Octopus 本身的影响,这要感谢这个约束强加的明确限制。

名称

在这些初始对话中,另一个具有挑战性的方面是,你正在谈论尚未命名的新兴概念。

谈论没有名字的事物会很快变得令人沮丧。你倾向于根据自己的背景为别人不理解的事情起自己的名字,你会在谈话中花费过多的精力,只是为了确保每个人都在谈论相同的事情,而不是关注更重要的细节。

令人惊讶的是,一旦你建立了一种共同的语言,对话开始变得如此容易,而不再只是试图解释你正在谈论的架构中的哪个逻辑组件或子系统。

这里最好的方法是召集你的团队,拿出一个白板(虚拟的或物理的),画出你的概念模型,然后开始头脑风暴命名。

当我们这样做时,我们建立了一些指导原则,以确保这些名称不仅在我们的团队中有意义,而且对更广泛的受众也有意义:

指导方针

  • 我们希望用对顾客有意义的方式命名
  • 命名应该简单和自我描述
  • 示例:“我想开发一个自定义步骤!”“我想开发一个部署到 X Cloud 的步骤。”

我们抽象地描述了每个事物,例如“提供步骤 UI 的事物”。然后我们试图给每一个抽象的事物命名。

在流程结束时,我们有了子系统和组件的清晰地图,在讨论我们的架构时,我们可以更自由地谈论它们:

Naming for step components

决策

在 Octopus,我们坚信在决策时要达成共识,然后满怀信心地执行。几乎所有有影响力的决策都会被仔细审查。

在定义架构时,这种审查允许您预见架构选择的系统级影响。这是构建新架构时最难做的事情之一,但也是最重要的事情。

做出高质量决策的第一步是与你的团队紧密合作以达成决策。您已经与他们一起研究了候选解决方案,并对它们的影响达成了共识。在任何足够复杂的系统中,您总是需要从其他人那里获得输入,以确保您能够看到新架构可能带来的所有潜在影响,而您的团队是这方面的最佳起点。

我们在 Octopus 用来征求意见的一个工具是稻草人提案。您向您的团队展示您为给定组件或子系统提出的设计,并对其进行足够详细的解释,以便团队能够对其进行推理。对于稻草人,你不希望你的团队同意它,你希望他们挑战它,指出它的缺陷,提出替代方案和改进意见。这种类型的对话会产生深刻的见解和解决方案选项,引导您做出高质量的决策。

在您的团队给出输入之后,您还需要确保合适的专家已经为您的架构决策提供了输入。

例如,在我们的新架构中,发现了与项目 Bento 的重叠,这是我们新的项目导入/导出系统。通过与开发 Bento 的团队交谈,我们发现在我们的两个计划下有一个共享的系统部分——步骤的输入模型。Bento 需要知道一组给定的输入是否包含一个帐户,或者 Octopus 中其他特定于域的资源。它将使用这些知识来抓取它需要跨空间导出/导入的资源集。我们建议重新定义输入在 Octopus 中的建模方式,所以我们需要确保我们提出的架构仍然能够满足 Bento 的需求。

广泛征求意见并不意味着由委员会设计。所有权很重要,作为架构师,您应该拥有您开发的架构。然而,这意味着您有责任广泛地寻求对您的架构设计的输入,并在您的专业领域之外找到可能需要影响您的设计的其他子系统的专家。

目标和决策

另一件有助于决策的事情是保持目标在头脑中。虽然约束限制了我们的选择,所以我们知道哪些事情我们不需要做出决定,但目标可以帮助我们在多个潜在的有效选项之间做出决定。

目标可以作为试金石。这个决定是让我们朝着这个目标前进,还是让我们离目标更远?确保所有基本决策都考虑到他们。在决定如何实现支撑新架构的各种 API 时,我们已经多次重温了我们的目标,即步骤“简单且易于开发”。

复杂性

架构内的复杂性往往分为两类:

  • 静态复杂性,处理系统的组件及其关系。
  • 突发的复杂性,它来自用户以新颖独特的方式使用你的软件,以及使用对系统影响的整体变化。

架构决策需要将两者都考虑在内。

静态复杂性

静态的复杂性倾向于影响意义的形成;如果你工作的领域非常复杂,就很难做出决定。很难推理出您的决策可能影响各种子系统的所有方式。

这个问题的解决方法是深入分析。我们在 Octopus 中大量使用了异想天开的,但是其他图表工具在这些场景中也会有所帮助。您需要一个工具来构建流程图,并可视化特定上下文中各种子系统之间的连接。这将有助于你确定做决定时需要考虑的所有地方。

这种分析是不可避免的。如果你不这样做,你会做出错误的假设,这些假设会在以后伤害你。这种类型的分析也很难“外包”。有人可能能够描述特定子系统的内部工作方式,但是如果他们没有详细的地图提供给你,你很可能需要找出代码并绘制图表。

涌现复杂性

突发的复杂性来自于试图预测人类如何与系统交互,或者系统的使用如何随着时间的推移而改变,以及您将需要如何适应这种改变。

我们可以尝试限制我们架构中出现的复杂性,或者承认并控制它。

为了限制它,我们可以回到约束。我们能限制用户使用我们系统的方式吗?这将限制可能出现的复杂性,也将简化我们的决策。

如果我们有适当的约束,我们可以看看某些实现决策如何帮助我们控制突发的复杂性。

当我们决定如何表达一个步骤的 UI 时,我们面临着一个决定:我们应该让用户自带 HTML、JavaScript 和框架来表达步骤 UI 吗?让他们只提供一些 HTML 怎么样?如果是用代码写的 API 呢?简单的老式声明性 JSON 怎么样?这些解决方案中的每一个都会对突发的复杂性产生非常不同的影响。

为了应对这种复杂性并帮助做出决策,我们创建了一个决策矩阵来帮助可视化每个选项如何解决或不解决每个复杂性。

Decision matrix for step UI

通过列举一个解决方案可能对我们有贡献的属性,并根据这些属性评估每个候选解决方案,我们可以做出考虑到我们的突发复杂性的决策。

我们决定实现一个定制的 UI 框架,也就是说,可以用来表达一个步骤的 UI 的代码。这给了人们用代码和熟悉的工具实现 UI 的能力和灵活性,但它避免了人们提供任意 HTML 和 JavaScript 所带来的复杂性。

“感受”

能力,或系统质量属性,指的是系统可能需要遵守的非功能需求。

一个好的架构会释放出支持你认为重要的“特性”的属性。这些“能力”倾向于贯穿一个架构中的所有子系统。

对于新的 steps 架构来说,一个我们非常重视的“能力”的例子是可维护性

如果我们需要在步骤和 Octopus 本身之间的接口边界的一侧进行更改,我们希望确保不需要强制将更改传播到数百个步骤中(或者要求步骤作者做同样的事情)。

知道这对我们的架构很重要,我们非常详细地关注我们的 API 表面,它形成了 steps 和 Octopus 之间的接口,关注版本控制和兼容性。

在 steps 体系结构中有许多兼容性表面。确保这些表面具有显式的版本控制,将允许我们随着时间的推移对它们进行更改,并随着它们的发展对它们的兼容性做出慎重的决定。

在早期对可能对您的架构重要的各种“能力”进行头脑风暴是很重要的,以便您可以在您的架构发展过程中考虑它们。

结论

好的软件架构允许您随着软件的增长而降低软件的复杂性,使您能够快速而自信地开发新的功能,或者更改现有的功能。

优秀的架构:

  • 建立在一系列与业务目标相关的明确目标之上。
  • 表达了限制体系结构需要支持的复杂性的强约束。
  • 为子系统和组件建立通用语言。
  • 是在许多高质量决策的基础上发展起来的,这些决策已经应用了适当的分析、审查和专家意见。
  • 承认复杂性,确保通过深入分析来理解复杂性,并为紧急情况而设计。
  • 解决重要的“问题”,确保在架构中定义的所有子系统中考虑和设计这些问题。

使用 CloudFormation - Octopus Deploy 创建 RDS 实例

原文:https://octopus.com/blog/creating-rds-instance-cloudformation

亚马逊关系数据库服务 (RDS)实施托管数据库,支持多种平台,如 MySQL、MariaDB、Oracle、Postgres 和 SQL Server。几乎每个定制应用程序都需要持久的数据存储,而 RDS 提供了一个方便、可伸缩且高度可用的解决方案。

在本文中,您将学习如何使用 CloudFormation 模板部署 RDS 实例。

RDS 云形成模板

下面的模板将 RDS 实例部署到具有两个专用子网的新 VPC 中:

Parameters:
  Tag:
    Type: "String"
  DBUsername:
    Type: "String" 
  DBPassword:
    Type: "String" 

Resources: 
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
      - Key: "Name"
        Value: !Ref "Tag"

  SubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 0
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.0.0/24"

  SubnetB:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 1
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.1.0/24"

  RouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref "VPC"

  SubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupName: "subnetgroup"
      DBSubnetGroupDescription: "Subnet Group"
      SubnetIds:
      - !Ref "SubnetA"
      - !Ref "SubnetB"

  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "RDS traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0"

  InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup"

  RDSCluster:
    Type: "AWS::RDS::DBCluster"
    Properties:
      DBSubnetGroupName: !Ref "SubnetGroup"
      MasterUsername: !Ref "DBUsername"
      MasterUserPassword: !Ref "DBPassword"
      DatabaseName: "products"
      Engine: "aurora"
      EngineMode: "serverless"
      VpcSecurityGroupIds:
      - !Ref "InstanceSecurityGroup"
      ScalingConfiguration:
        AutoPause: true
        MaxCapacity: 16
        MinCapacity: 2
        SecondsUntilAutoPause: 300

Outputs:
  VpcId:
    Description: The VPC ID
    Value: !Ref VPC 

VPC、子网和路由表在之前的文章中有描述。然后,该模板将大量附加资源放入 VPC 中,以支持或创建 RDS 实例。

RDS 实例至少需要 2 个子网来实现高可用性。这些子网被组合在一个AWSRDSDBSubnetGroup资源中:

 SubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupName: "subnetgroup"
      DBSubnetGroupDescription: "Subnet Group"
      SubnetIds:
      - !Ref "SubnetA"
      - !Ref "SubnetB" 

对 RDS 实例的网络访问是在一个安全组中定义的,由一个AWSEC2security group资源表示。该安全组允许所有出站流量,但不为入站流量指定任何规则。入站流量规则由另一个资源负责:

 InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "RDS traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0" 

公共流量很难访问生产数据库。事实上,像 Aurora Serverless(您接下来创建的)这样的 RDS 解决方案只能在 VPC 中使用:

您不能为 Aurora 无服务器 v1 DB 群集提供公共 IP 地址。您只能从 VPC 中访问 Aurora 无服务器 v1 DB 集群。

要授予 VPC 中的资源对 RDS 实例的访问权限,您需要创建一个入口规则,将网络流量授予已被分配了上述安全组的任何资源。允许共享一个安全组的资源相互通信是对相关资源进行分组的一种便捷方式,而不必将它们划分到特殊的 CIDR 块中。

入口规则由AWSEC2security group press资源表示:

 InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup" 

现在,部署 RDS 实例的一切都已就绪,由 AWS RDS DBCluster 资源表示。下面的例子创建了一个无服务器 Aurora 实例:

 RDSCluster:
    Type: "AWS::RDS::DBCluster"
    Properties:
      DBSubnetGroupName: !Ref "SubnetGroup"
      MasterUsername: !Ref "DBUsername"
      MasterUserPassword: !Ref "DBPassword"
      DatabaseName: "products"
      Engine: "aurora"
      EngineMode: "serverless"
      VpcSecurityGroupIds:
      - !Ref "InstanceSecurityGroup"
      ScalingConfiguration:
        AutoPause: true
        MaxCapacity: 16
        MinCapacity: 2
        SecondsUntilAutoPause: 300 

结论

RDS 提供了一个可管理的、可伸缩的、高度可用的数据库平台,支持许多流行的数据库提供商。

这篇文章建立在我们描述带有私有子网的 VPC 的文章的基础上,并展示了部署一个无服务器的 Aurora RDS 实例所需的资源,该实例带有安全组,可以连接到任何需要数据库访问的额外资源。

我们有其他关于云形成模板的帖子,你可能也会觉得有帮助。

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

Selenium 系列:创建框架- Octopus Deploy

原文:https://octopus.com/blog/selenium/4-creating-the-framework/creating-the-framework

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

WebDriver API 的优势之一是它是浏览器不可知的。你可以从之前的帖子中看到,在我们的测试中,只需要一个新的二进制驱动程序和一个新的驱动程序类就可以启动 Firefox,而不是 Chrome。

尽管 WebDriver 允许我们编写测试而不用担心哪个浏览器会运行它们,但我们仍然需要创建和配置各种驱动程序类,如ChromeDriverFirefoxDriver。为了使这个过程尽可能灵活,我们将创建一个名为AutomatedBrowserFactory的工厂类来为我们配置这些对象。

在创建这个类之前,我们需要在项目中添加一个新目录来保存 Java 文件。我们在以前的文章中创建的目录src/test/java/com/octopus是只在测试中使用的文件的默认位置。在src/main/java/com/octopus下有第二个目录保存常规的 Java 类,我们需要创建这个目录结构。

右键点击src目录,选择新➜目录。

输入main/java/com/octopus作为目录名,点击OK

和以前一样,新的目录结构被创建,但是它还没有被 IntelliJ 识别为保存 Java 类的目录。

要解决这个问题,打开Maven Projects工具窗口并点击Reimport All Maven Projects按钮。

java目录现在显示为蓝色图标,这表示它将保存 Java 类。

我们现在可以在src/main/java/com/octopus目录中创建类AutomatedBrowserFactory。要创建新类,右击octopus文件夹并选择新➜ Java 类。

Name字段输入AutomatedBrowserFactory并点击OK按钮。

在下面的代码片段中,我们有一个工厂框架,它包含一个名为getAutomatedBrowser()的方法,该方法接受我们想要测试的浏览器的名称。这个方法返回一个AutomatedBrowser接口的实例:

package com.octopus;

public class AutomatedBrowserFactory {

  public AutomatedBrowser getAutomatedBrowser(final String browser) {

    if ("Chrome".equalsIgnoreCase(browser)) {
      return getChromeBrowser();
    }

    if ("Firefox".equalsIgnoreCase(browser)) {
      return getFirefoxBrowser();
    }

    throw new IllegalArgumentException("Unknown browser " + browser);

  }

  private AutomatedBrowser getChromeBrowser() {
    return null;
  }

  private AutomatedBrowser getFirefoxBrowser() {
    return null;
  }
} 

接口公开了我们将对浏览器执行的所有交互。首先,我们将定义一些方法来初始化 WebDriver 实例,打开一个 URL,并与通过 ID 定位的元素进行交互。

要创建AutomatedBrowser接口,右击octopus目录并选择新➜ Java 类。

T34

Name字段中输入AutomatedBrowser,从Kind字段中选择Interface选项,点击OK按钮。

然后将以下代码粘贴到新文件中:

package com.octopus;

import org.openqa.selenium.WebDriver;

public interface AutomatedBrowser {

  WebDriver getWebDriver();

  void setWebDriver(WebDriver webDriver);

  void init();

  void destroy();

  void goTo(String url);

  void clickElementWithId(String id);

  void selectOptionByTextFromSelectWithId(String optionText, String id);

  void populateElementWithId(String id, String text);

  String getTextFromElementWithId(String id);

} 

我们将利用装饰模式来构建AutomatedBrowser接口的实例,我们最终将调用该接口来与浏览器交互。

那么为什么要使用装饰模式而不是直接实现AutomatedBrowser的类层次结构呢?

在实现 decorator 模式的过程中,我们赋予自己创建一系列独立实现的能力,这些实现具有增强和定制我们与浏览器交互方式的特性,而不需要试图用一个深层次的类来表示这些实现。

两个明显的实现是配置ChromeDriverFirefoxDriver类的实例,允许我们打开 Chrome 或 Firefox 浏览器。但是当我们浏览这个博客系列时,我们将介绍一系列实现代理、移动浏览器不支持的功能的存根方法、远程浏览器等特性的装饰器。

所有这些灵活性的框架从这里开始。

为了让我们更容易创建装饰类,我们将创建一个名为AutomatedBrowserBase的类,它将实现AutomatedBrowser,并将所有方法调用传递给AutomatedBrowser的父实例。

因为AutomatedBrowserBase类提供了AutomatedBrowser接口中每个方法的实现,所以扩展AutomatedBrowserBase的装饰类只能覆盖特定于它们的方法。这大大减少了创建装饰器所需的锅炉板代码的数量。

注意AutomatedBrowserBase类是在com.octopus.decoratorbase包中创建的。将这个类放在它自己的包中是一个重要的设计决策,我们将在后面的课程中研究这些特性。

要创建新的包,右击octopus目录,选择新➜包。

输入名称decoratorbase,点击OK按钮。

T32

然后,新的包被添加到目录结构中。

T35【

com.octopus.decoratorbase包中,用下面的代码创建一个名为AutomatedBrowserBase的新类。在AutomatedBrowser接口中定义的每个方法都是通过传递给automatedBrowser实例变量来实现的(如果它不是null):

package com.octopus.decoratorbase;

import com.octopus.AutomatedBrowser;
import org.openqa.selenium.WebDriver;

public class AutomatedBrowserBase implements AutomatedBrowser {

  private AutomatedBrowser automatedBrowser;

  public AutomatedBrowserBase() {

  }

  public AutomatedBrowserBase(AutomatedBrowser automatedBrowser) {
    this.automatedBrowser = automatedBrowser;
  }

  public AutomatedBrowser getAutomatedBrowser() {
    return automatedBrowser;
  }

  @Override
  public WebDriver getWebDriver() {
    if (getAutomatedBrowser() != null) {
      return getAutomatedBrowser().getWebDriver();
    }
    return null;
  }

  @Override
  public void setWebDriver(WebDriver webDriver) {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().setWebDriver(webDriver);
    }

  }

  @Override
  public void init() {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().init();
    }
  }

  @Override
  public void destroy() {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().destroy();
    }
  }

  @Override
  public void goTo(String url) {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().goTo(url);
    }
  }

  @Override
  public void clickElementWithId(String id) {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().clickElementWithId(id);
    }
  }

  @Override
  public void selectOptionByTextFromSelectWithId(String optionText, String id) {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().selectOptionByTextFromSelectWithId(optionText, id);
    }
  }

  @Override
  public void populateElementWithId(String id, String text) {
    if (getAutomatedBrowser() != null) {
      getAutomatedBrowser().populateElementWithId(id, text);
    }
  }

  @Override
  public String getTextFromElementWithId(String id) {
    if (getAutomatedBrowser() != null) {
      return getAutomatedBrowser().getTextFromElementWithId(id);
    }

    return null;
  }
} 

现在让我们扩展AutomatedBrowserBase类来创建ChromeDecorator类。ChromeDecorator将覆盖init()方法来创建一个ChromeDriver类的实例。

ChromeDecorator类将被放在com.octopus.decorators包中,所以创建新的decorators包,就像你创建decoratorbase包一样。

com.octopus.decorators包中,用下面的代码创建一个名为ChromeDecorator的类。

注意,ChromeDecorator类只实现了一个方法。这就是扩展AutomatedBrowserBase类而不是AutomatedBrowser接口的好处:

package com.octopus.decorators;

import com.octopus.AutomatedBrowser;
import com.octopus.decoratorbase.AutomatedBrowserBase;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class ChromeDecorator extends AutomatedBrowserBase {
    public ChromeDecorator(final AutomatedBrowser automatedBrowser) {
        super(automatedBrowser);
    }

    @Override
    public void init() {
        final WebDriver webDriver = new ChromeDriver();
        getAutomatedBrowser().setWebDriver(webDriver);
        getAutomatedBrowser().init();
    }
} 

我们遵循相同的过程来创建FirefoxDecorator类,它创建了FirefoxDriver类的一个实例:

package com.octopus.decorators;

import com.octopus.AutomatedBrowser;
import com.octopus.decoratorbase.AutomatedBrowserBase;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class FirefoxDecorator extends AutomatedBrowserBase {

    public FirefoxDecorator(final AutomatedBrowser automatedBrowser) {
        super(automatedBrowser);
    }

    @Override
    public void init() {
        final WebDriver webDriver = new FirefoxDriver();
        getAutomatedBrowser().setWebDriver(webDriver);
        getAutomatedBrowser().init();
    }
} 

ChromeDecoratorFirefoxDecorator类包含我们打开 Chrome 或 Firefox 浏览器所需的逻辑,它们通过创建ChromeDriverFirefoxDriver类的实例来实现这一点。然后这些驱动类被传递给ChromeDecoratorFirefoxDecorator通过调用getAutomatedBrowser().setWebDriver(webDriver)包装AutomatedBrowser实例。

最后一步是通过调用getAutomatedBrowser().init()初始化驱动程序。调用init()方法现在没有任何作用,但是我们稍后将使用这个方法来配置驱动程序的一些高级特性。

我们需要的最后一个装饰器是一个使用 WebDriver API 对由ChromeDecoratorFirefoxDecorator类初始化的浏览器执行操作的装饰器。为此,我们将创建WebDriverDecorator类。

WebDriverDecorator类将托管一个WebDriver实例,并通过getWebDriver()setWebDriver()方法公开它。方法destroy()将关闭 web 浏览器,方法goTo()打开提供的 URL。

注意WebDriverDecorator有一个默认的构造函数。这与ChromeDecoratorFirefoxDecorator不同,它们都提供一个带AutomatedBrowser的构造函数。这种差异的存在是因为WebDriverDecorator旨在成为其他装饰者包装的基础AutomatedBrowser。当我们更新AutomatedBrowserFactory类时,我们将看到这一点。

在上一篇文章中,我们已经看到了许多进入WebDriverDecorator类的代码,其中webDriver.get()方法打开一个 URL,而webDriver.quit()方法关闭浏览器:

package com.octopus.decorators;

import com.octopus.AutomatedBrowser;
import com.octopus.decoratorbase.AutomatedBrowserBase;
import org.openqa.selenium.WebDriver;

public class WebDriverDecorator extends AutomatedBrowserBase {

  private WebDriver webDriver;

  public WebDriverDecorator() {

  }

  public WebDriverDecorator(final AutomatedBrowser automatedBrowser) {
    super(automatedBrowser);
  }

  @Override
  public WebDriver getWebDriver() {
    return webDriver;
  }

  @Override
  public void setWebDriver(final WebDriver webDriver) {
    this.webDriver = webDriver;
  }

  @Override
  public void destroy() {
    if (webDriver != null) {
      webDriver.quit();
    }
  }

  @Override
  public void goTo(final String url) {
    webDriver.get(url);
  }
} 

装饰器完成后,我们需要更新AutomatedBrowserFactory来使用它们。

先前的getChromeBrowser()getFirefoxBrowser()方法返回了null。现在我们可以创建装饰类的实例来构建定制的AutomatedBrowser接口实例,以打开 Chrome 或 Firefox。

注意装饰器构造函数是如何包装彼此的。这是 decorator 模式的关键,意味着我们可以混合和匹配 decorator 类来构造各种各样的对象,而无需创建具有继承性的深层类层次结构:

private AutomatedBrowser getChromeBrowser() {
  return new ChromeDecorator(
    new WebDriverDecorator()
  );
}

private AutomatedBrowser getFirefoxBrowser() {
  return new FirefoxDecorator(
    new WebDriverDecorator()
  );
} 

下图显示了装饰者如何包装彼此,并将方法调用传递给他们装饰的实例。

让我们创建一个测试,利用我们的工厂和它创建的AutomatedBrowser实例。

因为这是一个测试类,所以它将被创建在src/test/java/com/octopus目录中:

package com.octopus;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;

@RunWith(Parameterized.class)
public class FactoryTest {

  private static final AutomatedBrowserFactory AUTOMATED_BROWSER_FACTORY
    = new AutomatedBrowserFactory();

  private String browser;

  public FactoryTest(final String browser) {
    this.browser = browser;
  }

  @Parameterized.Parameters
  public static Iterable data() {
    return Arrays.asList(
      "Chrome",
      "Firefox"
    );
  }

  @Test
  public void openURL() {
    final AutomatedBrowser automatedBrowser =
      AUTOMATED_BROWSER_FACTORY.getAutomatedBrowser(browser);
    automatedBrowser.init();
    automatedBrowser.goTo("https://octopus.com/");
    automatedBrowser.destroy();
  }
} 

FactoryTest类利用 JUnit 参数化,用不同的输入多次运行测试方法。我们将使用这一功能,用一种测试方法对 Chrome 和 Firefox 浏览器进行测试。

为了启用参数化,测试类需要注释@RunWith(Parameterized.class):

@RunWith(Parameterized.class)
public class FactoryTest {

} 

然后它需要一个静态方法来返回将被传递给FactoryTest构造函数的值。在我们的例子中,这些值是我们将要测试的浏览器名称的字符串:

@Parameterized.Parameters
public static Iterable data() {
  return Arrays.asList(
    "Chrome",
    "Firefox"
  );
} 

最后,FactoryTest()构造函数被配置为接受一个参数,该参数将被设置为由data()方法返回的值之一。在这种情况下,我们将参数保存到browser实例变量:

private String browser;

public FactoryTest(final String browser) {
  this.browser = browser;
} 

然后,测试方法可以利用browser实例变量来启动 Chrome 或 Firefox 浏览器,作为测试的一部分。

这种在运行时通过AutomatedBrowserFactory选择浏览器的能力将为我们以后的测试提供极大的灵活性:

@Test

public void openURL() {
  final AutomatedBrowser automatedBrowser =
    AUTOMATED_BROWSER_FACTORY.getAutomatedBrowser(browser);

  automatedBrowser.init();
  automatedBrowser.goTo("https://octopus.com/");
  automatedBrowser.destroy();
} 

作为这个博客的一部分,我们已经创建了许多新的类,您最终应该会得到一个类似这样的目录结构。

要运行测试,单击FactoryTest类旁边的绿色箭头并选择Run 'FactoryTest'选项。

你会看到 Chrome 和 Firefox 都打开,显示https://octopus.com,然后再次关闭。

现在我们有了一个简单的框架来运行针对多种浏览器的测试,我们需要一个网页来进行交互,我们将在下一篇文章中创建这个网页。

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

使用 Octopus REST API 导入变量- Octopus Deploy

原文:https://octopus.com/blog/creating-variables-with-the-api

Octopus 2.0 引入了一个全面的 REST API,可以用来执行 UI 可以执行的任何事情。我们知道这一点,因为 UI 本身完全构建在 REST API 之上。今天,一些人问他们如何使用 API 自动导入变量,我将在下面演示。

变量的集合存储在一个VariableSet资源中。在 UI 中编辑变量时,您正在编辑VariableSet。例如,在这个截图中,四个变量属于一个VariableSet:

Variable editing

变量集资源支持GETPUT操作。这个想法是,你GET变量集,进行修改(从集合中添加、修改、删除变量),然后PUT它回来。

VariableSets属于一个Project(或者属于一个发布,因为我们对每个发布的变量进行快照)。所以你需要这个项目来获取变量集。

使用章鱼。客户

这里有一个使用 Octopus 的完整例子。客户端:

var octopus = new OctopusRepository(new OctopusServerEndpoint("http://your-octopus", "API-YOURKEY"));

// Find the project that owns the variables we want to edit
var project = octopus.Projects.FindByName("My Project");

// Get the variables for editing
var variableSet = octopus.VariableSets.Get(project.Link("Variables"));

// Add a new variable
variableSet.Variables.Add(new VariableResource()
{
    Name = "ConnectionString",
    Value = "Server=(local);Database=Foo;trusted_connection=true",
    Scope = new ScopeSpecification()
    {
        // Scope the variable to two environments using their environment ID
        { ScopeField.Environment, new ScopeValue("Environments-1", "Environments-2" )}
    }, 
});

// Save the variables
octopus.VariableSets.Modify(variableSet); 

正在发生什么

首先,客户机点击/api并返回一个文档,如下所示:

{
  "Application": "Octopus Deploy",
  "Version": "2.0.12.1092",
  "ApiVersion": "3.0.0",
  "Links": {
    ...
    "Projects": "/api/projects{/id}{?skip}",
    ...
    "Variables": "/api/variables{/id}"
  }
} 

跟随项目链接,我们得到一个项目列表:

{
  "ItemType": "Project",
  "IsStale": false,
  "TotalResults": 7,
  "ItemsPerPage": 30,
  "Items": [
    {
      "Id": "projects-65",
      "Name": "My Project",
      "VariableSetId": "variableset-projects-65",
      ...
      "Links": {
        "Self": "/api/projects/projects-65",
        "Releases": "/api/projects/projects-65/releases{/version}{?skip}",
        "Variables": "/api/variables/variableset-projects-65",
        "DeploymentProcess": "/api/deploymentprocesses/deploymentprocess-projects-65",
        "Web": "/app#/projects/projects-65"
      }
    },
    {
      "Id": "projects-129",
      "Name": "My Project 2",
      "VariableSetId": "variableset-projects-129",
      ... 

项目上的变量链接将我们指向变量集资源:

{
  "Id": "variableset-projects-129",
  "OwnerId": "projects-129",
  "Variables": [
    {
      "Id": "80bcaf5a-632a-470d-a8aa-a366f20c302e",
      "Name": "ConnectionString",
      "Value": "Server=(local);Database=Foo;trusted_connection=true",
      "Scope": {
        "Environment": [
          "Environments-1",
          "Environments-2"
        ]
      },
      "IsSensitive": false,
      "IsEditable": true,
      "Prompt": null
    }
  ],
  "ScopeValues": {
    "Environments": [
      {
        "Id": "Environments-1",
        "Name": "Automation Testing"
      },
      {
        "Id": "Environments-34",
        "Name": "EC2 Production"
      },
      {
        "Id": "Environments-33",
        "Name": "EC2 Staging"
      },
      {
        "Id": "Environments-97",
        "Name": "Octopus"
      },
      {
        "Id": "Environments-2",
        "Name": "Release To Web"
      }
    ], 

我们现在可以对此资源进行更改,并将其放回原处。

注意,VariableSet 文档实际上定义了不同的范围值选项。在 UI 中,我们使用它来填充定义范围时的下拉列表。在设置范围选项时,您可以使用它将诸如“EC2 Production”之类的名称映射到“Environments-34”。

我希望这有助于提供如何使用八达通的样本。客户端 API 来修改变量,以及了解幕后实际发生的事情。愉快的部署!

创建自定义 Docker 注册表- Octopus Deploy

原文:https://octopus.com/blog/custom-docker-registry

你想知道当你做docker pushdocker pull时会发生什么吗?在幕后,像 Docker Hub 这样的存储库实现了 Docker V2 HTTP API 规范,响应这些请求来接收或交付 Docker 映像。不过,这个规范对任何人都是开放的,您可以从一个最小的 docker 注册实现中了解更多关于 Docker 的知识。

在这篇文章中,我们创建了一个成功响应docker pushdocker pull命令的 C#服务器。在这个过程中,您可以看到组成 Docker 图像的各个组件。

用于推和拉图像的 Docker API

这些是我们的应用程序必须实现以支持推和拉图像的路径:

  • GET /v2 : Docker 访问这个路径来验证服务器是否支持 Docker HTTP API 的版本 2。
  • HEAD {name}/blobs/{digest}:该路径用于判断服务器上是否存在该图层。
  • GET {name}/blobs/{digest}:一旦客户端确定该层存在于服务器上,该路径返回该层。
  • POST {name}/blobs/uploads:这个路径被调用来启动层上传。
  • PATCH {name}/blobs/uploads/{uuid}:该路径接收分块层上传。
  • PUT {name}/blobs/uploads/{uuid}:一旦分块层上传完成,就调用这个路径。
  • HEAD {name}/manifests/{reference}:调用这个路径来确定服务器上是否存在清单。
  • GET {name}/manifests/{reference}:这个路径返回一个层,一旦客户端确定它存在于服务器上。
  • PUT {name}/manifests/{reference}:这个路径在服务器上创建一个清单。

推送图像

推送 Docker 图像的流程是:

  1. 联系/v2以确认服务器支持正确的 API。
  2. {name}/blobs/{digest}进行头部查询,其中name是类似alpine的图像名称,digest是图层的哈希。
  3. 如果该层不存在,执行提交到{name}/blobs/uploads。来自该路径的响应包括格式为{name}/blobs/uploads/{uuid}Location报头,在此执行层上传。
  4. 然后上传该层,可能会上传与对{name}/blobs/uploads/{uuid}的补丁请求一样多的小块。被发送的数据在Range头中被捕获,服务器被期望用每个传入的块增量地填充层文件。
  5. 当上传一个层时,对{name}/blobs/uploads/{uuid}进行一个 PUT 调用。这个请求包括一个名为digest的查询参数,它的值是已经完成的层的散列。
  6. 一旦层被上传,在{name}/manifests/{reference}上的头部查询确定清单是否已经存在于服务器上。
  7. 如果清单不存在,那么对{name}/manifests/{reference}的 PUT 请求会创建它。

提取图像

拉图像比推图像容易:

  1. 联系/v2确认服务器支持正确的 API。
  2. {name}/manifests/{reference}上执行头部查询,以确定清单是否存在。
  3. 如果清单存在,用对{name}/manifests/{reference}的 GET 请求检索它。
  4. 对于清单中列出的每个映像,在{name}/blobs/{digest}上执行头部查询以验证它是否存在。
  5. 然后通过对{name}/blobs/{digest}的 GET 请求下载图像。

示例应用程序

我们的示例应用程序并不漂亮,您不能用它来承载生产工作负载。但它的功能足以允许推拉图像。这为深入了解 Docker 图像的幕后情况提供了一个很好的视角。

这里描述的源代码可以从 GitHub 获得。

从一个将其所有方法都放在v2根路径下的控制器开始。这个根路径是 Docker 规范中的一个硬性要求。一些混合工件存储库在其自己的端口上公开一个 Docker 注册表,以确保这个根路径不会与其他 API 冲突:

namespace Controllers
{
    [Route("v2/")]
    public class DockerRegistry : ControllerBase
    { 

每次启动应用程序时,创建一个新的临时目录来保存 Docker 图像和层。这对于测试来说非常好,因为你可以重启应用程序并拥有一个空的注册表:

 private static readonly string LayerPath;

        static DockerRegistry()
        {
            LayerPath = GetTemporaryDirectory();
            Console.Out.WriteLine($"Saving artifacts to {LayerPath}");
        }

        static string GetTemporaryDirectory()
        {
            var tempFolder = Path.GetTempFileName();
            System.IO.File.Delete(tempFolder);
            Directory.CreateDirectory(tempFolder);

            return tempFolder;
        } 

这个处理程序响应/v2根路径上的 GET 请求。您返回一个 HTTP 200 OK 响应,让客户端知道您支持 V2 API:

 [HttpGet]
        public IActionResult Root(string path)
        {
            return Ok();
        } 

接下来,构建处理程序以允许图像被推送到服务器。您需要响应 HEAD 请求,检查服务器上是否已经存在图像。name参数是图像名称(如alpine),而digest参数是标识层的散列,如sha256:11ad9c3e3069bdb53ff873af66070ca6c4309e85581cf3befe05459f889fd729

在这里采取一个小捷径,将图层保存为 SHA 哈希,减去sha256:前缀。如果文件存在,返回 200 OK,用content-length头表示图像的大小。如果图层不存在,返回 404 未找到:

 [HttpHead("{name}/blobs/{digest}")]
        public IActionResult Exists(string name, string digest)
        {
            var hash = digest.Split(":").Last();

            if (System.IO.File.Exists(LayerPath + "/" + hash))
            {
                Response.Headers.Add("content-length", new FileInfo(LayerPath + "/" + hash).Length.ToString());
                Response.Headers.Add("docker-content-digest", digest);
                return Ok();
            }

            return NotFound();
        } 

如果该层不存在,客户端将使用这个 POST 请求启动上传。响应包括一个带有唯一 URL 的location头,实际图层数据将发送到该头:

 [HttpPost("{name}/blobs/uploads")]
        public IActionResult StartUpload(string name)
        {
            var guid = Guid.NewGuid().ToString();
            Response.Headers.Add("location", "/v2/" + name + "/blobs/uploads/" + guid);
            Response.Headers.Add("range", "0-0");
            Response.Headers.Add("content-length", "0");
            Response.Headers.Add("docker-upload-uuid", guid);
            return Accepted();
        } 

Docker 支持整体分块上传。该处理程序支持分块上传方法(示例应用程序不支持整体上传)。

客户端可能会也可能不会提供一个指示正在上传的块的content-range头。通常整个层作为单个块上传,并且不提供content-range头。

在这个方法中,您将请求的主体保存到一个文件中,该文件带有在StartUpload方法中生成的随机 GUID:

 [DisableRequestSizeLimit] 
        [HttpPatch("{name}/blobs/uploads/{uuid}")]
        public async Task<IActionResult> Upload(string name, string uuid)
        {
            var start = Request.Headers["content-range"].FirstOrDefault()?.Split("-")[0] ?? "0";
            await using (var fs = System.IO.File.OpenWrite(LayerPath + "/" + uuid))
            {
                fs.Seek(long.Parse(start), SeekOrigin.Begin);
                await Request.Body.CopyToAsync(fs);

                Response.Headers["range"] = "0-" + (fs.Position - 1);
            }

            Response.Headers["docker-upload-uuid"] = uuid;
            Response.Headers["location"] = $"/v2/{name}/blobs/uploads/{uuid}";
            Response.Headers["content-length"] = "0";
            Response.Headers["docker-distribution-api-version"] = "registry/2.0";
            return Accepted();
        } 

上传图层后,将调用此方法来表示上传完成。规范提到,这个方法可能会被调用,最终的内容块将被保存到层,因此您需要将 PUT 主体中的任何内容追加到层文件中。

现在使用来自digest查询参数的散列将文件从临时 GUID 重命名为散列:

 [HttpPut("{name}/blobs/uploads/{uuid}")]
        public async Task<IActionResult> FinishUpload(string name, string uuid)
        {
            if (Request.Headers["content-length"].First() != "0")
            {
                var ranges = Request.Headers["content-range"].First().Split("-");
                await using var fs = System.IO.File.OpenWrite(LayerPath + "/" + uuid);
                fs.Seek(long.Parse(ranges[0]), SeekOrigin.Begin);
                await Request.Body.CopyToAsync(fs);
            }

            var rawDigest = Request.Query["digest"];
            var digest = Request.Query["digest"].First().Split(":").Last();
            System.IO.File.Move(LayerPath + "/" + uuid, LayerPath + "/" + digest);
            Response.Headers.Add("content-length", "0");
            Response.Headers.Add("docker-content-digest", rawDigest);

            return Created("/v2/" + name + "/blobs/" + digest, "");
        } 

在层被上传之后,清单(你可以认为是 Docker 图像)被创建。首先,客户端发出 HEAD 请求,查看清单是否存在。

奇怪的是,reference可能是一个标签名,比如latest,也可能是一个散列。为方便起见,将清单保存在两个位置:

  • 一个基于标签名
  • 一个基于散列

这效率不高,但对于您的示例应用程序来说,这是一个简单的解决方案:

 [HttpHead("{name}/manifests/{reference}")]
        public IActionResult ManifestExists(string name, string reference)
        {
            var hash = reference.Split(":").Last();
            var path = LayerPath + "/" + name + "." + reference + ".json";
            var hashPath = LayerPath + "/" + hash + ".json";
            var testedPath = System.IO.File.Exists(path) ? path :
                System.IO.File.Exists(hashPath) ? hashPath :
                null;

            if (testedPath != null)
            {
                Response.Headers.Add("docker-content-digest", "sha256:" + Sha256Hash(path));
                Response.Headers.Add("content-length", new FileInfo(path).Length.ToString());

                var content = System.IO.File.ReadAllText(path);
                var mediaType = JObject.Parse(content)["mediaType"].ToString();

                Response.Headers.Add("content-type", mediaType);

                return Ok();
            }

            return NotFound();
        } 

如果清单不存在,用对该方法的 PUT 请求保存它。再次注意,您必须将清单保存在两个地方:

  • 一个文件名中带有标签
  • 另一个在文件名中包含哈希:
 [HttpPut("{name}/manifests/{reference}")]
        public async Task<IActionResult> SaveManifest(string name, string reference)
        {
            var path = LayerPath + "/" + name + "." + reference + ".json";

            await using (var fs = System.IO.File.OpenWrite(path))
            {
                await Request.Body.CopyToAsync(fs);
            }

            var hash = Sha256Hash(path);
            Response.Headers.Add("docker-content-digest", "sha256:" + hash);

            System.IO.File.Copy(path, LayerPath + "/" + hash + ".json", true);

            return Created($"/v2/{name}/manifests/{reference}", null);
        } 

这些端点允许你完成一个docker push命令。

提取图像需要另外两种方法。

第一个使用 GET 请求将图层数据返回给以下方法:

 [HttpGet("{name}/blobs/{digest}")]
        public async Task<IActionResult> GetLayer(string name, string digest)
        {
            var hash = digest.Split(":").Last();
            var path = LayerPath + "/" + hash;

            if (System.IO.File.Exists(LayerPath + "/" + hash))
            {
                Response.Headers.Add("content-length", new FileInfo(path).Length.ToString());
                await using (var fs = new FileStream(path, FileMode.Open))
                {
                    await fs.CopyToAsync(Response.Body);
                    return Ok();
                }
            }

            return NotFound();
        } 

清单数据随对以下方法的 GET 请求一起返回。就像 HEAD 请求一样,您需要基于标记名或散列码搜索清单文件,因为reference可能是这两个值中的任何一个。

注意,这里您加载了清单文件,将其解析为 JSON,并提取了mediaType属性。这作为content-type头发送回客户端:

 [HttpGet("{name}/manifests/{reference}")]
        public async Task<IActionResult> GetManifest(string name, string reference)
        {
            var hash = reference.Split(":").Last();
            var path = LayerPath + "/" + name + "." + reference + ".json";
            var hashPath = LayerPath + "/" + hash + ".json";
            var testedPath = System.IO.File.Exists(path) ? path :
                System.IO.File.Exists(hashPath) ? hashPath :
                null;

            if (testedPath != null)
            {
                Response.Headers.Add("docker-content-digest", "sha256:" + Sha256Hash(testedPath));

                var content = System.IO.File.ReadAllText(testedPath);
                var mediaType = JObject.Parse(content)["mediaType"].ToString();

                Response.Headers.Add("content-type", mediaType);
                Response.Headers.Add("content-length", new FileInfo(testedPath).Length.ToString());

                await using (var fs = new FileStream(testedPath, FileMode.Open))
                {
                    await fs.CopyToAsync(Response.Body);
                }

                return Ok();
            }

            return NotFound();
        } 

这样,您就拥有了支持拉取图像所需的所有端点。

测试服务器

web 应用程序已通过launchSettings.json文件配置为监听所有 IP 地址。下面是显示applicationUrl设置的精简文件,它被配置为监听0.0.0.0,这意味着应用程序响应所有 IP 地址上的请求。

我在测试中发现这是必要的,因为推送至localhost在基于 Windows 的机器上不起作用,所以我不得不推送至机器的本地 IP 地址:

{
...

  "profiles": {

    ...

    "dockerregistry": {
      "applicationUrl": "https://0.0.0.0:5002;http://0.0.0.0:5001",
    }
  }
} 

默认情况下,Docker 试图通过 HTTPS 联系所有外部存储库。web 应用程序有一个自签名证书,所以为了测试,您希望 Docker 使用 HTTP。

我的笔记本电脑的 IP 地址是 10.1.1.37。要指示 Docker 通过您的 IP 访问注册表,您需要编辑 Docker 配置文件中的insecure-registries数组:

使用以下内容运行应用程序:

dotnet run 

存放工件的临时目录显示在控制台中:

$ dotnet run
Building...
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://0.0.0.0:5002
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://0.0.0.0:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\Matthew\Octopus\dockerregistry
Saving artifacts to C:\Users\Matthew\AppData\Local\Temp\tmp5E8E.tmp 

下载 Docker 映像并根据本地存储库重新标记它:

$ docker pull alpine
$ docker tag alpine 10.1.1.37:5001/alpine 

然后将映像推送到本地服务器:

$ docker push 10.1.1.37:5001/alpine 

在临时目录中创建了四个文件:两个图像层,以及与标签和散列一起保存的清单:

现在从你的本地电脑上删除图像。这可以确保此图像的任何下载都不能重复使用您以前缓存的图像:

$ docker image rm 10.1.1.37:5001/alpine
$ docker image rm alpine 

使用以下命令从您的服务器下载映像:

$ docker pull 10.1.1.37:5001/alpine 

现在,您已经从最小 Docker 存储库中推送和提取了图像。仍然缺少一些功能,比如删除图像和搜索,但是我们将把实现留在这里。

结论

Docker 是许多开发工作流的核心,但有趣的是,关于如何实现 Docker API 的信息并不多。官方文档有点密集(正如规范通常的那样),所以在这篇文章中,我们看了一个非常简单的实现,它允许使用常规的 Docker 客户端来推和拉 Docker 图像。

希望这能揭开围绕 Docker 图像传输的一些过程,如果您希望将自己的应用程序与 Docker 客户机集成,这是一个有用的起点。

愉快的部署!

Octopus - Octopus Deploy 中的自定义 kubectl 脚本

原文:https://octopus.com/blog/custom-kubectl-scripting-in-octopus

Custom kubectl scripting in Octopus

本系列之前的博客都关注于如何使用 Octopus 中固执己见的步骤来执行 Kubernetes 部署。但是有时候你需要直接进入脚本。也许你想利用定制的脚本工具,如 istioctl 或者使用 Kubernetes 资源提供的一些高级或不常见的属性。对于这些情况,Octopus 允许您针对 kubectl 编写定制脚本。

在本帖中,我们将探讨一些技巧,您可以利用这些技巧来创建针对您的 Kubernetes 集群运行的灵活且可重用的脚本。

创建一个 kubectl 脚本

运行 kubectl CLI 脚本步骤展示了针对 Kubernetes 集群编写脚本的能力:

这一步类似于 Octopus 中的其他脚本步骤,只是它必须针对 Kubernetes 目标运行。在幕后,Octopus 获取 Kubernetes 目标的细节,并构造一个配置文件,其作用范围是正在运行的脚本。它通过将环境变量KUBECONFIG设置为新生成的配置文件的路径来实现这一点,然后允许所有对kubectl的后续调用都指向 Kubernetes 目标。

下面是一个示例 PowerShell 脚本,它显示了环境变量和配置文件的内容:

echo "KUBECONFIG environment variable is set to: $($env:KUBECONFIG)"
echo "kubectl config view returns:"
kubectl config view 

以下是结果截图:

从输出中,我们可以看到 Kubernetes 配置文件已经保存到了C:\Octopus\Master K8S Worker\Work\20200520001931-474360-35\kubectl-octo.yml,这是一个临时目录,用来保存该步骤所需的工作文件。我们还可以看到配置文件是如何用保存在 Kubernetes 目标中的细节构建的。

在脚本中使用变量

当该步骤运行时,我们的脚本可以访问所有可用的变量。查看可用变量的最简单方法是将章鱼变量 OctopusPrintVariablesOctopusPrintEvaluatedVariables设置为True:

定义了这个变量后,详细日志将显示可用的变量及其值。这是浏览可在脚本中使用的变量的便捷方式:

参考 Docker 图像

Octopus 部署过程的优点之一是部署逻辑是相对静态的,即使包每次都在变化。这是通过在部署时选择包来实现的。

然而,这个过程并不是 Kubernetes 所固有的。例如,在下面的部署 YAML 中,您可以看到我们已经硬编码了对 Docker 映像mcasperson/mywebapp:0.1.7的引用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeployment
  labels:
    app: mydeployment
spec:
  selector:
    matchLabels:
      app: mydeployment
  replicas: 1
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: mydeployment
    spec:
      containers:
        - name: randomquotes
          image: mcasperson/mywebapp:0.1.7
          ports:
            - name: web
              containerPort: 80 

即使我们没有提供标签并使用了一个图像引用mcasperson/mywebapp,标签latest也是假定的,所以我们仍然有效地拥有一个对单个 Docker 图像的硬编码引用。

为了让上面的 YAML 可以部署不同版本的 Docker 图像,我们可以使用 Helm 这样的工具通过模板定义图像标签。但是仍然需要有人知道 Docker 映像的版本并将其提供给 Helm。

八达通提供了另一种选择。通过引用 Docker 映像作为附加包并将其设置为不被获取,Octopus 将在部署期间提示选择映像的版本,然后在运行时将该版本作为变量公开。以下是作为附加包引用的 Docker 图像:

然后在部署期间选择包版本:

最后,我们扫描日志中打印的变量,找到引用 Docker 图像的变量。您可以在下面的截图中看到,名为Octopus.Action.Package[mywebapp].Image的变量是完整的 Docker 图像名称,Octopus.Action.Package[mywebapp].PackageVersion是版本:

我们可以在脚本中使用这些变量。下面的示例脚本将一个 YAML 文件写入磁盘,然后使用kubectl来应用它。image 属性被定义为image: #{Octopus.Action.Package[mywebapp].Image},它将在每次部署时更新,以反映所选的 Docker 映像:

Set-Content -Path deployment.yml -Value @"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeployment
  labels:
    app: mydeployment
spec:
  selector:
    matchLabels:
      app: mydeployment
  replicas: 1
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: mydeployment
    spec:
      containers:
        - name: randomquotes
          image: #{Octopus.Action.Package[mywebapp].Image}
          ports:
            - name: web
              containerPort: 80
"@

kubectl apply -f deployment.yml 

在容器映像中运行

像 Octopus 这样的工具面临的一个挑战是它所集成的平台的数量,以及 Octopus 利用的工具。这个问题最初的解决方案是用 Octopus 本身打包工具,但是随着时间的推移,不同的工具版本、不同的操作系统以及新工具的不断引入使得这种方法无法维护。

这个问题的解决方案是引入工人工具 Docker 映像,可以在其中执行部署流程。这些 Docker 映像包含了一些常见的开源工具,可以独立于 Octopus 本身进行版本控制和发布。

Octopus 提供的图像包括 Kubernetes 工具的精选,包括kubectlistioctllinkerdhelm,这并不奇怪。

在下面的屏幕截图中,脚本步骤已被配置为在 Worker tool Docker 映像中运行:

但是,因为我们使用托管在 Docker with Kind 中的 Kubernetes 集群,所以我们必须做一些配置,以确保运行我们的 Octopus 步骤的 Docker 容器可以访问该集群。

首先,我们需要确保 Kubernetes 集群控制平面运行在名为bridge的默认 Docker 网络上。从版本 0.8.0 开始,Kind 将在一个名为kind的特殊网络中创建 Kubernetes 集群,它将集群控制平面与运行我们部署的容器隔离开来。要解决这个问题,将KIND_EXPERIMENTAL_DOCKER_NETWORK环境变量设置为bridge,以强制 Kind 使用默认网络。

您可能需要用kind cluster delete删除现有的集群。然后按照上一篇博文中的说明重新创建它,记住提取证书并重新上传到 Octopus 中,因为它们已经发生了变化。

我们还需要将我们的 Kubernetes 目标指向一个新的 IP 地址和端口。命令docker container ls向我们展示了托管 Kubernetes 控制平面的种类容器:

$ docker container ls
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                       NAMES
ebb9eb784a55        kindest/node:v1.18.2   "/usr/local/bin/entr…"   6 minutes ago       Up 6 minutes        127.0.0.1:59747->6443/tcp   kind-control-plane 

由此,我们可以看到端口6443是公开 Kubernetes API 的内部端口。

然后,我们用命令docker container inspect kind-control-plane获取容器的 IP 地址。以下是该命令输出的截断副本:

$ docker container inspect kind-control-plane
[
    {
        // ... removed the container details for brevity
        "NetworkSettings": {
            // ... removed networking details for brevity
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "29f0f93df185df5ecae63abcca94c7a1bdd24a13bc8cd0158b2534199a08b95e",
                    "EndpointID": "0dc06d6e58a17e169d1c58a4ddaec179252d7b3e79695c40eba52af3ae8b921a",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
] 

我们可以看到使用了bridge网络,这意味着KIND_EXPERIMENTAL_DOCKER_NETWORK环境变量按预期工作。然后我们看到IPAddress属性被设置为172.17.0.2。这意味着我们的 Kubernetes 集群的 URL 是https://172.17.0.2:6443:

既然我们已经为两个同级 Docker 容器配置了正确的网络来相互通信,我们可以通过运行脚本来验证 worker-tools 映像是否公开了我们期望的工具:

istioctl version
linkerd version
helm version 

正如所料,我们的脚本可以使用所有这些工具:

结论

通过暴露大量变量,允许在部署时选择 Docker 映像,并通过工人工具 Docker 映像提供广泛的工具,可以从 Octopus 针对 Kubernetes 编写复杂的部署和管理任务。

这篇文章研究了 Octopus 中一些有用的调试技术,并提供了利用包变量和工人工具 Docker images 的示例脚本,以突出使用 Octopus 来自动化 Kubernetes 集群的一些可能性。

使用 Runbooks 在数据库部署自动化管道中运行特定的 SQL 脚本——Octopus Deploy

原文:https://octopus.com/blog/database-deployment-automation-adhoc-scripts-with-runbooks

Octopus worker deploying an ad-hoc SQL script illustration

在之前的一篇文章中,我写了如何在数据库部署管道中运行特定的 SQL 脚本。在写的时候,我觉得那是我要求的最好的过程。新的 runbooks 功能,作为我们 2019.11 版本的一部分发布,看起来可能更适合这个过程。在本文中,我将使用 Operations Runbooks 特性完成一个新流程。

为什么我们需要特别的 SQL 脚本

在上一篇文章中,我详细介绍了为什么我们在自动化数据库部署管道中需要专门的 SQL 脚本。下面是一个快速总结,让你快速了解。

所有的软件都有 bug,但不是所有的 bug 都值得花时间和精力去修复。很难证明修复一个错误的工程成本是合理的,这个错误每个季度只发生一次,并且只在太阳、月亮和星星都排成一行时发生。然而,有时像这样的错误会导致数据进入不良状态。这个 bug 不太可能被修复,但是数据需要被修复,这样用户才能继续使用这个应用程序。通常,开发人员为 DBA 编写一个 SQL 脚本来修复数据。

我工作过和合作过的每家公司都有运行这类脚本的流程。这可能像给 DBA 发电子邮件要求他们运行脚本一样简单,也可能像需要大量签名的多页表单一样复杂,但是这个过程似乎总是手动的。

使用 Octopus Deploy 之类的工具自动完成这一过程有许多优点:

  • 审计 : Octopus Deploy 可以告诉您谁提出了请求,谁批准了请求,以及这一切是何时发生的。
  • 工件:使用 Octopus Deploy 内置的工件功能,可以存储和捕获运行的确切 SQL 脚本,但是,如果有人在文件共享之后更改了脚本,就无从得知了。
  • 批准:在某些情况下,让另一双眼睛来审视剧本是很重要的。Octopus Deploy 可以设置为基于一组标准有条件地批准脚本。
  • 自动化:不再需要手动发送邮件。不再需要手动发送确认。不再需要打开 SSMS 来运行 SQL 脚本。
  • Repeatable :在所有环境中使用相同的流程来运行脚本。

自动化要求

在看我以前的文章时,我看到了许多移动的部分。我想在移植到 runbook 时简化这个过程。这种复杂性是将特定的 SQL 脚本过程强加到部署过程中的结果,但是在我的需求中是否有其他因素导致了这种复杂性?

这些要求又出现了:

  • 章鱼展开。
  • 没有源代码管理。许多 DBA、支持工程师和业务分析师不熟悉源代码控制工具。
  • 自动化。当脚本准备好时,它们应该在五分钟内运行,而不必填写表格或通知任何人。
  • 剧本分析。如果脚本包含某些关键字,那么人们应该在运行它之前检查脚本。
  • 在任何环境下工作。我们希望鼓励人们在任何环境下运行这一功能,甚至是开发环境。

这一个很突出:

自动化。当脚本准备好时,它们应该在五分钟内运行,而不必填写表格或通知任何人。

有了 runbooks,我可以稍微调整一下需求:

自动化。当脚本准备好了,他们应该通过一个易于使用的形式提交,并立即执行。

操作手册流程

好了,需求更新了;现在是重新制定流程的时候了:

  1. 用户启动 runbook 运行,并在提示变量中输入数据库名称和 SQL 脚本。
  2. 向提交者发送确认消息。
  3. 运行一个脚本来评估提交的脚本。它查找模式更改命令,使用事务运行和回滚脚本,并检查更改的行数。如果所有条件都满足,则输出变量 DBA Approval Required设置为False,否则设置为True
  4. 向提交者发送自动批准的结果。
  5. 如果需要 DBA 审查脚本,则会发送通知,并且流程会暂停,以便进行手动干预
  6. 如果脚本是自动批准的,或者 DBA 批准了该脚本,则该脚本将运行。
  7. DBA 和提交者被告知运行的结果。

我将把那本操作手册投入到一个新项目中。这将有助于我在本文后面配置安全性:

【T2

自动批准脚本

自动审批脚本是整个操作的大脑。它确保符合标准,并且没有人试图潜入模式更改脚本。

您可以在我们的示例实例中查看该脚本。

以下是剧本中的一些亮点。首先,我将所有需要注意的模式更改命令放入一个变量中:

$scriptToRun = $OctopusParameters["Project.ScriptToRun.Text"]
$commandsToLookFor = $OctopusParameters["SQLServer.Commands.Warnings"]

$approvalRequired = $false
$messages = ""

Write-Highlight "Looping through the script to look for schema change commands"
$commandsToCheck = $CommandsToLookFor -split ","
foreach ($command in $commandsToCheck)
{
    Write-Host "Checking for command $command"
    $foundCommand = $scriptToRun -match "$command"

    if ($foundCommand)
    {
        $warningMessage = "A '$command' SQL Statement was found, approval by DBA is required."
        $messages += $warningMessage + "`r`n"
        Write-Highlight $warningMessage
        $approvalRequired = $true        
    }
} 

接下来,它尝试运行事务中提供的 SQL 脚本。无论如何,事务都会回滚:

$scriptToRun = $OctopusParameters["Project.ScriptToRun.Text"]
$databaseName = $OctopusParameters["Project.Database.Name"]
$databaseServer = $OctopusParameters["Project.Database.Server"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]
$environmentName = $OctopusParameters["Octopus.Environment.Name"]

Write-Highlight "Attempting test run of script in a transaction"
$sqlConnection = New-Object System.Data.SqlClient.SqlConnection
$sqlConnection.ConnectionString = $connectionString

$command = $sqlConnection.CreateCommand()
$command.CommandType = [System.Data.CommandType]'Text'
$command.CommandText = $scriptToRun

Write-Host "Opening the connection to $databaseName on $databaseServer"

$sqlConnection.Open()   

try
{
    Write-Host "Creating transaction"
    $command.Transaction = $sqlConnection.BeginTransaction()

    Write-Host "Running query now"
    $rowsChanged = $command.ExecuteNonQuery()
}
catch
{
    throw $_
}
finally
{
    $command.Transaction.Rollback()
} 

在上面的脚本中记下这一行:

$rowsChanged = $command.ExecuteNonQuery() 

最后的检查确保 SQL 脚本不会在没有人查看的情况下更改大量记录。它使用了$rowsChanged变量:

if ($rowsChanged -gt 10)
{
    $warningMessage = "The number of rows which will changed is $rowsChanged, approval by DBA is required."
    $messages += $warningMessage + "`r`n"
    Write-Highlight $warningMessage

    $approvalRequired = $true
}
elseif ($rowsChanged -le 0)
{
    $warningMessage = "No rows will be changed, verify change with DBA"
    $messages += $warningMessage + "`r`n"
    Write-Highlight $warningMessage
    $approvalRequired = $true
} 

最后,它设置一个输出变量,指示 DBA 是否需要批准该脚本:

Set-OctopusVariable -name "ApprovalRequired" -value $approvalRequired 

这只是脚本的第一次迭代。我希望阅读本文的任何人都能根据自己的业务需求修改脚本。

自动化即席 SQL 脚本运行

运行已提交数据更改的脚本是自动批准脚本的修改版本。我倾向于看到的一个需求是每个脚本都需要包装在一个事务中。要么全有,要么全无。我完全同意这个要求。运行已提交数据变更的脚本强制执行该要求:

$scriptToRun = $OctopusParameters["Project.ScriptToRun.Text"]
$databaseName = $OctopusParameters["Project.Database.Name"]
$databaseServer = $OctopusParameters["Project.Database.Server"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]

$sqlConnection = New-Object System.Data.SqlClient.SqlConnection
$sqlConnection.ConnectionString = $connectionString

$command = $sqlConnection.CreateCommand()
$command.CommandType = [System.Data.CommandType]'Text'
$command.CommandText = $scriptToRun

Write-Host "Opening the connection to $databaseName on $databaseServer"

$sqlConnection.Open()   

try
{
    Write-Highlight "Creating transaction"
    $command.Transaction = $sqlConnection.BeginTransaction()

    Write-Highlight "Running query now"
    $rowsChanged = $command.ExecuteNonQuery()

    Write-Highlight "Committing Transaction"
    $command.Transaction.Commit()
}
catch
{
    Write-Highlight "Exception with running script, rolling back transaction"
    $command.Transaction.Rollback()
    throw $_
} 

看到它的实际应用

现在是测试流程的时候了。我想做两个测试:

  1. 包含数据和模式更改的 SQL 脚本。我以为会触发手动干预。
  2. 只有数据更改的 SQL 脚本。无需人工干预。

包含数据和模式更改的 SQL 脚本

如前所述,我使用提示变量来运行数据库和脚本。当我创建 runbook 运行时,我必须为两者提供值:

正如所料,Create Table命令触发了手动干预:

有些情况下需要一个Create Table。也许该脚本正在创建一个临时表,但是在这种情况下,DBA 会拒绝这样的脚本:

仅包含数据更改的 SQL 脚本

在下一个测试中,我删除了 create table 命令:

不出所料,该变更被自动批准并立即生效:

数据库自动化和安全性

任何人都可以提交临时脚本,但是应该只允许 DBA 编辑这个过程。让我们在 Octopus 中建立两个团队来执行这个要求。

DBA 将被分配新项目的角色Runbook producer。这使他们能够在特定项目中编辑和执行操作手册:

同时,开发人员将被分配到新项目的Runbook consumer角色。这使他们只能执行操作手册:

最后,创建 runbook 运行时,不要跳过该过程中的任何步骤,这一点很重要。流程中的每一步都将按要求进行标记,以防止这种情况发生:

结论

我实话实说。比起我之前的过程,我更喜欢这个过程。Runbooks 使一切变得更简单,更容易维护。移动部件的数量已经减少。我真的很喜欢不必创建一个版本来运行一个特别的 SQL 脚本。我不必为了在任何环境中运行脚本而创建一个时髦的生命周期。

我认为这是一个更好的流程的良好开端。在我输入这段文字的时候,我可以想出更多的迭代来使这个过程更加有用。最终,保留策略将运行,runbook 运行将被清除。需要保留策略来保持 Octopus 的精简运行,但是我们不希望丢失特定环境的任何审计历史。我可以添加一个步骤,将提交的脚本和结果保存在文件共享中。

如果您想查看工作示例,可以访问我们的示例实例

下次再见,愉快的部署!


数据库部署自动化系列文章:

在数据库部署自动化管道中使用专用脚本——Octopus Deploy

原文:https://octopus.com/blog/database-deployment-automation-adhoc-scripts

Octopus worker deploying an ad-hoc SQL script illustration

自动化数据库部署在连续交付方面实现了巨大飞跃。我无法相信自动化数据库部署解决了这么多问题。无论是添加新表、修改存储过程还是创建索引。不管是什么,我不再需要确定环境之间的差异。

尽管有这些优势,一个常见的场景不断出现;在数据库服务器上运行即席查询。我见过的最常见的用例是修复数据。通常,当用户做了意想不到的事情时,数据会处于一种奇怪的状态。在某些情况下,根本问题不会被修复(这种情况发生得不够频繁),或者问题在一周左右的时间内不会被修复,但是数据需要立即修复。

当我过去遇到这种情况时,过程是:

  1. 开发人员创建脚本来修复数据。
  2. 他们将脚本发送给 DBA 或拥有更改数据所需权限的人。
  3. 有权运行脚本的人。
  4. 通知开发人员脚本已经运行。

这个过程有很多缺陷。在我职业生涯的某个阶段,我要么是开发人员,要么是运行脚本的人,这不是一个愉快的过程:

  1. 运行脚本的人不是系统专家。大多数情况下,在运行脚本之前,只是粗略地浏览一下。
  2. 拥有必要权限的人可能已经回家了,出去吃午饭了,或者正在开会。该脚本可能几个小时都不会运行。在某些情况下,必须立即修复数据。
  3. 通知开发人员是一个手动过程,这意味着脚本可能已经运行,但通知尚未发送。
  4. 大多数公司不给初级开发人员修改产品的权利。坦率地说,运行脚本的人还有其他更重要的职责。他们可能真的专注于某件事,被打断会打断他们的思路。
  5. 如果请求是通过电子邮件或 slack 完成的,就不会进行审计,电子邮件是文档死亡的地方。

Octopus Deploy 不仅仅可以部署软件。增加了许多新功能,使 Octopus 部署了一个更完整的 DevOps 工具。在这篇文章中,我将带您完成一个自动运行即席查询的过程。

我使用 Octopus Deploy 的原因(除了我在这里工作的事实之外)是因为它可以为这个过程提供以下内容:

  • 审计:Octopus Deploy 可以告诉您谁提出了请求,谁批准了请求,以及这一切是何时发生的。
  • 工件:使用 Octopus Deploy 内置的工件功能,可以存储和捕获运行的确切脚本,但是,如果有人在文件共享之后更改了脚本,就无从得知了。
  • 认可:在某些情况下,让另一双眼睛看剧本是很重要的。Octopus Deploy 可以设置为基于一组标准有条件地批准脚本。
  • 自动化:不再需要手动发送电子邮件。不再需要手动发送确认。不再打开 SSMS 和运行脚本。
  • 可重复:将在所有环境中使用相同的过程来运行脚本。

用例

为了这篇博文的目的。以下是使用案例:

  • 作为一名开发人员,我需要运行一个特别的查询来添加一个索引,看看这是否能解决一个性能问题。如果是,那么将该索引添加到数据库定义中,并将其推送到所有环境中。
  • 作为一名 DBA,我需要运行一个特殊查询来创建一个 SQL 登录。
  • 作为一名支持工程师,我需要运行一个特别的查询来授予开发人员选择权限。
  • 作为一名业务分析师,我需要为用户解决一个数据问题。

要求

考虑到用例,以下是流程的要求:

  • 章鱼展开。
  • 没有源代码管理。许多 DBA、支持工程师和业务分析师不熟悉源代码控制工具。
  • 自动化。当脚本准备好时,它们应该在五分钟内运行,而不必填写表格或通知任何人。
  • 对脚本的分析,如果脚本包含某些关键字,那么人应该在运行它之前检查脚本。
  • 在任何环境下工作。我们希望鼓励人们在任何环境下运行这个。甚至是戴夫。

设置

触须

我们的数据库部署文档建议您在 Octopus Deploy 和数据库服务器之间的跳转框上安装触角。当使用集成安全性时,这些触角在有权处理部署的服务帐户下运行。这些触角将处理正常部署。

您有几个选项来设置临时流程和权限:

  1. 继续使用部署触角,但赋予他们执行额外任务的提升权限。
  2. 创建一组具有提升权限的新服务帐户,并为这些新服务帐户创建新的触角。
  3. 选项 1 和选项 2 的组合。创建两条管道。一个用于数据修复,另一个用于其他更改。数据修复通过常规部署目标运行,但是其他更改通过一组新的部署目标运行,这些目标具有新的服务帐户。

生命周期

这个过程允许人们在生产中直接运行脚本。使用默认的开发生命周期来测试预生产到生产没有太大意义。创建新的生命周期,允许部署到任何环境。我称我的脚本生命周期为:

您可以通过创建一个阶段并将所有环境添加到该阶段来实现这一点:

项目和流程

对于这个过程,我创建了许多步骤模板。我不想把它们提交给社区图书馆,因为它们不够通用,但是你可以在我们的 GitHub 示例库中找到它们。

摄取脚本

我将为这个用例编写一个数据库脚本:

作为一名业务分析师,我需要为一名用户解决一个数据问题。

我想到了几个问题:

  1. 问:什么环境?答:生产。
  2. 问:什么 SQL 服务器?答:127.0.0.1。
  3. 问:SQL Server 上的什么数据库?答:RandomQuotes_Dev。
  4. 问:谁在提交剧本?鲍勃·沃克。

好了,我们知道了答案,我们如何把这些从我们的大脑中释放到章鱼的大脑中呢?为此,我将使用一个名为元数据的 YAML 文件,其中包含所有这些信息:

---
DatabaseName: RandomQuotes_Dev
Server: 127.0.0.1
Environment: Dev
SubmittedBy: Bob.Walker@octopus.com
... 

下一个问题是如何将 YAML 文件和 SQL 脚本发送到 Octopus Deploy 来运行?为了使提交脚本的人尽可能容易,我将使用一个热文件夹。我编写了一个 PowerShell 脚本,它将:

  1. 在常用文件夹中查找任何新目录。
  2. 使用 Octo.exe 打包文件夹。
  3. 将包推至 Octopus Deploy。
  4. 创建新版本。
  5. 使用 MetaData.yaml 文件确定要部署到哪个环境。
  6. 将文件夹移动到已处理的位置,以便脚本不会再次运行。

我可以设置一个在服务器上运行的计划任务。但是这个任务没有真正的可视性。如果它开始失败,我不会知道它失败了,直到我 RDP 到服务器上。

我没有经历那个噩梦,而是在 Octopus Deploy 中建立了一个新项目,名为“即席查询构建数据库包”该过程只有一个步骤,即运行 PowerShell 脚本来构建数据库包。记下生命周期,它只运行在一个虚拟环境中,我称之为SpinUp:

【T2

它有一个触发器,每五分钟创建一个新版本,并运行以下流程:

在事件中,我想扩展这个过程以支持其他类型的脚本,我将它作为一个步骤模板:

眼尖的读者会看到参数Octopus项目。这是运行脚本的项目。

运行脚本

为了满足上述要求,我希望该流程执行以下操作:

  1. 将软件包下载到跳转框中。
  2. 获取包中的所有文件,并将它们作为工件添加(如果需要审查的话)。
  3. 对脚本执行一些基本的分析。如果任何脚本没有使用事务,或者使用关键字 DropDelete ,那么我想触发一个手动干预。
  4. 需要手动干预时发出通知。我喜欢的工具是 slack。
  5. 运行脚本。
  6. 如果脚本失败,发送失败通知。
  7. 如果脚本成功,发送成功通知。

【T2

下载软件包的步骤非常简单。将软件包下载到服务器。不要运行任何配置转换。不要替换任何变量。只需部署软件包:

“从要检查的包中获取脚本”是一个步骤模板,它执行以下操作:

  1. 读取 YAML 文件并设置输出参数。
  2. 将包中的所有文件作为工件添加。
  3. 对 SQL 文件执行一些基本的分析。
  4. 如果分析失败,设置一个输出变量ManualInterventionRequired

这都是在一个步骤模板中完成的。唯一需要的参数是下载包的步骤:

Octopus Deploy 输出参数的格式可能很难记住。我知道我会输错一些东西,所以与其这样做,我使用变量。这样,如果我真的要改变什么,我只需要改变一个地方:

当我通知某人时,我可以很容易地包含这些信息。另外,请注意,该步骤将基于ManualInterventionRequired输出变量运行:

【T2

人工干预也是如此。运行条件基于ManualInterventionRequired输出变量:

运行 SQL 脚本步骤将遍历所有 SQL 文件并运行它们。同样,为了使它更容易,我使用了一个步骤模板。该流程使用了invoke-sqlcmd,它将捕获输出并添加任务历史:

假设一切顺利,成功通知可以发出:

否则,失败通知可能会发出:

流程演示

我准备了一个可以运行脚本:

MetaData.yaml 文件将脚本设置为在 Dev:

剧本本身没什么特别的。我不打算用一个事务来表明流程会选择它并强制进行手动干预:

我已将该文件夹复制到常用文件夹中:

章鱼拿起那个文件夹:

我现在看到 demo 文件夹已经移动到 processed 文件夹中。我在上面贴了一个时间戳,这样我就能确切地知道那个文件夹是什么时候被处理的:

查看运行脚本的项目,我可以看到已经创建了一个新的版本,并且有一个手动干预正在等待我:

我可以检查松弛通道,并看到批准消息已发送:

进入发布,我可以看到工件已经生成。如果我愿意,我可以下载它们并查看将要运行的确切脚本。当我查看审批详细信息时,我可以看到该消息与时差通知相同:

批准部署后,将运行脚本并捕获输出:

由于脚本成功运行,成功通知被发送到 slack:

常见问题解答

我如何阻止某人向开发环境提交脚本,但允许它用于生产 SQL Server?

使用集成安全性时,每个环境都要有一个触手。该触手只能访问其环境中的 SQL 服务器。使用 SQL 身份验证时,每个环境有单独的用户和密码。无论哪种情况,脚本都将失败,因为用来登录 SQL Server 的用户将无法登录。

如果我想让每个剧本在进入前期制作和制作阶段时都接受审查,该怎么办?

将手动干预步骤更改为始终运行。此外,将环境更改为生产前和生产环境。有条件批准是为了仅在满足某些条件时才要求批准。事实上,从一开始,我建议所有发送到前期制作和生产的脚本都要经过人工批准。当在流程中建立了信任后,就应该引入有条件的批准了。

这看起来有点过了。你不能在 Octopus Deploy 中使用提示变量吗?

绝对的!我有另一个项目来做这个。问题是,谁来提交这些脚本?他们应该有权利创建一个版本并投入生产吗?每个人都可以使用 Octopus Deploy 吗?对于我的用例,我的答案是否定的。我在这个过程中的主要目标是消除尽可能多的手动步骤。使用提示变量手动创建版本增加了太多的手动步骤。

结论

我第一个承认这个过程远非完美。这并不适用于每家公司。这篇文章的目的是提供一个流程的例子,你可以修改这个流程以用于你的公司。

下次再见,愉快的部署!


数据库部署自动化系列文章:

数据库部署自动化方法- Octopus Deploy

原文:https://octopus.com/blog/database-deployment-automation-approaches

Database deployment automation approaches

希望在阅读完之后,为什么要考虑数据库部署自动化?您已经准备好投入数据库部署自动化。根据您的公司,自动化您的数据库部署可能是一个很大的变化,它可能会引起摩擦。摩擦是变化的敌人;摩擦力越高,采用速度越慢。这篇文章的目的是帮助消除这种摩擦。

我用 Microsoft SQL Server 演示了这些原则,但是这些原则也适用于您选择的数据库技术。

这篇文章讨论了以下内容:

数据库部署自动化方法

部署数据库可能非常复杂,有多种方法。Octopus Deploy 集成了多种第三方工具和方法,但这种灵活性意味着有很多选择,当您评估第三方工具时,您会发现它们以两种方式之一进行部署,每种方式都有其优缺点。

排名第一的基于状态的数据库部署方法

使用基于状态或模型驱动的数据库部署方法,定义数据库的期望状态,并将状态保存到源代码控制中。在部署期间,该工具将所需状态与部署目标进行比较,并生成增量脚本。将为每个环境执行此过程。

数据库所需的状态作为文件存储在源代码管理中。根据您使用的工具,具有所需状态的文件可以是一系列创建脚本、XML 文件或完全不同的东西。重要的是要知道该工具将负责更新和维护这些文件。

基于州的优势

基于状态方法的工具通常与您的 IDE 集成在一起。例如,Redgate 的工具与 SQL Server Management Studio 集成,微软的 SSDT 工具与 Visual Studio 集成。使用 IDE 对模式进行更改,然后由 IDE 的插件接管。它运行一个比较来确定变更和当前在源代码控制中的内容之间的差异。然后,它对文件系统上的必要脚本进行更改。

所有文件系统交互都发生在幕后。该工具跟踪所有的更改,这使您可以专注于数据库的更改和测试。在您测试了这些更改之后,您可以使用该工具来更新源代码控制中的文件。

最后,一些工具允许您将一个表标记为静态数据,数据本身被签入到源代码控制中。在部署期间,该工具将检查目标表中的数据,如果目标表缺少数据或数据不正确,增量脚本将包括数据更改 T-SQL 语句。

基于州的骗局

在每个环境的部署过程中会生成一个唯一的增量脚本。这是因为一个变更可能应用于一个环境(开发),而不是一个更高的环境(生产前或生产)。这使得工具变得更加复杂,并且每隔一段时间,该工具将生成一个包含意外更改的增量脚本,尤其是在权限设置不正确的情况下。

工具想要控制关于数据库的一切,从表到模式再到用户。您必须配置该工具以忽略数据库的某些部分。

尽管工具很聪明,但它很难处理更复杂的变化。例如,当将一个列从一个表移动到另一个表时,该工具不知道这是您的意图,它将从旧表中删除该列,并在新表中创建一个新的空列。该工具通常包括某种迁移脚本功能,您可以在其中编写自己的迁移脚本,但是迁移脚本有自己的规则,您必须遵守。

这种缺乏控制有时会成为一种负担。您可能最终会创建一个与该工具一起工作的定制流程。例如,该工具可能不支持后期部署脚本,为了获得这一点,您必须创建一个可以打包并发送给 Octopus 的后期部署文件夹。然后,您必须在 Octopus 中更新您的流程,以查找文件夹并运行它找到的任何脚本。它起作用了,但是现在你要负责维护这个过程。

#2 数据库迁移脚本方法

数据库迁移脚本方法是手写所有必要的增量脚本。这也称为变更驱动或基于脚本。这些脚本被签入源代码控制。在部署期间,该工具将查看哪些迁移脚本尚未在目标数据库上运行,并以特定的顺序运行它们。

迁移脚本优点

使用迁移脚本方法,您可以完全控制所有脚本。当部署变更时,您确切地知道将要运行什么脚本。复杂的变化更容易处理;您只需要编写脚本并将其保存到源代码控制中。一些迁移框架允许您编写代码来进行迁移,以便更容易地实现更复杂的更改。此外,从部署中排除项目要容易得多。只是不要包含您想要排除的项目的脚本。

迁移脚本缺点

基于状态的方法确保整个目标数据库与期望的状态相匹配。基于脚本的方法则不然。可以在进程之外向目标数据库添加一个新表。每个有权更改数据库的人都必须参与并使用这个过程,因为一两个流氓开发人员可能会造成大混乱。

查看特定对象(如表或存储过程)的历史要困难得多。您必须进行搜索以找到对象发生更改的所有文件,而不是转到单个文件并查看历史记录。根据表更改的数量,可能很容易错过关键的更改。

最后,很多开发人员都不是 SQL 开发专家。他们使用 SQL Server Management Studio 用户界面来创建表和索引,他们不知道如何编写大量手工更改。记住 T-SQL 语法需要大量的练习。如果该工具允许您为更复杂的更改编写代码,那么理解语法和规则还有一条学习曲线。

选择一种方法

正确的方法是非常主观的。

当下列任一情况适用时,基于状态的方法效果最佳:

  • 你正处于一个项目的早期阶段,数据库有很多变动。
  • 有多个人/团队在更改数据库。
  • 您的公司首次涉足自动化数据库部署。
  • 你的代码库由成熟的数据库组成,你不期望有太多的变化。
  • 您希望强制开发人员自动遵循该过程(如果对目标数据库进行了更改,并且没有签入,该更改将被删除;对于某人来说,只需要发生一次就可以学会)。
  • 大多数将进行更改的开发人员缺乏制作复杂 T-SQL 语句的经验。

迁移脚本方法在以下情况下最有效:

  • 每个做出改变的人都足够自律,总是遵循这个过程。
  • 进行更改的人有进行复杂数据库更改的经验。
  • 你会不断碰到基于州的方法所强加的限制。
  • 每个人都想尽可能地控制过程。

如果您最初从基于状态的方法开始,几年后,决定转向变更驱动的方法,不要感到惊讶。当你选择一个供应商,红门,微软等。,确保他们提供的软件套件支持这两种方法。

转向专用数据库

无论您选择哪种工具,我都建议将开发人员转移到专用数据库。专用数据库的典型方法是在开发人员的笔记本电脑上安装 SQL Server,这提供了以下优势:

  1. 开发人员可以尝试有风险的改变,而不必担心会影响到其他人。如果他们打碎了什么东西,只有一个人会受到影响。
  2. 支持分支。对于共享模型,只有一个数据库。如果在没有相应代码更改的情况下对数据库进行了重大更改,那么每个人都将停止工作。现在,所有的更改都可以在一个分支上进行并签入,同时进行部署。
  3. 开发人员在准备好的时候应用新的变更。对于共享模型,突破性的变更要求开发人员停止他们正在做的事情,并应用最新的代码变更。有了专用的数据库,他们可以专注于当前的任务,并在准备好使用它的时候进行更改。

共享模式与此相反。当使用共享模型时,您不会获得分支的灵活性;每个人都必须更新他们的代码。不要误解我的意思,共享模型也可以工作,但是它最适合使用静态数据库模式的少数开发人员。随着团队向外扩展,共享模型很快瓦解。

沟通

自动化数据库部署引入了一个有趣的挑战。在自动化之前,开发人员可以对共享数据库进行更改,每个人都会立即看到。这使得更难踩到对方的脚趾。但是对于自动化数据库部署和专用数据库,风险要高得多。开发人员 A 可以在他们的分支中对一个表进行更改,开发人员 B 可以在他们的分支中对同一个表进行更改,这两种更改可能会相互冲突。这意味着需要有一种机制让其他人知道正在进行什么样的更改。它可以是一些简单的事情,比如一个松散的信息或者一次日常谈话中的讨论。重要的是确保每个人都在同一页上。

建立信任

过去,DBA 是在生产环境中运行脚本的人,因为他们有权限。现在,一个自动化的过程将完成这项工作,这可能真的很可怕。如果出了问题,数据丢失了,就很难恢复。每个人都必须信任流程和工具。根据我的经验,建立信任的最佳方式是使用 Octopus Deploy 工件并在 SQL Server 上设置权限。

章鱼部署神器

使用 Octopus Deploy 工件,您可以创建一个包含所有即将在触手上运行的脚本的文件,并将它上传到服务器。DBA 可以在数据库上运行脚本之前批准它。

在某些情况下,第三方提供的步骤模板直接内置了工件创建。例如,下面是使用 Redgate 部署工具的过程:

【T2

在部署过程中,它会自动创建工件:

工件通过不允许批准过程来帮助建立信任,但是它也提供了审计历史。三个月后,我可以回到这个部署,查看数据库发生了什么变化。

许可

在实现自动化数据库部署时,我听到的一个常见问题是,“我们如何防止有人插入脚本来授予自己 sysadmin 权限?”使用工件是一个好的开始,但并不能完全解决问题。如果 DBA 或审批人员忙得不可开交,他们很容易错过工件中特定的 SQL 语句。防止这种情况发生的最好方法是限制执行部署的帐户的权限。我们的文档提供了几个例子,从限制性最小到限制性最大。请记住,这些只是建议。我鼓励与你的团队交流,以确定你对什么满意。

触手安装在哪里

您不希望将触手直接安装在 SQL Server 上。SQL Server 通常是一个集群或高可用性组,触角将尝试同时将更改应用到所有节点。您不希望让部署 Windows 服务或 IIS web 应用程序的触角处理数据库部署。那些触角可能在非军事区。触手应该在特定的服务帐户下运行,并具有执行部署所需的权限。大多数工具利用端口 1433,并简单地运行一系列 T-SQL 脚本。触手可以安装在任何机器上,只要它连接到数据库。出于这些原因,我建议您使用位于 Octopus Deploy 和 SQL Server 之间的跳转框。

请参考我们的文档了解更多信息。

结论

乍一看,这似乎需要做很多准备工作,但重要的是要记住这些是指导方针。不要花几周的时间讨论和辩论,拿出一个初步的计划,迭代。在这个过程的开始,我们花了大约两天时间讨论和研究我们的初步计划。

我对此的建议是:

  1. 讨论上述项目,并提出一个初步计划。
  2. 建立一个试验团队来迭代任何问题。
  3. 等待试点团队成功部署到生产环境中。
  4. 一次一个地向其他项目推广。如果每次你把它推广到一个新项目时,你遇到一些新的东西,需要做一些改变,不要感到惊讶。
  5. 不要害怕接触和询问专家。我们可以提供一些初步的指导,但有时,你会需要更多的帮助。在这种情况下,有几家公司提供咨询服务,可以提供帮助。

数据库部署自动化系列文章:

使用 Octopus 和 Redgate Deployment Suite for Oracle 实现数据库部署自动化

原文:https://octopus.com/blog/database-deployment-automation-for-oracle-using-octopus-and-redgate-tools

Database deployment automation using Octopus and Redgate Deployment Suite for Oracle

在加入 Octopus Deploy 之前,我在一个. NET 应用程序上工作了大约三年,使用 Oracle 作为它的数据库。在 Octopus Deploy 版本发布前几年,我开始在那里工作。这些都是艰难的部署。一切都是手动的,我们只能在周六凌晨 2 点进行部署,部署需要两到四个小时。

谢天谢地,那些日子已经过去了。今天可用的工具比以前先进了好几光年。在这篇文章中,我将介绍如何将更改部署到 Oracle 数据库中。本文的目标是使用 TeamCity 作为构建服务器来构建整个 CI/CD 管道(尽管核心概念确实转移到了 Jenkins、Bamboo、TFS/VSTS/Azure DevOps),Octopus Deploy 作为部署工具,Redgate Oracle 工具集在数据库端完成繁重的工作。

免责声明:我在 2010 年至 2013 年间使用甲骨文。Oracle 实例是 10g,我用 Benthic 和 SQL Developer 查询 Oracle。从那时起,发生了很多变化。我是一名 SQL Server 人员,毫无疑问,我在本文中做了一些愚蠢的事情,这些事情不再是最佳实践。

入门指南

如果您想继续学习,您需要下载并安装以下工具:

为了从 Oracle 下载任何东西,您必须创建一个帐户。我选择了个人版,因为就像 SQL Server Developer Edition 一样,它功能齐全,但在使用许可证时会受到限制,而且我只在这个演示中使用它。

出于本文的目的,我将部署到运行在 Windows 机器上的同一个 Oracle 数据库。然而,在现实环境中,您应该遵循与此类似的设置。

通过这种设置,您可以在 Octopus Deploy 和 Oracle 数据库前面的 VIP 之间的一个工人或一个 jumpbox 运行触手上安装一个触手。

创建源数据库

Redgate 的 Oracle 工具集是一个基于状态的工具。对于那些没有读过我以前文章的人来说,基于状态的工具是一种将数据库的期望状态保存到源代码控制中的工具。在部署过程中,会生成一个独特的增量脚本来针对 Oracle 数据库运行。该增量脚本将仅用于该环境的数据库。下一个环境将获得一个新的增量脚本。

我要从头开始。我设置了一个虚拟机并安装了 Oracle。接下来,我将创建一个源数据库。这个数据库将代表我希望所有其他数据库处于的理想状态。我将设置我的数据库,然后使用 Redgate 的 Oracle 源代码控制将它签入源代码控制。

我使用 Oracle 提供的数据库创建助手来创建数据库。

我将添加一个包含两列的新表:

我还会加入一个序列。对于那些不熟悉 Oracle 的人来说,当您想对 ID 字段使用自动递增的数字时,需要一个序列。好吧,反正是在老派神谕里。看起来事情有点变化。

最后,我将把序列和 ID 字段绑定在一起。这为我提供了一些准备部署到空白数据库的模式更改。您的更改很可能会更加复杂。

将 Oracle 与源代码管理捆绑在一起

既然我们想要创建的表已经准备好了,是时候将表和序列定义放入源代码控制中了。为此,我使用 Redgate 的 Oracle 源代码控制。

当我们第一次启动应用程序时,我们看到一个选项,创建一个新的源代码管理项目… :

首先,您需要配置数据库连接。不要忘记测试您的连接,这样您就知道它工作正常:

数据库连接准备就绪。接下来,我们配置要使用的源代码控制系统。这个存储库将是一个 git 存储库,但是不使用内置的 git 功能。我更喜欢使用工作文件夹。这样我就可以使用我选择的 git GUI,并获得 git 的全部功能,特别是分支。一旦这个过程开始工作,下一步就是在每个开发人员的机器上安装 Oracle。使用专用实例,开发人员可以同时签入他们的源代码和数据库更改。而不是在共享服务器上进行数据库更改,然后等待代码被签入以利用该更改。

如您所见,我有一个空的工作文件夹:

我可以将该工作文件夹的目录输入到 Redgate 的 Oracle 源代码控制中:

我需要选择我想要使用的模式。我将表放在 SourceDB 模式中,因此我将选择这个模式:

最棒的是,它会显示将要创建的所有文件夹,以及将要创建这些文件夹的目录:

该工具将监控该数据库和模式的任何更改。我将保持名称不变,以免以后混淆:

有四个要签入的变更:

当我单击大箭头时,会出现一个摘要屏幕。对于那些使用过 Redgate 的 SQL 源代码控制的人来说,这应该很熟悉:

单击“保存”按钮会显示所有内容都已成功保存:

打开 Git 扩展,您可以看到已经创建的所有文件:

我将提交这些更改。现在是时候建立一个构建并将这些更改打包成一个 zip 文件供 Octopus Deploy 使用了。

设置构建服务器

在我的 TeamCity 实例中,我创建了一个简单的项目来打包数据库,将包发布到 Octopus Deploy,并在 Octopus Deploy 中创建一个新版本。首先是包数据库。对于这个构建步骤,我将打包整个 db/src 文件夹(包括任何附加的模式)。现在它只包含了 SourceDB 模式:

推销产品应该非常简单:

在 Octopus Deploy 中,我设置了一个非常简单的部署过程。此时的目标是确保所有东西都能成功打包、推送和部署。我还不太担心部署过程(这将在几分钟内完成):

回到 TeamCity,我将在我创建一个发布步骤中使用这个新项目:

现在是关键时刻,第一次运行构建。和...我搞砸了。当然,我做到了。没有什么事情第一次会完美无缺:

试了几次,但我让构建工作正常了:

问题是我在打包路径的开头放了一个/应该是 db/src,就像这样:

如果我从 Octopus 下载软件包并检查它,我可以看到所有创建的文件都在那里:

我们有构建服务器打包、发布和触发部署。现在是时候去 Octopus 并完成这个过程了。

配置目标数据库

如果您像我一样第一次设置所有东西,那么您将需要配置一个目标数据库。我使用数据库创建助手配置了一个名为 DestDB 的新数据库。该数据库的用户名也是 DestDB

如您所见,我没有在这个数据库上设置任何东西:

设置 Octopus 部署

如前面的截图所示,您应该在 Octopus Deploy 和 Oracle 数据库之间设置一个跳转框。这台机器需要安装 Redgate 的 Oracle Tool-belt、 SQL*Plus 和标准的 tnsnames.ora 文件。tnsnames.ora 文件需要包含您需要从这个跳转框连接到的所有主机(数据库服务器)。

由于 Redgate 的 Oracle Tool-belt 中的一个怪癖,您需要作为一个帐户而不是作为本地系统帐户运行触手;否则,您将得到错误消息,指出该工具未被激活,即使它已被激活。请按照这些说明进行设置。

我已经向社区库添加了两个新的步骤模板。 Redgate -创建 Oracle 版本运行 Oracle SQLPlus 脚本。请下载并安装在您的 Octopus Deploy 实例上。

我整理的过程非常简单。第一步生成一个报告和一个增量脚本,在生产前和生产中,DBA 批准更改,然后对数据库运行增量脚本。

我故意让这一步只生成一个增量脚本和一个报告文件。通过包含/deploy命令行开关,可以让 Redgate 的 Oracle 工具为您完成部署。我省略了命令行开关,因为我觉得首先在过程中建立信任并让一个人批准更改是很重要的。社区图书馆是开源的。您可以随意复制并调整该步骤以满足您的需求:

在第一步中,有几个选项需要完成。我已经尽力包含尽可能多的帮助文本。如果有不清楚的地方,请发电子邮件给 support@octopus.com,让我们知道,我们会解决的。

在该步骤的最后,要求您提供源模式和目标模式。这是由于设置红门的工具。step-template 包装了命令行,并提供了模式名作为选项,因此 step 模板也必须提供它作为选项:

第二步模板将采用任何脚本,并使用 SQL*Plus 针对 Oracle 数据库运行它。这一步只需要运行脚本的路径和访问 Oracle 数据库所需的凭证。

请注意,Redgate - Create Oracle 发布步骤将在导出目录中生成一个名为 Delta.sql 的文件。我想让这个脚本尽可能通用,这就是为什么您必须提供完整的路径:

我喜欢 Oracle 工具的一点是它生成的报告显示了包中存储的脚本和目标数据库之间的差异。Redgate - Create Oracle 版本将使其成为一个工件,供您下载和查看:

此外,它还使 delta 脚本成为一个可以下载的工件:

运行首次部署

Octopus 现在已经配置完毕,可以开始第一次测试了。我只打算部署到 dev。

让我们检查一下数据库来确定一下。是的,一切都在那里。

结论

为每个环境手动编写部署脚本的日子正在迅速结束。在本文中,我们为 Oracle 数据库创建了一个完整的 CI/CD 管道。它仍然缺少一些关键特性,比如处理任何类型的初始化数据和静态数据,但是这是一个好的开始。我鼓励你采取这个基本的过程,并开始添加它。

下次再见,愉快的部署!


数据库部署自动化系列文章:

使用 Octopus Deploy、Jenkins 和 Redgate - Octopus Deploy 向 Oracle 数据库部署添加部署后脚本

原文:https://octopus.com/blog/database-deployment-automation-for-oracle-using-octopus-jenkins-redgate

Add post-deployment scripts to Oracle database deployments using Octopus Deploy, Jenkins, and Redgate

在我的上一篇文章使用 Octopus Deploy 和 Redgate 部署到 Oracle 数据库中,我介绍了如何建立一个 CI/CD 管道来部署到 Oracle 数据库,其中 TeamCity 作为构建服务器,Octopus Deploy 作为部署服务器,Redgate 处理所有繁重的工作。这篇文章建立在上一篇文章的概念之上。

Redgate 的工具使用基于模型或期望状态的方法来部署数据库。开发人员按照自己的意愿配置数据库。在这里添加一个表,在那里添加一个视图,然后将数据库的整个状态签入源代码控制。在部署过程中,将所需的状态与目标数据库进行比较,并生成一个增量脚本。

对于开始自动化数据库部署的团队来说,这是一个非常容易掌握和适应的过程。通常情况下,使用这种方法的工具都有插件,或者有一个很好的 UI 的外部程序,来处理所有繁重的工作。每个人都可以继续使用他们现有的工具,所有人需要做的就是点击几个按钮进行一些改变。

基于模型的方法在大多数时候都很棒。它涵盖了 85%的场景。它没有涵盖复杂的数据库更改,例如重命名表、将列从一个表移动到另一个表、重命名列等。对于基于模型的方法,如果您要重命名并部署一个表,该工具将生成一个删除旧表的 drop table 脚本,并为新表生成一个 create table 脚本。

摔桌子从来都不是什么好事。这篇博客文章介绍了如何管理这个场景,以及如何将构建服务器从 TeamCity 转移到 Jenkins。

不间断数据库更改

重命名列、将列从一个表移动到另一个表以及合并表都是破坏性更改。为了部署它们,需要关闭系统;否则,代码将开始抛出错误。这意味着非工作时间部署。

更好的方法是进行不间断的数据库更改。让我们以将一列从一个表移动到另一个表为例。按原样使用工具,情况是这样的:

  1. ColumnA 被添加到 TableB 中。
  2. 列 a 从表 a 中删除。

实际上,您希望工具做到这一点:

  1. 列 a 被添加到表 b 中
  2. 表 a 中的数据被回填到表 b 中
  3. 从表 a 中删除列 a

工具不支持这种功能。是吗?这三个步骤真的需要在一个部署中运行吗?如果发现了一个停止显示的 bug,您需要回滚您的代码更改,会发生什么?除了在一次部署中完成所有三个步骤之外,您是否可以将其分成多次部署?

部署#1

  1. ColumnA 被添加到 TableB 中。
  2. 表 a 中的数据被回填到表 b 中。

部署#2

  1. 列 a 从表 a 中删除。

现在我们有所进展。您可以部署您的数据库更改,然后您的代码更改可以在滚动部署中跨 web 场进行部署。现在,这个过程看起来像这样:

部署#1

  1. ColumnA 作为可空列添加到 TableB 中。
  2. 代码被部署到一个网络场中。
  3. 表 a 中的数据被回填到表 b 中。

部署#2

  1. 列 a 从表 a 中删除。
  2. TableB 上的 ColumnA 被转换为不可为 null 的字段(如果需要)。

这种方法需要数据库开发人员和代码开发人员(如果他们是两个人的话)都遵守一些规则。当 TableB 上的 ColumnA 只有空值时,代码需要灵活地处理它。在随后的部署中,还需要记住从 TableA 中删除该列。

随着您在部署中获得灵活性,这一原则将会带来回报。随着您的数据库支持代码的两个最新版本,您可以开始研究更高级的部署策略,例如蓝/绿部署。

部署后脚本

在我的上一篇文章中,除了将数据从 TableA 回填到 TableB 的脚本之外,我们需要的一切都准备好了。所有必要的基础设施都已就绪,我们只需要对整个流程进行一些修改。

首先,我们在 db/src 文件夹中添加一个名为DLMPostDeploymentScripts的新文件夹:

该文件夹包含等幂脚本,这意味着脚本可以在部署后一直运行。换句话说,如果脚本将数据从一个表移动到另一个表,脚本应该包含不覆盖现有数据的逻辑。编写脚本时,假设它们会运行很多次。

Redgate 的 Oracle 源代码管理对该文件夹的存在没有意见。它不会抛出错误或类似的东西。它只是忽略了文件夹:

Git 不会忽略文件夹,这非常符合我们的需求。我添加了一个名为 001_TestScript.sql 的新文件。它所做的只是从 dual 中选择一个测试值:

詹金斯构型

我已经签入了那个测试文件并把它推了出来。现在是时候(再次)设置构建了。在这篇文章中,我将从 TeamCity 切换到 Jenkins。我并不是因为缺少功能而改变,而是因为我想展示使用 Octopus Deploy 配置任何构建服务器是多么容易。无论是詹金斯,团队城市,竹子,或 TFS/Azure DevOps。

让詹金斯准备章鱼部署

按照这些说明安装[Octopus Deploy 插件]](https://jenkins.io/doc/book/managing/plugins/)。

安装 Octopus 插件后,您需要配置一个 Octopus 服务器。为此,点击管理詹金斯,然后配置系统:

向下滚动一点,直到找到 Octopus Deploy plugin 部分。输入 ID、OctopusDeploy 服务器的 URL 和 API 密钥,然后单击屏幕底部的添加 Octopus Deploy 服务器按钮:

Octopus Deploy 插件将处理创建发行版和部署发行版,但是它不处理打包和发布这些包。为此,我们将使用 Octopus CLI。您可以从八达通下载页面下载最新版本。我将把 Octopus CLI 放在一个文件夹中,供构建访问。这种情况下会是C:\Utilities\Octo

项目设置

我将从头开始这个项目,所以我将创建一个新的自由式项目:

接下来,我指定了将要构建的 Git repo。

请注意:这是 GitHub 中的公共回购,这就是为什么我没有输入任何凭证:

对于这个演示,我告诉 Jenkins 使用 cron 表达式每三分钟轮询一次 GitHub。您可以根据需要对此进行调整,但对于我的目的来说,这就是我所需要的:

构建步骤是将 db\src 文件夹打包成一个. zip 文件,并将其推入。Octopus Deploy 的 zip 文件准备好部署内置存储库:

【T2

接下来,让我们创建发布。

请注意:我还没有对 Octopus Deploy 做任何更改,我只是想让这个构建工作起来并推进到 Octopus Deploy,当它成功时,我将对流程做一些更改:

就是这样!让我们开始构建,看看会发生什么!它失败了!我认为任何 CI 系统的前十个构建都应该算作 alpha 构建。我还没有一次构建成功的例子:

让我们对打包和推送流程做一些调整。使用%BUILD_NUMBER%代替$BUILD_NUMBER,让第二步做一个推而不是一个包,并做一些其他的调整。

这些是要使用的命令。

C:\Utilities\Octo\Octo.exe Pack --Id=RedgateOracle --format=Zip --version=2018.11.1.%BUILD_NUMBER% --BasePath=db\src
C:\Utilities\Octo\Octo.exe Push --Package=RedgateOracle.2018.11.1.%BUILD_NUMBER%.zip --Server=[Your Server URL] --ApiKey=[Your API Key] 

尝试了几次,但最终,Jenkins 推进了 Octopus Deploy,并将该版本部署到 dev。

项目更新

在前一篇文章的结尾,部署过程看起来像这样:

我们将扩展该流程以支持额外的脚本。首先,我们需要重新安排一些项目。让我们从将一些硬编码的值移出步骤并移入变量开始。例如,导出路径:

经过一些重新配置,这些是我现在拥有的变量:

在查看 Redgate - Create Oracle 发布步骤时,您可以看到我使用这些变量的所有地方。

自从我的上一篇文章以来,我根据用户的反馈更新了这个步骤模板。您现在有了更精细的控制。

同样的事情可以在最后一步看到。所有硬编码的值都已被变量替换:

我们将在流程中添加两个新步骤。第一步是将在 DLMPostDeploymentScripts 文件夹中找到的所有脚本合并成一个脚本,并作为工件上传。这允许审批者浏览脚本,以确保它不会做任何疯狂的事情。

我在库中创建了一个 step 模板,你可以使用它,叫做文件系统——将一个目录中的所有文件合并成一个文件。台阶将为您处理所有的繁重工作。你只需要给它提供必要的参数。

我要添加的下一步是另一个运行 Oracle SQLPlus 脚本步骤。这一次它将运行部署后脚本。

这个过程现在看起来像这样:

是时候运行部署了,看看会发生什么。如您所见,创建了一个额外的工件:

并且脚本运行成功:

结论

通过对流程进行一些小的修改,我们可以覆盖比以前多得多的场景。使用这个过程需要一点训练。您需要确保脚本可以运行多次。您还必须记住在脚本运行后删除它们,这样您就不会一遍又一遍地运行脚本。脚本将手动编写,因此在脚本第一次运行时,您有可能会遇到由小到大的错误。

但这些都是次要问题。当我在以前的公司帮助编写这个过程时,我很惊讶人们对它的接受程度。一旦他们知道他们可以编写自己的脚本来处理数据迁移或其他任何事情,我开始看到一些非常独特的用途。有人运行了一系列脚本,将一些初始化数据或种子数据插入到表中。我们进一步扩展了这个过程,以检查数据库是否存在。如果数据库没有,那么它将动态地创建它并用种子数据初始化它。这使得我们能够快速启动和关闭测试环境和客户。

这仅仅是开始。有了这些,就可以考虑蓝/绿部署策略了。可以建立一个流程来完成常规部署,当蓝/绿切换完成并成功测试后,可以运行最终的数据库脚本来清理数据。

下次再见,愉快的部署!


数据库部署自动化系列文章:

使用基于状态的 Redgate SQL 变更自动化- Octopus Deploy 实现数据库部署自动化

原文:https://octopus.com/blog/database-deployment-automation-using-redgate-sql-change-automation

Database deployment automation using state-based Redgate SQL Change Automation

我之前的博客文章讨论了为什么您应该考虑自动化数据库部署入门技巧

本文将使用基于状态的方法Redgate 的 SQL 变更自动化建立一个数据库部署自动化管道。我选择这个工具是因为它易于设置,可以与 SSMS 集成,而且我已经有了一个演示设置。我也偏向【Redgate 的工装。

在本文结束时,您将拥有一个可以演示的概念证明。

准备工作

对于这个演示,您需要一个正在运行的 SQL Server 实例、一个 Octopus Deploy 实例和一个 CI 服务器。我建议使用一个开发环境或您的本地机器来进行概念验证。

你需要以下工具。给出的例子使用了团队城市和 VSTS/TFS,但是即使你使用不同的工具,所有 CI 工具的核心概念和用户界面都是非常相似的。

  • 八达通部署:
  • Redgate SQL 工具带
  • 构建服务器/持续集成(CI)工具(选择一项):
  • SQL Server Management Studio (SSMS):
  • SQL Server:

安装软件

如果您在安装这些工具时遇到问题,请访问供应商网站寻求帮助。如果您在安装 Octopus Deploy 时需要任何帮助,请从我们的文档开始,或者联系支持

开发者工作站

这是您将用来进行模式更改并将它们签入源代码控制的机器。当你安装 Redgate 的 SQL Tool-belt 时,会提示你安装相当多的软件。您只需要安装以下软件:

  • SQL 源代码管理。
  • SQL 提示符(这不是必需的,但它使事情变得容易得多)。
  • SSMS 集成包。

Octopus Deploy 和 Redgate 都有主要构建服务器/持续集成工具的插件:

  • 詹金斯:
  • 团队城市:
  • VSTS/TFS:
  • 竹子:

部署目标或数据库工作者

在 SQL Server 上安装 Octopus 触手是一个大禁忌。我们的文档会更详细地解释为什么。

首选的解决方案是在 Octopus Deploy 和 SQL Server 之间配置一个跳转框。Octopus 为此支持两个选项:

  • 部署目标
  • 数据库工作者

在这篇文章中,我将添加一个部署目标,但是我想提到 workers 也是一个不错的选择。它们对于进行大量数据库部署的团队特别有用。

Workers 使您能够将部署工作转移到在池中运行的其他机器上,数据库部署是一个常见的用例。您可以创建一个专门的工作人员池,供多个项目和团队用于数据库部署。

更多信息见我们的文档

出于安全考虑,我建议以特定用户帐户运行触手/Worker。这样,您可以利用集成的安全性。您可以配置活动目录或者使用 SQL 用户来代替。

对于跳线盒,您需要安装以下项目:

  • SQL 变更自动化 PowerShell 3.0。
  • SQL 变更自动化。

示例项目

对于这个演练,我修改了 RandomQuotes 项目。这个示例的源代码可以在这个 GitHub repo 中找到。派生存储库,以便在阅读本文时可以进行修改。

配置 CI/CD 管道

您需要的一切都已经签入到源代码控制中。我们需要做的就是构建它并将其推送到 SQL Server。

Octopus 部署配置

您需要从 Redgate 到创建数据库版本部署数据库版本的步骤模板。当您浏览步骤模板时,您可能会注意到步骤模板直接从包中部署。SQL 变更自动化的基于状态的功能通过比较存储在 NuGet 包中的数据库的状态和目标数据库来工作。每次运行时,它都会创建一组新的增量脚本来应用。推荐的流程是:

  1. 将数据库包下载到跳转框中。
  2. 通过将跳转框上的包与 SQL Server 上的数据库进行比较来创建增量脚本。
  3. 查看 delta 脚本(这在 dev 中可以跳过)。
  4. 使用跳转框上的触手在 SQL Server 上运行脚本。

使用步骤模板从包中部署会阻止查看脚本的能力。

这是我为部署数据库而组织的流程。

该过程执行以下操作:

  • 数据库的主要 SQL 用户。
  • 数据库。
  • 将 SQL 用户添加到数据库中。
  • 将用户添加到角色中。

如果您希望您的流程这样做,您可以从 Octopus 社区步骤模板库下载这些步骤模板。

如果这是自动化数据库部署之旅的开始,您不必添加所有这些功能。上面截图中需要的主要步骤是:

让我们逐一介绍一下。下载包的步骤非常简单,除了选择包名之外没有自定义设置:

Redgate - Create 数据库发布步骤更有趣一些。导出路径是增量脚本将被导出到的位置。这必须是 Octopus Deploy 触手文件夹之外的目录,因为Redgate-Deploy from Database Release步骤需要访问该路径,而触手文件夹对于每个步骤都是不同的:

我喜欢使用项目变量:

该变量的完整值为:

 C:\RedGate\#{Octopus.Project.Name}\#{Octopus.Release.Number}\Database\Export 

该屏幕上的其他建议:

  • 我已经提供了用户名和密码。我建议使用集成安全性,并让触手作为一个特定的服务帐户运行。我的测试机器上没有配置 Active Directory,所以我在这个演示中使用了 SQL 用户。
  • 查看一下默认 SQL 比较选项,确保它们符合您的需求。如果没有,您需要在SQL Compare Options (optional)变量中提供您想要的。你可以在这里查看文档。如果您决定使用定制选项,我建议在库变量集中创建一个变量,这样这些选项可以在许多项目中共享。
  • 如果您希望限制部署过程可以更改的内容,请使用自定义过滤器。我写了一篇关于如何做到这一点的博文。我个人倾向于过滤掉所有用户,让 DBA 管理他们。更好的是,让章鱼来管理它们,因为它可以处理环境差异。

下一步是批准数据库发布。我建议创建一个定制团队来负责此事,但我更喜欢在开发和 QA 中跳过这一步:

创建数据库发布步骤利用了 Octopus Deploy 中内置的工件功能。这允许批准者下载文件并检查它们:

最后一步是部署数据库版本。这一步将 delta 脚本放在导出数据路径中,并在目标服务器上运行它,这就是为什么我建议将导出路径放在一个变量中:

这就是 Octopus 部署配置。现在是时候转移到构建服务器了。

构建服务器配置

在这篇博文中,我使用了 VSTS/TFS 和团队城市。至少,构建应该做到以下几点:

  1. 使用 Redgate 插件构建一个包含数据库状态的 NuGet 包。
  2. 使用 Octopus Deploy 插件将包推送到 Octopus Deploy。
  3. 为刚刚使用 Octopus Deploy 插件推出的包创建一个发布版本。
  4. 使用 Octopus Deploy 插件部署该版本。

VSTS / TFS 大楼

在 VSTS/TFS,构建和部署数据库只需三个步骤:

第一步将从源代码控制构建数据库包。突出显示的项目是您需要更改的项目。子文件夹路径变量是相对的。我正在使用一个示例 Git repo,这就是为什么redgatesqlchangeautomationstate based文件夹位于路径:

【T2

push package to Octopus 步骤要求您知道上一步生成的工件的完整路径。我不能 100%确定不经过反复试验你怎么会知道:

这是全部价值,如果你想复制的话:

 $(Build.Repository.Localpath)\RandomQuotes-SQLChangeAutomation.1.0.$(Build.BuildNumber).nupkg 

必须在 VSTS/TFS 配置 Octopus Deploy 服务器。你可以在我们的文档中看到如何操作。

最后一步是创建一个发布,并将其部署到 dev。用 Octopus Deploy 连接 VSTS/TFS 后,您可以读取所有项目名称。您还可以配置这个步骤,将发布部署到 dev。单击显示部署进度将停止构建并强制等待 Octopus 完成:

团队城市

团队城市的设置与 VSTS/TFS 的设置非常相似。只需要三个步骤:

第一步是构建数据库包步骤,它有类似于 VSTS/TFS 的选项。您需要输入文件夹以及包的名称:

您必须在高级选项中输入一个包版本,否则您将从 Redgate 工具中得到一个关于无效包版本的错误:

发布包步骤需要填充所有三个选项。默认情况下,Redgate 工具将在根工作目录中创建 NuGet 包:

最后一步是创建和部署版本。提供项目名称、版本号和您要部署到的环境:

查看 CI/CD 管道的运行情况

现在是时候看看这一切是如何运作的了。对于这个演示,我创建了一个新的数据库,RandomQuotes _ BlogPost _ Dev:

如您所见,我没有任何同名的数据库。我将该 SQL Server 用作自动化部署的测试平台:

让我们快速看一下存储在源代码控制中的表:

如果我们打开其中一个文件,我们可以看到由 Redgate 的 SQL 源代码控制生成的创建脚本:

启动一个构建,让我们看看整个管道运行情况。构建看起来很成功:

毫无疑问,在 Octopus Deploy 中部署是成功的。VSTS/TFS 版本被设置为等待 Octopus Deploy 完成数据库部署。如果部署失败,构建也会失败:

回到 SSMS,我们现在可以看到数据库和表已经创建:

更改数据库模式

这适用于现有的项目,但是让我们对数据库模式做一个小的更改,并测试这个过程。这涉及到更多的设置:

  1. 将分叉的回购克隆到本地机器上。
  2. 打开 SSMS,在你的本地机器上创建一个随机报价数据库。
  3. 在 SSMS,将受源代码管理的数据库绑定到新创建的数据库。你可以在文档中阅读如何操作。

将数据库链接到源代码管理时,需要提供存储源代码管理的文件夹的完整路径。我将所有代码存储在一个名为 C:\Code.git 的文件夹中。

C:\Code.git\AutomatedDatabaseDeploymentsSamples\RedGateSqlChangeAutomationStateBased\db\src\ 

现在我们可以对数据库进行更改了。对于这个测试,让我们添加一个将返回值的存储过程:

现在我们可以将更改提交到源代码控制:

假设 CI/CD 管道被设置为在提交时触发,您应该看到新的存储过程出现在 dev 中。

结论

数据库部署自动化确实需要一些准备工作,但是付出的努力是值得的。光是审计就值得了。有了这个工具,我现在可以看到谁做了更改,何时做了更改,以及更改何时投入生产。过去,它保存在另一个位置,有 50%的更新机会。

当您开始这一旅程时,我的建议是将手动验证步骤添加到所有环境中,直到建立信任为止。这将确保您不会意外地签入一个会吹走团队一半数据库变更的变更。

下次再见,愉快的部署!


数据库部署自动化系列文章:

实施数据库部署的经验教训- Octopus 部署

原文:https://octopus.com/blog/database-deployments-lessons-learned

Lessons learned implementing database deployments

在组织中实施数据库部署可能是一项艰巨的任务。在这篇文章中,我分享了我以前工作中的一些经验,以及在数据库部署中需要注意的一些事情。

紧密耦合的数据库

这种情况时有发生,我们大多数人都见过,两个不同的系统访问彼此的数据库。他们需要从对方那里获得信息,而获得信息最快最简单的方法就是伸手进去拿走。它可能从单个表或视图开始,但是随着每个系统的增长,耦合变得越来越紧密,这带来了大量的问题。

数据库依赖性

我记得在一个应用程序(我们称之为应用程序 A)部署后召开了一次紧急会议,这次会议导致另一个应用程序(我们称之为应用程序 B)开始出现故障。双方的脾气都很大,双方都坚定地指责对方。

作为配置经理,我负责部署,所以我在主持会议。最新的部署引入了对 A 的数据库模式的更改,这导致 B 失败。这种情况最麻烦的部分是,A 不知道 B 在拉数据,所以自然地,A 不明白为什么 B 这么激动。

循环依赖

还有一次,我在评估 Microsoft SQL Server DACPAC 作为自动化数据库部署方法时,遇到了循环依赖问题。我创建的 Microsoft SQL Server 数据库项目如果没有对它所联接的另一个数据库的 DACPAC 引用,将无法编译。当我试图为第二个数据库编译项目时,失败了,因为第二个项目依赖于第一个项目,并且不会编译它的 DACPAC 引用。

Redgate SQL 源代码控制和三部分命名约定

我工作的组织决定使用 Redgate SQL 源代码控制作为维护数据库模式的方法。当时的标准是总是使用由三部分组成的命名约定来编写连接,database.schema.object。对于 Redgate SQL 源代码控制,这导致了一个问题。当在对象引用中指定数据库时,它认为这是一个外部数据库调用,在确定对象的构建顺序时没有考虑它。这有时会导致在基础表存在之前就试图构建视图。

没有名称的约束

Microsoft SQL Server 可能相当宽容,有时甚至是有害的。我们遇到的一个问题是没有给默认约束命名。以下是使用默认约束创建表的有效 SQL 语法:

CREATE TABLE Persons
(
    P_Id int NOT NULL,
    LastName varchar(255) NOT NULL,
    FirstName varchar(255),
    Address varchar(255),
    City varchar(255) DEFAULT 'Sandnes'
) 

在这种情况下,微软 SQL Server 将通过给约束一个生成的名称来帮助:

基于状态(也称为基于模型)的部署从脚本文件夹创建一个临时数据库。当执行确保目标数据库处于所需状态的部署后检查时,部署会失败,因为约束没有相同的名称。

混合部署技术

我们使用 Redgate SQL 源代码控制进行模式更改,使用 DbUp 进行数据更改。大多数(如果不是全部的话)基于迁移的部署技术( DBupFlywayRoundhousE )在目标数据库中创建一个表,以跟踪哪些脚本已经被执行,这样它们就不会再次运行。基于状态的方法将删除状态中不存在的任何对象。我们没有考虑到这一点,Redgate SQL 源代码控制一直删除 DbUp 创建的用于跟踪以前运行的脚本的schemaversions表。这导致原本要执行一次的脚本在每次部署时都要运行。

软件中的错误

基于状态的部署软件非常强大,同样复杂。我对该技术如何以正确的顺序生成脚本以将数据库转换到期望的状态印象深刻;然而,就像任何软件一样,总有不太正常的边缘情况。

这方面的一个例子是更改一个列,使它不是一个标识列。这通常没问题,但是这个表也被配置用于静态数据维护。Redgate SQL 源代码控制成功地生成了修改表的正确脚本,但是由于静态数据维护,它在填充表时包含了IDENTITY INSERT ON。由于标识列已被删除,该语句失败。它已经被修复了,但是当我们试图发布时,这个错误引起了一些问题。

结论

除了令人生畏之外,数据库部署问题调试起来也很麻烦。我希望这些提示能为您节省数小时甚至数天的调查和故障排除时间。

使用 Redgate SQL 变更自动化、GitHub Actions 和 Octopus Deploy - Octopus Deploy 进行数据库部署

原文:https://octopus.com/blog/database-deployments-with-github-actions-and-redgate

Database Deployments with Redgate SQL Change Automation, GitHub Actions, and Octopus Deploy

在本文中,我将向您展示如何使用 GitHub Actions 构建一个 Redgate SQL 变更自动化包,并将其推送到 Octopus Deploy 进行部署。

我一直在准备一个关于数据库部署的网络研讨会。我的示例应用程序已经准备好了,我的部署服务器也准备好了(毫无疑问应该是哪个),但是我应该使用哪个构建服务器呢?构建服务器只是网上研讨会的一小部分,我不喜欢在 Azure DevOps 中构建整个项目或建立一个示例 Jenkins 实例。Ryan 最近写道用 GitHub Actions 发布了一个包给 Octopus,所以我决定尝试一下。这种方法带来了几个额外的挑战:

  • 安装 Redgate 的 SQL 变更自动化来构建数据库包。
  • Redgate 的 SQL 变更自动化只能在 Windows 上运行(它需要。NET 框架)。
  • 构建 Redgate SQL 变更自动化包需要创建一个临时数据库,以确保所提议的变更在语法上是正确的。
  • 我还需要安装 Octopus CLI,但是 Windows 没有内置软件包管理器。
  • 我认为有一种更简单的方法来定义版本号。

多亏了 GitHub Action 的优秀文档、一些例子和一些尝试和错误,我克服了所有这些挑战。请继续阅读,看看我是如何做到的。

入门指南

我选择从头开始创建 GitHub 动作工作流文件。名字之后的第一个决定是监视什么。监控的可能性相当惊人。这是我的第一个行动,我想在变复杂之前先从简单开始。监控主分支变化的 GitHub 操作如下所示:

name: Package Database

on:
  push:
    branches: 
      - master 

配置为在 Windows 上运行

定义触发器后,就该定义作业和各个步骤了。我查看的样本被配置为run on Ubuntu:

jobs:
  build:

    runs-on: ubuntu-latest 

Redgate SQL 变更自动化需要在 Windows 上运行。我并不热衷于站在我自己的服务器上,GitHub 称之为 runners,这样才能工作,但谢天谢地我不需要这么做。查看 GitHub 动作的文档,GitHub 动作可以运行在:

关于托管跑步者的文档提供了更多的见解。点击足够多的链接,你会看到一个页面,上面列出了安装在提供的跑步者上的软件。

在 Windows runner 的安装软件的最顶端是我最喜欢的 Windows 包管理器,Chocolatey。

现在我们在做饭。我对我需要的 Windows 版本并不挑剔,所以我选择了windows-latest

name: Package Database

on:
  push:
    branches: 
      - master

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest 

安装 Octopus CLI

我发现的大多数 GitHub 动作示例都使用了 bash 脚本。Redgate 的 SQL 变更自动化使用 PowerShell cmdlets。好消息是我有一个选择。我的选择是:

  • Bash(所有平台)
  • PowerShell 核心(所有平台)
  • Python(所有平台)
  • Sh (Linux/MacOS)
  • PowerShell (Windows)
  • 批处理或 cmd (Windows)

这意味着我可以使用 Chocolatey 来安装包含 Octopus CLI 的 Octopus Tools 包:

name: Package Database

on:
  push:
    branches: 
      - master      

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2

    - name: Install Octopus Tooling
      run: choco install octopustools -y
      shell: powershell 

安装 Redgate SQL 变更自动化

这很好,但是 SQL 变更自动化呢?很长一段时间以来,SQL 变更自动化一直是 PowerShell 的一个模块。PowerShell 模块有点独特;您可以将它们安装到中央静态位置或指定的文件夹中。我不确定这方面的最佳实践/建议,所以我将 SQL 变更自动化包安装到工作目录下的子文件夹中。

我使用 GitHub actions 的环境变量特性为子文件夹名设置一个静态值,这样如果我重命名子文件夹,就不必在多个地方更改它。我添加了创建该文件夹的步骤,然后是将 PowerShell 模块安装到新文件夹的步骤:

name: Package Database

on:
  push:
    branches: 
      - master   

env:  
  PACKAGES_FOLDER: Modules    

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2

    - name: Install Octopus Tooling
      run: choco install octopustools -y
      shell: powershell

    - name: Make Install Modules Folder
      run: New-Item "$PSScriptRoot\${env:PACKAGES_FOLDER}" -ItemType Directory -Force
      shell: powershell    

    - name: Install Redgate Tooling
      run: |
        $LocalModules = "$PSScriptRoot\${env:PACKAGES_FOLDER}"

        Get-PackageProvider NuGet -ForceBootstrap | Out-Null
        Import-PackageProvider PowerShellGet 
        Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue

        Save-Module -Name SqlChangeAutomation -Path $LocalModules -Force -ErrorAction Stop -AcceptLicense        
      shell: powershell 

构建 Redgate SQL 变更自动化包

构建 Redgate SQL 变更自动化包涉及许多小决策:

  • 输出文件夹:包将要保存到的地方。
  • 包名:应用程序的包名。
  • 版本号:包的版本号,对于 Octopus Deploy 应该是自动递增的。
  • 临时数据库 : Redgate SQL Change Automation 将创建一个临时数据库,并尝试运行源代码控制中存储的所有脚本。这样做是为了确保数据库语法正确。

对于这一部分,我不会展示整个 YAML 文件(这将是相当长的),而是其中的一部分,以突出重点。

输出文件夹

我在这里使用了另一个环境变量,并基于它创建了一个新的输出目录:

name: Package Database

on:
  push:
    branches: 
      - master   

env:  
  PACKAGES_FOLDER: Modules
  OUTPUT_FOLDER: PackagesOutput

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2        

    - name: Make Packages Output Folder
      run: New-Item "${env:OUTPUT_FOLDER}" -ItemType Directory
      shell: powershell 

包名

构建过程应该是定义包名的过程,我不希望包名改变,所以我为包名设置了一个环境变量:

name: Package Database

on:
  push:
    branches: 
      - master

env:  
  PACKAGES_FOLDER: Modules
  OUTPUT_FOLDER: PackagesOutput
  PACKAGE_NAME: MySampleApplication 

版本号

我遵循以下经验法则来设置版本号:

  • 为整个应用程序定义一次
  • 跨应用保持一致
  • 易于维护和更新

我个人认为设置版本号应该在 GitHub 操作或任何构建服务器之外进行。通过将它放在 GitHub 动作中,意味着只有开发人员可以更改它。太隐蔽了。也就是说,大多数时候,开发人员是唯一改变它的人。

我认为 GitHub 动作中应该有计算版本号的必要逻辑。你可以让它看看分行名称。或者,在我的例子中,从源代码控制的文件中提取版本前缀。GitHub 动作提供了许多预定义的环境变量。我感兴趣的是 GITHUB_RUN_NUMBER,因为它总是在增加。

GitHub Actions 现在能够设置工作流中其他步骤可以使用的环境变量。语法有点...有趣的是:

echo "::set-env name=[VARIABLE NAME]::[VARIABLE VALUE] 
name: Package Database

on:
  push:
    branches: 
      - master      

env:  
  PACKAGES_FOLDER: Modules
  OUTPUT_FOLDER: PackagesOutput
  PACKAGE_NAME: OctopusTrident.Redgate.Database  

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2

    - name: Set environment variables      
      run: |        
        $versionFromFile = Get-Content versionprefix.md 
        Write-Host "Found $versionFromFile in versionprefix.md"
        $versionNumber = "$versionfromFile.${env:GITHUB_RUN_NUMBER}"
        Write-Host "Setting environment version number to: $versionNumber"        

        echo "::set-env name=PACKAGE_VERSION::$versionNumber"                
      shell: powershell 

临时数据库

这个问题难倒我的时间比我愿意承认的要长。我走上了建立一个 Azure SQL 服务器的道路,使用一个永久的构建数据库。与传统的 SQL 服务器不同,Azure SQL Server 不提供使用Create Database T-SQL 命令创建数据库的能力。你必须使用门户、ARM 模板、TerraForm 或 Azure CLI。基本上除了 T-SQL 什么都有。但是我对 Azure SQL Server 的默认设置有点太严格了。

构建数据库包 cmdlet 的文档说它使用 LocalDB 作为默认数据库。一时兴起,我在已安装应用列表上做了一个快速查找。你瞧,localdb是预装应用的一部分:

敏感变量和调用 Octopus CLI

您可以使用 secrets 功能存储 GitHub 操作的敏感变量。这可以通过访问 GitHub UI 中的存储库,点击设置➜机密来访问:

关于 GitHub 秘密的几点观察。

  • 一个秘密只能写一次。如果您需要更新一个密码,您必须删除该密码,然后重新创建它。
  • GitHub Actions 会竭尽全力阻止你将秘密写入日志。
  • 在 PowerShell 步骤中访问秘密不像 bash 那样简单。

我被最后一个要点绊倒了。语法最终看起来是这样的:

 - name: Handoff to Octopus Deploy
      env:
        OCTOPUS_URL: ${{ secrets.OCTOPUS_SERVER_URL }}
        OCTOPUS_API_KEY: ${{ secrets.OCTOPUS_API_KEY }}        
      run: |        
        octo push --package="${env:OUTPUT_FOLDER}\${env:PACKAGE_NAME}.${env:PACKAGE_VERSION}.nupkg" --server="${env:OCTOPUS_URL}" --apiKey="${env:OCTOPUS_API_KEY}" --space="${env:OCTOPUS_SPACE_NAME}"

        octo create-release --project="${env:OCTOPUS_PROJECT_NAME}" --packageVersion="${env:PACKAGE_VERSION}" --releaseNumber="${env:PACKAGE_VERSION}" --server="${env:OCTOPUS_URL}" --apiKey="${env:OCTOPUS_API_KEY}" --space="${env:OCTOPUS_SPACE_NAME}" --deployTo="${env:ENVIRONMENT_NAME}" 

把所有的放在一起

我已经准备好了构建 Redgate SQL 变更自动化包、将它推送到 Octopus Deploy 并创建一个版本所需的所有细节:

name: Package Database

on:
  push:
    branches: 
      - master   

env:  
  PACKAGES_FOLDER: Modules
  OUTPUT_FOLDER: PackagesOutput
  PACKAGE_NAME: OctopusTrident.Redgate.Database
  OCTOPUS_PROJECT_NAME: Redgate - Feature Branch Example  
  OCTOPUS_SPACE_NAME: Target - SQL Server
  ENVIRONMENT_NAME: Dev

jobs:
  build:
    name: Build and Push Database

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2

    - name: Set environment variables      
      run: |        
        $versionFromFile = Get-Content versionprefix.md 
        Write-Host "Found $versionFromFile in versionprefix.md"
        $versionNumber = "$versionfromFile.${env:GITHUB_RUN_NUMBER}"
        Write-Host "Setting environment version number to: $versionNumber"

        echo "::set-env name=PACKAGE_VERSION::$versionNumber"        
      shell: powershell

    - name: Install Octopus Tooling
      run: choco install octopustools -y
      shell: powershell

    - name: Make Install Modules Folder
      run: New-Item "$PSScriptRoot\${env:PACKAGES_FOLDER}" -ItemType Directory -Force
      shell: powershell

    - name: Make Packages Output Folder
      run: New-Item "${env:OUTPUT_FOLDER}" -ItemType Directory
      shell: powershell

    - name: Install Redgate Tooling
      run: |
        $LocalModules = "$PSScriptRoot\${env:PACKAGES_FOLDER}"

        Get-PackageProvider NuGet -ForceBootstrap | Out-Null
        Import-PackageProvider PowerShellGet 
        Save-Module -Name PowerShellGet -Path $LocalModules -MinimumVersion 1.6 -Force -ErrorAction SilentlyContinue

        Save-Module -Name SqlChangeAutomation -Path $LocalModules -Force -ErrorAction Stop -AcceptLicense        
      shell: powershell    

    - name: Build Redgate Packages      
      run: |
        $LocalModules = "$PSScriptRoot\${env:PACKAGES_FOLDER}"
        $env:PSModulePath = "$LocalModules;$env:PSModulePath"

        Import-Module SqlChangeAutomation

        $project = "db/src"
        $validatedProject = $project | Invoke-DatabaseBuild        

        $buildArtifact = New-DatabaseBuildArtifact $validatedProject -PackageId "${env:PACKAGE_NAME}" -PackageVersion "${env:PACKAGE_VERSION}"
        Export-DatabaseBuildArtifact $buildArtifact -Path "${env:OUTPUT_FOLDER}" 
      shell: powershell   

    - name: Handoff to Octopus Deploy
      env:
        OCTOPUS_URL: ${{ secrets.OCTOPUS_SERVER_URL }}
        OCTOPUS_API_KEY: ${{ secrets.OCTOPUS_API_KEY }}        
      run: |        
        octo push --package="${env:OUTPUT_FOLDER}\${env:PACKAGE_NAME}.${env:PACKAGE_VERSION}.nupkg" --server="${env:OCTOPUS_URL}" --apiKey="${env:OCTOPUS_API_KEY}" --space="${env:OCTOPUS_SPACE_NAME}"

        octo create-release --project="${env:OCTOPUS_PROJECT_NAME}" --packageVersion="${env:PACKAGE_VERSION}" --releaseNumber="${env:PACKAGE_VERSION}" --server="${env:OCTOPUS_URL}" --apiKey="${env:OCTOPUS_API_KEY}" --space="${env:OCTOPUS_SPACE_NAME}" --deployTo="${env:ENVIRONMENT_NAME}"

      shell: powershell 

为什么移交给 Octopus Deploy?

DBA 是一群挑剔的人。我曾经和一个 DBA 一起工作,他反复告诉我,“我想要的只是简单愚蠢的东西。”接下来,他说,“我还想确保开发人员不会向数据库管理员扔垃圾。我们需要知道什么样的变化被部署到生产中。如果有些东西看起来不对劲,我需要阻止它出去。”最后,他告诉我,“当我们部署到生产时,我不希望出现任何意外。我已经厌倦了这种狂野西部的东西,你给我一个你几个小时前写的剧本。”

我相信在工作中使用正确的工具。必要时,我可以用磁铁钉钉子。但是磁石不是锤子。这同样适用于车间工具、开发工具或 CI/CD 工具。我可能会使用 GitHub Actions 拼凑一些东西来满足这些需求,但这不是它的设计目的。有了 Octopus Deploy,我可以满足所有这些需求。

我将 GitHub 动作视为任何 CI 工具。它做什么,它做得很好。但是不要试图强迫它成为它不是的东西。

结论

总而言之,我对 GitHub 操作的整合程度印象深刻。我能够使用他们的文档、一些例子和几个小时内的一点点尝试和错误,将整个 GitHub 操作放在一起。此后,我扩展了这个操作来处理特性分支和其他逻辑。但那是以后的事了。

下次再见,愉快的部署!

数据库部署自动化系列文章:

Octopus 和 Redgate SQL 版本的数据库部署- Octopus Deploy

原文:https://octopus.com/blog/database-deployments-with-octopus-and-redgate-sql-release

如果您使用 SQL Server,您可能熟悉我们在红门的朋友。他们是 SQL server 工具的领先开发者。虽然他们的一些最著名的工具(如 SQL Compare)专注于使手动过程更容易,但最近他们在数据库自动化方面投入了大量的时间和精力。他们的最新创造是一个名为 SQL Release 的插件,它与 Octopus 集成在一起,支持自动化数据库部署。以下是来自 SQL 发布团队的布莱恩·哈里斯的客座博文。

什么是 SQL 版本?

在 SQL 发布团队中,我们希望发布新版本的数据库就像发布其他应用程序一样简单。麻烦在于数据库包含数据;因此,您需要升级实时数据库状态以匹配新版本,而不是部署一个全新的数据库。

我们在 SQL Release 中实现的解决方案是生成一个 SQL 更新脚本,将现有数据库升级到新版本。在运行这个脚本之前,您可以检查将要进行的更改,并查看引发的任何警告(例如,删除表时的数据丢失)。

当您对更新感到满意时,SQL Release 可以运行更新脚本。为了提高安全性,在运行脚本之前,SQL Release 会检查自脚本生成以来目标数据库是否发生过更改。

SQL 版本是作为一系列 PowerShell cmdlets 实现的,便于与 Octopus Deploy 集成。在未来,我们计划与其他发布管理工具集成。

演练:将 SQL Release 与 Octopus Deploy 一起使用

为了说明这在实践中是如何工作的,我们将通过一个简单的场景,使用 SQL Release 和 Octopus 从开发数据库直接部署到生产数据库。

开始之前,我们需要设置以下内容:

  • 章鱼部署
  • 一个名为 Production 的 Octopus 部署环境
  • 配置了 db-server 角色的章鱼触手
  • SQL Release 安装在 Octopus 触手的同一台机器上(触手安装 SQL Release 后需要重启)

创建新的 Octopus 部署项目

首先,我们需要创建一个新的 Octopus 项目,并给它命名。该项目由一系列管理数据库部署过程的步骤组成。当我们完成时,它将看起来像这样:

Redgate SQL Release Octopus Integration

注意,我们不需要为这个项目中的任何步骤指定目标环境。如果您没有指定环境(在本例中没有问题,因为只有一个生产环境),默认情况下,项目将部署到所有环境中。

添加“发布数据库”步骤

我们添加的第一步是创建部署 SQL 脚本。在项目流程选项卡上,选择添加步骤并选择运行 PowerShell 脚本

在字段中输入这些详细信息:

Name:Make database releaseMachine roles:d b-server
Script:该脚本使用了一组项目变量,我们将在后面定义:

# This step uses SQL Release to create a directory containing the
# Database Release: all the artifacts relating to the deployment.
#
# The directory, 'DatabaseRelease', has the structure:
#   - States
#       - Target: SQL Compare scripts folder of the state the database
#                 is in BEFORE the deployment.
#       - Source: SQL Compare scripts folder of the state the database
#                 will be in AFTER the deployment.
#   - Update.sql: The SQL change script that updates the target database from the
#                 target state to source state.
#   - Reports
#       - Changes.html: An HTML report showing which database objects
#                       will be changed in the deployment and how.
#       - Warnings.xml: An XML report containing warnings relating to the
#                        deployment.
# Makes sure the directory we're about to create doesn't already exist.
If (Test-Path $DatabaseReleaseDirectory) {
    rmdir $DatabaseReleaseDirectory -Recurse -Force
}
# Sets up connection string for the target database.
$developmentDatabase = "Data Source=$DevelopmentDatabaseServer; `
                        Initial Catalog=$DevelopmentDatabaseName; `
                        User ID=$DevelopmentSQLServerUsername;Password=$DevelopmentSQLServerPassword"
$productionDatabase =  "Data Source=$ProductionDatabaseServer; `
                        Initial Catalog=$ProductionDatabaseName; `
                        User ID=$ProductionSQLServerUsername;Password=$ProductionSQLServerPassword"
# Creates the DatabaseRelease directory.
New-DatabaseRelease -Target $productionDatabase `
                    -Source $developmentDatabase `
                    -Verbose `
| Export-DatabaseUpdate -Path $DatabaseReleaseDirectory
# Imports the changes report, deployment warnings, and update script
# as Octopus artifacts, so you can review them in Octopus.
New-OctopusArtifact "$DatabaseReleaseDirectory\Reports\Changes.html"
New-OctopusArtifact "$DatabaseReleaseDirectory\Reports\Warnings.xml"
New-OctopusArtifact "$DatabaseReleaseDirectory\Update.sql" 

点击保存

添加“审查数据库发布”步骤

接下来,我们添加一个步骤来暂停部署,以便手动检查脚本和其他资源,这样我们可以确保在发布之前我们对发布感到满意。上一步导入了一个变更报告、更新脚本和一个警告文件,它们将在 Octopus UI 中显示以供查看。

项目流程选项卡上,点击添加步骤并选择需要手动干预

在字段中输入这些详细信息:

名称:
【审核数据库发布】

说明:

Please review the deployment artifacts on the right:
(1) Update.sql: SQL change script that updates the target database.
(2) Warnings.xml: XML report containing warnings relating to the deployment.
(3) Changes.html: HTML report showing which database objects will be changed in the deployment and how. 

添加“运行更新脚本”步骤

最后一步是部署我们在审核步骤获得批准后生成的脚本。

项目流程选项卡上,点击添加步骤并选择运行 PowerShell 脚本。这是部署步骤。

在字段中输入这些详细信息:

名称:运行更新脚本
机器角色 : db-server
脚本:

#This step uses SQL Release to deploy the database release we previously generated and wrote to disk.
$targetDatabase = "Data Source=$ProductionDatabaseServer; `
                   Initial Catalog=$ProductionDatabaseName; `
                   User ID=$ProductionSQLServerUsername;Password=$ProductionSQLServerPassword"

Import-DatabaseRelease $DatabaseReleaseDirectory | Use-DatabaseRelease -DeployTo $targetDatabase 

点击保存

设置项目变量

变量名 价值
基本目录 # {章鱼。触手. agent . applicationdirectorypath } \ # { Octopus。Environment . Name } \ # {章鱼。Project.Name}#
数据库名
数据库服务器
数据库释放目录 # {基本目录} \数据库版本
章鱼。action . package . custominstallationdirectory #
packageextractddirectory # {基本目录} \数据库包
SQLServerPassword
SQLServerUsername

运行部署

我们现在准备在 Octopus 中部署数据库更新。部署数据库版本的过程与使用 Octopus Deploy 部署普通更新的过程相同。

摘要

我向您展示了一个简单的用例:作为 Octopus Deploy 版本的一部分,从一个数据库模式部署到另一个数据库模式,但是 SQL 版本可以用于更复杂的情况。

我们可以使用 SQL Release 首先发布到预生产环境,在验证部署成功后,我们可以将其提升到我们的生产环境。

这种方法的好处是,我们可以针对生产运行与我们在生产前数据库上测试的完全相同的脚本。作为部署 SQL 版本检查的一部分,生产数据库仍处于我们预期的初始状态,因此我们可以确定针对预生产的试运行是对生产版本的良好测试,并且可以安全地重用脚本。

您还可以从数据库 NuGet 包中部署更改。这意味着可以对数据库进行源代码控制,在构建服务器中构建并测试它,然后使用 SQL 版本部署该构建,并提供所有的安全性和可靠性。

如果你想尝试 SQL Release,你可以在这里下载它。

数据库功能分支部署- Octopus 部署

原文:https://octopus.com/blog/database-feature-branch-deployments

Database Feature Branch Deployments

在我的上一篇文章重新思考特性分支部署中,我分享了我如何调整对特性分支部署的思考。像那篇文章那样写思想实验是一回事,但把它付诸实践又是另一回事。在本文中,我描述了如何为特性分支建立数据库部署过程。

快速回顾

TL;DR;对重新思考的特性分支部署是:

使用 git 时, 开发➜测试➜试运行➜生产 的静态工作流不工作。Git 使得创建特性分支变得非常容易。静态工作流本质上要求每个人将他们的代码签入到主干(主或开发)中进行适当的测试。这导致了三个问题:

  1. 未完成的代码进入master,需要被测试。
  2. 对于 bug 修复,没有清晰的路径通向生产,需要一个变通办法,比如建立一个 bug 修复分支,只包含准备➜生产。
  3. 开发人员同时处理 2 到 N 个特性是很常见的。这意味着将未完成的代码合并到一个主干中。这增加了不正确的合并冲突解决的机会,这反过来减慢了测试和开发。

为解决这些问题,应进行以下更改:

  1. DevTest 组合成一个环境: Test
  2. 测试中,每个特性分支都应该有一个独立的沙箱。
  3. 在 QA 测试和验证了一个特性分支之后,它应该被合并到主特性中。
  4. 主设备的部署从阶段开始,从不经过测试
  5. 测试成为一个动态环境,根据需要添加和删除资源。分期生产是静态稳定的。

潜在的目标是代码不应该被合并到master中,直到它准备好进入生产

功能分支数据库部署业务规则

看到这些变化,自然会有很多疑问。

  • 何时创建要素分支沙盒?
  • 它应该是一个全新的数据库,还是应该恢复一个备份?
  • 如果是恢复的备份,应该备份什么?生产还是分期?
  • 应该多久进行一次备份?
  • 最后,那个沙盒什么时候被拆掉?

那些问题仅仅是关于在测试中特性分支沙箱的创建!我还经常遇到其他一些问题。许多这样的问题都集中在数据库部署过程中建立信任。

  • 数据库管理员应该在什么时候参与进来?生产为时已晚,而在测试中创建特性分支为时过早。
  • 谁应该触发生产的部署?
  • 是否可以调度生产部署,并且只在出现问题时呼叫数据库管理员?
  • 我们能在不做生产部署的情况下看到生产吗?

对于我的流程,我做了以下决定:

  1. 每个部署将检查功能分支数据库,如果它没有看到,创建一个新的。
  2. 创建特征分支数据库时,恢复暂存的副本。
  3. 将在每次部署到转移时创建转移备份。
  4. 当特征分支数据库合并到主数据库时,备份并删除特征分支数据库。
  5. 试运行部署还将为生产生成一份增量报告。这样,数据库管理员只需批准一次发布。
  6. 数据库管理员将批准所有部署到暂存
  7. 逻辑将确保 DBA 只有来批准某些模式变更;创建表、删除表、创建视图、删除视图、更改表等。以保持低信噪比。
  8. 数据库管理员将触发生产部署。他们可以安排或立即开始。

在开发生命周期中,让数据库管理员在生产部署之前参与进来是至关重要的。他们负责保持生产运行,并要求他们在生产部署失败时快速检查变更。虫子就是这么溜进来的。DBA 是很忙的人,他们可以就潜在的数据库更改进行咨询,但是在开发一个特性时,不应该要求他们批准数据库更改。当一个特性开始开发的时候和当这个特性最终被 QA 签署的时候,这个表的结构看起来会有很大的不同。

另一个选择是将 DBA 作为拉请求的评审者。这样,代码就不会被合并到还没有准备好进入产品master中。所有这些都取决于 DBA 的数量、他们必须支持的团队的数量,以及在给定的一天中被合并到master的变更的数量。要求两个 DBA 组成的团队审查合并到master的 20 个团队的所有拉请求,平均每天一两次,这让 DBA 很容易失败。

Octopus 部署配置

对于本文,我将部署到托管在 AWS RDS 中的 SQL Server。我选择 AWS RDS 没有别的原因,只是因为我被要求使用 AWS RDS 设置一个示例。我在这个示例项目中做的大约 95%的事情可以在 Azure SQL 或自托管 SQL Server 中完成。我利用了一些特定于 AWS RDS 的特性,但不是很多。

您可以在我们的公共示例实例中找到这个示例项目/空间。你可以作为一个客人登录,四处打探。

生活过程

我在 Octopus Deploy 中创建了两个生命周期,AWS Default LifecycleAWS Feature Branch Lifecycle。在构建这个例子时,我使用了前缀AWS将它们与我的其他项目分开:

AWS 帐户

我使用 IaC 来加速和减速 AWS RDS 实例。我选择的 IaC 技术是 AWS CloudFormation ,因为我的同事 Shawn 已经有了一个 PostgreSQL 的 RDS 示例,我可以为 SQL Server 复制和操作它。为了从 Octopus 调用 CloudFormation,我需要在 Octopus 中注册一些 AWS 帐户:

静态 AWS 基础设施和库变量集

对于我的例子,我使用 IaC 或基础设施作为代码,来启动和关闭 AWS RDS 实例。在现实世界中,我不认为这是现实的,特别是对于生产暂存数据库。即使当我在 AWS 中使用 IaC 功能时,我也喜欢有一些静态基础设施,即VPC安全组子网。虽然可以使用 IaC 上下旋转它们,但这在现实世界中并不实用。许多公司喜欢在他们的数据中心和 AWS 之间建立 P2P VPN 连接。你必须使用 IaC 来旋转它,如果你能让它工作,那就太棒了。我发现它有点挑剔。

我知道我会共享 VPC、安全组、子网等。,所以我喜欢创建多个变量集来存储不同的变量组。我从不建议使用一个庞大的全局变量集。一段时间后,它变成了一个变量的垃圾抽屉:

变量集本身包含相关的变量。我通常不会让安全组和子网成为敏感变量。然而,这个实例是公开的,我不想与外界分享这些信息:

您会注意到我的变量遵循名称间距格式[variablesetname].[group].[variablename]。我这样做是为了以后更容易找到变量。以我知道的Project开头的变量存储在项目变量中,而以我知道的AWS开头的变量存储在AWS变量集中。它还有助于防止变量名冲突。

工人和安全

将 AWS RDS 服务器暴露在互联网上是不明智的,即使是我正在旋转的示例服务器也是如此。我将使用 S3 来存储我的数据库的备份。暴露这一点也是不明智的。即使是章鱼云也不行。我将我的安全组配置为只接受来自端口 80、端口 443 和端口 10933 的请求。我在 AWS 中创建了一个 EC2 实例,并将其注册为一个 worker 。该 EC2 实例具有通过端口 1433 上传到 S3 以及连接到 AWS RDS 实例的权限:

【T2

在现实世界的生产配置中,我会使用不止一个工人。我有多个工作池,一个用于不同的环境,并使用变量作用域来动态选择合适的工作池。但是这个例子已经够复杂了,没必要再增加复杂性了。

Octopus 部署项目

现在,所有的脚手架都已拆除,是时候专注于项目了。

我使用了社区库中的一些步骤模板。以下是列表,因此您可以自己安装它们:

运行手册

我的项目中有五个run book来处理特性分支部署的各种维护任务。我将它们都放在同一个项目中,以便您在查看我们的示例实例时更容易找到它们。使用我创建的 Run Octopus Deploy Runbook 步骤模板,Runbook 可以存在于任何项目中。如果我在现实世界中进行设置,所有这些操作手册都将位于一个单独的特性分支项目中,以供任何流程利用。

在您的用例中,您可能没有上下旋转 SQL Servers。重点关注的三个操作手册是创建屏蔽数据库备份、删除特征分支数据库和恢复特征分支的屏蔽备份。

创建屏蔽的数据库备份

在此期间,我发现 AWS RDS 的一个特性是能够将数据库备份到 S3 。太酷了。我不必担心设置文件共享和映射所有驱动器。我运行存储过程msdb.dbo.rds_backup_database来将数据库备份到 S3,或者运行存储过程msdb.dbo.rds_restore_database来恢复数据库。我发现调用这些存储过程会启动这项工作。你必须运行msdb.dbo.rds_task_status来检查备份或恢复的状态。如果您有兴趣尝试,我在我们的示例实例上创建了几个自定义步骤模板:

在这个特定的 runbook 中,过程是将现有的Staging数据库备份到 S3,恢复一个名为【数据库名称】的数据库副本。然后,该进程将运行一个清理脚本来清理Staging。之后,该过程在 S3 存储桶中创建新的备份供功能分支使用:

有一些屏蔽数据的技术,也有你可以购买的工具,比如 Redgate 的 SQL Server 数据屏蔽器或者你可以使用你自己的屏蔽脚本。在这个例子中,我选择滚动自己的表,因为我只关心一个表。

删除特征分支数据库

删除功能分支 runbook 需要的工作比我最初想象的要多一点。起初,我打算只删除数据库。但是当我想得更多的时候,首先备份数据库然后删除它更有意义。如果我需要再次启动数据库来修复一个错误,我可以使用那个备份。添加备份步骤使事情变得有点棘手,因为我需要首先检查数据库是否存在。如果数据库不存在,AWS 提供的用于备份数据库的存储过程将会失败:

Check for Existing Database步骤将输出变量设置为TrueFalse。备份和删除步骤的运行条件在运行条件中使用该值:

恢复功能分支的屏蔽数据库备份

恢复屏蔽数据库备份操作手册还有一个Check for Existing Database步骤:

本操作手册包含额外的逻辑。大多数情况下,如果数据库尚不存在,该过程只应创建一个新的特征分支数据库。但是,在一些用例中,他们需要使用全新的数据库副本重新开始。为了帮助解决这个问题,我创建了一个提示变量,默认值为False。当设置为True时,将创建数据库的新副本:

频道

在我需要设置的各种变量之外,项目脚手架的最后一块是项目通道。每个项目都有一个Default通道,它是在项目创建时创建的。我为Feature Branches添加了另一个频道:

部署流程

我不打算粉饰它,为了满足之前的所有业务规则,部署过程是复杂的:

前两步创建或拆除特征分支脚手架。我不需要指定频道,因为测试测试在不同的频道,但是我这样做是为了让其他人更容易阅读:

您可能想知道,Octopus 如何知道特性分支的名称来操作适当的基础设施?并没有。构建服务器通过提示变量传递该信息。当合并的分支值为空时,将跳过删除要素分支数据库步骤:

您可能已经在 runbooks 流程的每个截图中看到了下一步:

这是必要的,因为该过程会启动和关闭 AWS RDS 服务器。它运行一个快速 CLI 脚本来获取要在连接字符串中使用的完整实例名称:

部署流程中接下来的六个步骤是实际的部署。它生成增量脚本,查看这些增量脚本中的特定 SQL 语句,获得批准,最后部署更改:

【T2

Check SQL Artifacts for Schema Change Commands是我写的一个自定义步骤模板。你可以在之前的文章中读到更多关于这个步骤的内容。

你会注意到生产的增量脚本正在暂存中生成。这不是在生产部署中使用的增量脚本。这是为了让 DBA 了解稍后将部署到生产中的内容。在 99%的情况下,它对批准非常有效。在增量脚本生成时间和它们实际运行的时间之间存在偏差的风险。但是,总的来说,这种风险很小,漂移量通常也很小。通过批准 Staging 中的所有内容,DBA 可以将部署安排到生产中,而不必在线。

部署过程的最后步骤处理清理和通知。在每个阶段部署之后,会创建一个数据库的新的屏蔽副本。这确保了要素分支始终具有最新的模式更改:

构建服务器

对于静态环境,构建服务器的过程非常简单:

  1. 建设
  2. 试验
  3. 推动章鱼展开
  4. 在 Octopus 部署中创建一个版本
  5. 部署发布

现在构建服务器需要在调用 Octopus Deploy 之前做出一些决定。它必须知道它是在一个特征分支上,在主分支上,还是刚刚发生了一个合并。触发构建的事件将改变构建服务器发送给 Octopus 的信息。主要区别在于:

  • 入住Master:
    • 如果合并拉请求,获取特征分支名称,将其作为合并分支值发送给 Octopus Deploy。
    • 将目标环境设置为暂存
    • 将包版本号设置为标准版本号,比如2020.2.1.{Build Number}
    • 将频道名称设置为“默认”。
  • 检入要素分支:
    • 提取特征分支名称,并将其作为特征分支值发送给 Octopus Deploy。
    • 将目标环境设置为测试
    • 将软件包版本号设置为 2020.99.99。-.
    • 将通道名称设置为“特征分支”。

这是我的 GitHub 操作中的 PowerShell 脚本:

$branchName = ((${env:GITHUB_REF} -replace "refs/heads/", "") -replace "feature/", "") -replace "hotfix/", ""
Write-Host "$branchName"  

$versionFromFile = Get-Content versionprefix.md
Write-Host "Found $versionFromFile in versionprefix.md"
$versionNumber = "$versionfromFile.${env:GITHUB_RUN_NUMBER}"

$channelName = "Default"
$deployEnvironment = "Staging"
$mergedBranch = ""

$commitMessage = git log -1 --pretty=oneline
Write-Host "The commit message is: $commitMessage"

if ($branchName -ne "master")
{
    $versionNumber = "2020.99.99.${env:GITHUB_RUN_NUMBER}-$branchName"    
    $channelName = "Feature Branches"
    $deployEnvironment = "Test"           
}
elseif ($branchName -eq "master" -and $commitMessage -like "*Merge pull request*")
{          
    $indexOfSlash = $commitMessage.ToString().IndexOf('/')
    Write-Host "The index of the slash is $indexOfSlash"
    $mergedBranch = $commitMessage.SubString($commitMessage.IndexOf("/") + 1)
    Write-Host "The merged branch before replacement is $mergedBranch"
    $mergedBranch = ($mergedBranch -replace "feature/", "") -replace "bugfix/", ""          
    Write-Host "The merged branch is now $mergedBranch"
}

Write-Host "Setting environment variable PACKAGE_VERSION to: $versionNumber"
Write-Host "Setting environment variable BRANCH_NAME to: $branchName"
Write-Host "Setting environment variable CHANNEL_NAME to: $channelName"
Write-Host "Setting environment variable ENVIRONMENT_NAME to: $deployEnvironment"
Write-Host "Setting environment variable MERGED_BRANCH to: $mergedBranch"

echo "::set-env name=BRANCH_NAME::$branchName"
echo "::set-env name=PACKAGE_VERSION::$versionNumber"
echo "::set-env name=CHANNEL_NAME::$channelName"
echo "::set-env name=ENVIRONMENT_NAME::$deployEnvironment"
echo "::set-env name=MERGED_BRANCH::$mergedBranch" 

结论

为特性分支建立沙箱的概念是我的一个爱好。我在很多地方工作过,但我们没有这样做,这很令人头疼。这不是一项简单的任务,但是通过利用 Octopus Deploy 中的多个特性,这项任务变得更加容易。我在 Octopus Deploy 中使用了一些新功能。Runbooks 带来了巨大的变化,让事情变得简单多了。但是回想起来,我希望我知道这种方法,因为我知道我可以得到一些工作,即使是在 3.x 版本的 Octopus Deploy 上。这将涉及更多的 API 脚本,但我知道我可以让它工作。

很难相信这其实是一个简单的例子。在本文中,我只介绍了数据库;我没有介绍 web 服务器或应用服务器。除此之外,我没有涉及涉及多个应用程序的大项目。对于这篇文章,我想把重点放在一件事情上,即数据库。我的想法是,如果我可以解决数据库,其余的应该相当简单。

下次再见,愉快的部署!

数据库迁移经验教训- Octopus 部署

原文:https://octopus.com/blog/database-migrations-lessons-learned

Database migrations lessons learned

数据库迁移是一种以可控方式更新应用程序数据库的流行方式,这种方式可以将风险降至最低。这种方法也称为模式迁移、数据库升级脚本、变更驱动或基于脚本的更新。Octopus Deploy 从产品开始就使用数据库迁移,随着 Octopus 的规模和复杂性的增长,我们学到了很多东西。

在这篇文章中,我将介绍数据库迁移,分享一些常见的框架,并涵盖我们从近十年的经验中学到的教训。

什么是数据库迁移?

大多数应用程序需要将应用程序的状态信息保存在文件或数据库中。对象的形状几乎总是随着时间的推移而发展和变化,这意味着需要某种迁移代码或脚本来保持持久数据的形状同步(或一致)。没有它们,应用程序代码将需要从一开始就处理文档的所有可能版本。

大多数框架都支持这一概念,为您提供了一种提供迁移脚本来操作数据库的方法。有些甚至让您指定回滚操作。通常,它们会跟踪已经应用到表中的脚本。

几乎每个平台都有许多选项,每个选项通常都有一些独特的属性:

节点 j

巨蟒

红宝石

。网

Java

经验教训

第 1 课:让您的迁移脚本远离您的生产代码

作为软件开发人员,我们被训练重用和避免重复代码。这是我们希望复制代码的一次,因为我们希望我们的迁移脚本的行为在时被捕捉到。

下图描述了一个示例。每个脚本(编号为 001 到 005)负责将数据库状态从 A 更改为 B 。如果脚本引用了生产代码中的常量或函数(比如生成新属性的默认值的函数),那么在编写时,该脚本仍将按预期工作。

It might work now... 现在可能行得通...

如果引用的代码在几个月内发生了变化(比如常量的值发生了变化),那么脚本实际上依赖于未来。这种行为上的变化是微妙而难以察觉的,只有当客户直接从脚本首次引入之前的旧版本升级时才会出现

But will it work later? 但是以后管用吗?

第二课:保持低技术含量,不要反序列化

这一步适用于使用对象关系映射器(ORM)或文档数据库(即 NoSQL)的团队。Octopus 使用一种叫做永不超生的微 ORM,它将 SQL Server 视为一个文档存储。

您可能会尝试反序列化数据库记录,以便可以使用代码完成(即 IntelliSense)或类型安全可用的对象。实际上,您将自己暴露在序列化和类型转换器的复杂性中。

如果您直接使用文本或文档模型,升级脚本会更简单。这通常意味着您需要处理不同格式的抽象。例如,Octopus 是用。这意味着我们直接用JObject处理JSON文档,或者用XElement处理XML文档。其他平台使用等效的概念和框架。

第 3 课:编写测试来单独测试每个迁移脚本

让测试运行单个脚本极大地增加了对迁移脚本的信心。手动测试迁移是一件痛苦的事情,因为您需要拍摄数据库的快照,并在测试后恢复它。

您可能还想对旧数据库样本的备份运行完整的迁移,以捕捉任何未预料到的数据或单个测试无法涵盖的错误假设。

4.考虑在线运行长时间迁移

Octopus 服务器在启动时同步运行其迁移脚本。这有利于在应用程序启动时使数据库处于已知的可预测状态。当然,缺点是在大型表上运行迁移会导致长时间的停机,这意味着我们要避免涉及大型表的某些结构变化。

当应用程序启动并运行时,我们已经开始尝试在后台异步运行大型迁移作业。这意味着停机时间更少,客户可以更快地恢复使用该应用程序,但这也带来了新的问题:

  1. 应用程序必须支持旧状态和新状态的数据库
  2. 当您对同一表进行后续更改时,升级作业可能会受到影响。然后,您需要在所有三种可能的状态下支持数据库,或者强制客户升级到中间版本。

这方面我们还没有具体的建议,但我们会继续评估。

5.考虑对文档进行版本控制

虽然使用迁移脚本来更新数据库模式更改和文档属性很方便,但是如果将这两个问题分开,您将获得一些灵活性。例如,如果您对文档进行了版本控制并单独升级了文档结构,那么您可以重用相同的升级代码从文件中导入相同的文档。

结论

从这些经验中最重要的一点是将你的升级脚本从主要的生产代码中分离出来,这样它的行为就能在时间中被捕捉到。

总之,我们的数据库迁移经验是:

  • 让您的迁移脚本远离您的生产代码。
  • 保持低科技,不要反序列化。
  • 编写测试来单独测试每个迁移脚本。
  • 考虑在线运行长时间迁移。
  • 考虑对文档进行版本控制。

SQL 回滚和自动化数据库部署的缺陷——Octopus 部署

原文:https://octopus.com/blog/database-rollbacks-pitfalls

Pitfalls with SQL rollbacks and automated database deployments

虽然可以执行 SQL 回滚来恢复数据库更改,但问题是,您应该这样做吗?回滚数据库更改不像回滚代码更改那样简单。数据库是应用程序的生命线。不成功的回滚可能会导致数据损坏或被删除。本文介绍了导致坏数据或删除数据的陷阱,以及为什么前滚是更好的方法。

TL;博士

向前滚动是更好的选择。在特定情况下,可以回滚数据库更改,但这种情况很少见。花在设计数据库回滚过程上的精力应该集中在尽可能快速和安全的部署上。快速安全的数据库部署允许您进行前滚。

本文是我们撰写的关于自动化数据库部署系列文章的一部分。

回滚场景

回滚需求通常分为三类:

  • 在部署期间。
  • 在部署后的验证期间。
  • 经过部署和验证。

部署和验证是有意分开的,因为用户可能在验证期间使用应用程序。这取决于部署策略(中断、金丝雀或蓝/绿部署)和您的应用程序支持的内容。例如,Octopus Deploy 具有维护模式特性,该特性防止其他非管理员部署代码,即使他们仍然可以访问它。维护模式等功能支持部署后的验证。

让我们来看一个典型应用程序的部署过程。该应用程序有一个数据库、一个用 React 编写的前端、一个 RESTful API 后端和一个后台服务。部署过程可能如下所示:

  1. 批准生产部署。
  2. 部署数据库更改。
  3. 部署后台服务。
  4. 对于每个 web 服务器:
    1. 将服务器从负载平衡器中取出。
    2. 部署 RESTful API。
    3. 部署前端。
    4. 运行健全性检查测试。
    5. 将服务器重新添加到负载平衡器中。
  5. 通过运行一整套测试来验证版本。
  6. 将发布通知相关人员。

对于某些部署,数据库更改很简单。存储过程的新索引或效率调整。回滚这些更改不会有什么影响。回滚复杂的更改、添加列、移动列、添加表或拆分表都会产生重大影响。不要忘记迁移脚本。这些回滚要复杂得多。

为什么要回滚部署?

我听到的三个常见答案是:

  1. 部署出了问题。
  2. 我们运行一系列测试,其中一个或多个测试失败。
  3. 用户发现节目停止错误。

好吧,酷,但是出问题了有点模糊。部署失败可能有多种原因,例如:

  • 由于网络问题而失败。可能是网络管理员重启了交换机,导致与部署目标的连接中断。
  • DBA 应用角色成员资格更改,但新角色没有数据读取权限。
  • 部署目标已关闭。
  • 用于部署的服务帐户在当天早些时候更改了密码,但部署仍使用旧密码。
  • 更新的应用程序需要。网芯 3.0,但仅此而已。已安装 NET Core 2.2。
  • 部署过程中的错误配置。

我认为这些都不能证明回滚数据库更改是正确的。通常重试可以解决这些问题。这就是为什么我们在 Octopus Deploy 的部署中增加了引导失败功能,它允许你选择如何处理部署失败。

第二个原因,因为一个或多个测试失败而回滚。所有的测试都需要通过吗?就像部署失败一样,测试失败可能有多种原因,例如:

  • 集成测试导致应用程序调用外部系统。外部系统预定明天才更新,并返回一个意外的结果。这个结果对于当前版本的外系统是正确的,但是对于明天即将发布的版本是不正确的。
  • 有人不小心改了一些测试客户的名字。测试期望测试用户,但是现在数据是测试用户#1
  • 外部系统与您的应用程序同时更新。测试将会失败,直到 20 分钟后外部系统启动并运行。

同样,我不认为这些失败证明回滚数据库更改是正确的。对于集成测试来说,只有当星星排成一行时才能工作,这是很常见的。

正在部署的更改量也会影响回滚。当进行几个更改时,回滚的愿望要比进行几十个更改时强烈得多。当发现一个停止显示的 bug 时,回滚的愿望会成倍增加。缺点是它们通常会被用户发现。这就变成了第二十二条军规。为了找到一个停止显示的 bug,用户必须使用这个应用程序。然而,一旦用户开始使用应用程序,回滚就变得更加困难。

备份和恢复的陷阱

您可能会想,“为什么我们不在部署过程开始之前进行数据库备份呢?这将解决回滚这些复杂更改的问题。”备份的有用性是有限的。在以下情况下,备份变得无用:

  1. 用户开始使用应用程序的新版本。
  2. 进行下一次定时备份。

这些事件可能在 1 分钟或几小时后发生。回滚意味着更改数据。或者更糟,删除数据。

通过备份恢复的回滚不会在真空中发生。应用程序共享数据是非常常见的。

在 Octopus Deploy 工作之前,我从事贷款发放系统的工作,主要目的是收集数据并发送给决策引擎。决策引擎决定贷款是应该被拒绝、自动批准,还是需要贷款官员收集更多数据。在信贷员输入更多数据后,他们可以提交这些数据以进行后续决策。贷款发放系统和决策引擎是具有独立数据库的独立应用程序。决策引擎处理第一个决策的方式不同于第二个决策。它使用贷款发放系统发送的唯一标识符来跟踪所做的决策。

我们第一次在测试环境中从备份中恢复贷款发放系统时,我们开始从决策引擎中获得意想不到的结果。问题是决策引擎的数据库不是从备份中恢复的。我们看到了两个问题:

问题#1:

  1. 用户提交贷款。
  2. 决策引擎自动批准贷款。
  3. 从备份进行还原。
  4. 用户再次提交贷款。
  5. 决策引擎将此视为第二个请求,并拒绝贷款,认为这是一个欺诈贷款。

问题 2:

  1. 用户为客户 a 提交贷款。
  2. 决策引擎自动批准贷款。
  3. 从备份进行还原。
  4. 还原删除了整个贷款记录,以及许多其他贷款记录。
  5. 用户重新创建贷款并提交它,但是它获得一个不同的惟一 ID 发送给决策引擎。
  6. 决策引擎已经有了客户 B 的唯一 ID,它基于客户 A 和 B 的信息做出决策。

如您所见,备份不应该用于回滚。它们应该仅用于灾难恢复。我看到的典型备份计划是每周一次完整备份,每天一次部分备份,全天进行时间点备份。这些备份是在数据库服务器或数据中心丢失时进行的。

陷阱回滚脚本

我见过一些公司制定了这样的规则:对于数据库的每一次更改,都必须编写相应的 SQL 回滚脚本。然而,回滚脚本也有自己的缺陷。

编写 SQL 回滚脚本需要时间。如果脚本从未运行过,那么它就是在浪费时间。如果您有几十个成功的部署,编写回滚脚本的动机就会降低。随着时间的推移,人们开始公开质疑为什么首先需要创建它。最终,脚本变成了回滚的最小量。编写它们是为了选中一个复选框。

更重要的是,脚本必须经过测试。这引出了进一步的问题,例如:

  • 谁测试他们?
  • 他们什么时候被测试?
  • 是自动化测试还是手动测试?

如果这些问题的答案是, Bob 在产品发布之前的早上测试它们,那么测试有可能只发生一半的时间。如果它不是一个阻塞任务,如果它不是自动化的,那么它就不会被完成。

回滚脚本的生命周期是有限的,就像数据库备份一样。当回滚是非中断的,例如,删除一个新的索引,或者恢复一个调整过的存储过程,生命周期很长。当变更中断时,例如,一个新的列或表被删除,生命周期是有限的。

例如,如果向表中添加了一列,回滚脚本将删除该列。简单。但是,如果该列在生产环境中运行了两天,并且数百个用户将数据保存到该列,回滚脚本还应该删除该列吗?在大多数应用程序中,删除数据是一件大事。

有一种诱惑,想出一个决策树,说明什么时候回滚脚本是必要的。最终,这将使事情变得更糟。一个接一个的场景将会被添加到树中,它将会变得一团糟。

向前滚动:一种不同的思维方式

一旦用户在部署后开始使用应用程序,你就已经破釜沉舟。有人可能会说,一旦核查开始,就已经破釜沉舟了。

通常情况下,成功地回滚一个部署的努力远远超过将一个补丁推向生产的努力。

*贷款发放系统的数据库部署实现自动化后,部署时间不到 10 分钟。数据库部署只需 3-4 分钟。我们有四个环境,当我们知道问题是什么时,我们可以检入一个变更,验证它,并在不到 30 分钟的时间内将其推送到所有四个环境。如果我们想跳过两个较低的环境,不到 20 分钟。

当在部署后危机期间提出回滚主题时,团队必须:

  • 分析之前的部署有哪些变化。通常需要打开一个 diff 工具,一行一行地检查变化。
  • 创建一个测试区域列表,以确保回滚后应用程序不会崩溃。
  • 确定哪些数据将被更改。
  • 确定哪些数据将被删除。
  • 确定是需要回滚数据库还是只回滚代码。
  • 创建生产备份,并在测试服务器上恢复它。
  • 回滚之前的部署并开始测试。

在部署后危机期间,这种分析不是在真空中进行的。用户要求知道正在发生什么。上级要求状态报告。必须联系数据库管理员以获得生产数据库的副本来进行测试。期望在不到两个小时内完成这些步骤是不现实的。我见过团队花一整天来完成所有这些步骤。

理想的解决方案是:向前滚动并进行向后兼容的更改

数据库部署通常是部署中风险最大的部分。有可能降低这种风险吗?

我回想起我做过的包括数据库在内的所有生产部署。当这些变化在几小时或几天前上演时,压力水平是不存在的。这给了我们时间来验证工作时间的变化。代码仍在部署窗口期间部署。部署代码通常非常快。部署窗口期间的验证要快得多,因为大部分工作已经完成。

只有当数据库变化是向后兼容的时候,这才是可能的,这需要大量的训练。关于如何进行向后兼容数据库更改的详细信息,请参见我的文章蓝/绿数据库部署。这个例子有点极端,但我认为花在这上面的时间是值得的。

这种方法还意味着部署数据库变更的过程需要与部署代码变更的过程分开。它还需要更多的规划,因为您将不得不同时处理两个生产部署。即使这样,我认为回滚应该是最后的手段。数据库不需要回滚,但是代码需要回滚。这可能会删除功能并让用户感到沮丧。

这样做并不总是可行的。我的个人原则是:

  • 如果可能,使数据库更改向后兼容。
  • 如果可能的话,在代码部署前的几个小时甚至几天内,准备数据库的变更。
  • 除非发生灾难性的事情,否则就向前滚。

结论

成功地回滚部署的努力远远超过了将补丁推向生产的努力。回滚出错的几率比前滚高得多。对于数据库回滚来说尤其如此。一天只有这么多时间,与其花时间担心回滚和所有可能的情况,不如花时间改进部署。

*愉快的部署!

如果你喜欢这篇文章,我们有一整个系列的自动化数据库部署供你阅读。**

自动化蓝/绿数据库部署- Octopus 部署

原文:https://octopus.com/blog/databases-with-blue-green-deployments

Illustration showing two database (one green and one blue) on a seesaw

没有人想在周六凌晨 2 点进行部署,但几年前,我开发了一个应用程序,那是唯一一次可以安排长时间停机。快速部署和验证花了两个小时。典型的部署需要四个小时。我想实施蓝/绿部署,这允许零停机部署,但是应用程序的数据库使一切变得非常复杂,足以阻止我们转向蓝/绿部署。

在这篇文章中,我将介绍我从那时起学到的一些技术,使数据库的蓝/绿部署变得简单可行。

我将介绍高级概念和建议,但不会详细介绍如何使用 Octopus Deploy 进行蓝/绿部署。我们将在以后的文章中讨论这个问题。

蓝/绿部署简介

蓝绿色部署有两个相同的生产环境,一个标记为Blue,另一个标记为Green。只有一个环境是活动的,部署总是在非活动环境中进行。例如,如果Green是活动环境,则部署到Blue(非活动)环境,并且在发生验证之后,发生切换,这使得Blue环境成为活动环境,而Green环境成为非活动环境。

Blue/Green Deployments

这种方法有几个优点。回滚只是从Blue切换到Green或者从Green切换到Blue的问题。当切换发生时,它们是无缝的,因为代码已经运行,不需要等待它编译或预热。更改在生产中得到验证,无需任何客户点击代码,这降低了部署中的风险。如果某些东西不起作用,你不要切换,你可以再试一次。

需要注意的是,并非所有应用都可以利用蓝/绿部署。它是架构、应用程序的状态以及所使用的技术的组合。应用程序的无状态性和解耦性越强,蓝/绿部署策略就越适合。例如,与需要来自负载平衡器的粘性会话并且没有业务逻辑或数据层的 ASP.NET Web forms 应用程序相比,具有倾斜前端的. NET Core Web API 更适合蓝/绿部署。

场景

这篇文章涵盖了一个复杂的场景,使用一个单页面应用程序 (SPA)和一个连接到专用数据库的 ASP.NET Web API 后端。将对应用程序进行以下更改:

  • 在 UI 中,字段First NameLast Name被合并成一个名为Full Name的字段。并非所有的文化都使用名和姓。
  • 名为CustomerFullName的新列将被添加到 customer 表中。
  • 预先存在的CustomerFirstNameCustomerLastName列也在客户表上。
  • CustomerFullNameNull时,将通过组合CustomerFirstNameCustomerLastName来填充CustomerFullName
  • 如果数据存在于CustomerFirstNameCustomerLastName中,并且 API 请求仅与CustomerFullName一起发送,则不要将这些列设置为Null。此外,不要试图分割CustomerFullName,因为这非常困难且容易出错。

诚然,这个场景相对复杂。大多数数据库更改不会将两列合并成一列并尝试回填新列,但是如果这种情况可以解决,那么大多数其他情况也可以解决。我们将逐一介绍每项变更、要考虑的问题、解决这些问题的建议,以及成功的蓝/绿部署必须完成的许多不同的变更。

本文中我们部署的蓝色和绿色环境如下所示:

  • Green是直播。
  • Blue不活跃。

也就是说,我们将部署到Blue,在Blue通过验证后,它将成为活动环境,Green将变为非活动状态。

本文中的具体步骤只是建议,并不涵盖您可以对数据库进行的所有可能的更改。我的目标是给你提供一些你可以修改的东西来满足你的需求。

数据库更改如何增加复杂性

下班后部署是为了避免用户在部署和验证代码和数据库更改时访问应用程序。因为所有的更改都是同时发生的,所以将编写一个回填脚本来将CustomerFullName设置为CustomerFirstNameCustomerLastName。如果用户在这些变化发生时访问应用程序,将会导致错误和混乱。

这种变化的一个非常简单的部署过程如下所示:

  1. 禁止用户访问或关闭网站。
  2. 运行脚本来添加列CustomerFullName
  3. 将代码部署到Production
  4. 运行回填脚本来填充CustomerFullName
  5. 运行脚本删除CustomerFirstNameCustomerLastName
  6. 验证Production中的代码。

要完成蓝/绿部署的相同变化,需要更多的规划。如果没有蓝/绿部署,唯一的担心是有人可能会在部署和验证一切之前使用该应用程序。如果用户得到一个错误,指出有一列丢失了,不要担心,无论如何,他们不应该出现在系统中。蓝/绿部署的情况并非如此。用户将在应用程序中;它们将运行在Green服务器上,这意味着数据将被操作和查询。

以下是对蓝/绿部署进行相同更改时要考虑的一些情况:

  • 应用程序和数据库更改将被部署到Blue,包括新列CustomerFullName。应用程序使用任何存储过程吗?具体来说,在 customer 表中插入/更新数据?运行在Green上的代码不会知道添加到那些存储过程中的任何新参数。
  • 当变更被部署到Blue时,CustomerFullName列中将没有数据。何时应该回填该列?使用为停机部署创建的相同回填脚本?
  • 在将应用程序和数据库的更改部署到Blue之后,需要进行验证。在此期间,用户将使用Green上的应用程序,该应用程序仍在运行引用CustomerFirstNameCustomerLastName列的代码。在验证完成且Blue激活之前,不能删除这些列。应该在什么时候删除这些列?一旦Blue激活?
  • Blue上验证更新的代码和数据库时,用户将使用Green上的代码在客户表中添加和更新记录。如果在验证之前运行回填脚本,这些更改将不会生效。是否应该重新运行回填脚本?
  • 因为这是一个 SPA 应用,JavaScript 不知道什么时候Blue变成活动的,什么时候Green变成非活动的。JavaScript 存储在用户的浏览器中。当Blue开始运行时,使用该应用程序的用户将会发送只包含CustomerFirstNameCustomerLastName字段的 API 请求。在此期间不会发送CustomerFullName字段。在用户开始从服务器请求更新的 JavaScript 文件之前,可能需要一分钟到几天的时间。

针对此变更的蓝/绿部署流程的第一次尝试可能如下所示:

  1. 运行脚本来添加列CustomerFullName
  2. 运行回填脚本来填充CustomerFullName
  3. 将代码部署到Blue环境中。
  4. Blue环境中验证代码和数据库的变化。
  5. 将现场环境从Green切换到Blue
  6. 运行回填脚本来填充CustomerFullName

这不是理想的蓝/绿部署流程。它多次运行回填脚本,它不回答什么时候应该删除CustomerFirstNameCustomerLastName,但它是一个开始。

数据库更改

我坚信数据库应该只存储数据。数据库不应包含业务规则或业务逻辑。只有代码应该存储业务规则和业务逻辑。当数据库存储业务规则或业务逻辑时,会使蓝/绿部署变得更加困难。

数据库中的业务逻辑包括但不限于格式化、计算、IsNull 检查的包含、if/then 语句、while 语句、默认值以及不仅仅按 ID 进行过滤。即使它是一个空字符串或零,这些都是值。如果一列没有值,需要设置为Null,也就是没有值。在数据库中拥有业务规则和业务逻辑是自找麻烦。与 C#、JavaScript 或 Java 等代码相比,它们更难编写单元测试,即使使用 tSQLt 这样的工具也是如此。开发人员也很难找到它们,因为通常大多数开发人员使用他们选择的 IDE 进行搜索,不包括数据库。

本节介绍了支持蓝/绿部署的表更改、存储过程和视图更改的建议。它还将涵盖如何避免在数据库中包含业务规则和业务逻辑的技术。

表格更改

在进行蓝/绿部署时,您应该进行非破坏性的数据库更改。在我们的场景中,这意味着在添加时使CustomerFullName可为空。在没有默认值的情况下使列不可为空将是一种破坏性的改变。对Green的 Insert 语句将停止工作,因为它不知道这个新列。这并不意味着应该用默认值使列不可为空;即使是空字符串。请记住,默认值是一个业务规则。

另一个问题是,对于大多数数据库服务器(即 SQL Server 或 Oracle)来说,添加一个不可为空的列需要花费相当多的时间。当添加不可为空的列时,表定义会随每条记录一起更新。当添加可为空的列时,只更新表定义。如果您必须有一个默认值,那么脚本应该添加一个可为空的列,更新所有记录,然后将该列设置为不可为空。这个脚本可以被调整到惊人的速度。

破坏性数据库更改的一些其他示例包括重用现有列或重命名现有列。蓝/绿部署会失败,因为Green中的代码会抛出错误或显示不准确的数据。

到目前为止,本节已经介绍了如何向 customer 表中添加新列CustomerFullName。老的CustomerFirstNameCustomerLastName栏目呢?在场景部分,它实际上是说不要去管CustomerFirstNameCustomerLastName。不要用Null覆盖它,也不要试图猜测那些值会是什么。此外,一旦Blue开始使用这些更改,任何新记录都不会有CustomerFirstNameCustomerLastName的值,这意味着CustomerFirstNameCustomerLastName应该可以为空。

在某个时候,CustomerFirstNameCustomerLastName列将被删除。蓝绿色部署也使这变得更加复杂。假设CustomerFullName已经部署到Blue上,并且处于活动状态。运行脚本从所有表、视图、函数和存储过程中删除CustomerFirstNameCustomerLastName会导致错误。当CustomerFullName为空时,运行在Blue上的代码使用CustomerFirstNameCustomerLastName字段来填充CustomerFullName

从数据库中删除这些列需要多次部署。

  1. 部署将CustomerFullName添加到客户表中。需要CustomerFirstNameCustomerLastName,因为旧代码仍然引用它们。
  2. 另一个部署发生了,代码删除了对CustomerFirstNameCustomerLastName的所有引用。
  3. 最终部署从数据库中删除CustomerFirstNameCustomerLastName

有了这样的限制,如何删除CustomerFirstNameCustomerLastName列呢?要回答这个问题,请看一个有中断的典型标准部署。

这些部署不一定要在几天之内完成。我看到每一次部署都相隔几个月。

存储过程和视图

假设应用程序有两个存储过程:

  • usp_GetCustomerById
  • usp_GetAllCustomers

从根本上说,它们都是从数据库中获取客户。CustomerFullName是一个新列,但它是空的。如前所述,回填脚本提出了很多问题。一种选择是完全跳过回填脚本,让存储过程进行快速的 IsNull 检查:

Select CustomerFirstName,
       CustomerLastName,
       IsNull(CustomerFullName, CustomerFirstName + ' ' + CustomerLastName) as CustomerFullName
from dbo.Customer 

只隐藏坏的或丢失的数据;它不能解决丢失的数据。它是一行程序,很容易复制粘贴到其他存储过程。在这个例子中,只有一个其他的存储过程,但是如果有人添加了一个存储过程,他们需要记住包括这个一行程序。当需要从数据库中删除CustomerFirstNameCustomerLastName时,也有可能忘记更新存储过程。想象一下,如果一个存储过程有 50 列,而CustomerFullNameCustomerFirstNameCustomerLastName之间有几十行。

检索到的存储过程和视图应该按原样返回数据,不进行任何空检查或格式化:

Select CustomerFirstName,
       CustomerLastName,
       CustomerFullName
from dbo.Customer 

创建或更新存储过程略有不同。Green将在Blue被验证时激活。Green没有CustomerFullName的概念,这意味着它将调用不包含新列的存储过程。当Blue激活时,它将不再向存储过程发送CustomerFirstNameCustomerLastName字段。存储过程需要处理来自BlueGreen的调用:

ALTER procedure [dbo].[usp_UpdateCustomer] (
    @CustomerId int,
    @CustomerFirstName varchar(128) = null,
    @CustomerLastName varchar(128) = null,
    @CustomerFullName varchar(256) = null
)
Begin
    Update dbo.Customer
        set CustomerFirstName = @CustomerFirstName,
            CustomerLastName = @CustomerLastName,
            CustomerFullName = @CustomerFullName
    where CustomerId = @CustomerId
End
Go 

可以在不指定列的情况下运行插入命令,但是您不应该这样做。指定所有列。这只会带来麻烦,因为 insert 语句中的列顺序必须与表中的列顺序相匹配。如果在表的中间添加了一列(这种情况会发生!),insert 语句将开始随机失败或将数据插入错误的列。插入存储过程将类似于更新存储过程,在这种情况下,所有参数都将默认值设置为Null以处理调用它的BlueGreen:

ALTER procedure [dbo].[usp_InsertCustomer] (    
    @CustomerFirstName varchar(128) = null,
    @CustomerLastName varchar(128) = null,
    @CustomerFullName varchar(256) = null
)
Begin
    Insert into dbo.Customer (CustomerFirstName, CustomerLastName, CustomerFullName)
        value (@CustomerFirstName, @CustomerLastName, @CustomerFullName)

    select SCOPE_IDENTITY()  
End
Go 

视图在提供抽象层方面非常出色,如果编写正确,可以极大地提高性能。然而,我见过太多写得很差的,最终导致性能问题的。为了帮助解决这些性能问题,打破了数据库中没有业务逻辑的规则。我建议尽可能避免这种情况。就像存储过程一样,返回所有三列,CustomerFirstNameCustomerLastNameCustomerFullName

视图的一个日常用例是帮助数据仓库。抽象层视图提供了一个流程,可以轻松地将所有数据复制到数据仓库中,商业智能团队可以用它来创建报告。没有一种正确的自动化方法让数据仓库知道这种变化。我的建议是尽快把变化通知他们。

我意识到将业务逻辑移出数据库是一个相当大的变化。如果你真的决定做出改变,那就务实一点。不要试图一下子改变一切。代码和数据库现在工作正常。这一部分是关于数据库未来的变化。我总是试图让数据库和代码处于比我发现它时更好的状态。也就是说,如果我对某一列进行了更改,并且在数据库中发现了该列的业务规则,我会进行一些分析。如果改变相对容易,我会去做。如果它很复杂,我将创建一个卡片,并把它放在我们的技术债务待办事项中,以便以后解决(希望如此)。

代码更改

如数据库更改一节所述,业务规则和业务逻辑应该存在于代码中。该场景定义了一些需要放入代码中的业务规则。

  • CustomerFullNameNull时,将通过组合CustomerFirstNameCustomerLastName填充CustomerFullName
  • 如果数据存在于CustomerFirstNameCustomerLastName中,并且 API 请求仅与CustomerFullName一起发送,则不要将这些列设置为Null。此外,不要试图分割CustomerFullName,因为这非常困难且容易出错。

下一节将使用 C#作为示例语言,介绍如何将这些规则放入代码中的技术。

处理 CustomerFullName 中的 Null

在存储过程中放置这样的检查非常容易,但是如前所述,这不是一个好主意。

IsNull(CustomerFullName, CustomerFirstName + ' ' + CustomerLastName) as CustomerFullName 

另一种选择是将格式规则放在表示客户表的模型中:

public class CustomerModel
{
    private string _fullName;

    public int CustomerId {get; set;}
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public string FullName
    {
        get
        {
            if (string.IsNullOrDefault(_fullName))
            {
                return this.GetFormattedFullName();
            }

            return _fullName;
        }
        set
        {
            if (string.IsNullOrDefault(value))
            {
                _fullName = this.GetFormattedFullName();
            }
            else
            {
                _fullName = value;
            }
        }
    }

    public string GetFormattedFullName()
    {
        return $"{this.FirstName} {this.LastName}";
    }
} 

如果所有层(UI、业务和数据库)都使用相同的模型,那就很好了。这需要很强的纪律性,因为模型和数据库必须是一对一的匹配,而事实往往并非如此。

以下是将所有逻辑放入模型的替代方法:

public Interface ICustomer
{
    public int CustomerId {get;set;}
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public string FullName {get;set;}
}

public Interface ICustomerDataAdapter
{
    void InsertCustomer(ICustomer customer);
    void UpdateCustomer(ICustomer customer);
    ICustomer GetCustomerById(int customerId);
}

public static class CustomerNameFormatter
{
    public string GetFullName(this ICustomer customer)
    {
        if (string.IsNullOrWhiteSpace(customer.FullName) == false)
        {
            return customer.FullName;
        }

        return $"{customer.FirstName} {customer.LastName}";        
    }
}

public class CustomerFacade
{
    private ICustomerDataAdapter _customerDataAdapter;

    public CustomerFacade(ICustomerDataAdapter customerDataAdapter)
    {
        _customerDataAdapter = customerDataAdapter
    }

    public void GetCustomerById(int customerId)
    {
        var customer = _customerDataAdapter.GetCustomerById(customerId);

        customer.FullName = customer.GetFullName();        

        _customerDataAdapter.UpdateCustomer(customer);
    }
} 

使用相同的格式化程序扩展方法,插入客户记录的逻辑如下所示:

public Interface ICustomer
{
    public int CustomerId {get;set;}
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public string FullName {get;set;}
}

public Interface ICustomerDataAdapter
{
    void InsertCustomer(ICustomer customer);
    void UpdateCustomer(ICustomer customer);
    ICustomer GetCustomerById(int customerId);
}

public static class CustomerNameFormatter
{
    public string GetFullName(this ICustomer customer)
    {
        if (string.IsNullOrWhiteSpace(customer.FullName) == false)
        {
            return customer.FullName;
        }

        return $"{customer.FirstName} {customer.LastName}";        
    }
}

public class CustomerFacade
{
    private ICustomerDataAdapter _customerDataAdapter;

    public CustomerFacade(ICustomerDataAdapter customerDataAdapter)
    {
        _customerDataAdapter = customerDataAdapter
    }

    public void InsertCustomer(ICustomer customer)
    {
        var existingCustomer = _customerDataAdapter.GetCustomerById(customer.CustomerId);

        customer.FullName = customer.GetFullName();        

        _customerDataAdapter.InsertCustomer(customer);
    }
} 

不要覆盖 CustomerFirstName 或 CustomerLastName

Blue上的更改生效后,在CustomerFirstNameCustomerLastName列中仍然会有数据,将这些数据设置为 null 对代码来说是不好的。对于新记录,这些数据将不会出现。代码不应该试图猜测如何将名字和姓氏分开。对于现有记录,代码应该保持数据不变。对于新记录,需要将这些列设置为Null

就像以前一样,有一种让 update 语句在参数中检查 null 的诱惑(它只有一行,有什么坏处呢?):

 set CustomerFirstName = IsNull(@CustomerFirstName, CustomerFirstName) 

Blue的代码应该是空支票所在的地方。将IsNull支票放入数据库隐藏了一个业务规则,这是我们想要避免的:

public Interface ICustomer
{
    string FirstName {get;}
    string LastName {get;}
    string FullName {get;}
}

public Interface ICustomerDataAdapter
{
    void InsertCustomer(ICustomer customer);
    void UpdateCustomer(ICustomer customer);
    ICustomer GetCustomerById(int customerId);
}

public class CustomerFacade
{
    private ICustomerDataAdapter _customerDataAdapter;

    public CustomerFacade(ICustomerDataAdapter customerDataAdapter)
    {
        _customerDataAdapter = customerDataAdapter
    }

    public void UpdateCustomer(ICustomer customer)
    {
        var existingCustomer = _customerDataAdapter.GetCustomerById(customer.CustomerId);

        customer.FirstName = string.IsNullOrEmpty(customer.FirstName) ? existingCustomer?.FirstName : null;
        customer.LastName = string.IsNullOrEmpty(customer.LastName) ? existingCustomer?.LastName : null;

        _customerDataAdapter.UpdateCustomer(customer);
    }
} 

用数据回填新列

我见过的大多数回填脚本只不过是一个更新底层数据的 SQL 脚本。这意味着格式化逻辑将同时存在于代码和回填脚本中。很容易更新代码以更新格式规则,但忘记更新回填脚本。我也遇到过这种事。相信我,这是糟糕的一天。

此外,对于蓝/绿部署,您必须决定何时运行脚本:

  • 在代码已经部署到Blue并且数据库更改已经推出之后,但是在验证开始之前?
  • 还是在Blue上线后,而Green已经失效?

您还必须考虑:

  • 传统的回填 SQL 脚本更新低级数据,并可能锁定表。
  • 根据记录的数量,脚本可能需要很长时间。也许决定在 SQL 脚本中一次更新 1000 条记录。
  • 如果用户在更新与脚本相同的记录时发生死锁,该怎么办;如果脚本赢得死锁,而不是用户更新,可以吗?
  • 根据脚本运行的时间,它可能需要有适当的保护条款来防止意外更新。

此外,大多数数据库部署工具,如 Redgate、DBUp、RoundhousE 和 SSDT,都没有多次运行特定 SQL 脚本的机制。如果没有内置的功能,就需要一个黑客来支持它。

代码已经有了格式化规则。它有必要的逻辑来处理各种场景。它应该有单元测试来覆盖它。这是 QA 验证的。回填脚本应该用 PowerShell 或者 Bash 编写,调用 API,而不是直接更新数据库。它可以查询数据库来查找客户列表,然后使用该客户列表来访问 API。可以将逻辑添加到脚本中,以最小化脚本和用户同时更新记录的风险。也许与客户相关的用户有一个时区偏好,脚本通过一个自动化的过程每小时运行一次,并且只在用户的时区在凌晨 2 点到 3 点之间时更新记录

下面是用 PowerShell 编写的回填脚本示例:

$url = "https://myapp/api"
$apiKey = "Some Key"
$header = @{ "apiKey" = $apiKey }
$connectionString = "Server=MyServer;Database=ApplicationDatabase;integrated security=true;"

$sqlConnection = New-Object System.Data.SqlClient.SqlConnection
$sqlConnection.ConnectionString = $connectionString

$command = $sqlConnection.CreateCommand()
$command.CommandType = [System.Data.CommandType]'Text'
$command.CommandText = "Select CustomerId
        from Customer
        where IsNull(CustomerFullName, '') = ''
            and (IsNull(CustomerFirstName, '') <> '' or IsNull(CustomerLastName, '') <>'')"            

$sqlConnection.Open()

$dataAdapter = new-object System.Data.SqlClient.SqlDataAdapter $SqlCommand
$dataSet = new-object System.Data.DataSet
$dataAdapter.Fill($dataSet)

$sqlConnection.Close()

$customerTable = $dataSet.Tables[0]

foreach($customerRow in $customerTable)
{
    $customerId = $customerRow["CustomerId"]

    Write-Host "Getting customer $customerId"
    $customer = (Invoke-RestMethod "$url/customers/$customerId" -Headers $header)

    Write-Host "Updating customer $customerId"
    $updateCustomerResult = (Invoke-RestMethod "$url/customer/$customerId" -Headers $header -Method Post -Body $customer -ContentType "application/json")
} 

该脚本是非破坏性的,可以随时运行,因此不需要在部署期间运行。让它在部署之外运行意味着它不会阻碍任何事情。脚本可以被启动,并且它可以根据需要插入任意长的时间。所有的时间问题都考虑到了。没有必要担心多次运行回填脚本来确保记录不会以某种方式丢失。也没必要担心跑完要多长时间。这将有助于尽可能无缝地从Green过渡到Blue

版本化存储过程、视图和 API

典型的经验法则是:

如果你做了一个突破性的改变,那么就修改 API/存储过程/视图的版本。

理论上,这是一个很好的规则。实际上,这一规则很快就会瓦解。版本控制给维护代码的人带来了很大的负担。维护人员知道他们需要担心多个代码路径或多个实例。旧版本存在的时间越长,就越难转向新版本。我见过一些公司在项目上花费数百个小时让人们放弃旧版本。

我的建议是,默认情况下应该尽可能向后兼容地进行更改。有一个单一的代码库,单一的存储过程,单一的视图,每个人都使用。一个单一的代码库将会使维护变得更加容易,并且它将会使修改变得更加容易(随着时间的推移)。观察多次部署,随着时间的推移做出小的改变,而不是大的改变。在进入版本池之前,探索所有的选项。在用尽所有其他选项后,应该考虑版本控制。

何时应该部署数据库更改

在测试场景中考虑这篇文章中的所有内容;我认为数据库的变化不需要和代码同时部署。唯一的要求是,数据库更改必须在代码之前部署。在部署代码前几天甚至几周部署数据库更改可能有额外的好处。如果另一个应用程序正在使用同一个数据库(即使只是一个视图),这将给他们时间来修改和测试他们的代码。一旦数据库更改完成,其他团队就可以在准备好的时候进行部署。

真正的问题是,在代码发布前几天甚至一周部署数据库更改有意义吗?这个问题有点难回答。我的建议是根据场景做有意义的事情,但是在部署过程中做的更改越少越好。一般来说,变更越少意味着风险越小。在代码包含风险之前将数据库变更推向生产环境。一旦开发、测试或 UAT 开始,可能需要额外的数据库更改。如果第一个变更成功进入生产环境,其他变更将需要再次进入生产环境。

我倾向于将代码和数据库存储在同一个存储库中。让所有东西都在一个功能分支中工作。同时合并特性分支中的所有变更,同时进行测试和验证。

我真正喜欢蓝/绿部署的是它为数据库部署提供的灵活性。在蓝/绿部署之前,一切都必须在部署期间同时进行。现在有一个选择。

测试和验证

在与Green交换之前对Blue的自动化测试和验证(反之亦然)使得一切进行得更快。自动测试和验证并不是蓝/绿部署的要求,Octopus Deploy 等部署工具具有手动干预步骤,可以暂停部署并等待直到有人批准继续进行。

大多数开始蓝/绿部署的人在第一天都没有可以在生产中运行的完整测试套件。关键是从某个地方开始。建立测试套件和克服技术障碍需要时间。根据工具的不同,即使是一个简单的场景,用Green改变Blue的 URL 也需要一点时间。当测试上线时,将它们包含在部署管道中,以帮助自动化验证。希望有一天,绝大多数用例都可以得到验证,手动干预步骤可以被移除。

常见的数据库更改场景

这篇文章介绍了一个非常复杂的场景,将两个专栏合并成一个。下面的列表中详细列出了其他几种数据库更改场景。对于每个场景,我都包括了哪些部分可以应用于该场景。

  • 添加新列:除了删除旧列和处理遗留列之外,遵循所有步骤。
  • 重命名列:不要重命名。按照上述步骤添加新列并删除旧列。
  • 增加新表:类似于增加新列。
  • 重命名表格:不要重命名。按照上述步骤添加新列并删除旧列。
  • 将一列移动到另一个表:非常类似于添加新列和删除旧列。唯一的区别是添加列的位置。
  • 删除表格:非常类似于删除列。希望不再使用该表,并且所有数据都已经迁移到其他表中。
  • 添加新视图/存储过程:非常类似于添加新列。更新后的代码将使用新的视图/存储过程;旧代码不会使用视图/存储过程。
  • 更新现有的视图/存储过程:只要没有从视图中删除列,这应该没问题。如果删除了列,那么应该遵循从上面删除列的过程。
  • 删除视图/存储过程:与删除列非常相似的过程。除了没有数据,只有代码中的引用和潜在的其他存储过程。

总结

如您所见,数据库的蓝/绿部署需要在编写代码和更改数据库的方式上做一些小的改变。与停机时进行标准部署相比,它还需要对如何实施变更进行更多的规划。

蓝/绿部署流程的第一次尝试是这样的:

  1. 运行脚本来添加列CustomerFullName
  2. 运行回填脚本来填充CustomerFullName
  3. 将代码部署到Blue环境中。
  4. Blue环境中验证代码和数据库的变化。
  5. 将现场环境从Green切换到Blue
  6. 运行回填脚本来填充CustomerFullName

这个过程现在已经演变成了这样:

  1. 运行脚本来添加列CustomerFullName
  2. 将代码部署到Blue环境中。
  3. Blue环境中验证代码和数据库的变化。
  4. 将现场环境从Green切换到Blue

回填脚本根本不包含在内;它在部署之后运行,以帮助填充新的CustomerFullName列。事实上,在需要修改代码来删除CustomerFirstNameCustomerLastName列之前,该脚本不需要运行。

在结束这篇文章之前,我想指出蓝/绿部署并不适合每一种应用。它需要架构、基础设施和工具的正确组合。如果您正在考虑蓝/绿部署,请选择一个有数据库更改的特性,并通过蓝/绿部署策略来实现它。你必须改变你的应用程序的架构吗?是否有必要的基础架构,如负载平衡器和额外的虚拟机?您的 CI/CD 工具能支持蓝/绿部署吗?

就时间而言,蓝/绿部署的初始成本可能很高。如果有可能在一天当中部署变更,那么这一成本是值得的。能够做到这一点开启了许多不同的可能性。很快,对话将从“我如何在中午进行部署”转移到“蓝/绿部署就绪后,我能做些什么?”蓝绿色部署,或无缝的日间部署,感觉就像是 CI/CD 之旅的终点。我认为这只是开始。

下次再见,愉快的部署!

使用 DbUp 和 Octopus workers 实现数据库部署自动化- Octopus Deploy

原文:https://octopus.com/blog/dbup-database-deployment-automation

Using DbUp and Octopus workers for database deployment automation

在过去十年中,数据库部署最令人兴奋的一个方面是已经发布的工具数量。看一下我以前关于这个话题的帖子,你会发现我明显偏向于 Redgate 的工具,但是我是 Redgate 的朋友,这是有原因的。

在这个帖子里,我用的是 DbUp 。DbUp 是一个免费的开源工具,我们在 Octopus Deploy 中使用它进行数据库部署。每当您安装或升级 Octopus Deploy 时,DbUp 都会运行脚本来更新您的数据库。我们的创始人 Paul Stovell 在 2012 年写了一篇关于如何使用 DbUp 部署到 SQL Server 的博客文章。在很大程度上,那篇博文至今仍然有效。

这篇文章是那篇旧文章的更新。DbUp 和 Octopus Deploy 中添加了许多新特性,我将介绍其中的一些特性,并创建一个过程来使用它进行数据库部署。它甚至包括一个 DBA 批准的审查步骤。

对 DbUp 的更改

本质上,DbUp 是一个脚本运行器。对数据库的更改是通过脚本完成的:

  • Script001_AddTableA.sql
  • script 002 _ addcolumntesttotablea . SQL
  • script 003 _ addcolumnstategaintotablea . SQL

DbUp 通过您自己编写的控制台应用程序运行,因此您可以控制使用哪些选项,并且不需要大量代码:

static int Main(string[] args)
{
    var connectionString =
        args.FirstOrDefault()
        ?? "Server=(local)\\SqlExpress; Database=MyApp; Trusted_connection=true";

    var upgrader =
        DeployChanges.To
            .SqlDatabase(connectionString)
            .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
            .LogToConsole()
            .Build();

    var result = upgrader.PerformUpgrade();

    if (!result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.ResetColor();

        return -1;
        }
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Success!");
    Console.ResetColor();
    return 0;
} 

您捆绑这些脚本并告诉 DbUp 运行它们。它将该列表与存储在目标数据库中的列表进行比较。将运行不在该目标数据库列表中的任何脚本。脚本按字母顺序执行,每个脚本的结果都显示在控制台上。非常容易实现和理解。

当您部署到开发或测试环境时,这非常有用。我交谈过的许多公司更喜欢他们的 DBA 在投入生产之前批准脚本。也可能是一个试运行或预生产环境。这个批准过程是必不可少的,尤其是当您第一次开始部署数据库时。

HTML 报告

迁移脚本是一把双刃剑,就像 C++中的内存管理一样。你拥有完全的控制权,这给了你巨大的力量。但是,也很容易搞砸。这完全取决于所做更改的类型和作者的 SQL 技能。当没有经验的 C#开发人员编写这些迁移脚本时,DBA 对这个过程的信任度会很低。

最近,DbUp 增加了生成 HTML 报告的功能。这是一个扩展方法,您可以给它您想要生成的报告的路径。这意味着这部分从:

var result = upgrader.PerformUpgrade();

if (!result.Successful)
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(result.Error);
    Console.ResetColor();

    return -1;
    }
} 

收件人:

// --generateReport is the name of the example argument.  You can call it anything
if (args.Any(a => "--generateReport".Equals(a, StringComparison.InvariantCultureIgnoreCase)))
{
    upgrader.GenerateUpgradeHtmlReport("C:\\DeploymentLocation\\UpgradeReport.html");
}
else
{
    var result = upgrader.PerformUpgrade();

    if (!result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.ResetColor();
        return -1;
    }
} 

该代码将生成一个包含所有将要运行的脚本的报告。

始终运行脚本和脚本分组

默认情况下,DbUp 将运行一次脚本,大多数情况下这没问题,但有时总是运行一个脚本或一组脚本也不错。一个例子是刷新所有视图的部署后脚本。或者,使用一个脚本来重建所有索引并重新生成统计数据。您不希望为每个部署编写新的脚本。

DbUp 最近增加的另一个特性是能够将一组脚本标记为AlwaysRun并提供一个运行组:

var upgradeEngineBuilder = DeployChanges.To
    .SqlDatabase(connectionString, null) //null or "" for default schema for user
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.PreDeployment."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 1})
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.Scripts."), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 2})
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.PostDeployment."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 3})
    .LogToConsole();

var upgrader = upgradeEngineBuilder.Build();

var result = upgrader.PerformUpgrade();

// Display the result
if (result.Successful)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Success!");
}
else
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(result.Error);
    Console.WriteLine("Failed!");
} 

创建 DbUp 控制台应用程序

有了这些新特性,我们将构建一个. NET 核心 DbUp 控制台应用程序来部署到 SQL Server。然后,我们将在 Octopus 部署中整合一个流程来运行控制台应用程序。

下面的所有代码都可以在 GitHub repo 中找到。

我选择了。网核结束。NET 框架,因为它可以在任何地方构建和运行。DbUp 是一个. NET 标准库。DbUp 在. NET Framework 应用程序中也能很好地工作。

让我们启动我们选择的 IDE,创建一个. NET 核心控制台应用程序。我使用 JetBrain 的 Rider 来构建这个控制台应用程序。比起 Visual Studio 我更喜欢它。

脚手架

控制台应用程序已经创建。现在我们需要引入 DbUp NuGet 包。让我们转到我们的 NuGet 包管理器:

【T2

接下来,我们选择 DbUp-SqlServer 包。该包包括核心包以及部署到 SQL Server 的必要代码。如果您想部署到 PostgreSQL、MySQL、Oracle 或 SQLite,您可以选择:

控制台应用程序需要一些脚本来部署。我将添加三个文件夹,并用一些脚本文件填充它们:

建议你加个前缀,比如 001,002 等。,添加到脚本文件名的开头。DbUp 按字母顺序运行脚本,该前缀有助于确保脚本按正确的顺序运行。

默认情况下,。NET 在构建控制台应用程序时不会包含这些脚本文件,我们希望将这些脚本文件作为嵌入式资源包含在内。幸运的是,我们可以通过在.csproj文件中包含这段代码来轻松地添加对这些文件的引用:

 <ItemGroup>
        <EmbeddedResource Include="BeforeDeploymentScripts\*.sql" />
        <EmbeddedResource Include="DeploymentScripts\*.sql" />
        <EmbeddedResource Include="PostDeploymentScripts\*.sql" />
    </ItemGroup> 

整个文件如下所示:

Program.cs 文件

启动这个应用程序的最后一步是在Program.cs中添加必要的代码来调用 DbUp。应用程序接受来自命令行的参数,Octopus Deploy 将被配置为发送以下参数:

  • ConnectionString :在这个演示中,我们将它作为参数发送,而不是存储在配置文件中。
  • PreviewReportPath :保存预览报表的完整路径。完整路径参数是可选的。当它被发送进来时,我们为 Octopus Deploy 生成一个预览 HTML 报告,以变成一个工件。当它没有被发送进来时,代码将执行实际的部署。

让我们从命令行参数中提取连接字符串开始:

static void Main(string[] args)
{    
    var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));

    // We expect the connection string to be there.  If it doesn’t this will throw an error.  
    connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(@"""", string.Empty); 

DbUp 使用流畅的 API。我们需要告诉它我们的文件夹,每个文件夹的脚本类型,以及我们希望运行脚本的顺序。如果您使用带有 StartsWith 搜索的嵌入在汇编选项中的脚本,您需要在您的搜索中提供完整的名称空间。

var upgradeEngineBuilder = DeployChanges.To
    .SqlDatabase(connectionString, null)
    // Pre-deployment scripts, set them to always run first
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.BeforeDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 0 })
    // Main Deployment scripts, they run once and run in the second group
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.DeploymentScripts"), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
    // Post deployment scripts, always run these scripts and run after everything has been deployed
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.PostDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
    // By default all the scripts are run in the same transaction
    .WithTransactionPerScript()
    // Set this so it can report back to Octopus Deploy how things are going
    .LogToConsole();

var upgrader = upgradeEngineBuilder.Build();

Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired()); 

升级程序已经构建好了,可以运行了。这一部分是我们注入升级报告参数检查的地方。如果设置了该参数,请不要运行升级。相反,为 Octopus Deploy 生成一个报告作为工件上传:

if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
{
    // Generate a preview file so Octopus Deploy can generate an artifact for approvals
    var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
    report = report.Substring(report.IndexOf("=") + 1).Replace(@"""", string.Empty);

    var fullReportPath = Path.Combine(report, "UpgradeReport.html");

    Console.WriteLine($"Generating the report at {fullReportPath}");

    upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
else
{
    var result = upgrader.PerformUpgrade();

    // Display the result
    if (result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Success!");
    }
    else
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.WriteLine("Failed!");
    }
} 

当我们把它们放在一起时,它看起来像这样:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using DbUp;
using DbUp.Engine;
using DbUp.Helpers;
using DbUp.Support;

namespace DbUpSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));

            connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(@"""", string.Empty);

            var upgradeEngineBuilder = DeployChanges.To
                .SqlDatabase(connectionString, null)
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.BeforeDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 0 })
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.DeploymentScripts"), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.PostDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
                .WithTransactionPerScript()
                .LogToConsole();

            var upgrader = upgradeEngineBuilder.Build();

            Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired());

            if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
            {
                // Generate a preview file so Octopus Deploy can generate an artifact for approvals
                var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
                report = report.Substring(report.IndexOf("=") + 1).Replace(@"""", string.Empty);

                var fullReportPath = Path.Combine(report, "UpgradeReport.html");

                Console.WriteLine($"Generating the report at {fullReportPath}");

                upgrader.GenerateUpgradeHtmlReport(fullReportPath);
            }
            else
            {
                var result = upgrader.PerformUpgrade();

                // Display the result
                if (result.Successful)
                {
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.WriteLine("Success!");
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine(result.Error);
                    Console.WriteLine("Failed!");
                }
            }
        }
    }
} 

在启用报告参数的情况下运行控制台应用程序会生成我们预期的报告:

未来的工作

创建脚手架和编写 program.cs 文件的代码应该只需要做一次。有了我们的设置,你需要做的就是将文件添加到PreDeploymentPostDeploymentDeployment文件夹中。

使用这种设置,很容易删除旧文件,但是 DbUp 不喜欢这样做。DbUp 背后的想法是,它提供所有数据库更改的历史。例如,当您想要在新的开发人员的机器上创建一个新的数据库时,您只需要运行这个命令行应用程序。它将遍历并运行所有脚本,以启动并运行数据库。删除文件最终可能会删除一个键序列,例如创建一个表、添加一个关键列或者将列从一个表移动到另一个表。有这些额外的文件并不会对性能造成太大的影响。DbUp 将看到他们已经运行,并将他们从运行列表中排除。

您可以将较旧的文件移动到一个新的文件夹中,并添加一个新的命令行参数来选择这些文件。

Octopus 部署配置

我将假设您知道如何构建一个. NET 核心应用程序并打包它。如果你没有,这里是快速 TL;博士:

  • 在项目上运行dotnet publish命令(不要忘记输出路径)。
  • 运行octo pack来打包输出路径(或者使用 Octopus Deploy build server 插件)。
  • 使用octo push命令将包推送到 Octopus Deploy(或者使用 Octopus Deploy 构建服务器插件)。

为了让您的生活更轻松,对于这个演示,我在 GitHub repo 的根目录中以 zip 文件的形式包含了示例应用程序的 1.0.0.1 版本。将软件包上传到 Octopus 内置存储库:

我赞同 Octopus Deploy 项目应该负责自我引导的理论。对于数据库部署,这意味着在部署之前确保数据库存在,并创建必要的 SQL Server 用户。

在进入流程之前,我们需要定义一些变量。因为这是一篇博文的演示,所以我在所有环境中使用相同的数据库服务器:

为了创建数据库和用户,这个过程将使用我为以前的博客文章创建的社区步骤模板。请参见文档了解如何在你的 Octopus 服务器上下载和安装这些社区步骤模板。

我在数据库工作者池中的一个工作者上运行第一步。我选择使用单个工作池,因为我使用 SQL 身份验证。我不必担心每个环境的集成安全性和独特的服务帐户。如果我在这个过程中使用集成安全性和每个环境的唯一服务帐户,还有一些额外的设置要做,但是我将在后面的文章中介绍。现在,我想尽可能简单地解释一下:

首先,我们要搭建好脚手架,创建数据库、用户,并分配用户数据库:

下一组步骤将从 DbUp 部署数据库更改。如果你还记得我之前的 Redgate 文章,这是分四步完成的:

  1. 下载软件包。
  2. 运行 Redgate 创建数据库版本。
  3. DBA 批准部署。
  4. 运行 Redgate 部署数据库版本。

我对这个过程有几个问题。也就是说,下载包步骤被设计成提取包并把它留在触手上。默认情况下,它将永远保留在那里,除非配置了保留策略。部署完成后,没有必要将提取的包放在触手上。SQL 脚本已经运行,现在它们正在占用空间。此外,该流程的步骤 2 和 4 引用了步骤 1。感觉有很多额外的工作。

如果您正在使用 Octopus Deploy 2018.8 或更高版本,好消息是我们现在可以引用来自运行脚本步骤的包。包将被下载和提取,在步骤完成后,它将删除提取的包。除了清理不需要的内容,在运行脚本步骤中使用包引用非常适合在 workers 上运行部署,假设每个步骤都是独立的。这也意味着不同的工人可以完成每一步的工作。

该流程部署部分的第一步是生成 HTML 报告,并将其作为工件上传到 Octopus Deploy。点击添加按钮,将包引用添加到步骤中:

当模式窗口出现时,选择要提取的包:

现在我们可以添加一个脚本来处理部署。跑步。NET 核心控制台应用程序与运行。NET Framework 控制台应用程序。

这完全取决于您在构建和发布应用程序时设置的开关。您可以创建一个自包含的控制台应用程序(。exe)以及所有必要的。dll,但这样做会增加包的大小。或者,您可以将其设置为仅创建一个. dll,并引用所有外部依赖项。在示例包中,我创建了一个自包含的包,但是我排除了。zip 文件中的。这样,您就不必担心运行恢复了:

# How you reference the extracted path
$packagePath = $OctopusParameters["Octopus.Action.Package[DbUpSample].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]
$reportPath = $OctopusParameters["Project.HtmlReport.Location"]

$dllToRun = "$packagePath\DbUpSample.dll"
$generatedReport = "$reportPath\UpgradeReport.html"

if ((test-path $reportPath) -eq $false){
    New-Item $reportPath -ItemType "directory"
}

# How you run this .NET core app
dotnet $dllToRun --ConnectionString="$connectionString" --PreviewReportPath="$reportPath"

New-OctopusArtifact -Path "$generatedReport" 

完成后,整个步骤如下所示:

人工干预没什么特别的。在本例中,我将其配置为仅在试运行和生产环境中运行,并让数据库管理员批准该部署:

部署步骤类似于生成增量报告步骤,除了它将部署变更,而不用担心报告的生成。这一步的 PowerShell 是:

# How you reference the extracted path
$packagePath = $OctopusParameters["Octopus.Action.Package[DbUpSample].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]

$dllToRun = "$packagePath\DbUpSample.dll"

# How you run this .NET core app
dotnet $dllToRun --ConnectionString="$connectionString" 

现在最后的过程是:

变数就在那里。流程设置完毕。让我们部署一些数据库更改:

哎呦!忘记安装了。我的员工的网络核心:

快速跳转到脚本控制台来运行 chocolatey 安装:

这是成功的:

让我们再试试那个版本。事后看来,我本可以告诉它重试发布,但我决定创建一个新的:

这一次很成功。您可以看到由流程创建的工件。这是数据库管理员将下载并在试运行和生产中审查的内容:

如果我们看一下数据库,我们会看到项目是按预期创建的:

【T2

综合安全和工人

在这个演示中,我使用了 SQL 身份验证。然而,你们中的许多人正在使用集成安全性。为了增加一层安全性,每个环境都有自己的 Active Directory 服务帐户。这完全有道理,我推荐这种方法。

对于现在这一代员工,你如何做到这一点?这并不像它应该的那样直截了当(我们希望在 workers v2 中解决这个问题)。我将指导您完成设置它的必要步骤。

首先,我们需要为每个环境创建一个专用的工作人员池。

接下来,我们需要创建云区域部署目标。

您需要为每个环境创建一个云区域。我为这些云区域创建了一个名为DbWorker的新角色,因为我想要一种区分这些新部署目标的方法:

完成后,我有了四个新的云区域:

我将更改流程的执行位置,使其在该环境中使用DbWorker角色的目标上运行:

对流程中每个步骤重复相同的更改:

当部署一个新版本来测试时,选择Test Database Worker Region:

结论

最近对 DbUp 的修改有助于为数据库创建一个健壮的部署管道。现在 DBA(和其他人)可以在部署之前通过 Octopus Deploy 检查变更。拥有审查变更的能力应该有助于在过程中建立信任,并有助于加速采用。


数据库部署自动化系列文章:

使用 DbUp 和 Octopus workers 实现数据库部署自动化- Octopus Deploy

原文:https://octopus.com/blog/dbup-database-deployments

Using DbUp and Octopus workers for database deployment automation

在过去十年中,数据库部署最令人兴奋的一个方面是已经发布的工具数量。看一下我以前关于这个话题的帖子,你会发现我明显偏向于 Redgate 的工具,但是我是 Redgate 的朋友,这是有原因的。

在这个帖子里,我用的是 DbUp 。DbUp 是一个免费的开源工具,我们在 Octopus Deploy 中使用它进行数据库部署。每当您安装或升级 Octopus Deploy 时,DbUp 都会运行脚本来更新您的数据库。我们的创始人 Paul Stovell 在 2012 年写了一篇关于如何使用 DbUp 部署到 SQL Server 的博客文章。在很大程度上,那篇博文至今仍然有效。

这篇文章是那篇旧文章的更新。DbUp 和 Octopus Deploy 中添加了许多新特性,我将介绍其中的一些特性,并创建一个过程来使用它进行数据库部署。它甚至包括一个 DBA 批准的审查步骤。

对 DbUp 的更改

本质上,DbUp 是一个脚本运行器。对数据库的更改是通过脚本完成的:

  • Script001_AddTableA.sql
  • script 002 _ addcolumntesttotablea . SQL
  • script 003 _ addcolumnstategaintotablea . SQL

DbUp 通过您自己编写的控制台应用程序运行,因此您可以控制使用哪些选项,并且不需要大量代码:

static int Main(string[] args)
{
    var connectionString =
        args.FirstOrDefault()
        ?? "Server=(local)\\SqlExpress; Database=MyApp; Trusted_connection=true";

    var upgrader =
        DeployChanges.To
            .SqlDatabase(connectionString)
            .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
            .LogToConsole()
            .Build();

    var result = upgrader.PerformUpgrade();

    if (!result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.ResetColor();

        return -1;
        }
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Success!");
    Console.ResetColor();
    return 0;
} 

您捆绑这些脚本并告诉 DbUp 运行它们。它将该列表与存储在目标数据库中的列表进行比较。将运行不在该目标数据库列表中的任何脚本。脚本按字母顺序执行,每个脚本的结果都显示在控制台上。非常容易实现和理解。

当您部署到开发或测试环境时,这非常有用。我交谈过的许多公司更喜欢他们的 DBA 在投入生产之前批准脚本。也可能是一个试运行或预生产环境。这个批准过程是必不可少的,尤其是当您第一次开始部署数据库时。

HTML 报告

迁移脚本是一把双刃剑,就像 C++中的内存管理一样。你拥有完全的控制权,这给了你巨大的力量。但是,也很容易搞砸。这完全取决于所做更改的类型和作者的 SQL 技能。当没有经验的 C#开发人员编写这些迁移脚本时,DBA 对这个过程的信任度会很低。

最近,DbUp 增加了生成 HTML 报告的功能。这是一个扩展方法,您可以给它您想要生成的报告的路径。这意味着这部分从:

var result = upgrader.PerformUpgrade();

if (!result.Successful)
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(result.Error);
    Console.ResetColor();

    return -1;
    }
} 

收件人:

// --generateReport is the name of the example argument.  You can call it anything
if (args.Any(a => "--generateReport".Equals(a, StringComparison.InvariantCultureIgnoreCase)))
{
    upgrader.GenerateUpgradeHtmlReport("C:\\DeploymentLocation\\UpgradeReport.html");
}
else
{
    var result = upgrader.PerformUpgrade();

    if (!result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.ResetColor();
        return -1;
    }
} 

该代码将生成一个包含所有将要运行的脚本的报告。

始终运行脚本和脚本分组

默认情况下,DbUp 将运行一次脚本,大多数情况下这没问题,但有时总是运行一个脚本或一组脚本也不错。一个例子是刷新所有视图的部署后脚本。或者,使用一个脚本来重建所有索引并重新生成统计数据。您不希望为每个部署编写新的脚本。

DbUp 最近增加的另一个特性是能够将一组脚本标记为AlwaysRun并提供一个运行组:

var upgradeEngineBuilder = DeployChanges.To
    .SqlDatabase(connectionString, null) //null or "" for default schema for user
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.PreDeployment."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 1})
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.Scripts."), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 2})
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), script => script.StartsWith("SampleApplication.PostDeployment."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 3})
    .LogToConsole();

var upgrader = upgradeEngineBuilder.Build();

var result = upgrader.PerformUpgrade();

// Display the result
if (result.Successful)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Success!");
}
else
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(result.Error);
    Console.WriteLine("Failed!");
} 

创建 DbUp 控制台应用程序

有了这些新特性,我们将构建一个. NET 核心 DbUp 控制台应用程序来部署到 SQL Server。然后,我们将在 Octopus 部署中整合一个流程来运行控制台应用程序。

下面的所有代码都可以在 GitHub repo 中找到。

我选择了。网核结束。NET 框架,因为它可以在任何地方构建和运行。DbUp 是一个. NET 标准库。DbUp 在. NET Framework 应用程序中也能很好地工作。

让我们启动我们选择的 IDE,创建一个. NET 核心控制台应用程序。我使用 JetBrain 的 Rider 来构建这个控制台应用程序。比起 Visual Studio 我更喜欢它。

脚手架

控制台应用程序已经创建。现在我们需要引入 DbUp NuGet 包。让我们转到我们的 NuGet 包管理器:

【T2

接下来,我们选择 DbUp-SqlServer 包。该包包括核心包以及部署到 SQL Server 的必要代码。如果您想部署到 PostgreSQL、MySQL、Oracle 或 SQLite,您可以选择:

控制台应用程序需要一些脚本来部署。我将添加三个文件夹,并用一些脚本文件填充它们:

建议你加个前缀,比如 001,002 等。,添加到脚本文件名的开头。DbUp 按字母顺序运行脚本,该前缀有助于确保脚本按正确的顺序运行。

默认情况下,。NET 在构建控制台应用程序时不会包含这些脚本文件,我们希望将这些脚本文件作为嵌入式资源包含在内。幸运的是,我们可以通过在.csproj文件中包含这段代码来轻松地添加对这些文件的引用:

 <ItemGroup>
        <EmbeddedResource Include="BeforeDeploymentScripts\*.sql" />
        <EmbeddedResource Include="DeploymentScripts\*.sql" />
        <EmbeddedResource Include="PostDeploymentScripts\*.sql" />
    </ItemGroup> 

整个文件如下所示:

Program.cs 文件

启动这个应用程序的最后一步是在Program.cs中添加必要的代码来调用 DbUp。应用程序接受来自命令行的参数,Octopus Deploy 将被配置为发送以下参数:

  • ConnectionString :在这个演示中,我们将它作为参数发送,而不是存储在配置文件中。
  • PreviewReportPath :保存预览报表的完整路径。完整路径参数是可选的。当它被发送进来时,我们为 Octopus Deploy 生成一个预览 HTML 报告,以变成一个工件。当它没有被发送进来时,代码将执行实际的部署。

让我们从命令行参数中提取连接字符串开始:

static void Main(string[] args)
{    
    var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));

    // We expect the connection string to be there.  If it doesn’t this will throw an error.  
    connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(@"""", string.Empty); 

DbUp 使用流畅的 API。我们需要告诉它我们的文件夹,每个文件夹的脚本类型,以及我们希望运行脚本的顺序。如果您使用带有 StartsWith 搜索的嵌入在汇编选项中的脚本,您需要在您的搜索中提供完整的名称空间。

var upgradeEngineBuilder = DeployChanges.To
    .SqlDatabase(connectionString, null)
    // Pre-deployment scripts, set them to always run first
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.BeforeDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 0 })
    // Main Deployment scripts, they run once and run in the second group
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.DeploymentScripts"), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
    // Post deployment scripts, always run these scripts and run after everything has been deployed
    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.PostDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
    // By default all the scripts are run in the same transaction
    .WithTransactionPerScript()
    // Set this so it can report back to Octopus Deploy how things are going
    .LogToConsole();

var upgrader = upgradeEngineBuilder.Build();

Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired()); 

升级程序已经构建好了,可以运行了。这一部分是我们注入升级报告参数检查的地方。如果设置了该参数,请不要运行升级。相反,为 Octopus Deploy 生成一个报告作为工件上传:

if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
{
    // Generate a preview file so Octopus Deploy can generate an artifact for approvals
    var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
    report = report.Substring(report.IndexOf("=") + 1).Replace(@"""", string.Empty);

    var fullReportPath = Path.Combine(report, "UpgradeReport.html");

    Console.WriteLine($"Generating the report at {fullReportPath}");

    upgrader.GenerateUpgradeHtmlReport(fullReportPath);
}
else
{
    var result = upgrader.PerformUpgrade();

    // Display the result
    if (result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Success!");
    }
    else
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.WriteLine("Failed!");
    }
} 

当我们把它们放在一起时,它看起来像这样:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using DbUp;
using DbUp.Engine;
using DbUp.Helpers;
using DbUp.Support;

namespace DbUpSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var connectionString = args.FirstOrDefault(x => x.StartsWith("--ConnectionString", StringComparison.OrdinalIgnoreCase));

            connectionString = connectionString.Substring(connectionString.IndexOf("=") + 1).Replace(@"""", string.Empty);

            var upgradeEngineBuilder = DeployChanges.To
                .SqlDatabase(connectionString, null)
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.BeforeDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 0 })
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.DeploymentScripts"), new SqlScriptOptions { ScriptType = ScriptType.RunOnce, RunGroupOrder = 1 })
                .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), x => x.StartsWith("DbUpSample.PostDeploymentScripts."), new SqlScriptOptions { ScriptType = ScriptType.RunAlways, RunGroupOrder = 2 })
                .WithTransactionPerScript()
                .LogToConsole();

            var upgrader = upgradeEngineBuilder.Build();

            Console.WriteLine("Is upgrade required: " + upgrader.IsUpgradeRequired());

            if (args.Any(a => a.StartsWith("--PreviewReportPath", StringComparison.InvariantCultureIgnoreCase)))
            {
                // Generate a preview file so Octopus Deploy can generate an artifact for approvals
                var report = args.FirstOrDefault(x => x.StartsWith("--PreviewReportPath", StringComparison.OrdinalIgnoreCase));
                report = report.Substring(report.IndexOf("=") + 1).Replace(@"""", string.Empty);

                var fullReportPath = Path.Combine(report, "UpgradeReport.html");

                Console.WriteLine($"Generating the report at {fullReportPath}");

                upgrader.GenerateUpgradeHtmlReport(fullReportPath);
            }
            else
            {
                var result = upgrader.PerformUpgrade();

                // Display the result
                if (result.Successful)
                {
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.WriteLine("Success!");
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine(result.Error);
                    Console.WriteLine("Failed!");
                }
            }
        }
    }
} 

在启用报告参数的情况下运行控制台应用程序会生成我们预期的报告:

未来的工作

创建脚手架和编写 program.cs 文件的代码应该只需要做一次。有了我们的设置,你需要做的就是将文件添加到PreDeploymentPostDeploymentDeployment文件夹中。

使用这种设置,很容易删除旧文件,但是 DbUp 不喜欢这样做。DbUp 背后的想法是,它提供所有数据库更改的历史。例如,当您想要在新的开发人员的机器上创建一个新的数据库时,您只需要运行这个命令行应用程序。它将遍历并运行所有脚本,以启动并运行数据库。删除文件最终可能会删除一个键序列,例如创建一个表、添加一个关键列或者将列从一个表移动到另一个表。有这些额外的文件并不会对性能造成太大的影响。DbUp 将看到他们已经运行,并将他们从运行列表中排除。

您可以将较旧的文件移动到一个新的文件夹中,并添加一个新的命令行参数来选择这些文件。

Octopus 部署配置

我将假设您知道如何构建一个. NET 核心应用程序并打包它。如果你没有,这里是快速 TL;博士:

  • 在项目上运行dotnet publish命令(不要忘记输出路径)。
  • 运行octo pack来打包输出路径(或者使用 Octopus Deploy build server 插件)。
  • 使用octo push命令将包推送到 Octopus Deploy(或者使用 Octopus Deploy 构建服务器插件)。

为了让您的生活更轻松,对于这个演示,我在 GitHub repo 的根目录中以 zip 文件的形式包含了示例应用程序的 1.0.0.1 版本。将软件包上传到 Octopus 内置存储库:

我赞同 Octopus Deploy 项目应该负责自我引导的理论。对于数据库部署,这意味着在部署之前确保数据库存在,并创建必要的 SQL Server 用户。

在进入流程之前,我们需要定义一些变量。因为这是一篇博文的演示,所以我在所有环境中使用相同的数据库服务器:

为了创建数据库和用户,这个过程将使用我为以前的博客文章创建的社区步骤模板。请参见文档了解如何在你的 Octopus 服务器上下载和安装这些社区步骤模板。

我在数据库工作者池中的一个工作者上运行第一步。我选择使用单个工作池,因为我使用 SQL 身份验证。我不必担心每个环境的集成安全性和独特的服务帐户。如果我在这个过程中使用集成安全性和每个环境的唯一服务帐户,还有一些额外的设置要做,但是我将在后面的文章中介绍。现在,我想尽可能简单地解释一下:

首先,我们要搭建好脚手架,创建数据库、用户,并分配用户数据库:

下一组步骤将从 DbUp 部署数据库更改。如果你还记得我之前的 Redgate 文章,这是分四步完成的:

  1. 下载软件包。
  2. 运行 Redgate 创建数据库版本。
  3. DBA 批准部署。
  4. 运行 Redgate 部署数据库版本。

我对这个过程有几个问题。也就是说,下载包步骤被设计成提取包并把它留在触手上。默认情况下,它将永远保留在那里,除非配置了保留策略。部署完成后,没有必要将提取的包放在触手上。SQL 脚本已经运行,现在它们正在占用空间。此外,该流程的步骤 2 和 4 引用了步骤 1。感觉有很多额外的工作。

如果您正在使用 Octopus Deploy 2018.8 或更高版本,好消息是我们现在可以引用来自运行脚本步骤的包。包将被下载和提取,在步骤完成后,它将删除提取的包。除了清理不需要的内容,在运行脚本步骤中使用包引用非常适合在 workers 上运行部署,假设每个步骤都是独立的。这也意味着不同的工人可以完成每一步的工作。

该流程部署部分的第一步是生成 HTML 报告,并将其作为工件上传到 Octopus Deploy。点击添加按钮,将包引用添加到步骤中:

当模式窗口出现时,选择要提取的包:

现在我们可以添加一个脚本来处理部署。跑步。NET 核心控制台应用程序与运行。NET Framework 控制台应用程序。

这完全取决于您在构建和发布应用程序时设置的开关。您可以创建一个自包含的控制台应用程序(。exe)以及所有必要的。dll,但这样做会增加包的大小。或者,您可以将其设置为仅创建一个. dll,并引用所有外部依赖项。在示例包中,我创建了一个自包含的包,但是我排除了。zip 文件中的。这样,您就不必担心运行恢复了:

# How you reference the extracted path
$packagePath = $OctopusParameters["Octopus.Action.Package[DbUpSample].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]
$reportPath = $OctopusParameters["Project.HtmlReport.Location"]

$dllToRun = "$packagePath\DbUpSample.dll"
$generatedReport = "$reportPath\UpgradeReport.html"

if ((test-path $reportPath) -eq $false){
    New-Item $reportPath -ItemType "directory"
}

# How you run this .NET core app
dotnet $dllToRun --ConnectionString="$connectionString" --PreviewReportPath="$reportPath"

New-OctopusArtifact -Path "$generatedReport" 

完成后,整个步骤如下所示:

人工干预没什么特别的。在本例中,我将其配置为仅在试运行和生产环境中运行,并让数据库管理员批准该部署:

部署步骤类似于生成增量报告步骤,除了它将部署变更,而不用担心报告的生成。这一步的 PowerShell 是:

# How you reference the extracted path
$packagePath = $OctopusParameters["Octopus.Action.Package[DbUpSample].ExtractedPath"]
$connectionString = $OctopusParameters["Project.Database.ConnectionString"]

$dllToRun = "$packagePath\DbUpSample.dll"

# How you run this .NET core app
dotnet $dllToRun --ConnectionString="$connectionString" 

现在最后的过程是:

变数就在那里。流程设置完毕。让我们部署一些数据库更改:

哎呦!忘记安装了。我的员工的网络核心:

快速跳转到脚本控制台来运行 chocolatey 安装:

这是成功的:

让我们再试试那个版本。事后看来,我本可以告诉它重试发布,但我决定创建一个新的:

这一次很成功。您可以看到由流程创建的工件。这是数据库管理员将下载并在试运行和生产中审查的内容:

如果我们看一下数据库,我们会看到项目是按预期创建的:

【T2

综合安全和工人

在这个演示中,我使用了 SQL 身份验证。然而,你们中的许多人正在使用集成安全性。为了增加一层安全性,每个环境都有自己的 Active Directory 服务帐户。这完全有道理,我推荐这种方法。

对于现在这一代员工,你如何做到这一点?这并不像它应该的那样直截了当(我们希望在 workers v2 中解决这个问题)。我将指导您完成设置它的必要步骤。

首先,我们需要为每个环境创建一个专用的工作人员池。

接下来,我们需要创建云区域部署目标。

您需要为每个环境创建一个云区域。我为这些云区域创建了一个名为DbWorker的新角色,因为我想要一种区分这些新部署目标的方法:

完成后,我有了四个新的云区域:

我将更改流程的执行位置,使其在该环境中使用DbWorker角色的目标上运行:

对流程中每个步骤重复相同的更改:

当部署一个新版本来测试时,选择Test Database Worker Region:

结论

最近对 DbUp 的修改有助于为数据库创建一个健壮的部署管道。现在 DBA(和其他人)可以在部署之前通过 Octopus Deploy 检查变更。拥有审查变更的能力应该有助于在过程中建立信任,并有助于加速采用。


数据库部署自动化系列文章:

DDD 布里斯班 2013 -八达通部署

原文:https://octopus.com/blog/ddd-brisbane-2013

这个星期六我将在 DDD 布里斯班做一个报告。

使用 TeamCity 和 Octopus Deploy 的自动化部署

敏捷软件交付围绕着尽早将工作软件呈现在人们面前,当您交付一个小的演示应用程序时,这很容易。但是,当部署涉及到在生产和生产前环境中向多个 web、应用程序和数据库服务器交付多层解决方案时,部署本身可能是一个充满风险、令人紧张且耗时的过程。

在本次演讲中,我将探讨如何简化。NET 应用程序,从源代码控制到预生产和生产环境。按下按钮(或提交源代码),我们将获取代码,编译它,运行单元测试,在测试环境中将它部署到多个服务器上,运行冒烟测试,然后将其推广到生产环境中。我将展示 TeamCity 和 Octopus Deploy 如何合作,更快地将工作软件送到用户手中。

我对这个演示感到很兴奋,因为这是我们第一次在公共场合演示 Octopus 2.0!该会议将被记录下来,当它可用时,我将在这里发布一个链接。

我们也很荣幸成为今年 DDD 布里斯班的白银赞助商。

为 SSIS - Octopus 部署调试“集合属性中不存在元素”

原文:https://octopus.com/blog/debugging-element-does-not-exist-in-the-collection-properties-for-ssis

deploying SSIS with octopus deploy

在之前的一篇文章中,我介绍了如何使用 Octopus Deploy 部署 SQL Server Integration Services(SSIS)包。在这篇文章中,我讨论了一个我在包被部署到服务器上之后遇到的问题,这个问题是由于使用较新版本的 Visual Studio 来开发 SSIS 包,但是部署到了较旧版本的 SQL Server 上。

错误

成功部署后,开发人员尝试运行 SSIS 包,但收到以下错误消息:

Failed to configure a connection property that has the following path: \Package.Connections [WWI_Source_DB].Properties[ConnectByProxy]. Element "ConnectByProxy" does not exist in collection "Properties". 

为了进行研究,我首先查看了环境变量映射:

该属性存在并映射到适当的环境变量。

作为一个实验,我让开发人员直接从 Visual Studio 发布 SSIS 包,这个包工作正常。这两种方法的主要区别在于 Visual Studio publish 没有将包参数映射到环境变量。相反,它们被设置为使用包中的默认值。在这种情况下,默认值显示为False

要进入此窗口:

  1. 右键单击该包并选择配置
  2. 将范围下拉列表更改为所有包和项目
  3. 点击连接管理器选项卡

我使用 Octopus Deploy 重新部署了 SSIS 包。部署完成后,我编辑了包并选择了编辑值并选择了False。由于同样的错误,程序包再次失败,但是将值设置为使用程序包中的默认值成功。

问题是

经过几个小时的研究,我发现开发人员使用最新版本的 Visual Studio 来开发 SSIS 包,但它是部署到 SQL Server 的旧版本。

新版本的 Visual Studio 为连接管理器引入了旧版本的 SQL Server 所不知道的附加属性。该错误试图告诉我们这一点,但不清楚。

Element "ConnectByProxy" does not exist in collection "Properties"是说ConnectByProxy不存在于Properties的服务器集合中,而不是包本身。选择使用包中的默认值也具有误导性。

从 UI 来看,这个选择显示的值是False,然而,实际值是null(在读取包的 XML 时发现的)。

解决方案

有两种解决方案:

  • 手动将封装参数更新为使用封装设置中的值。
  • 使用 PowerShell 为您更新软件包参数。

手动方法

上面的提示描述了如何导航到包参数并手动更新它们。然而,这是低效的,因为它需要在每次部署后重复。

PowerShell

更好的解决方案是将“运行脚本”任务添加到部署过程中,以便为您执行编辑。

下面的脚本应该可以帮助您完成大部分工作:

# define functions
Function Import-Assemblies
{
    # display action
    Write-Host "Importing assemblies..."

    # get folder we're executing in
    $WorkingFolder = Split-Path $script:MyInvocation.MyCommand.Path
    Write-Host "Execution folder: $WorkingFolder"

    # Load the IntegrationServices Assembly
    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices") | Out-Null # Out-Null suppresses a message that would normally be displayed saying it loaded out of GAC
}

Function Get-Catalog
{
    # define parameters
    Param ($CatalogName, $IntegrationServices)

    # define working variables
    $Catalog = $null

    # check to see if there are any catalogs
    if($integrationServices.Catalogs.Count -gt 0 -and $integrationServices.Catalogs[$CatalogName])
    {
        # get reference to catalog
        $Catalog = $integrationServices.Catalogs[$CatalogName]
    }
    else
    {
        Write-Error  "Catalog $CataLogName does not exist or the Tentacle account does not have access to it."

        # throw error
        throw
    }

    # return the catalog
    return $Catalog
}

Function Get-Folder
{
    # parameters
    Param($FolderName, $Catalog)

    # try to get reference to folder
    $Folder = $Catalog.Folders[$FolderName]

    # check to see if $Folder has a value
    if(!$Folder)
    {
        Write-Error "Folder not found."
        throw
    }

    # return the folder reference
    return $Folder
}

Function Clear-Parameter
{
    # define parameters
    Param($ParameterName)

    # Create a connection to the server
    $sqlConnectionString = "Data Source=$SQLServer;Initial Catalog=master;Integrated Security=SSPI;"
    $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString
    $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices"

    # create integration services object
    $integrationServices = New-Object "$ISNamespace.IntegrationServices" $sqlConnection

    try
    {
        # get catalog reference
        $Catalog = Get-Catalog -CatalogName $CataLogName -IntegrationServices $integrationServices
        $Folder = Get-Folder -FolderName $FolderName -Catalog $Catalog

        # get reference to project
        $Project = $Folder.Projects[$ProjectName]

        # find specific parameter
        $Parameter = $Project.Parameters | Where-Object {$_.Name -eq $ParameterName}

        # set parameter to design time default value
        Write-Host "Clearing parameter $ParameterName"
        $Parameter.Clear()

        # set value
        $Project.Alter()
    }
    finally
    {
        # close connection
        $sqlConnection.Close()
    }
}

# get reference to assemblies needed
Import-Assemblies

$SQLServer = "#{Project.Database.Server.Name}"
$CataLogName = "SSISDB"
$FolderName = "#{Project.SSISDB.Folder.Name}"
$ProjectName = "#{Project.SSISDB.Project.Name}"

# fix the problem
Clear-Parameter -ParameterName "CM.WWI_Source_DB.ConnectByProxy" 

有了这个脚本,您可以调用Clear-Parameter来获取任何需要设置为Use default value from package on 的参数。

结论

在自动化 SSIS 部署时,我发现新版 Visual Studio 向连接管理器引入了旧版 SQL Server 不知道的附加属性。我希望这篇博客能让你避免遇到同样的问题。

愉快的部署!

解构 Kubernetes - Octopus 部署中的蓝/绿部署

原文:https://octopus.com/blog/deconstructing-blue-green-deployments

Deconstructing blue/green deployments in Kubernetes

除了 Kubernetes 本身支持的重新创建和滚动部署策略之外,Octopus 还提供了执行蓝/绿部署的能力。此复选框选项允许以蓝/绿方式部署单个 Kubernetes 部署,完成服务切换和旧资源清理。

但是有时这种复选框方法不够灵活。如果您想要暂停到新部署的切换,以允许手动测试、编排多个部署(例如,前端和后端应用程序),或者使用功能分支,那么您需要为自己重新创建蓝/绿部署流程。

在这篇博客文章和相关的截屏中,我将向您展示如何重新创建一个蓝/绿部署,并部署一个模拟特性分支作为该过程的演示。

截屏

https://www.youtube.com/embed/BiaDsKAPSdU

VIDEO

创建初始部署

对于这个演示,我们将把 httpd docker 映像部署到 Kubernetes 中。这给了我们一个 web 服务器,我们可以将浏览器和命令行工具指向它,我们将利用像2.4.46-alpine这样的标签来模拟特性分支。

我们首先通过部署 Kubernetes 容器步骤部署一个新的 Kubernetes 部署资源。

部署资源必须有一个惟一的名称,我们通过将字符串-#{Octopus.Deployment.Id | ToLower}附加到资源名称上来创建这个名称。对于每个部署来说,Octopus.Deployment.Id变量是唯一的,因此我们可以确定每个新的部署都会创建一个新的 Kubernetes 资源。

我们还定义了一个名为FeatureBranch的标签,它被设置为我们将在下一节中创建的名为PackagePreRelease的变量的值。

下面的 YAML 可以粘贴到部署 Kubernetes 容器步骤的编辑 YAML 部分来配置资源:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: 'httpd-#{Octopus.Deployment.Id | ToLower}'
  labels:
    FeatureBranch: '#{PackagePreRelease}'
spec:
  selector:
    matchLabels:
      octopusexport: OctopusExport
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        FeatureBranch: '#{PackagePreRelease}'
        octopusexport: OctopusExport
    spec:
      containers:
        - name: httpd
          image: index.docker.io/httpd
          ports:
            - name: web
              containerPort: 80 

定义变量

我们需要创建两个变量来获取特性分支的名称。

第一个变量叫做PackagePreRelease,值设置为#{Octopus.Action[Deploy HTTPD].Package[httpd].PackageVersion | VersionPreReleasePrefix}。这个模板字符串在称为Deploy HTTPD的步骤中从称为httpd的容器中提取版本(或图像标签),并通过VersionPreReleasePrefix过滤器(在 Octopus 2020.5 中可用)提取预发布字符串。

这意味着对于主线部署来说,PackagePreRelease变量将为空,而对于我们在本演示中称之为特性分支部署来说,变量将被设置为alpine

第二个变量叫做ServiceSuffix,值设置为#{if PackagePreRelease}-#{PackagePreRelease}#{/if}。如果PackagePreRelease不为空,该模板在PackagePreRelease的值前添加一个破折号。否则ServiceSuffix为空字符串。

这意味着对于主线部署来说,ServiceSuffix变量将为空,而对于我们在本演示中称之为特性分支部署来说,变量将被设置为-alpine

为绿色部署创建临时服务

为了对新部署的 pod(我们称之为蓝/绿部署的绿色部分)做任何有用的事情,我们需要公开服务背后的 pod。

对于这个演示,我们只需要在 Kubernetes 集群内部公开 pod 来执行我们的测试,因此我们创建了一个集群 IP 服务。

服务选择器基于Octopus.Deployment.Id标签匹配 pod,该标签匹配当前部署期间创建的任何 pod。

下面的 YAML 可以粘贴到部署 Kubernetes 服务资源步骤的编辑 YAML 部分来配置资源:

apiVersion: v1
kind: Service
metadata:
  name: 'httpdservice-green#{ServiceSuffix}'
spec:
  type: ClusterIP
  ports:
    - name: web
      port: 80
      nodePort: ''
      targetPort: ''
      protocol: TCP
  selector:
    Octopus.Deployment.Id: '#{Octopus.Deployment.Id}' 

执行手动运行状况检查

您需要创建自己的蓝/绿部署的一个环境是在 Kubernetes 公开的活动探测器之外运行健康检查。这可能是由人工完成的手动检查,或者,正如我们将在这里演示的,是使用外部工具的自动化测试。在我们的例子中,我们将使用curl来检查我们部署的健康状况。

这是在一个运行 kubectl CLI 脚本步骤中完成的,在这里我们调用kubectl来运行集群中安装了curl的容器以完成检查:

echo "Testing http://httpdservice-green#{ServiceSuffix}"
kubectl run --attach=true --restart=Never test-#{Octopus.Deployment.Id | ToLower} --image=#{Octopus.Action.Package[curl].PackageId}:#{Octopus.Action.Package[curl].PackageVersion} -- --fail http://httpdservice-green#{ServiceSuffix}
exit $? 

映像名称是使用由附加包引用公开的变量构建的:

创建持久服务

在健康检查通过之后,我们需要创建(如果这是第一次部署)或者重定向外部客户机用来访问我们正在部署的应用程序的持久服务。重定向这个持久服务上的流量是我们将流量从旧的蓝色部署切换到新的绿色部署的方式。

该服务与我们之前创建的临时服务非常相似,只是它是一个具有公共 IP 地址的负载平衡器,我们不会在部署结束时删除它:

apiVersion: v1
kind: Service
metadata:
  name: 'httpdservice#{ServiceSuffix}'
spec:
  type: LoadBalancer
  ports:
    - name: web
      port: 80
      nodePort: ''
      targetPort: ''
      protocol: TCP
  selector:
    Octopus.Deployment.Id: '#{Octopus.Deployment.Id}' 

清理资源

在流量被重定向到我们的新应用程序后,我们可以清理旧的资源。这是通过另一个运行 kubectl CLI 脚本步骤来执行的:

kubectl delete service httpdservice-green#{ServiceSuffix}
kubectl delete deployment -l Octopus.Project.Id=#{Octopus.Project.Id | ToLower},Octopus.Environment.Id=#{Octopus.Environment.Id | ToLower},Octopus.Deployment.Tenant.Id=#{unless Octopus.Deployment.Tenant.Id}untenanted#{/unless}#{if Octopus.Deployment.Tenant.Id}#{Octopus.Deployment.Tenant.Id | ToLower}#{/if},Octopus.Deployment.Id!=#{Octopus.Deployment.Id | ToLower},FeatureBranch=#{PackagePreRelease} 

请注意,我们基于旧资源的Octopus.Deployment.Id标签与当前部署的 ID 不匹配这一事实来匹配旧资源。此外,因为我们在FeatureBranch标签上匹配,主线和特性分支部署可以共存,不需要一个清理另一个。

结论

此处提供的示例是实施蓝/绿部署所需的最低要求,但是通过向流程中添加新步骤,您可以按照自己的意愿自定义工作流。可以添加手动干预步骤,以允许 QA 人员验证新的部署,可以执行更复杂的自动化测试,或者可以将多个资源作为一个组进行部署和切换。

愉快的部署!

定义 Tomcat 上下文路径- Octopus Deploy

原文:https://octopus.com/blog/defining-tomcat-context-paths

web 应用程序的上下文路径定义了最终用户访问应用程序的 URL。像myapp这样简单的上下文路径意味着可以从 http://localhost:8080/myapp 这样的 URL 访问 web 应用程序。像myapp/v1这样的嵌套上下文路径意味着可以从 http://localhost:8080/myapp/v1 这样的 URL 访问 web 应用程序。

Tomcat 提供了许多方法来定义 web 应用程序的上下文路径,尽管配置并不像您预期的那样简单。

在这篇博文中,我们将探索 Tomcat 为部署 web 应用程序和定义它们的上下文路径提供的选项。

如果您正在寻求自动化您的 Java 部署,请点击此处开始免费的 Octopus 试用。

<Host>配置元素

Tomcat 中用于部署应用程序的许多选项都是在config/server.xml文件的<Host>元素中定义的。

Tomcat 9.01 中默认的<Host>元素如下所示:

<Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true"> 

下面我们将探讨这些属性如何影响 Tomcat 中的部署。

展开部署与战争包

部署 Java web 应用程序有两种方法。

第一种方法是部署一个 WAR 文件。WAR 文件只是一个 ZIP 存档文件,其目录结构可以被像 Tomcat 这样的 Java 应用服务器识别。WAR 文件很方便,因为它们是易于复制的单个包,并且 WAR 文件的内容被压缩,使其成为一个非常紧凑的包。

第二种方法是部署组成 web 应用程序的所有单个文件。这被称为爆炸式部署,或爆炸式战争。这种部署在开发过程中非常有用,因为像 HTML 页面和 CSS 文件这样的文件可以在应用程序动态部署和重新加载时进行编辑。

默认情况下,当您将 WAR 文件部署到 Tomcat 时,它将被提取到展开的部署中。在下面的截图中,您可以看到部署名为demo.war的文件的最终结果是名为demo的目录,其中提取了demo.war档案的上下文:

Tomcat Exploded Deployment

可以通过将<Host>元素上的unpackWARs属性设置为false来禁用这种行为,这将阻止 WAR 文件在部署过程中被解包。

webapps目录

webapps目录是 Tomcat 中部署的应用程序所在的位置。

webapps目录是默认的部署位置,但是这可以用<Host>元素上的appBase属性来配置。

如果 Tomcat 设置为自动部署应用程序(默认情况下是这样设置的),那么任何复制到webapps文件夹中的 WAR 文件或展开的部署都会在 Tomcat 运行时自动部署。

通过将<Host>元素上的autoDeploy属性设置为false,可以禁用应用程序的自动部署。在这种情况下,应用程序将在启动时部署。

反过来,可以通过将<Host>元素上的deployOnStartup属性设置为false来禁用启动时的应用程序部署。

如果autoDeploydeployOnStartup都为假,您可以通过在conf/server.xml文件的<Host>元素中手动添加一个<Context>元素来部署应用程序。参见“在server.xml文件中定义上下文”一节中的示例。

在(展开的)WAR 文件名中嵌入路径

当从webapps目录部署应用程序时,它将在与 WAR 文件名或展开的部署复制到的webapps下的目录名相匹配的上下文路径下可用。

例如,如果您部署一个名为demo.war的 WAR 文件,它将在demo上下文中可用。同样,如果您将一个爆炸的战争部署到webapps/demo,它也将在demo的上下文中可用。

Tomcat 支持嵌套的上下文路径。这些被嵌入到 WAR 文件名的单个散列字符之后。例如,如果您部署一个名为demo#v1.war的 WAR 文件,它将在demo/v1上下文中可用。上下文可以有多个层次,所以如果您部署一个名为demo#v1#myfeature.war的 WAR 文件,它将在demo/v1/myfeature上下文中可用。

同样的模式也适用于存放展开部署的目录。例如,如果您将展开的 war 部署到webapps/demo#v1,它将在demo/v1上下文中可用。

server.xml文件定义上下文路径

通过在conf/server.xml文件的<Host>元素中添加一个<Context>元素,可以配置 WAR 文件或展开的部署目录。这里有一个例子:

<Host name="localhost"  appBase="webapps"
      unpackWARs="false" autoDeploy="false" deployOnStartup="false">
      <Context path="/mydemo/version1" docBase="demo#v1.war"/>
      ...
</Host> 

docBase属性是 WAR 文件或展开部署目录的路径。虽然可以使用绝对路径,但它是相对于webapps目录的。

path属性是我们最感兴趣的,因为它定义了应用程序的上下文路径。在这种情况下,我们已经在/mydemo/version1上下文中公开了 web 应用程序。

只有当战争或展开部署目录不在webapps目录下,或者<Host>元素上的autoDeploydeployOnStartup属性为false时,才能定义path属性。

在这个例子中,我们引用了文件webapps\demo#v1.war,这意味着<Host>元素上的autoDeploydeployOnStartup属性必须是false

引用文档中的话:

如果不遵守这一规则,很可能会导致双重部署。

server.xml文件中定义<Context>元素不是最佳实践。该信息应在保存在conf/Catalina/localhost/下的文件中定义。更多信息参见“令人困惑的context.xml文件案例”。

context.xml档案的疑案

到目前为止,我们已经看到了两种定义上下文路径的方法:

  1. 来自 WAR 文件的名称或展开的部署目录。
  2. 来自server.xml文件中<Context>元素的path属性(注意被部署的应用程序不在webapps目录下,或者如果在webapps目录下,则<Host>元素的autoDeploydeployOnStartup属性为false)。

Tomcat 还允许我们在 web 应用程序中包含一个名为META-INF/context.xml的文件,或者在 Tomcat 目录下创建文件conf/Catalina/localhost/<context>.xml。这些文件包含与server.xml文件中的<Host>元素相同的<Context>元素。

这自然会让您认为可以在这些 XML 文件中的<Context>元素上定义path属性,Tomcat 会将应用程序部署到定义的上下文路径中。

然而,事实并非如此。

例如,让我们假设下面的 XML 被保存为名为demo#v1.war的 WAR 文件中的META-INF/context.xml文件:

<Context path="/mydemo/version1"/> 

当 Tomcat 将demo#v1.war文件放在webapps文件夹中并进行部署时,它将在demo/v1上下文中可用。path属性被忽略。

同样,如果将相同的 XML 上下文保存到conf/Catalina/localhost/demo#v1.xml文件中,应用程序仍然可以在demo/v1上下文中使用。

这有点违背直觉,但在文档中有清楚的说明:

只有在 server.xml 中静态定义上下文时,才能使用[path]属性。xml 上下文文件或文档库。

这意味着定义上下文路径的是 WAR 文件或展开的部署目录的名称,或者是conf/Catalina/localhost下的 XML 文件的名称。

事实上,当为了定义从webapps目录部署的应用程序的上下文而在conf/Catalina/localhost目录下创建 XML 文件时,XML 文件需要与 WAR 文件或展开的部署目录同名。

例如,如果您有一个名为webapps\demo#v1.war的文件,那么相应的 XML 文件必须名为conf/Catalina/localhost/demo#v1.xml。这些文件需要有匹配的文件名,文件名定义了上下文。

当在webapps目录之外为部署配置上下文时,必须定义docBase属性。该属性指向 WAR 文件或展开的部署。

在这种情况下,定义上下文的仍然是 XML 文件的名称。例如,如果下面的 XML 保存到conf/Catalina/localhost/application#version1.xml,来自/apps/myapp#v1.war的应用程序将在上下文application/version1下可用。在这种情况下,WAR 文件名不用于生成上下文。

<Context docBase="/apps/myapp#v1.war"/> 

通过管理应用程序上传

最后,通过 manager REST API 上传应用程序时,可以定义应用程序的上下文路径。这可以通过对http://localhost:8080/manager/text/deploy?path=/fooPUT请求来完成,其中请求数据是要部署的 WAR 文件,而path查询参数是所需的上下文路径。

/manager/html的请求需要来自manager-gui组的用户的凭证。您可以通过网络浏览器访问此 URL 来查看管理器应用程序。

/manager/text的请求需要来自manager-script组的用户的凭证。这个 URL 被认为是管理器 API。

仅仅因为您有一个可以通过浏览器访问管理器应用程序的用户,并不一定意味着该用户可以与 API 进行交互。事实上,单个用户成为manager-guimanager-script组的一部分被认为是一种不好的做法。引用文件中的话:

建议永远不要向拥有 manager-gui 角色的用户授予 manager-script 或 manager-jmx 角色。

该文件上传将导致一个部署,其上下文路径嵌入在webapps文件夹内的文件名中。因此,实际上通过管理器应用程序上传文件并不是定义应用程序上下文的新方法,它只是确保正确命名的 web 应用程序被复制到webapps目录中的一种便捷方式。

结论

该表总结了各种上下文路径,这些路径将被分配给从webapps部署的、在server.xml文件中引用的或从conf/Catalina/localhost/下的文件中引用的 web 应用程序。

配置 语境
部署在webapps/app.war下的 WAR 文件 app
webapps/app下的分解展开 app
部署在webapps/app#v1.war下的 WAR 文件 app/v1
webapps/app#v1下的分解展开 app/v1
部署在webapps/app#v1#feature.war下的 WAR 文件 app/v1/feature
webapps/app#v1#feature下的分解展开 app/v1/feature
<Context path="/mydemo/version1" docBase="/apps/demo#v1.war"/>conf/server.xml /mydemo/version1
<Context path="path/is/ignored" docBase="/apps/myapp#v1.war"/>conf/Catalina/localhost/mydemo#version1.xml中(即/apps/myapp#v1.war的配置) /mydemo/version1
conf/Catalina/localhost/mydemo#version1.xml中的<Context path="/path/is/ignored"/>(即webapps/mydemo#version1.war的配置) /mydemo/version1

如果您对将 Java 应用程序自动部署到 Tomcat 感兴趣,开始免费试用 Octopus Deploy ,看看我们的文档

了解更多信息

在多租户环境中定义变量——Octopus 部署

原文:https://octopus.com/blog/defining-variable-templates

这篇文章是我们 Octopus 3.4 博客系列的一部分。在我们的博客或我们的推特上关注它。

Octopus Deploy 3.4 已经发货!阅读博文今天就下载


Octopus 中的多租户部署为 Octopus 体验增加了一个新的维度。它能够以一种安全和可重复的方式向多个客户部署版本,这在以前是很难做到的。

在设计多租户部署时,我们必须考虑的一个重要特性是如何为租户处理定制变量。如果你想象一个可以为多个客户定制的 SaaS(软件即服务)web 应用程序,那么需要为每个客户或租户指定许多变量。有一些常见的特定于客户的详细信息需要更改,如网站名称、标题/页眉、图像/徽标 URL 和联系信息,以及服务器名称和数据库连接设置等技术细节。我们对此的解决方案是Variable Templates。变量模板允许您指定将项目成功部署到租户所需的变量。它们可以在一个项目或一个库变量集上定义,我们根据它们定义的位置以稍微不同的方式解释它们。

项目变量模板

在项目中定义变量模板意味着对于租户所连接的每个环境,变量可以有不同的值。这是管理技术设置(如数据库连接字符串或其他特定于环境的细节)的好方法。特定于租户的项目变量模板值在“项目变量”选项卡下的“租户变量”页面上进行管理。

库变量模板

在库中定义变量模板意味着无论项目和环境如何,该租户的变量都将有一个常量值。特定于租户的库变量模板值在“通用变量”选项卡下的租户页面上进行管理。

缺少变量

如果租户缺少任何必需的变量,我们也会向您发出警告,并帮助您快速轻松地修复它们。

项目变量和库变量集

值得一提的是,虽然变量模板使您能够管理所需的特定于租户的变量,但仍然可以在项目和库变量集中定义“普通”变量,然后适当地确定范围。Octopus 3.4 还引入了将变量作用于租户标记的能力。

租户可变快照

最后,需要注意的是,特定于租户的变量不会被拍摄快照。当您在 Octopus 中创建一个版本时,我们会对部署过程和项目变量的当前状态进行快照,但是我们不会对租户变量进行快照。这使您可以随时添加新的租户,并为其部署现有版本。此外,这意味着您可以对租户变量进行更改,并立即部署它们。这消除了在生产之前创建新版本并在众多环境中部署它的需要。


要了解更多信息,我强烈推荐阅读我们的多租户部署指南,并特别关注使用租户特定变量页面。

为什么有这么多提前期的定义?-章鱼部署

原文:https://octopus.com/blog/definitions-of-lead-time

当有人提到软件交付中的交付周期时,通常不清楚他们是指精益软件开发中的交付周期定义,还是 DevOps 中的交付周期定义,或者其他完全不同的定义。

在这篇文章中,我将探讨为什么有这么多提前期的定义,以及如何使用它们。

提前期定义

DevOps 对变更提前期的定义是从开发人员将代码提交到版本控制到有人将变更部署到生产环境之间的时间。这个定义比精益定义覆盖了软件交付过程的一小部分。

Mary 和 Tom Poppendieck 在精益制造运动的基础上创建了精益软件开发,他们测量了从你发现一个需求到某人满足该需求的前置时间。

基于丰田生产系统的精益运动将交付周期定义为客户下订单和收到汽车之间的时间。

交付时间是客户的衡量标准

所有这些交付周期都代表了一种客户衡量。但是它们不同,因为客户不同。

  • 丰田从汽车购买者的角度来衡量这个系统
  • Poppendiecks 测量用户眼中的软件开发系统
  • DevOps 从作为客户的开发人员的角度来衡量部署管道
研制周期 顾客 开始 结束
丰田生产系统 汽车购买者 命令 交付
精益软件开发 用户 要求 工作软件
DevOps 开发者 代码提交 生产部署

成功测量交付周期的关键是展示客户如何看待消耗的时间。

如果你经营一家咖啡店,你可能会计算顾客下订单和把咖啡递给他们之间的时间。你可能会认为 2 分钟的提前期很好,因为你的竞争对手从下单到完成订单需要 3 分钟。

然而,你的竞争对手使用的是全系统提前期,从客户加入队列开始。他们增加了一名咖啡师,将排队时间从 15 分钟减少到 7 分钟。他们的顾客 10 分钟后就能拿到咖啡,但你的顾客要等 17 分钟(而且你正在失去那些看到排队就离开的顾客)。

除非你的交付周期代表了客户对系统的完整看法,否则你很可能优化了错误的东西。

周期时间

当您测量系统的一部分时,您正在收集一个周期时间。在汽车行业,跟踪一辆汽车在生产线上移动需要多长时间是很有用的。在软件交付中,收集从工作项目开始到结束的周期时间是很常见的。这表明了软件交付的性能,而没有工作开始前可能发生的变化的等待时间。

如咖啡店示例所示,您的客户不关心周期时间。虽然您可以使用周期时间来度量系统的不同部分,以确定限制工作流程的瓶颈,但是您应该始终记住整个系统。

在软件交付中,通常会发现很大一部分运行时间是由于工作在队列中等待。例如,一个需要几天才能交付的需求可能会积压几个月,或者一个拉取请求可能要等待几个小时甚至几天才能得到批准。您可以通过细分您的系统并测量每个部分来识别这些延迟。

An example software development process showing different lead and cycle time definitions

提前期衡量的是系统的实际产出,但是周期时间可以帮助你找到系统的约束。

所有测量都是有用的

交付周期很有价值,因为它代表了客户的认知。识别您的客户并跟踪他们看到的交付周期,确保您做出的任何改进都会影响他们的体验。

如果你的改进没有减少交付时间,你就优化了系统的错误部分。在某些情况下,减少系统错误部分的时间甚至会增加总提前期,如果它增加了约束条件的额外压力的话。

一个约束是限制整个系统流动速度的瓶颈。解决约束会导致瓶颈移动,因此识别和解决约束的过程是连续的。

软件交付对大多数组织来说是一个限制,因为技术是一个关键的竞争优势。然而,这不是一个足够细粒度的识别来进行改进。您需要查看您的软件交付价值流,并在它们增加系统中工作流程的地方进行改进。

Eli Goldratt 创立的约束理论告诉我们,在一个系统中总是至少有一个约束。在约束之外的任何地方进行优化都无法提高整个系统的性能。

周期时间和其他部分系统计时器可以帮助您找出优化可能减少总交付时间的地方,因此您可以使用周期时间和交付时间来评估改进。

常见的软件交付限制

软件交付中有一些常见的约束:

  • 大批量工作
  • 提取请求批准队列
  • 分支过多的,或分支存在时间过长的
  • 人工测试
  • 政策约束,如不必要的批准
  • 功能孤岛之间的移交(例如开发、测试和操作)

这些约束中的一些反映在连续交付提交周期中,该周期具有以下推荐的时间:

  • 每 15 分钟提交一次
  • 初始构建和测试反馈在 5 分钟内完成
  • 10 分钟后修复任何故障或恢复更改

【T2 The Continuous Delivery commit cycle

结论

交付周期的不同定义反映了客户对同一流程各部分的不同理解。您可以根据需要使用任意多的提前期和周期时间度量来查找和解决系统中的约束。您可以长期跟踪交付周期,并将周期时间临时用作特定改进练习的一部分。

当你改进或优化时,提前期可以帮助你了解你是否对整个系统产生了积极的影响。

愉快的部署!

Kubernetes 资源的批量删除- Octopus 部署

原文:https://octopus.com/blog/deleting-kubernetes-resources

Kubernetes 使一次创建许多资源变得容易,用kubectl apply -f filename.yaml命令在一个复合 YAML 文件中创建所有资源。但是,如何删除多个资源而不单独指定它们呢?

在本文中,我将向您展示如何批量删除 Kubernetes 资源。

Kubernetes 部署示例

让我们来看一个描述 Kubernetes 部署和服务的典型 YAML 文件:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx-svc
  labels:
    app: nginx
spec:
  type: LoadBalancer
  ports:
  - port: 80
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80 

当这个 YAML 保存到一个名为nginx.yaml的文件中时,使用以下命令创建资源:

kubectl apply -f nginx.yaml 

然后,您可以查看使用以下命令创建的新资源:

kubectl get pods
kubectl get deployments
kubectl get services 

您会看到创建了三个 pod、一个部署和一个服务。pods 不是在 YAML 文件中直接定义的,而是由部署创建的,由于replicas属性被设置为3,所以创建了三个 pods。

从文件中删除资源

删除这些资源最简单的方法是使用delete命令并传递最初创建资源时使用的相同文件:

kubectl delete -f nginx.yaml 

如果您重新运行上面的kubectl get命令,您会看到 pod、部署和服务被删除。因为窗格由部署管理,所以删除部署也会删除窗格。

手动删除资源

为了手动删除特定类型的资源,kubectl delete命令接受一个定义要删除的资源类型的--all参数。例如,以下命令删除所有服务:

kubectl delete --all services 

您可以使用以下命令确认服务已删除:

kubectl get services 

此命令删除所有窗格:

kubectl delete --all pods 

该命令的输出如下所示:

pod "my-nginx-6595874d85-88jlr" deleted
pod "my-nginx-6595874d85-9w52c" deleted
pod "my-nginx-6595874d85-dpzds" deleted 

然而,当您确认 pod 被删除时,有趣的事情发生了。运行以下命令以列出任何窗格:

kubectl get pods 

请注意,仍然有 3 个 pod,输出如下所示:

NAME                        READY   STATUS    RESTARTS   AGE
my-nginx-6595874d85-2j4g8   1/1     Running   0          76s
my-nginx-6595874d85-4vrfb   1/1     Running   0          76s
my-nginx-6595874d85-4wj9p   1/1     Running   0          76s 

如果仔细观察,kubectl get pods命令显示的 pod 名称与kubectl delete --all pods命令返回的名称不同。这是因为 pod 由部署管理,当部署发现它管理的 pod 已被删除时,它会重新创建新的 pod 来完成其replica计数。

删除由部署管理的 pod 实质上是重新创建它们,如果您想要强制 pod 重新启动,这是很有用的。但是永久删除 pod 的唯一方法是删除它们的父部署。这是通过以下命令完成的:

kubectl delete --all deployments 

删除部署后,没有部署或窗格。

删除命名空间

命名空间是对相关资源进行分组的一种便捷方式。使用以下命令创建一个名为foo的新名称空间:

kubectl create namespace foo 

然后使用以下命令在新的名称空间中创建 NGINX 资源:

kubectl apply -f nginx.yaml -n foo 

使用命令列出资源:

kubectl get pods -n foo
kubectl get deployments -n foo
kubectl get services -n foo 

然后使用以下命令删除该命名空间:

kubectl delete namespace foo 

这会导致命名空间以及其中包含的所有资源被删除。

速记“所有”资源

当调用kubectl来引用 Kubernetes 资源类型的公共子集时,可以为资源类型传递all。因此,以下命令将删除服务、部署和 pod:

kubectl delete all --all 

all类型包括:

  • 豆荚
  • 服务
  • 达蒙塞特
  • 部署
  • 复制集
  • 状态集
  • 工作
  • 克朗乔布斯

删除与标签匹配的资源

标签用于丰富资源,元数据通常描述资源的用途、环境和版本等内容。您可以根据这些标签选择资源并删除它们。这使您可以有选择地删除资源组。

以下命令删除标签名为app设置为nginx的部署:

kubectl delete deployments -l app=nginx 

同样,您可以删除带有相同标签的服务:

kubectl delete service -l app=nginx 

试运行

批量删除资源很方便,但是很危险。幸运的是,kubectl--dry-run参数,可以让您看到将要删除的资源,但不会实际删除它们。以下命令预览与all资源类型匹配的资源:

kubectl delete all --all --dry-run 

结论

使用kubectl批量删除资源很容易,在这篇文章中,你学习了如何删除资源:

  • 在 YAML 文件中定义
  • 匹配单一资源类型
  • 分组在all资源类型中
  • 包含在命名空间中
  • 带有匹配的标签

您还学习了如何使用--dry-run参数来预览任何将被删除的资源。

愉快的部署!

通过命令行删除发布- Octopus Deploy

原文:https://octopus.com/blog/deleting-releases-via-command-line

我们的待办事项中有一项是能够为项目设置一个保留策略,它可以自动清理旧的部署应用程序、缓存的 NuGet 包,以及来自 Octopus UI 的发布/部署。

这一项还没有完成,但与此同时,如果你需要删除发布/部署,你现在可以使用最新版本的 Octo.exe 和运行 1.0.31 或更高版本的 T2 章鱼服务器来完成。

在命令行中,语法是:

octo delete-releases --project=MyProject --minversion=1.0.0 --maxversion=2.0.0 --apikey=ABCDEF... --server=http://<your-octopus> 

版本号包含在内,可以部分指定,例如2.0.02.0.1982.12981比较版本号时使用 SemVer 规则。

正如我所说的,这还不足以成为一个完整的保留策略特性,但是如果您由于构建/部署脚本中的循环依赖而意外地创建了几百个版本,这可能是有用的:)

探索一个活的 Octopus Deploy 服务器

原文:https://octopus.com/blog/demo-server

如果不能与 Octopus Deploy 这样的产品进行交互,就很难了解它是如何工作的。我们有十个简短的视频展示如何设置,但这与点击鼠标探索真正的服务器不太一样。

为了提供帮助,我们创建了一个现场演示服务器:

查看直播演示服务器

*演示服务器运行最新版本的 Octopus,有两个项目部署到七个触角上。您可以作为来宾登录来查看服务器和浏览。我们已经将访客账户设置为只读的 Octopus 管理员;您可以查看任何内容,但实际上您无法更改系统。

Octopus dashboard

还有一个演示团队城市服务器,它被配置为编译代码并部署到 Octopus。每小时它触发一次构建和部署,每周它向验收环境发布一次。

演示项目突出了几个不同的特性,包括滚动 web 应用部署库变量集配置 Windows 服务。我希望你会发现它是一个有用的参考服务器!*

如何:使用 Octopus Deploy 部署 SQL Server 数据库

原文:https://octopus.com/blog/howto/deploy-a-sql-database

在尝试自动化部署时,数据库可能是最棘手的组件之一。在这篇文章中,我将向您介绍一种处理自动化 SQL Server 数据库部署的方法。这不是唯一的方法,但这是一种多年来对我很有效的方法。

更新:你可能也想看看一个叫做 ReadyRoll 的第三方工具,它可以为 Octopus Deploy 制作软件包

目标

也许您有一个现有的数据库,并且希望从现在开始自动部署对数据库的更改。或者,对于一个新的应用程序,这可能是一个全新的数据库,您希望从一开始就做正确的事情。无论是哪种情况,我们都应该努力实现一些目标:

  1. 我们希望它简单
  2. 我们希望它是可重复的
  3. 我们希望对变更的开发、QA 和生产部署使用相同的流程
  4. 我们不想变得过于依赖 Octopus Deploy 或其他工具

第四点可能听起来出乎我的意料,但实际上,Octopus Deploy 的目标之一是你创建的包应该是独立有用的,不需要依赖 Octopus。这就是为什么 Octopus 使用 web.config 文件、appSettings、XML 转换和 PowerShell 等标准约定。在最坏的情况下,您可以将 NuGet 包重命名为. ZIP,手动提取文件,手动调用脚本,这样您就部署好了。章鱼的存在只是为了让它更容易。

控制数据库:创建脚本

几年前,我写了一篇关于我如何实现数据库迁移的哲学的博客,我将在这里继续。在我们开始考虑自动化部署之前,我们需要控制数据库。我们将使用变更脚本方法来管理部署。

例如,假设 Sally 想要向表中添加一列。为此,她可以使用 SQL Management Studio 中的设计器来添加列并生成一个脚本(Management Studio 中有一个按钮可以完成这项工作)。或者她可能会使用类似于 Red Gate SQL Compare 的工具来帮助创建脚本。或者,她可能对 T-SQL 非常了解,足以手写它。

无论用来创建的过程是什么,脚本都是无关紧要的。重要的是,将会有剧本。该脚本描述了如何将数据库模式(和数据)从一种状态转换到另一种状态。这个脚本应该放在源代码控制中。

例如:

alter table dbo.Customer 
add PhoneNumber varchar(20) 

这将作为一个文件保存在磁盘上,命名为类似于Script0091 - Add phone number to customer.sql的东西。注意名字中的数字;这是因为迁移脚本总是需要以特定的顺序运行(如果一个脚本添加了一个列,而下一个脚本对其进行了重命名,那么不按顺序运行它们是没有意义的)。

这个想法是脚本描述了数据库如何从一个版本转换到另一个版本。没有人能在不编写脚本并将其签入源代码控制的情况下更改数据库——即使是 DBA 也不行!😃

通过这样做,您已经成功地拥有了一个更易于维护的数据库:

  1. 它在源代码控制中,所以您可以更好地查看数据库的历史
  2. 脚本是连续的,您可以使用任何旧的数据库,通过运行尚未运行的脚本,轻松地将其升级到最新版本
  3. 您在 QA 中运行的脚本将与您在生产中运行的脚本完全相同

自动化执行

下一步是自动运行这些脚本。这里有几个不同的选项,同样,没有一个是 Octopus Deploy 特定的——您应该能够在不依赖 Octopus Deploy 的情况下处理数据库部署。

一种选择是让 PowerShell 脚本获取脚本,对它们进行排序,并将它们传递给 SQLCMD 实用程序来执行。

另一个选择是使用开源工具,如 DbUp塔伦蒂诺,甚至是商业工具,如 SSW SQL Deploy 。在这个例子中我将使用 DbUp,因为我认为它是最简单的工具。

使用 DbUp 运行脚本

DbUp 是一个类库,您可以从控制台应用程序调用它,所以我将在 Visual Studio 中创建一个控制台应用程序:

Create console app

接下来,我将添加 DbUp NuGet 包:

Install DbUp

接下来,我将添加一个脚本文件夹,并添加我的 SQL 脚本。在每个脚本文件上,我将设置构建操作,以便将它们嵌入到程序集中:

Add scripts

然后,我将把 Program.cs 中的Main()方法替换为:

static int Main(string[] args)
{
    var connectionString = ConfigurationManager.ConnectionStrings["DatabaseConnection"].ConnectionString;

    var upgrader =
        DeployChanges.To
            .SqlDatabase(connectionString)
            .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
            .LogToConsole()
            .Build();

    var result = upgrader.PerformUpgrade();

    if (!result.Successful)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(result.Error);
        Console.ResetColor();
        return -1;
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Success!");
    Console.ResetColor();
    return 0;
} 

注意,连接字符串来自我的 app.config 文件的ConnectionStrings部分。在我的配置文件中,我添加了以下内容:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add 
      name="DatabaseConnection" 
      connectionString="Server=(local)\SQL2012;Database=SampleDb;Trusted_connection=true" />
  </connectionStrings>
</configuration> 

至此,我已经自动完成了数据库更改——我可以从命令行运行我的应用程序来执行脚本:

First run

下次运行该应用程序时,DbUp 将检测到它已经运行过:

Second run

它通过使用一个SchemaVersions表来跟踪已经运行的脚本。您可以使用 DbUp API 自定义这种行为,但我认为这是一个很好的默认设置。

Schema versions table

这也给开发人员带来了很好的体验——我团队中的其他开发人员可以获得最新版本,并运行控制台应用程序来更新他们自己的数据库本地副本。我们不再需要使用共享数据库来保持同步。

打包 Octopus Deploy 的更改

让我们回顾一下到目前为止我们所拥有的。我们有一套描述数据库需要如何改变的脚本。我们有一个运行这些脚本的控制台应用程序。控制台应用程序从配置文件中获取其连接字符串。所有这些都不依赖于 Octopus 来运行,如果必须的话,我们可以手动运行脚本(或者请 DBA 为我们运行它们)。剩下要做的就是将所有东西打包到一个 NuGet 包中,以便 Octopus 可以运行它们。

我将从添加一个描述我的包的 NuSpec 文件开始:

<?xml version="1.0"?>
<package >
  <metadata>
    <id>OctoSample.Database</id>
    <title>Octopus Sample - Database Scripts</title>
    <version>1.0.0</version>
    <authors>OctopusDeploy</authors>
    <owners>OctopusDeploy</owners>
    <licenseUrl>http://octopusdeploy.com</licenseUrl>
    <projectUrl>http://octopusdeploy.com</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Database deployment scripts for the sample application.</description>
  </metadata>
</package> 

接下来,我将添加一个非常简单的 Deploy.ps1 PowerShell 脚本,它将由 Octopus 自动执行:

& .\OctoSample.Database.exe | Write-Host 

注意:记得在Deploy.ps1的属性上设置Copy to Output Directory = Copy if newer以确保它被复制到输出目录

最后,我将安装 OctoPack NuGet 包,它将帮助我创建 Octopus 的最终包:

Install-Package OctoPack 

此时,如果我将我的构建配置更改为发布模式,我将在我的 bin 目录中获得一个 NuGet 包:

NuGet package

检查这个包,我可以看到它包含了我所有的脚本和运行它们的可执行文件:

NuGet contents

使用 Octopus 部署软件包

最后一步是让 Octopus 部署这个包。首先,我将创建一个步骤,并选择它将要运行的机器:

Create step in Octopus

接下来,在 Variables 下,我将为每个环境配置一个带有连接字符串的变量。

Create variables

Octopus 会根据我部署到的环境自动更新 app.config 文件的connectionStrings部分。

现在,我可以将更改部署到暂存:

Deploy release to staging

检查输出日志,我看到:

Deployment log

这就是用 Octopus Deploy 实现的自动化数据库部署。

摘要

在这篇文章中,我演示了一种使用 Octopus Deploy 实现自动化数据库部署的技术。还有很多其他的解决方案,如果您使用的是 Entity Framework 或 NHibernate,这些工具内置了迁移支持,但是核心方法是相同的。

了解更多信息

使用部署 Azure 应用服务步骤- Octopus 部署

原文:https://octopus.com/blog/deploy-an-azure-app-service-step

Octopus Deploy 提供了部署到 Microsoft Azure 的流程步骤。一个新的步骤Deploy a Azure App Service可用于将容器部署到 Azure App Service。

为此,您需要:

设置 Azure Web 应用程序

若要设置您将部署的 Azure Web 应用:

  1. 导航到您的资源组,然后点击创建,然后点击 Web 应用
  2. 给 web 应用命名,并检查“Docker 容器”中的发布设置。
  3. 选择适当的位置并创建 web 应用程序。
  4. 您将看到一个转到资源的选项。该 URL 将是托管 Web 应用程序的地址。

Azure Web App Home

配置 Octopus 部署

在 Octopus 部署实例中,导航到基础设施,然后是部署目标,然后是添加部署目标。选择 Azure 选项卡,然后选择 Azure Web App

Add deployment target

填充以下字段:

  • 环境 -您希望部署到的环境
  • 目标角色 -标识部署目标的角色,如果它不存在,您可能需要创建一个
  • 账户-Octopus Deploy 中关联的 Azure 账户
  • Azure Web App -你之前创建的 Web App

进入,然后外送,然后加送

填充以下字段:

  • 进给类型 - Docker Container Registry
  • Name -为提要命名

这一步激活您稍后将使用的公共 Docker 注册表提要。点击保存

部署 Azure 应用服务步骤添加到您的项目流程中。

Octopus Azure deploy step

用以下值填充字段:

  • 工作线程池——运行在特定工作线程池中的一个工作线程上:Hosted Ubuntu
  • 代表——您在部署目标步骤中创建的角色(我的是azure)
  • 容器映像——在一个工作容器中运行:容器注册表:Docker 映像:octopusdeploy/worker-tools:3.2.0-ubuntu.18.04
  • -从容器部署映像包提要:docker 包 ID: octopussamples/randomquotes

在本例中,我们部署了一个托管在 Docker Hub 上的示例 Docker 映像。下图显示了我的配置结果。点击保存

Octopus Azure deploy step configuration

点击创建版本,然后点击部署按钮,将 Web 应用部署到 Azure。

Deploy Success

查看您的 Web 应用程序

导航至您的网络应用程序的网址- [your-url].azurewebsites.net

Random Quotes

结论

在本文中,您将使用新的部署 Azure 应用程序服务步骤建立一个 Octopus 部署项目来部署 Web 应用程序。

愉快的部署!

从 Maven - Octopus Deploy 部署和使用 ZIP 文件

原文:https://octopus.com/blog/deploy-and-consume-zip-files-from-maven

Deploy and consume ZIP files from Maven

Maven 是一个多功能的工件存储库,它超越了传统的 Java 包,如 jar 和 WARs,提供了托管通用 ZIP 存档的能力。在这篇博文中,我将介绍如何将通用档案发布到 Maven 存储库中,以及如何在 Octopus 项目中使用它们。

Maven 存储库配置

第一步是在~/.m2/settings.xml文件中配置 Maven 存储库。该文件包含 Maven 存储库凭证等设置。

以下示例定义了 Nexus Maven 存储库的默认凭据:

<settings 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <servers>
    <server>
      <id>Nexus</id>
      <username>admin</username>
      <password>admin123</password>
    </server>
  </servers>
</settings> 

创建一个包

接下来,我们需要创建将要上传的包。在这个例子中,我创建了一个名为package.zip的标准 ZIP 存档,其中保存了文件test.txt:

zip package.zip test.txt 

上传包

为了上传这个包,我使用了 Maven deploy:deploy-file目标。下载 Maven

该命令中的repositoryId系统属性需要匹配settings.xml文件中的<id>元素。

mvn deploy:deploy-file \
  -DgroupId=org.example \
  -DartifactId=package \
  -Dversion=0.0.1 \
  -Dpackaging=zip \
  -Dfile=package.zip \
  -DrepositoryId=Nexus \
  -Durl=http://nexus-host:8081/repository/maven-releases 

创建外部 Maven 提要

为了使用 Octopus 中的新工件,我们需要添加 Nexus 服务器作为外部 Maven 提要。这是在库➜外部馈送下完成的:

Maven Repo

然后,我们可以通过搜索工件org.example:package来测试存储库,工件org.example:package是与artifactId相结合的groupId:

Maven Test

转移包裹

现在我们可以访问 Maven 提要了,我使用 Transfer a package 步骤将工件下载到目标机器上。我们再次用org.example:package引用 Maven 工件:

Transfer a package

结论

通过托管通用的 ZIP 文件,Maven 存储库可以用于管理各种部署的包,而不仅仅是针对 Java 的包,并且通过对 Maven 提要的本机支持,Octopus 可以轻松地将这些工件作为部署过程的一部分。

将 ASP.NET 应用程序部署到 Azure 网站- Octopus Deploy

原文:https://octopus.com/blog/deploy-aspnet-applications-to-azure-websites

现在回到最初的博文...

最近,越来越多的人希望从 Octopus 部署他们的 Azure 网站。问题是目前还没有这方面的 OOTB 功能。

由于目前没有内置的方法来完成它,我们的一个用户创建了一个 step 模板(它可以在 Octopus Library 网站上找到,还有一堆其他有用的 step 模板),它运行一个 PowerShell 脚本,该脚本使用 Web Deploy 将您的应用程序部署到 Azure,考虑到这一点,我想我应该写一个小的(有点)博客帖子,逐步介绍如何使用这个 step 模板设置您的 ASP.NET 应用程序并准备好部署到 Azure。

出于这篇博文的目的,我将在 Visual Studio 2013 中创建一个演示 ASP.NET MVC 应用程序。

创建您的 ASP.NET 应用程序

首先让我们选择我们的项目类型,并给它一个名字

Create New Project

然后指定要使用的模板,我将只使用提供的 MVC 模板,我将保留Host in the cloud复选框未选中,因为我想使用 Octopus Deploy 来处理我的部署。

Specify Web Template

一旦项目被创建,按 F5 运行你的新的和闪亮的 ASP.NET MVC 应用程序。

Web site up and running

没什么太激动人心的,但是它给了我们一个起点,我们可以从这里开始部署设置和运行。

创建 NuGet 包

由于 Octopus Deploy 在部署您的应用程序时使用 NuGet 包,我们创建了一个小工具,它将从您构建项目时创建的输出文件中创建一个 NuGet 包。

边注:OctoPack 创建的 NuGet 包和你从 NuGet 图库安装的 NuGet 包略有不同。我们的 NuGet 包只是一堆文件和文件夹,它们组成了你的应用程序的结构。

将 OctoPack NuGet 包添加到您的项目中

要将 OctoPack 添加到我们的项目中,右键单击您的解决方案并选择 Manage NuGet Packages for Solution,搜索octopack并单击Install按钮。

Install OctoPack

选择要安装 OctoPack 的项目,在我的例子中,我只有一个项目,所以我选择它并单击确定。

Select project to install OctoPack in

OctoPack Installed

使用 MSBuild 从命令行构建项目并生成 NuGet 包

现在 OctoPack 已经安装好了,当我们从命令行构建我们的解决方案时,我们可以告诉它为我们生成一个 NuGet 包。

在命令提示符下输入:

C:\Code\OctoWeb\OctoWeb>msbuild OctoWeb.sln /t:build /p:RunOctoPack=true 

如果一切正常,您应该会看到类似下面的输出:

Microsoft (R) Build Engine version 12.0.30723.0
[Microsoft .NET Framework, version 4.0.30319.34014]
Copyright (C) Microsoft Corporation. All rights reserved.

Building the projects in this solution one at a time. To enable parallel build, please add the "
/m" switch.
Build started 23/09/2014 3:25:24 PM.
Project "C:\Code\OctoWeb\OctoWeb\OctoWeb.sln" on node 1 (build target(s)).
ValidateSolutionConfiguration:
  Building solution configuration "Debug|Any CPU".
Project "C:\Code\OctoWeb\OctoWeb\OctoWeb.sln" (1) is building "C:\Code\OctoWeb\OctoWeb\OctoWeb\
OctoWeb.csproj" (2) on node 1 (default targets).
...
CopyFilesToOutputDirectory:
  OctoWeb -> C:\Code\OctoWeb\OctoWeb\OctoWeb\bin\OctoWeb.dll
OctoPack:
  OctoPack: Get version info from assembly: C:\Code\OctoWeb\OctoWeb\OctoWeb\bin\OctoWeb.dll
  Using package version: 1.0.0.0
  OctoPack: Written files: 101
  OctoPack: A NuSpec file named 'OctoWeb.nuspec' was not found in the project root, so the file
   will be generated automatically. However, you should consider creating your own NuSpec file  
  so that you can customize the description properly.
  OctoPack: Packaging an ASP.NET web application
  OctoPack: Add content files
  ...
  OctoPack: Add binary files to the bin folder
  ...
  OctoPack: Attempting to build package from 'OctoWeb.nuspec'.
  OctoPack: Successfully created package 'C:\Code\OctoWeb\OctoWeb\OctoWeb\obj\octopacked\OctoWe
  b.1.0.0.0.nupkg'.
  OctoPack: Copy file: C:\Code\OctoWeb\OctoWeb\OctoWeb\obj\octopacked\OctoWeb.1.0.0.0.nupkg
  OctoPack: OctoPack successful
Done Building Project "C:\Code\OctoWeb\OctoWeb\OctoWeb\OctoWeb.csproj" (default targets).

Done Building Project "C:\Code\OctoWeb\OctoWeb\OctoWeb.sln" (build target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.86 

如果你看一下 OctoPack 说它创建了 NuGet 包的文件夹,你应该看到它确实在那里。

OctoPack NuGet package created

如果您在 NuGet Package Explorer 中打开生成的 NuGet 包,您应该看到 OctoPack 已经打包了您的网站,因为它将被部署到您的 web 服务器。

NuGet Package Explorer

如果您想让 OctoPack 将创建的 NuGet 包复制到本地文件夹或文件共享,您可以使用下面的调用msbuild

C:\Code\OctoWeb\OctoWeb>msbuild OctoWeb.sln /t:build /p:RunOctoPack=true /p:OctoPackPublishPackageToFileShare=C:\NuGet 

或者,发布到 Octopus 中的内置存储库中

C:\Code\OctoWeb\OctoWeb>msbuild OctoWeb.sln /t:build /p:RunOctoPack=true /p:OctoPackPublishPackageToHttp=http://your-octopus-server/nuget/packages /p:OctoPackPublishApiKey=API-ABCDEFGMYAPIKEY 
修改。csproj 在构建项目时生成 NuGet 包

如果您想在每次构建解决方案时生成 NuGet 包并将其发布到本地文件共享,那么您可以修改您的.csproj文件并将以下 OctoPack 标记添加到项目属性组中:

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    ...
    <RunOctoPack>true</RunOctoPack>
    <OctoPackPublishPackageToFileShare>C:\Packages</OctoPackPublishPackageToFileShare>
</PropertyGroup> 

或者,如果您希望在执行调试构建时将 NuGet 包发布到本地文件共享,并且仅在执行发布构建时发布到内置存储库:

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    ...
    <RunOctoPack>true</RunOctoPack>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <OctoPackPublishPackageToFileShare>C:\Packages</OctoPackPublishPackageToFileShare>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    ...
    <OctoPackPublishPackageToHttp>http://your-octopus-server/nuget/packages</OctoPackPublishPackageToHttp>
    <OctoPackPublishApiKey>API-ABCDEFGMYAPIKEY</OctoPackPublishApiKey>
</PropertyGroup> 

设置您的新 Azure 网站

现在我们将设置我们将要部署到的 Azure 网站。

登录到 Azure 管理门户并创建一个新网站。

Create New Azure Web Site

一旦创建了网站,

Azure Web Site created

单击它可以访问网站的设置。

下载发布配置文件

从起始页,我们将下载发布配置文件设置文件,以获取在 Octopus Deploy 中设置部署流程所需的值。所以点击Download the publish profile链接下载所需的设置文件。

Download Publish Profile

现在我们已经获得了 Octopus 部署设置之外的所有东西,我们可以继续到您的 Octopus 服务器来获得我们的项目和部署过程设置,并准备好部署您的新 Azure 网站。


将 Web 部署步骤模板添加到 Octopus

我们需要做的第一件事是,现在我们已经将我们的网站打包到一个 NuGet 包中,从 Octopus 库站点导入Web Deploy - Publish Website (MSDeploy)步骤模板。

从 Octopus 库中获取“Web 部署-发布网站(MSDeploy)”步骤模板

在 Octopus 库站点上,搜索 web deploy

Web Deploy Step Template

单击返回的结果,然后单击绿色的大按钮Copy to clipboard

Web Deploy Step Template details

将步骤模板导入 Octopus Deploy

登录到你的八达通服务器,进入图书馆->步骤模板。在步骤模板选项卡上,单击Import链接

Import Step Template

这将显示Import对话框,将您从 Octopus 库站点复制的 Web Deploy 步骤模板粘贴到提供的文本区域中。

Import

点击Import按钮,步骤模板将被导入

Imported

很好,现在我们已经准备好设置我们的项目和部署流程,开始将我们的 ASP.NET MVC 应用程序部署到 Azure 网站。


在 Octopus Deploy 中设置项目

接下来要做的是设置我们的新项目,并定义部署流程,我们将使用该流程将我们的 ASP.NET MVC 应用程序部署到我们的 Azure 网站。

创建我们的项目

进入“项目”->“所有项目”,然后点击项目组上的Add Project按钮。

Create New Octopus Project

Create New Octopus Project Details

Octopus Project

恭喜你,你得到了一个闪亮的新项目!;)

定义您的项目变量

为了使设置您的项目来部署多个环境变得容易,我们将创建可以限定环境、角色和机器范围的项目变量。

打开项目站点上的变量选项卡,然后为网站名称、发布 URL、用户名和密码添加变量。我们将把密码变量设为Sensitive Variable,这样我们就可以保密。

打开您先前下载的发布概要文件,并从 Web Deploy 的发布概要文件中获取publishUrlmsdeploySiteuserNameuserPWD(例如<publishProfile profileName="octowebdemo - Web Deploy">,然后在适当的变量中填入值。然后点击Save

Octopus Project Variables

在这个演示中,我不会限定变量的范围,因为我只有一个环境、一台机器和一个角色。

定义您的部署流程

好了,现在我们来谈谈整个过程的商业方面。

现在我们开始为我们的项目指定部署过程,它将由两个步骤组成,一个 NuGet 包步骤和我们为 Web Deploy 导入的 PowerShell 步骤。

可选地 一旦部署完成,您可以添加另一个步骤来“预热”网站。恰好在图书馆网站上,我们有一个步骤模板。搜索Test URL并导入返回的步骤模板。

打开项目页面上的Process选项卡。

Deployment Process tab

添加“部署 NuGet 包”步骤

点击Add Step按钮。

选择Deploy a NuGet package步骤。

NuGet Package step

NuGet Package step setup

填写必要的细节,从发布它的 NuGet 提要(在我的例子中是磁盘上的本地文件夹)中指定您的 web 应用程序 NuGet 包。

NuGet Package step completed

点击Save

Project deployment process with 1 step

添加“Web 部署-发布网站(MSDeploy)”步骤

现在是时候添加我们的 Web 部署步骤了,再次点击Add Step按钮并选择Web Deploy - Publish Website (MSDeploy)

Web Deploy step

填写必要的细节,使用变量绑定来指定 Azure 特定的细节。

Web Deploy step details completed

所需的Package Step Name是提取 NuGet 包中包含的文件的步骤的名称,这用于在磁盘上定位需要上传到 Azure 网站的文件。

点击Save

Project deployment process with 2 steps

就这样,我们现在准备创建一个版本并将其部署到 Azure。

创建一个版本

要创建一个发布,点击项目页面顶部的Create Release按钮。

Create a Release

在 release 页面上,您可以选择为 Octopus Deploy 将为您预先填充的内容指定一个不同的版本号(基于您选择的项目设置),为您的 web 应用程序指定要使用的 NuGet 包的版本号(我只有 1 个版本,所以我将使用它)以及发行版中应该包含的任何发行说明。

Release details completed

点击Save。这将带您进入发布概述页面。

Release Overview

现在我们想把这个版本部署到我们的 Azure 网站上,所以点击Deploy this release按钮。选择要部署到的环境。在我的例子中,我只有我的Dev环境设置,所以我将选择这个。

Deploy Release to Dev

Deploy Release to Dev details

在 deployment 页面上,您可以选择将发布安排在稍后的日期/时间,以及部署哪些步骤(以及部署到什么机器)。

我将坚持使用默认设置,只需点击Deploy Release按钮。

Deploy progressed

部署完成后,打开一个浏览器,然后浏览 Azure 中运行的 web 应用程序的 URL。

Deploy completed

Azure Web Site running

恭喜,您已经使用 Web Deploy 从 Octopus Deploy 部署了您的 Azure 网站!


将应用程序更新并重新部署到 Azure

现在我们已经将应用程序从 Octopus 部署到 Azure,让我们对应用程序做一些修改,然后将新版本部署到 Azure。

我将更新应用程序的名称,以及使用的一些颜色。

Modified web site running

重新创建您的 NuGet 包

当从命令行重新创建 NuGet 包时,不使用存储在AssemblyInfo.cs文件的[assembly: AssemblyVersion]中的版本,我将通过将OctoPackPackageVersion参数传递给 MSBuild 来覆盖它。

C:\Code\OctoWeb\OctoWeb>msbuild OctoWeb.sln /t:build /p:RunOctoPack=true /p:OctoPackPackageVersion=1.0.0.1 

最终结果应该类似于下图

 OctoPack: Attempting to build package from 'OctoWeb.nuspec'.
  OctoPack: Successfully created package 'C:\Code\OctoWeb\OctoWeb\OctoWeb\obj\octopacked\OctoWeb.1.0.0.1.nupkg'. 

将新的 NuGet 包复制到 Octopus 可以访问的位置。

在 Octopus Deploy 中创建新版本

在 Octopus 中,返回到发布选项卡并点击Create Release。Octopus 现在应该拿起最新的包(v1.0.0.1)。

Create a new release

点击Save

将最新版本部署到 Azure 网站

现在剩下的就是将新版本(0.0.2)部署到 Azure。

点击Deploy this release,选择要部署到的环境,最后点击Deploy Release

Create new release completed

Deploy new release in progress

此部署应该比初始部署快得多,因为它将只上传已更改的文件。部署完成后,Azure 网站应该会根据所做的更改进行更新。

Deploy new release completed

Modified Azure Web Site running


用章鱼-章鱼展开舵图

原文:https://octopus.com/blog/deploy-helm-chart-with-octopus

Deploy a Helm chart with Octopus

赫尔姆已经成为事实上的 Kubernetes 包经理。它提供了丰富的模板、强大的 CLI 工具、用于共享图表的集中存储库,最近发布的 Helm 3 解决了之前 Helm 版本的安全问题。

Octopus 为部署 Helm 图表提供了本地支持,在这篇博客文章中,我们将看看如何在 Octopus 中管理 Helm 部署到 Kubernetes 集群,该集群是在之前的一篇博客文章中创建的。

样本图表

在博文《通过 Octopus 将您的第一个容器部署到 Kubernetes》中,我们介绍了创建 Docker 映像并将其推送到 Docker Hub 的过程。最终结果是图像 mcasperson/mywebapp 。我们将从我们的舵图中重用这个 Docker 图像。

舵图示例可以在 GitHub 上找到。这个图表创建了一个 Kubernetes 部署和服务来公开嵌入在 Docker 映像中的 web 应用程序。

首先克隆 GitHub 存储库,并用命令helm package .\SampleHelmChart将文件打包成图表:

$ helm package .\SampleHelmChart
Successfully packaged chart and saved it to: C:\Code\SampleHelmChart-0.1.0.tgz 

结果图表的文件名为SampleHelmChart-0.1.0.tgz。通常,这个文件会被上传到一个图表库,比如由 ChartMuseum 托管的图表库。Octopus 本身支持这样的图表存储库,对于那些希望利用 Helm 的人来说,建立一个图表存储库是一个非常有效的选择:

然而,从 2020.3.0 版本开始,Octopus 提供了将舵图直接上传到内置提要的能力。这消除了配置单独图表存储库的需要。将图表上传到内置提要的唯一要求是在文件名后附加一个包版本,这意味着我们需要在上传之前将图表文件从SampleHelmChart-0.1.0.tgz重命名为SampleHelmChart.0.1.0.tgz:

【T2

部署图表

为了部署舵图,我们将使用升级舵图步骤:

然后,我们引用上传到内置提要的包,并为 Helm 版本定义一个名称:

仅此而已。只需几个简单的步骤,我们就可以在 Kubernetes 上部署我们的第一个 Helm chart。

结论

通过在内置提要中托管掌舵图表并使用本地掌舵步骤来消费它们,Octopus 使得向 Kubernetes 共享和部署掌舵图表变得非常简单。

如果你已经部署了一个或者希望利用可用的公共存储库,Octopus 也可以访问外部舵图存储库。任何成熟的 Kubernetes 基础设施将不可避免地包括一系列外部服务,如入口控制器、仪表盘、监控解决方案和其他 Kubernetes 运营商。这些第三方应用程序通常会以 Helm charts 的形式发布,因此访问外部 Helm feeds 允许 Octopus 像管理内部应用程序一样管理它们的部署。

从 Maven - Octopus Deploy 部署和使用 ZIP 文件

原文:https://octopus.com/blog/deploy-maven-zip-files

Deploy and consume ZIP files from Maven

Maven 是一个通用的工件存储库,它超越了传统的 Java 包,如 jar 和 WARs,提供了托管通用 ZIP 存档的能力。在这篇博文中,我将介绍如何将通用档案发布到 Maven 存储库中,以及如何在 Octopus 项目中使用它们。

Maven 存储库配置

第一步是在~/.m2/settings.xml文件中配置 Maven 存储库。该文件包含 Maven 存储库凭证等设置。

以下示例定义了 Nexus Maven 存储库的默认凭据:

<settings 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <servers>
    <server>
      <id>Nexus</id>
      <username>admin</username>
      <password>admin123</password>
    </server>
  </servers>
</settings> 

创建一个包

接下来,我们需要创建将要上传的包。在这个例子中,我创建了一个名为package.zip的标准 ZIP 存档,其中保存了文件test.txt:

zip package.zip test.txt 

上传包

为了上传这个包,我使用了 Maven deploy:deploy-file目标。下载 Maven

该命令中的repositoryId系统属性需要匹配settings.xml文件中的<id>元素。

mvn deploy:deploy-file \
  -DgroupId=org.example \
  -DartifactId=package \
  -Dversion=0.0.1 \
  -Dpackaging=zip \
  -Dfile=package.zip \
  -DrepositoryId=Nexus \
  -Durl=http://nexus-host:8081/repository/maven-releases 

创建外部 Maven 提要

为了使用 Octopus 中的新工件,我们需要添加 Nexus 服务器作为外部 Maven 提要。这是在库➜外部馈送下完成的:

Maven Repo

然后,我们可以通过搜索工件org.example:package来测试存储库,工件org.example:package是与artifactId相结合的groupId:

Maven Test

转移包裹

现在我们可以访问 Maven 提要了,我使用 Transfer a package 步骤将工件下载到目标机器上。我们再次用org.example:package引用 Maven 工件:

Transfer a package

结论

通过托管通用的 ZIP 文件,Maven 存储库可以用于管理各种部署的包,而不仅仅是针对 Java 的包,并且通过对 Maven 提要的本机支持,Octopus 可以轻松地将这些工件作为部署过程的一部分。

使用 Octopus - Octopus Deploy 将 PHP 站点部署到 IIS

原文:https://octopus.com/blog/deploy-php-to-iis-with-octopus

正如你可能已经读过的,我的背景是 LAMP 环境中的 PHP。做系统管理工作,手动发布和部署 PHP 站点。因此,当一位客户最近询问他们是否可以在 IIS 上部署一个 PHP 站点时,Paul 明智地要求我写一篇关于此事的博文。

一切都在于包装

正如我们所知,Octopus 依赖 NuGet 包来部署文件。所以首先我们需要把所有的 PHP 文件放到 NuGet 包中。幸运的是, NuGet Package Explorer 已经解决了这个问题。添加文件和创建 NuGet 包真的是再简单不过了。

NuGet Explorer

Development Folder

一旦我的包被创建,并上传到 Octopus 中的内部 NuGet 存储库,我就准备好了!

NuGet Repository

在 Octopus 中创建项目

然后,我创建了一个新项目,并选择了 NuGet 包步骤。

Package Deployment Step

如您所见,我选择了自定义安装目录,因为我有一个预先存在的站点设置,我希望在此实例中始终部署到相同的位置。但是我们使用的是 IIS,所以您可以选择其他 IIS 选项。我还添加了一些其他的 Octopus 特性来展示它可以与。php 文件。因此,我让定制安装目录使用一个变量,并且我还创建了一个测试配置文件,其中包含了在部署到生产环境时需要替换的变量。

Test Config File

所以我使用了文件中的替代变量特性并定义了我的config.php文件。

Test Variables

所有这三个变量,用于我的自定义安装目录,还有我在config.php文件中的两个变量。

部署时间到了

既然我的包被选择为一个步骤,我的变量被配置并确定了范围,那么是时候测试一个版本了。我想我应该创造一个!

Creating a Release

部署时间!

Deployment Time

这是该包的部署日志:

Deployment Log

服务器上的结果

我们完成了,Octopus 已经部署了我的 PHP 文件。所以让我们来看看结果。这是我可爱的phpinfo()页面。

PHPINFO display

并且我的文件部署在 IIS 服务器上的正确位置。

Files on Disk

我们也不能忘记我的config.php,它的变量被很好地替换了。

Config.php Contents

完全成功

因此,我在 IIS 服务器上部署 PHP 文件取得了圆满成功。我能够利用 Octopus 已经做的一切,我唯一要做的就是手动创建我的包,但这实际上是一个“找到一个文件夹并添加”的例子。是的,这个部署是一个 IIS 服务器,大部分 PHP 运行在 Linux 上,但是也许部署现实离我们的未来并不太远。

用部署发布步骤协调项目- Octopus 部署

原文:https://octopus.com/blog/deploy-release-step/deploy-release-step

除非你正在建造一个真正的整体,否则你的项目不会孤立存在。我们的行业越来越朝着系统由更细粒度的组件组成的方向发展。有人称之为面向服务的架构,有人称之为微服务,但是从发布管理的角度来看,关键是它需要协调。

Synchronized Swimming

在 Octopus 总部,我们喜欢这种趋势,因为坦率地说,你的发布和部署过程中的移动部分越多,Octopus 带来的价值就越大。

这样做的自然结果是,用户希望能够在 Octopus 中协调多个项目的发布。这是我们最重要的用户声音建议之一。

两个关键场景是:

  • Bundle: 您想要创建一个“Bundle”发布,以允许多个项目的发布在您的环境中一起进行。
  • 依赖关系:您想要明确地建模项目 A 依赖于已经部署的项目 B 的特定版本。

介绍部署发布步骤

Deploy Release Step Card

为了解决这个问题,我们创建了一个新的步骤:部署一个发布版本Deploy a Release 步骤允许您选择另一个 Octopus 项目进行部署。

Deploy Release Step - Select a Project

当您创建一个包含一个或多个 Deploy a Release 步骤的项目发布时,您可以选择要部署的子项目的发布版本。正如在创建包含部署包的步骤的项目发布时选择包的版本一样。

【T2 Deploy Release Step - Create Release

将此作为一个步骤来实现的好处是,所有常规的 Octopus 功能都像预期的那样工作。您可以将部署发布步骤与其他步骤类型穿插在一起。例如,如果您正在创建一个 bundle 项目,那么您的第一步可能是一个手动干预步骤(批准发布),而您的最后一步可能是发送一个 Slack 通知。 Deploy a Release 步骤也可以配置为仅针对特定的环境、渠道或租户运行,就像任何其他步骤一样。它们可以配置为并行或串行运行,就像任何其他步骤一样。

Example Project Process

部署发布步骤运行时,它触发指定项目的部署。这种部署与直接触发的部署没有什么不同。它将在八达通仪表板上可见。

Example Project Dashboard

有条件部署

您可以配置部署子项目的条件:

  • 始终部署(默认)。
  • 如果所选版本不是环境中的当前版本。
  • 如果所选版本的版本高于环境中的当前版本。

这允许您指定组件版本之间的关系。

微服务示例

例如,您正在部署一个应用程序, Acme。店铺,靠的是一个伐木微服务,极致。装运

到了极致。商店项目可以包含一个部署发布步骤,该步骤被配置为部署 Acme。在展开条件字段设置为If the selected release has a higher version than the current release in the environment的情况下装运

这将允许团队在 Acme 上工作。发布独立工作的微服务项目,在准备就绪时部署到 Octopus 环境中。

当团队工作在的极致时。商店创建了一个版本,他们选择了 Acme 的一个版本。发货,例如2.0.0。这有效地指定了每个环境中必须有的最低版本。随着的极致。车间发布在环境中进行,它将触发 Acme 的部署。只有当环境中还没有包含极致的>= 2.0.0时才发货。装运

变量

变量可以传递给由部署发布步骤触发的部署。就像任何其他项目变量一样,这些变量可用于子部署过程。

Pass Variables to Deployment

部署发布步骤触发的部署的输出变量被捕获,并作为部署发布步骤的输出变量公开。

这允许父流程使用子部署的输出,甚至传递到由后续部署发布步骤触发的部署中。这使得许多协调场景成为可能。

我什么时候能拿到它?

该功能将在 Octopus 2018.2 版本中提供,该版本将于 2 月初发布。

愉快的(多项目)部署!

了解更多信息

使用 Octopus - Octopus 部署到 AWS 弹性豆茎

原文:https://octopus.com/blog/deploy-to-aws-beanstalk

Slingshot deploying a new application release to a beanstalk vine

Elastic Beanstalk 是 AWS 提供的平台即服务(PaaS ),允许开发人员部署用各种语言编写的代码,例如。NET、Java、PHP、Node.js、Go、Python 和 Ruby 移植到预先配置好的基础设施上。您只需上传您的应用程序,Elastic Beanstalk 就会自动处理容量供应、负载平衡、伸缩和应用程序健康监控等细节。

Beanstalk 应用程序和 Octopus 的生命周期有许多共同之处,例如,Beanstalk 将应用程序部署到多个隔离的环境中,每个环境都可能有独特的设置。这种重叠意味着 Octopus 可以帮助部署 Beanstalk。

然而,Beanstalk 有一些我们需要考虑的独特需求,以便正确地与 Octopus 集成。在这篇博文中,我们将探索一个示例部署脚本,它允许 Octopus 将. NET 核心应用程序部署到 AWS Elastic Beanstalk。

Beanstalk 基础设施概述

在部署到 Beanstalk 之前,我们需要了解 Beanstalk 是如何组织的。

任何 Beanstalk 基础设施的顶层都是应用程序。从概念上讲,应用程序是包含应用程序版本和环境的空间。通常,命名应用程序是为了标识它所代表的部署。在本文中,我们将部署随机报价应用程序,因此 Beanstalk 应用程序将被称为随机报价。

应用程序版本是部署到环境中的代码的副本。每个应用程序版本都有一个唯一的版本标签。不过,版本标签并不强制要求任何特定的版本化方案,只是每个版本都是唯一的。

最后,我们有环境。环境是运行代码的物理基础设施。Beanstalk 为支持的编程语言以及支持的 Docker 容器提供了多种预配置的环境平台。您还可以选择为更高级的场景创建自己的定制平台。每个环境都相互独立,可以部署任何应用程序版本。

青苗部署生命周期。

豆茎应用程序包装要求

另一个要考虑的主要问题是 Beanstalk 期望如何为部署构建包。

Beanstalk 接受的包的类型很大程度上取决于用来创建代码的编程语言。

对于简单的部署,Beanstalk 接受一个标准的 ZIP 文件,或者对于 Java,接受一个 JAR 或 WAR 文件。这些文件与 Octopus 配合得很好,因为它们都可以存储在内置提要中,并且在部署步骤中很容易使用。

但是,在某些情况下,Beanstalk 希望接收嵌套的归档文件,并且。NET Core 部署就是需要嵌套归档的一个例子。

在...的情况下。可部署的工件是一个 ZIP 文件,包含一个 JSON 清单文件和另一个包含应用程序代码的嵌套 ZIP 文件。你可以在下面的截图中看到这些文件。文件aws-windows-deployment-manifest.json是 Beanstalk 清单文件,文件site.zip包含。NET 核心代码。

包含要部署到 Beanstalk 的. NET 核心应用程序的 ZIP 文件的内容。

我们需要记住这些嵌套的档案,以便在部署过程中充分利用 Octopus 提供的特性。

示例应用程序

我们正在部署的示例应用程序叫做随机报价。这是一个简单的。NET Core web 应用程序,它不一定是为部署到 Beanstalk 而设计的。

此应用程序的一般性质突出了部署到 Beanstalk 必须考虑的一些挑战,例如,如何在不维护特定于环境的工件的情况下实现特定于环境的设置。

Beanstalk 环境可以定义它们自己的环境变量集,这是配置它们运行的应用程序的特定于环境的方面的首选方式。将所有配置存储在环境变量中是 12 因子应用方法推荐的方法。

但是我们的示例应用程序仍然将配置保存在文件中,特别是 appsettings.json 文件。虽然我们可以利用ASPNETCORE_ENVIRONMENT环境变量来选择适当的设置文件,但是对于这个部署过程,我们将在部署期间直接定制appsettings.json文件。

为了允许 Octopus 在部署过程中修改文件,我们可以而不是将编译好的代码打包到一个嵌套的 ZIP 存档中。Octopus 有许多功能可以替换文件中的值,如变量替换JSON 配置变量,但这些功能都依赖于处理包含文本文件的档案,而不是额外的嵌套档案。

这意味着 Octopus 托管的打包代码将包含调用dotnet package生成的文件。然后由我们将这个文件重新打包到 Beanstalk 要求的嵌套归档中。

Random Quotes 利用 GitHub 动作来构建代码,你可以在这里看到工作流程。在这个工作流程中,有四个重要的工作突出了我们如何打包这个应用程序。

首先,我们构建代码:

- name: Build with dotnet
  run: dotnet build --configuration Release 

接下来,我们打包代码,将运行该应用程序所需的所有文件放入一个名为site的目录中:

- name: Publish with dotnet
  run: dotnet publish -o site --configuration Release 

Octo CLI 将site目录打包成一个 ZIP 文件:

- name: Pack Beanstalk App
  run: >-
    /opt/octo/Octo pack .
    --outFolder /home/runner/work/RandomQuotes/RandomQuotes
    --basePath /home/runner/work/RandomQuotes/RandomQuotes/RandomQuotes/site
    --id RandomQuotes
    --version $(cat /home/runner/work/RandomQuotes/RandomQuotes/version.txt)
    --format zip 

并将包推送到 Octopus 服务器:

- name: Push to Octopus
  run: >-
    if [[ ! -z "${{ secrets.OctopusUrl }}" && ! -z "${{ secrets.OctopusApiKey }}" ]]; then
    /opt/octo/Octo push
    --server ${{ secrets.OctopusUrl }}
    --apiKey ${{ secrets.OctopusApiKey }}
    --package /home/runner/work/RandomQuotes/RandomQuotes/RandomQuotes.$(cat /home/runner/work/RandomQuotes/RandomQuotes/version.txt).zip
    --overwrite-mode IgnoreIfExists;
    fi 

这个过程的最终结果是一个 ZIP 文件,其中包含运行应用程序所需的 dll 和任何其他配置文件。在下面的截图中,你可以看到这个 ZIP 文件的内容。

重要的是,这个 ZIP 文件不是我们可以以当前形式部署到 Beanstalk 的东西。创建可以部署到 Beanstalk 的归档文件是 Octopus 部署的一部分。

创建 Beanstalk 应用程序

我们现在需要创建 Beanstalk 应用程序和环境。有许多方法可以创建 Beanstalk 基础设施,但是为了简单起见,我们在这里通过 AWS 控制台创建它。

所以我们有一个名为Random Quotes的应用程序,它有两个环境:DevelopmentTest

筹备八达通项目

在我们部署任何东西之前,我们的 Octopus 项目需要配置一些变量。这是我们必须定义的变量的表格。

名字 描述
Application Beanstalk 应用程序的名称。
AppSettings:EnvironmentName appsettings.json文件中EnvironmentName属性的值。JSON 配置变量特性使用这个变量。
AWS 将用于执行部署的 AWS 帐户。
BucketName 将保存 Beanstalk 部署档案的 S3 存储桶的名称。
Environment Octopus 环境到 Beanstalk 环境的映射。

为 Octopus 项目定义的变量。

章鱼部署

现在我们已经创建了 Beanstalk 应用程序和环境,我们的。NET 核心应用代码打包上传到 Octopus,是时候实现部署了。

Octopus 没有部署到 Beanstalk 的专用步骤,但是我们可以利用Run an AWS CLI script步骤使用捆绑的 AWS CLI 来执行部署。

下面的代码是执行部署的 Octopus 步骤的内容:

<#
.DESCRIPTION Waits for the given environment to finish any processing
.PARAMETER application The name of the Beanstalk application
.PARAMETER environment The name of the Beanstalk environment
#>
function Wait-ForEnvironmentToBeReady ($application, $environment) {
    do {
      $result = aws elasticbeanstalk describe-environments `
          --environment-names $environment `
          --application-name $application `
          --output json |
          ConvertFrom-Json |
          Select-Object -ExpandProperty Environments |
          Select-Object -First 1

      if ($null -eq $result) {
          throw "Could not find the environment $environment in the application $application"
      }

      Write-Host "Environment $environment is $($result.Status)"
      Start-Sleep 10
    } while (-not ($result.Status -in @("Ready", "Terminated")))
  }

  <#
  .DESCRIPTION Creates a new application version
  .PARAMETER application The name of the Beanstalk application
  .PARAMETER version The name of the Beanstalk application version
  .PARAMETER s3Bucket The S3 bucket that holds the application code
  .PARAMETER s3Key The S3 file of the application code
  #>
  function New-ApplicationVersion($application, $version, $s3Bucket, $s3Key) {
    Write-Host "Creating application version $version"
    aws elasticbeanstalk create-application-version `
        --application-name $application `
        --version-label $version `
        --source-bundle S3Bucket="$s3Bucket",S3Key="$s3Key" |
        Out-Null

  }

  <#
  .DESCRIPTION Uploads a file to S3
  .PARAMETER file The file to upload
  .PARAMETER s3Bucket The S3 bucket that holds the application code
  .PARAMETER s3Key The S3 file of the application code
  #>
  function Add-File($file, $s3Bucket, $s3Key) {
    Write-Host "Uploading File"
    aws s3 cp $file "s3://$s3Bucket/$s3Key" | Out-Null
  }

  <#
  .DESCRIPTION Updates a Beanstalk environment with the supplied application version
  .PARAMETER application The name of the Beanstalk application
  .PARAMETER environment The name of the Beanstalk environment
  .PARAMETER version The name of the Beanstalk application version
  #>
  function Update-Environment($application, $environment, $version) {
    Write-Host "Updating Environment $environment to $version"
    aws elasticbeanstalk update-environment `
        --application-name $application `
        --environment-name $environment `
        --version-label $version |
        Out-Null
  }

  function New-ManifestFile($name, $file) {
      Set-Content -Path "aws-windows-deployment-manifest.json" -Value @"
      {
        "manifestVersion": 1,
        "deployments": {
            "aspNetCoreWeb": [
            {
                "name": "$name",
                "parameters": {
                    "appBundle": "$file",
                    "iisPath": "/",
                    "iisWebSite": "Default Web Site"
                }
            }
            ]
        }
    }
"@
  }

  $VersionLabel = $OctopusParameters["Octopus.Action.Package[RandomQuotes].PackageId"] +
      "." +
      $OctopusParameters["Octopus.Action.Package[RandomQuotes].PackageVersion"] +
      "." +
      $OctopusParameters["Octopus.Deployment.Id"]

  New-ManifestFile "random-quotes" "site.zip"

  # Compress the extracted DotNET application code
  Compress-Archive `
      -Path "$($OctopusParameters["Octopus.Action.Package[RandomQuotes].ExtractedPath"])\*" `
      -DestinationPath "site.zip"

  # Compress the application code with the manifest file to create the Beanstalk deployment    
  Compress-Archive `
      -Path "site.zip", "aws-windows-deployment-manifest.json" `
      -DestinationPath "$VersionLabel.zip"

  # Upload the Beanstalk deployment to S3    
  Add-File "$VersionLabel.zip" $BucketName "$VersionLabel.zip"

  # Use the new file in S3 to create a Beanstalk application version
  New-ApplicationVersion $Application $VersionLabel $BucketName "$VersionLabel.zip"

  # Wait for any pending changes to the environment to finish
  Wait-ForEnvironmentToBeReady  $Application $Environment

  # Deploy the application version to the environment
  Update-Environment $Application $Environment $VersionLabel

  # Wait for the new deployment to finish
  Wait-ForEnvironmentToBeReady  $Application $Environment 

让我们来分解这个代码。我们将从调用组成部署过程的自定义函数开始,然后,我们将讨论函数本身。

创建应用程序版本标签

首先,我们创建一个应用程序版本标签。如果您还记得的话,这个标签必须是唯一的,但是除此之外不强制任何特定的格式。这段代码将创建一个包含 Octopus 包 ID、包版本和 Octopus 部署 ID 的标签。这种组合确保 Octopus 执行的任何部署都将产生唯一的版本标签。

这样做的最终结果是一个类似于RandomQuotes.1.0.1+45.Deployments-4147的字符串。

$VersionLabel = $OctopusParameters["Octopus.Action.Package[RandomQuotes].PackageId"] +
    "." +
    $OctopusParameters["Octopus.Action.Package[RandomQuotes].PackageVersion"] +
    "." +
    $OctopusParameters["Octopus.Deployment.Id"] 

创建清单文件

正如我们前面讨论的,部署到 Beanstalk 的归档文件包含一个清单文件和一个包含应用程序代码的嵌套归档文件。我们上传到 Octopus 的工件只包含编译过的。NET 代码,但不是清单文件。所以我们在这里创建了清单文件:

New-ManifestFile "random-quotes" "site.zip" 

New-ManifestFile函数保存一个名为aws-windows-deployment-manifest.json的 JSON 文件:

function New-ManifestFile($name, $file) {
    Set-Content -Path "aws-windows-deployment-manifest.json" -Value @"
    {
      "manifestVersion": 1,
      "deployments": {
          "aspNetCoreWeb": [
          {
              "name": "$name",
              "parameters": {
                  "appBundle": "$file",
                  "iisPath": "/",
                  "iisWebSite": "Default Web Site"
              }
          }
          ]
      }
  }
"@
} 

调用该函数的结果是一个名为aws-windows-deployment-manifest.json的文件,其内容如下:

{
  "manifestVersion": 1,
  "deployments": {
      "aspNetCoreWeb": [
      {
          "name": "random-quotes",
          "parameters": {
              "appBundle": "site.zip",
              "iisPath": "/",
              "iisWebSite": "Default Web Site"
          }
      }
      ]
  }
} 

创建 Beanstalk 部署档案

现在我们有了清单文件,我们需要将它和包含应用程序代码的嵌套存档一起添加到 ZIP 存档中。

的。NET 应用程序存档作为参考包包含在此步骤中。我们将这个包称为RandomQuotes,并将其设置为在部署期间提取,这意味着可以在$OctopusParameters["Octopus.Action.Package[RandomQuotes].ExtractedPath"]变量引用的路径下找到包的内容。

引用包的摘要。

参考包的详细信息。

因为我们已经启用了JSON configuration variables特性,并将其配置为处理名为appsettings.json的文件,所以 Octopus 变量AppSettings:EnvironmentName的值将替换 JSON 文件中现有的EnvironmentName。这样,我们就从一个通用的应用程序包中创建了一个特定于环境的部署。

您可以以同样的方式使用substitute variables in files功能。

台阶特征。

JSON 配置变量设置。

用章鱼提取和处理了。NET 核心应用程序包,我们将文件压缩回一个名为site.zip的文件中:

# Compress the extracted DotNET application code
Compress-Archive `
    -Path "$($OctopusParameters["Octopus.Action.Package[RandomQuotes].ExtractedPath"])\*" `
    -DestinationPath "site.zip" 

接下来,我们创建第二个归档文件,包含 Beanstalk 清单文件和。NET 核心应用程序档案:

# Compress the application code with the manifest file to create the Beanstalk deployment    
Compress-Archive `
    -Path "site.zip", "aws-windows-deployment-manifest.json" `
    -DestinationPath "$VersionLabel.zip" 

这样做的最终结果是一个类似于RandomQuotes.1.0.1+45.Deployments-4147.zip的文件。

把文件上传到 S3

Beanstalk 应用程序档案首先被上传到 S3。大多数 AWS 服务使用 S3 作为使用应用程序代码的舞台,Beanstalk 也不例外。

# Upload the Beanstalk deployment to S3    
Add-File "$VersionLabel.zip" $BucketName "$VersionLabel.zip" 

Add-File函数是对 AWS CLI 上传文件的简单调用:

function Add-File($file, $s3Bucket, $s3Key) {
  Write-Host "Uploading File"
  aws s3 cp $file "s3://$s3Bucket/$s3Key" | Out-Null
} 

创建应用程序版本

使用 S3 中的代码,我们可以创建一个新的应用程序版本,将工件与版本标签相关联。

变量$Application$BucketName由 Octopus 提供,并映射到项目变量的值。

# Use the new file in S3 to create a Beanstalk application version
New-ApplicationVersion $Application $VersionLabel $BucketName "$VersionLabel.zip" 

AWS CLI 用于使用我们上传到 S3 的文件,并为其分配一个版本标签。完成后,我们的 Beanstalk 应用程序将有一个新的应用程序版本,可以部署到一个环境中:

function New-ApplicationVersion($application, $version, $s3Bucket, $s3Key) {
  Write-Host "Creating application version $version"
  aws elasticbeanstalk create-application-version `
      --application-name $application `
      --version-label $version `
      --source-bundle S3Bucket="$s3Bucket",S3Key="$s3Key" |
      Out-Null
} 

等待环境处于就绪状态

如果由于某种原因 Beanstalk 环境已经被更新了(可能是通过 AWS 控制台进行的更改),我们需要等待它进入Ready状态。我们通过调用Wait-ForEnvironmentToBeReady来做到这一点。

变量$Environment由 Octopus 提供,映射到当前部署环境范围内的变量值。

Wait-ForEnvironmentToBeReady  $Application $Environment 

Wait-ForEnvironmentToBeReady功能轮询环境描述并等待状态为ReadyTerminated:

function Wait-ForEnvironmentToBeReady ($application, $environment) {
    do {
      $result = aws elasticbeanstalk describe-environments `
          --environment-names $environment `
          --application-name $application `
          --output json |
          ConvertFrom-Json |
          Select-Object -ExpandProperty Environments |
          Select-Object -First 1

      if ($null -eq $result) {
          throw "Could not find the environment $environment in the application $application"
      }

      Write-Host "Environment $environment is $($result.Status)"
      Start-Sleep 10
    } while (-not ($result.Status -in @("Ready", "Terminated")))
  } 

创建应用程序版本并更新环境

我们现在在 Beanstalk 中创建了一个新的应用程序版本,所以下一步是将其部署到一个环境中。对Update-Environment的调用是部署到 Beanstalk 的地方:

# Deploy the application version to the environment
Update-Environment $Application $Environment $VersionLabel 

在 Beanstalk 中,用应用程序版本“更新”环境是我们部署新代码的方式:

function Update-Environment($application, $environment, $version) {
  Write-Host "Updating Environment $environment to $version"
  aws elasticbeanstalk update-environment `
      --application-name $application `
      --environment-name $environment `
      --version-label $version |
      Out-Null
} 

等待部署完成

我们最后一次调用Wait-ForEnvironmentToBeReady来等待新的应用程序版本被部署到环境中。通话结束后,部署就完成了:

# Wait for the new deployment to finish
Wait-ForEnvironmentToBeReady  $Application $Environment 

执行部署

让我们继续执行到Dev环境的部署。

部署日志。

日志消息Performing JSON variable replacement on 'C:\Octopus\Work\20190902230557-24738-504\RandomQuotes\appsettings.json'显示 Octopus 已经成功处理了appsettings.json文件,并注入了我们想要覆盖的值。

豆茎应用程序包被上传到 S3。

S3 水桶。

应用程序版本已创建。

应用程序版本。

最后,使用新的应用程序版本更新了环境。

环境。

如果我们打开最终部署的应用程序,我们可以看到它显示了我们部署到的名为Dev的环境。这与appsettings.json文件中的默认值有细微的变化,该文件将环境列为DEV(全部大写)。

开发环境中部署的应用程序。

将部署提升到Test环境使得替换更加清晰。

T32

测试环境中部署的应用程序。

结论

在本文中,我们讨论了 Beanstalk 服务的高级架构,然后实现了一个定制的 PowerShell 脚本,该脚本通过以下方式将应用程序部署到 Beanstalk:

  • 创建一个 Beanstalk 清单文件。
  • 创建一个 Beanstalk 档案,包括。NET 核心应用程序档案和清单文件。
  • 把文件上传到 S3。
  • 从 S3 的文件创建应用程序版本。
  • 用应用程序版本更新环境。

我们构建这个过程是为了让我们利用 Octopus 的特性,比如 JSON 配置变量。这意味着我们可以部署特定于环境的应用程序,而不仅仅依赖于环境变量。

部署到牧场主与章鱼部署-章鱼部署

原文:https://octopus.com/blog/deploy-to-rancher-with-octopus

Deploy to Rancher with Octopus Deploy

从命令行管理 Kubernetes 既麻烦又乏味,尤其是当您有多个集群需要管理时。为了减轻这一负担,已经开发了一些工具来轻松地创建和管理 Kubernetes 集群。牧场主就是一个例子。在本文中,我将向您展示如何将由 Rancher 管理的 Kubernetes 集群添加到 Octopus Deploy 中,作为您可以部署到的部署目标。

Rancher 入门

Rancher 是独一无二的,因为你可以在任何地方安装它。Rancher 在 Docker 容器中运行,并且可以在安装了 Docker 的任何地方运行。这是您开始工作所需的全部内容:

$ sudo docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher 

当然,还有更高级的高可用性安装和一系列其他的选项,但是对于测试 Rancher,这就是你所需要的。

当容器启动并运行时,用浏览器连接到它并设置admin密码。设置密码后,您就可以创建集群了。

创建集群

牧场主可以与:

  • 内部基础设施
  • 云基础设施提供商:
    • 亚马逊 EC2
    • 蔚蓝的
    • 数字海洋
    • 利诺德
    • vShere
  • 云构建服务:
    • 亚马逊弹性库柏服务(EKS)
    • 蓝色库柏服务(AK)
    • 谷歌 Kubernetes 引擎(GKE)

在本文中,我创建了两个 Kubernetes (K8s)集群来演示使用 Rancher 和 Octopus Deploy 的多功能性;一个在内部,一个使用云 Kubernetes 服务。

第一组

Rancher 使创建集群的过程变得非常简单和容易。在 UI 中,点击添加集群

我为我的本地集群创建了三个 Ubuntu 虚拟机,所以我从现有节点(自定义)中选择了作为我的第一个集群:

**Rancher select cluster type screen

我在下一个屏幕上给这个集群命名,接受默认值,然后点击 Next

创建过程的最后一个屏幕显示了三个复选框。由 Rancher 管理的集群需要有充当三种角色的节点:

选中或取消选中一个框会更新屏幕上的命令,以便针对集群成员运行。可以为一个节点分配所有三个角色,但是对于本文,我为每个虚拟机选择了一个角色(图中显示了选择的所有三个选项):

Rancher node roles

在每个节点上运行命令时,屏幕上将显示一个弹出窗口,指示有多少节点已注册到集群。添加完节点后,单击 Done 开始配置过程。配置过程完成后,您将拥有一个全新的集群。

第二组

对于第二个集群,我选择使用 GKE 的云 Kubernetes 服务:

Rancher select cluster type screen

GKE 的创建流程与内部创建略有不同:

  1. 创建有足够权限创建集群资源的 Google 服务帐户
  2. 为服务帐户创建一个 JSON 密钥,因为 Rancher 需要这个密钥向 Google 进行认证。
  3. 粘贴 JSON(或使用从文件中读取按钮),点击下一步:配置节点

与内部设置不同,在 GKE 上创建集群是自动进行的。使用服务帐户,Rancher 连接到 Google 并为您提供所有资源。

将集群连接到 Octopus Deploy

Rancher 不仅提供了一个集中的接口来管理 Kubernetes 集群,还提供了一个集中的集群通信方式。这意味着 Octopus Deploy 可以连接到 Rancher,而不是连接到它单独管理的集群。

证明

在添加 Rancher 管理的集群之前,我们必须创建一种对其进行身份验证的方法。这可以通过使用 Rancher UI 创建一个访问密钥来实现。

  1. 登录 Rancher,然后点击右上角的个人资料
  2. 选择 API &键
  3. 点击添加键
  4. 给 API 密钥一个有效期和范围。
  5. 添加一个描述,这样你就知道这个键的用途了,然后点击创建

单击 create 后,您将看到 API 密钥信息。请保存此信息,因为您以后将无法检索到它。

牧场主集群端点

您可以通过 Rancher 代理与集群的通信。您可以在 Rancher 中使用 API 端点来发出命令,而不是直接连接到各个 K8s API 端点。URL 的格式为:https://<RancherUrl>/k8s/clusters/<ClusterId>

找到正确 URL 的一个快速方法是从提供的 Kubeconfig 文件信息中获取它。对于您定义的每个集群,Rancher 提供了一个可以直接从 UI 下载的Kubeconfig file。要找到它,从全球仪表盘中选择您需要的集群,并点击 Kubeconfig 文件按钮。

下一个屏幕显示了 Kubeconfig 文件,该文件包含将集群连接到 Octopus Deploy 所需的特定 URL:

Kubeconfig file

将帐户添加到八达通部署

对于部署到集群的 Octopus Deploy,它需要登录的凭证。在 Octopus Web 门户中,导航到基础设施选项卡并单击帐户,我们将添加我们在 Rancher 中创建的 API 密钥令牌:

  1. 点击添加账户
  2. 选择您要创建的帐户类型。
  3. 输入您选择的值,然后点击保存

现在我们已经创建了一个帐户,我们准备创建我们的 Kubernetes 目标。

创建一个 Kubernetes 部署目标

准备工作完成后,现在可以将牧场主管理的 Kubernetes 集群添加到 Octopus Deploy 中。添加目标的方式与添加任何其他 Kubernetes 目标完全相同。

  1. 点击 基建➜部署目标
  2. 点击添加部署目标
  3. 点击 KUBERNETES 集群类别。
  4. 然后在 Kubernetes 集群上点击添加

Kubernetes 部署目标表单的两个最重要的部分是:

  • 证明
  • Kubernetes 详细信息

证明

选择与您选择的连接到 Rancher 的方式相对应的单选按钮。我选择了 Token。

Kubernetes 详细信息

这是我们使用从 Rancher 的 kubeconfig 文件中获取的 URL 的地方。第一个集群是https://rancher1/k8s/clusters/c-v4cbx。我的集群正在使用自签名证书,因此我选择了跳过 TLS 验证

我为我的集群创建了三个名称空间:

  • 发展
  • 试验
  • 生产

点击保存就完成了。

要验证配置,您可以观察初始运行状况检查。

部署到集群

部署到通过 Rancher 管理的集群与部署到非 Rancher 管理的集群是一样的。

为了演示,我将使用与我的超越 Hello World:构建真实世界的 Kubernetes CI/CD 管道帖子中相同的示例流程,改为针对 Rancher 托管集群进行修改。

当我部署该版本时,我可以看到它在本地集群 Rancher-dev 和 Google 云集群上执行:

【T2

现在,我们已经成功部署到由 Rancher 管理的 Kubernetes 集群。

结论

在本文中,我演示了如何在 Rancher 中定义 Kubernetes 集群,然后将 Rancher 与 Octopus Deploy 集成来部署您的项目。

观看网络研讨会

https://www.youtube.com/embed/6ZILha86JDo

VIDEO

我们定期举办网络研讨会。请参见网络研讨会第页,了解以往网络研讨会的档案以及即将举办的网络研讨会的详细信息。

愉快的部署!**

通过 Octopus - Octopus Deploy 将您的第一个容器部署到 Kubernetes

原文:https://octopus.com/blog/deploy-your-first-container-to-kubernetes

Deploy your first container to Kubernetes via Octopus

之前的帖子中,我们看到了如何用 Kind 创建一个本地测试 Kubernetes 集群,并在 Octopus 中配置它。在本文中,我们将学习如何使用 Octopus 中的步骤向本地 Kubernetes 集群部署和公开单个 Docker 容器。

创建并推送 Docker 映像

Octopus 使用提要从 Docker 存储库中访问 Docker 图像。有许多工具可以让你托管自己的 Docker 存储库,但是当你第一次开始时,公共存储库 Docker Hub 是最简单的选择。

如果您还没有帐户,请创建一个新帐户,然后使用命令docker login登录 Docker Hub:

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: mcasperson
Password:

Login Succeeded 

接下来,我们需要构建 Docker 映像。在这篇文章中,我们将使用来自 https://github.com/OctopusSamples/RandomQuotes-Java的样例应用程序。使用命令docker build . -t mcasperson/mywebapp:0.1.7构建映像(用 Docker Hub 用户名替换mcasperson)。

很重要的一点是,标签(本例中为0.1.7)是一个有效的 SemVer 版本字符串。Docker 标签不强制任何版本规则,但是 Octopus 希望它部署的所有包都可以被比较以找到最新的。这是通过要求 Docker 标签是 SemVer 字符串来实现的。

Octopus 会忽略没有 SemVer 兼容标签的 Docker 图像。

构建完成后,您可以使用命令docker images "mcasperson/mywebapp"验证映像是否已经创建:

$ docker images "mcasperson/mywebapp"
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
mcasperson/mywebapp   0.1.7               fadf80ecf48a        1 second ago        129MB 

最后,使用命令docker push mcasperson/mywebapp:0.1.7将图像推送到 Docker Hub:

$ docker push mcasperson/mywebapp:0.1.7
The push refers to repository [docker.io/mcasperson/mywebapp]
d817461c3564: Pushed
11276c4aac8e: Pushed
f955d35132bf: Pushed
edd61588d126: Mounted from library/openjdk
9b9b7f3d56a0: Mounted from library/openjdk
f1b5933fe4b5: Mounted from library/openjdk
0.1.7: digest: sha256:0eb09072c3ab7768e9e5f9cae994e63a2d5c8d6957a2d0cd85baae31ee8cc6d7 size: 1573 

一旦推送,图像就可以在 Docker Hub 上查看。

创建 Docker 提要

Octopus 在部署过程中引用的所有包都来自提要。为了在 Kubernetes 部署中使用我们的新 Docker 映像,我们需要通过 URLhttps://index . Docker . io将 Docker Hub 配置为 Docker 提要:

然后,我们可以搜索我们的新图像:

部署映像

Octopus 附带了许多支持 Kubernetes 部署的步骤。概括地说,它们分为三类:

  • 针对部署、服务、进入、机密和配置映射的自以为是、以用户界面为中心的步骤。
  • 生 YAML 的部署。
  • 舵图的展开。
  • 针对kubectl的自定义脚本。

因为这是我们第一次部署到 Kubernetes 集群中,固执己见的步骤将使我们快速启动并运行,而无需了解 Kubernetes YAML 的详细信息,因此我们将向我们的 Octopus 项目添加一个部署 Kubernetes 容器步骤:

这一步将 Kubernetes 部署资源与可选的服务、入口、秘密和配置映射结合起来。在 Kubernetes 中部署应用程序时,这些资源通常作为一个紧密耦合的单元一起部署。但是,在我们的示例中,我们不会部署入口、秘密或配置映射,因此可以禁用这些功能以简化步骤 UI:

该步骤公开了大量选项,但是对于这个示例,我们只需要注意两个选项。

第一个是容器的定义,第二个是服务端口。下面的截图突出显示了这些内容:

容器定义引用了我们之前推送到 Docker Hub 的映像:

它还公开了 TCP 端口 80 和一个名为 web 的 Kubernetes 端口:

然后,服务端口将容器上的端口 80 公开为服务上的端口 80:

这就是我们将我们的映像部署到 Kubernetes 所需的全部配置。当我们部署这个项目时,Octopus 将在幕后执行一些逻辑来创建 Kubernetes 部署和服务资源,并将两者链接在一起。链接这些资源使我们从一些手工工作中解脱出来,否则将需要用服务来公开部署。

创建 Octopus 部署时要注意的一件事是,我们在部署时选择 Docker 映像版本(如果您还记得,这是我们在构建映像时分配给它的标记)。使用 Octopus 管理 Kubernetes 部署的优点之一是在部署时选择映像版本,并在默认情况下选择最新版本。通常,Docker 映像的新版本不需要对引用它们的 Kubernetes 资源进行任何更改,因此只需创建一个新的 Octopus 部署并引用新的 Docker 映像,就可以将新版本的代码推送到 Kubernetes:

当部署完成时,我们可以用命令kubectl get deployments验证 Kubernetes 包含部署资源:

$ kubectl get deployments
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
randomquotes   1/1     1            1           20m 

然后,我们验证部署使用命令kubectl get pods创建了 pod:

$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
randomquotes-65cbb7c849-5vvnw   1/1     Running   0          30s 

然后我们验证服务是用命令kubectl get service randomquotes创建的:

$ kubectl get service randomquotes
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
randomquotes   ClusterIP   10.99.245.202   <none>        80/TCP    19m 

为了从我们的本地 PC 访问服务,我们需要使用kubectl将一个本地端口代理到服务端口,这是我们使用命令kubectl port-forward svc/randomquotes 8081:80完成的。然后,我们可以在 http://localhost:8081:

通过 Octopus 检查集群

对于经验丰富的 Kubernetes 管理员来说,在本地运行kubectl非常好,并且在创建新的部署来调试和验证事情是否按预期工作时经常需要。不过,它也有一些缺点:

  • 它需要在本地安装kubectl,并配置管理员凭证。
  • 如果凭证改变,本地kubectl配置需要手动更新。
  • 对群集执行的操作可能很难在以后重新构建。
  • 决策所依据的值通常会丢失,例如 pod 日志的内容或 pod 的状态。
  • 使用kubectl需要对 Kubernetes 管理有很好的了解。

随着 Kubernetes 基础设施的成熟,自动化常见的管理任务是有利的。通过自动化这些任务,机构知识可以嵌入到 Octopus 中,从而更容易将日常操作交给可能不是 Kubernetes 专家的团队。

为了自动化我们在上一节中对kubectl的调用,我们将利用一个名为Kubernetes-Inspect Resources的社区步骤模板:

首先,我们将获得部署列表。这是通过将资源选项设置为Deployment,将库对象动词设置为Get,并通过将创建工件设置为True来捕获输出来实现的:

现在,任何人都可以通过 Octopus UI 运行该运行手册,解决了上述限制:

  • 这些凭证由 Octopus 管理,只需在一个地方更新。
  • kubectl可执行文件只需要在 worker 上可用,消除了用户在本地安装该工具的需要。
  • 这个 runbook 的输出被捕获到 Octopus 日志中,并作为该步骤生成的一个工件,使得返回并查看导致采取某些操作的集群的状态变得容易。
  • kubectl命令嵌入在一个步骤中,不需要支持人员记忆命令。

现在让我们添加第二个步骤来查询 pod。注意这里的资源名称已经被设置为randomquotes\*。这是由Kubernetes-Inspect Resources步骤增加的便利,允许 Kubernetes 资源通过通配符进行匹配,这在kubectl中是不可用的。这对于由部署创建的 pod 尤其方便,因为 Kubernetes 会为这些 pod 名称分配随机后缀:

最后,我们得到了服务:

在此基础上,我们创建了一个可以由支持人员运行的操作手册,作为诊断集群任何问题的第一步,生成的工件提供了一个有用的日志集合,可以在以后查看或传递给更高级别的支持。

结论

在这篇文章中,我们编译了一个新的 Docker 映像并将其推送到 Docker Hub,将 Docker Hub 作为一个提要添加到 Octopus 中,然后将该映像作为一个 Kubernetes 部署进行部署,该部署由一个服务公开给我们在上一篇博文中用 Kind 创建的测试集群。

有人说 Kubernetes 让简单的事情变得困难,让困难的事情成为可能。即使是一个简单的 Kubernetes 部署看起来也有许多移动部分,但是如果您已经达到了这一步,那么您已经实现了一个坚实的基础,可以在此基础上使用 Kubernetes 构建更复杂的生产就绪型基础设施。事实上,即使是这个简单的例子,您也已经创建了:

  • 任何人都可以通过 Octopus 门户网站启动可重复的部署,不需要本地工具或 Kubernetes 专业知识。
  • 得益于 Octopus 的内置功能,可对部署进行审核。
  • 通过许多可用的插件,一个连续的交付管道随时可以从 CI 系统中触发。
  • 多环境部署的基础——参见本指南的了解更多详情。
  • 一些帮助负责集群的支持人员的初步操作手册。

使用管理器应用程序- Octopus Deploy 在 Docker 中启动 Tomcat

原文:https://octopus.com/blog/deployable-tomcat-docker-containers

Booting Tomcat in Docker with the manager app

当使用 Tomcat 测试 Java 部署时,官方的 Tomcat Docker 映像提供了一种方便的方法来启动和运行服务器。但是,要加载和访问管理器应用程序,有一些技巧。

在这篇博客文章中,我们将讨论如何引导 Tomcat Docker 映像来接受新的部署。

定义用户

首先,我们需要定义一个可以访问 manager 应用程序的 Tomcat 用户。该用户在名为tomcat-users.xml的文件中定义,将被分配manager-guimanager-script角色,这两个角色授予对经理 HTML 界面以及 API 的访问权限:

<tomcat-users>
  <role rolename="manager-gui"/>
  <role rolename="manager-script"/>
  <user username="tomcat" password="s3cret" roles="manager-gui,manager-script"/>
</tomcat-users> 

揭发经理

默认情况下,管理器应用程序将只接受来自localhost的流量。请记住,从 Docker 映像的上下文来看,localhost意味着容器的环回接口,而不是主机的环回接口。启用端口转发后,到公开端口的流量通过容器的外部接口进入 Docker 容器,默认情况下会被阻止。这里我们有一个禁用了网络过滤的管理器应用程序context.xml文件的副本:

<Context antiResourceLocking="false" privileged="true" >
  <!--
    <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
  -->
  <Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>     
</Context> 

这篇博客文章提供了更多关于码头工人与转发端口联网的细节。

运行容器

需要跨越的最后一个障碍是,Tomcat Docker 映像默认情况下不加载任何应用程序。默认应用程序,如管理器应用程序,保存在一个名为/usr/local/tomcat/webapps.dist的目录中。我们需要将这个目录移动到/usr/local/tomcat/webapps。这是通过覆盖启动容器时使用的命令来实现的。

下面的命令映射我们上面创建的两个定制 XML 文件(在本例中保存到/tmp,将/usr/local/tomcat/webapps.dist移动到/usr/local/tomcat/webapps,最后启动 Tomcat:

sudo docker run \
  --name tomcat \
  -it \
  -p 8080:8080 \
  -v /tmp/tomcat-users.xml:/usr/local/tomcat/conf/tomcat-users.xml \
  -v /tmp/context.xml:/tmp/context.xml \
  tomcat:9.0 \
  /bin/bash -c "mv /usr/local/tomcat/webapps /usr/local/tomcat/webapps2; mv /usr/local/tomcat/webapps.dist /usr/local/tomcat/webapps; cp /tmp/context.xml /usr/local/tomcat/webapps/manager/META-INF/context.xml; catalina.sh run" 

访问管理应用程序

要打开管理器应用程序,请打开 URL http://localhost:8080/manager/html。输入tomcat作为用户名,输入s3cret作为密码:

结论

Tomcat 映像维护人员选择不启用默认应用程序作为安全预防措施,但是通过两个自定义配置文件和覆盖 Docker 命令,可以用一个全功能的管理器应用程序来引导 Tomcat。

在本文中,我们提供了示例配置文件和 Docker 命令,以便在运行 Tomcat 之前恢复默认应用程序。

部署失败。ps1 支持- Octopus 部署

原文:https://octopus.com/blog/deployfailed

今天的 Octopus Deploy 版本包括对一个DeployFailed.ps1脚本的支持,这个特性我在最近的一篇关于 Octopus 如何处理回滚的博文中提到过。

背景

在部署过程中,触手通常会运行以下步骤:

  1. 从 NuGet 中提取包
  2. 运行PreDeploy.ps1
  3. 运行 XML 配置转换
  4. 替换 XML 配置appSettingsconnectionStrings
  5. 运行Deploy.ps1
  6. 配置 iis 网站
  7. 运行PostDeploy.ps1

(有关这些步骤的详细信息,请参见我们在 XML 配置PowerShell 脚本支持IIS 网站上的页面)

部署失败. ps1

如果第 2-7 步失败,那么现在触手将寻找一个DeployFailed.ps1脚本,并调用它。它将可以访问其他 PowerShell 脚本获得的相同的变量。这是一个放置恢复操作的好地方。

请注意,如果步骤 1 失败,则DeployFailed.ps1 不会被调用。这有几个原因:

  1. DeployFailed.ps1无论如何也不可能被提取
  2. 它所依赖的文件可能没有被提取
  3. 在安装包之前,触手会检查可用的磁盘空间,所以这种情况应该很少发生
  4. 解压软件包是一项独立的任务,所以首先没有什么要恢复的

一个很好的特性是传递最后安装的包的路径,恢复脚本可以在回滚时使用它。触须还没有足够的信息来做到这一点,但当自动清除触须实现时,它会做到,因为两者都需要保存一份以前安装的应用程序的列表。

Selenium 系列:部署简单的 Lambda 函数——Octopus Deploy

原文:https://octopus.com/blog/selenium/31-deploying-a-simple-lambda-function/deploying-a-simple-lambda-function

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

在之前的文章中,我们配置了 Lambda 函数所需的所有先决条件:

  • 创建了一个 AWS 帐户,并在本地配置了凭据。
  • 无服务器应用程序已安装。
  • Lambda Chrome 发行版和二进制驱动程序被上传到 S3。
  • Maven build 现在生成了一个 UberJAR。

我们现在正处于可以开始编写 Lambda 代码的阶段。为此,我们需要添加三个新的依赖项:

  • com.amazonaws:aws-lambda-java-core
  • com.amazonaws:aws-java-sdk-lambda
  • commons-io:commons-io

前两个依赖项为我们提供了作为 Lambda 函数运行所需的库。第三个依赖项在处理文件时提供了一些方便的实用函数:

<project 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <!-- ... -->
  <properties>
    <!-- ... -->
    <aws.lambda.version>1.2.0</aws.lambda.version>
    <aws.sdk.version>1.11.305</aws.sdk.version>
    <commons.io.version>2.6</commons.io.version>
  </properties>
  <!-- ... -->
  <dependencies>
    <!-- ... -->
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>${aws.lambda.version}</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-java-sdk-lambda</artifactId>
      <version>${aws.sdk.version}</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>${commons.io.version}</version>
    </dependency>
  </dependencies>
</project> 

在传统的 Java 应用程序中,我们以一个static main()方法开始执行。Lambda 函数则不同。Lambda 函数的入口点可以是任何带有签名的方法:

returntype methodname(inputType input, Context context) 

或者,如果不需要Context(我们的目的也不需要),则该签名也有效:

returntype methodname(inputType input) 

此方法的返回和输入类型可以是任何类型,方法本身可以有任何名称。让我们写一个最简单的 Lambda 函数。

下面的代码定义了一个 Lambda 函数,该函数返回一个总是为trueboolean类型。这段代码不是很有用,但足以测试 Lambda 的工作情况:

package com.octopus;

import com.amazonaws.services.lambda.runtime.Context;

public class LambdaEntry {
  public boolean runCucumber(String feature) throws Throwable {
    return true;
  }
} 

为了部署这个 Lambda,我们需要在项目的根目录下创建一个名为serverless.yml的文件。无服务器应用程序使用这个配置文件来配置和部署 Lambda。

C:\0b4b22e1bd61e3d5249b0db50c8cde7f

service:
  name: cucumber-chrome-aws

provider:
  name: aws
  runtime: java8
  region: us-east-1

package:
  artifact: target/webdrivertraining-1.0-SNAPSHOT.jar

functions:
  runCucumber:
    handler: com.octopus.LambdaEntry::runCucumber
    timeout: 300
    memorySize: 512 

让我们把这个文件分解一下。

我们首先定义服务的名称,它将成为 Lambda 的名称:

service:
  name: cucumber-chrome-aws 

然后,我们定义要部署到的云平台的详细信息。该无服务器应用程序与云无关,可用于部署到多个云提供商,如 AWS、Azure 和 Google Cloud。我们使用 AWS,因此 providers 部分将配置 AWS Lambda 服务的全局属性。

name属性是云提供商的名称,在本例中设置为aws

runtime属性定义了编写 Lambda 函数的语言,即java8

region属性定义了我们将 Lambda 部署到的 AWS 区域。AWS 在全球有很多地区,你可以在https://docs . AWS . Amazon . com/general/latest/gr/rande . html # Lambda _ region找到支持 Lambda 的完整地区列表。这里我们将使用us-east-1区域:

provider:
  name: aws
  runtime: java8
  region: us-east-1 

package 部分定义了 Lambda 代码的位置。在我们的例子中,Lambda 代码在文件target/webdrivertraining-1.0-SNAPSHOT.jar中,我们通过artifact属性引用它。请注意,这个文件是 UberJAR,它将我们的整个应用程序及其依赖项打包在一个文件中:

package:
  artifact: target/webdrivertraining-1.0-SNAPSHOT.jar 

functions部分是我们定义 Lambda 函数的地方。

runCucumber部分定义了一个单独的功能。这个部分可以有任何名称,为了方便起见,我们使用了与入口点方法相同的名称。

属性定义了入口点方法名。这个方法名由完全限定的类名、两个冒号和方法名组成。值com.octopus.LambdaEntry::runCucumber意味着这个 Lambda 函数将执行com.octopus包中LambdaEntry类的方法runCucumber()

timeout属性设置该函数可以运行的最长时间。Lambda 有一个 5 分钟的硬限制,我们也将超时设置为 5 分钟(表示为 300 秒)。

属性定义了我们的 Lambda 环境可以使用多少内存。我们已经把自己限制在 512MB 了。请注意,该值包括外部应用程序(如 Chrome)使用的任何内存,以及我们自己的代码。

增加timeoutmemorySize会增加每个 Lambda 执行的成本:

functions:
  runCucumber:
    handler: com.octopus.LambdaEntry::runCucumber
    timeout: 300
    memorySize: 512 

在部署 Lambda 函数之前,我们需要确保文件target/webdrivertraining-1.0-SNAPSHOT.jar是最新的。在部署之前,Serverless 不会为我们重新构建应用程序,因此由我们手动重新构建。点击 Maven 项目➜包来重建 JAR 文件。

C:\929577e8ad0a8809d3e7d19cfcf21570

我们现在可以部署 Lambda 函数了。打开终端、命令提示符或 PowerShell 窗口,并切换到项目根目录。然后运行命令:

$ serverless deploy 

您将看到如下输出:

Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (19.78 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.....................
Serverless: Stack update finished...
Service Information
service: cucumber-chrome-aws
stage: dev
region: us-east-1
stack: cucumber-chrome-aws-dev
api keys:
  None
endpoints:
  None
functions:
  runCucumber: cucumber-chrome-aws-dev-runCucumber 

Serverless 在后台做了很多工作来上传我们的 JAR 文件作为 Lambda 函数,这是使用 Serverless 而不是手动上传我们的 Lambda 函数的好处之一。

如果我们返回 AWS Lambda 控制台,我们现在可以看到新的 Lambda 函数已经部署。单击功能链接。

C:\30bd315ba4b2ee81f05c718344025294

本页向我们展示了 Lambda 函数的细节。

要测试该功能是否工作,点击Test按钮。

C:\b24d832e8c4c491083526bb2ca815e1d

用字符串替换测试数据(任何字符串都可以)。因为我们的 Lambda 函数的第一个参数接受一个字符串,所以我们需要在测试时提供一个字符串。

Lambda 函数总是将 JSON 作为输入,它被转换成一个 Java 对象。在这种情况下,字符串是一个有效的 JSON 结构,然后被转换成 Java 字符串。

Lambda 函数也总是将返回的对象转换成 JSON。

Lambda 函数只接受 JSON 作为输入,并提供 JSON 作为输出,这一事实在我们稍后将这个函数链接到 HTTP 端点时非常重要。

然后填充Event名称字段,并点击Create按钮。

C:\1d66f3aba2a8529623be190d8215c526

现在我们有了一个测试事件,再次点击Test按钮。

C:\9f1e25d4726ec56aade10c28d902412b

我们的测试 Lambda 函数已经通过返回true成功执行。

C:\25e8e70a701bd868bc63f982e0521af0

尽管这个 Lambda 函数没有做任何有用的事情,但它确实证明了我们已经编写了一个有效的 Lambda 函数,并且可以使用无服务器应用程序来部署它。完成这些工作后,我们可以继续编写实际运行 WebDriver 测试的 Lambda 函数,这将在下一篇文章中进行。

这篇文章是关于创建 Selenium WebDriver 测试框架的系列文章的一部分。

使用 GitHub Actions 部署到亚马逊 EKS-Octopus Deploy

原文:https://octopus.com/blog/deploying-amazon-eks-github-actions

在 DevOps 流程中,CI 服务器(如 Github Actions)构建代码存储库,并将软件工件推送到容器注册中心,准备进行部署。在 GitHub Actions 推出之前,像 Jenkins 这样的第三方工具必须在 GitHub 存储库上执行 DevOps 操作。

GitHub Actions 在您的 GitHub 存储库中引入了 DevOps 操作,使您更容易实现 DevOps 流程。

在本文中,您将在 GitHub Actions 工作流中构建一个 Docker 映像,将该映像发布到 Amazon Elastic Container Registry(ECR ),并将其部署到 Amazon Elastic Kubernetes Service(EKS)。

先决条件

要跟进这篇文章,你需要:

  • 亚马逊网络服务(AWS)帐户
  • GitHub 账户

这个帖子使用了 Octopus 水下应用库。您可以派生存储库并跟随它。或者,github-deployment 分支包含完成本文中的步骤所需的模板文件。你必须用你自己的价值观来代替一些价值观,但是我已经把我的价值观作为参考。

亚马逊网络服务设置

要为 GitHub 操作设置 AWS,您需要创建一个访问键和一个 ECR 存储库来存储图像。

要创建访问密钥,请转到亚马逊控制台,然后 IAM ,然后用户[your user],然后安全凭证,然后创建访问密钥

您的浏览器将下载一个包含访问密钥 ID 和秘密访问密钥的文件。这些值将在 Jenkins 中用于向 Amazon 认证。

要创建存储库,请转到亚马逊控制台,然后转到 ECR ,然后转到创建存储库

您需要为发布的每个图像建立一个图像存储库。给存储库起一个您想让图像起的名字。

你会在亚马逊 ECR 下看到你的仓库,然后是仓库。记下它所在的区域,在 URI 场。

ECR Repository

AWS 集群设置

按照我们的文章中的步骤在 AWS 中设置集群,在 AWS 中创建 EKS 集群

GitHub 设置

对于这个示例,您使用一个示例 web 应用程序,该应用程序显示一个带有有用链接的水下动画场景。

https://github.com/OctopusSamples/octopus-underwater-app分叉存储库。

进入设置,然后机密,然后新储存库机密

  • REPO_NAME -您创建的 AWS ECR 存储库的名称
  • AWS_ACCESS_KEY_ID -之前的访问密钥 ID
  • AWS _ SECRET _ ACCESS _ KEY-之前的秘密访问密钥
  • AWS_ACCOUNT_ID -您的亚马逊账户 ID

首先,您需要为 GitHub actions 创建一个部署 YAML 文件,以部署到 EKS。用下面的代码在存储库的根级别创建一个名为git-deployment.yml的文件:

 apiVersion: apps/v1
kind: Deployment
metadata:
  name: underwater-app-github
  labels:
    app: octopus-underwater-app
spec:
  selector:
    matchLabels:
        app: octopus-underwater-app
  replicas: 3
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: octopus-underwater-app
    spec:
      containers:
        - name: octopus-underwater-app
          image: 720766170633.dkr.ecr.us-east-2.amazonaws.com/octopus-underwater-app:latest
          ports:
            - containerPort: 80
              protocol: TCP
          imagePullPolicy: Always 

然后您需要在存储库中创建一个工作流文件。

Github Actions 工作流包含对代码库执行操作的说明。几个预构建的步骤模板允许您在代码存储库上执行许多不同的任务。在本例中,您使用一个步骤模板来构建代码,并将代码推送到 AWS ECR 存储库,并将其部署到 EKS。

创建一个名为main.yml的文件。根文件夹的 github/workflow 目录。将以下代码粘贴到 main.yml 文件中:

 on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

name: AWS ECR push

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
    - name: Install Octopus CLI
      uses: OctopusDeploy/install-octopus-cli-action@v1.1.1
      with:
          version: latest
    - name: Checkout
      uses: actions/checkout@v2

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-2

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push the image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.REPO_NAME }}
        IMAGE_TAG: "latest"

      run: |
        # Build a docker container and push it to ECR 
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        echo "Pushing image to ECR..."
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 

GitHub Success

您需要本地端口转发来检查服务。使用此命令检查 web 应用程序。端口 28015 是根据 Kubernetes 文档中的示例选择的:

kubectl port-forward deployment/underwater-app-github  28015:80 

在浏览器中转至 IP 地址http://127.0.0.1:28015/查看您的 web 应用程序。

Octopus Underwater App

GitHub Actions 可以在 EKS 这样的 Kubernetes 云平台上构建、推送和部署 GitHub 存储库。与云平台和其他工具的集成依赖于社区构建的 step 模板。根据我的经验,这些步骤模板并不是标准化的。我尝试了几种不同的模板,根据调用的变量,有些模板的工作方式不同。

我发现在 GitHub 中使用新的步骤模板每次都需要一些新的学习。像 Octopus 这样的工具也使用 step 模板,但是它们在 Octopus Deploy 应用程序中共享一个标准设计。这意味着 Octopus 部署步骤模板体验是一致的。

结论

Github Actions 允许开发人员在其 Github 存储库中执行 DevOps 操作,简化了部署过程。

在这篇文章中,您构建了一个 GitHub 存储库并将其推送到 Amazon ECR,并将其部署到 Amazon EKS。下一篇文章将讨论如何使用 Octopus Deploy 来管理部署过程。

模板可用于不同的第三方集成。然而,用户体验可能因模板而异,因为它们是由社区维护的。在以后的文章中,我们将介绍 Octopus Deploy 如何与 Github Actions 集成,并为持续部署提供标准化模板,同时提供丰富的用户体验。

查看我们关于使用 GitHub Actions、Kubernetes 和 Octopus Deploy 进行部署的其他帖子:

试用我们免费的 GitHub Actions 工作流工具,帮助您快速为 GitHub Actions 部署生成可定制的工作流。

您还可以了解更多关于使用 GitHub 构建和使用 Octopus 部署的信息,并在 GitHub 市场使用我们验证的行动。

观看我们的 GitHub 行动网络研讨会

https://www.youtube.com/embed/gLkAs_Cy5t4

VIDEO

阅读我们的持续集成系列的其余部分。

愉快的部署!

展开。NET 核心应用程序到带有 Octopus - Octopus 部署的 Raspberry Pi

原文:https://octopus.com/blog/deploying-an-octopus-pi

Octopus enjoying a Raspberry Pi

更新 2020 年 2 月
开箱对 Linux ARM 和 Linux ARM64 SSH 目标的支持分别包含在章鱼服务器 2019.11.22020.2.0 中。

这篇文章最初发表于 2018 年 2 月,但后来发生了一些变化,这是原始文章的更新版本。

。NET Core 在过去的几年里取得了长足的进步,Octopus Deploy 也是如此。我们最近增加了在没有 Mono 的情况下运行 Calamari 的支持,在这篇文章中,我将向你介绍如何部署。NET 核心应用程序到一个没有 Mono 的 Raspberry Pi 3+上。

在这篇文章中,我将向您展示部署和运行是可能的。NET 核心应用程序,我还将描述一些与 Octopus Deploy 服务器交互的不同方式。

开始前的要求

  • 编辑:Visual Studio,Visual Studio 代码,Rider。
  • 章鱼命令行
  • 章鱼服务器和一个 API 密匙
  • 。网芯: Windows 或者 Macos
  • 一个树莓 Pi 3+运行 Raspbian 与。NET core 2.0 或更高版本运行时已安装:
  • 对于角度或反作用应用:
    • 如果您选择的应用程序需要的话(angular 或 react),您的开发机器上的节点和 npm。
    • pi 上的 node.js
  • 卷曲

ASP.NET 在其捆绑包中包含了 NodeServices,这要求在它能够服务任何请求之前安装 Node。当你在 Raspberry Pi 上安装 Node.js 时,它会安装 4.x 版本,可执行文件名为nodejs,但是 NodeServices 会在你的路径中寻找node。您可以通过创建符号链接来解决这个问题:

sudo ln -s /usr/bin/nodejs /usr/bin/node 

构建应用程序

创建一个基本的。网络核心应用

dotnet new angular 

修改应用程序以侦听外部请求

默认情况下,ASP.NET 核心应用程序将只为http://localhost:5000的请求提供服务。要允许 web 主机向您的本地网络提供请求,请在Program.cs中的.UseStartup<Startup>()后添加以下内容:

.UseKestrel(options => {
    options.Listen(System.Net.IPAddress.Any, 5000);
}) 

有关配置 Kestrel Web 主机的更多信息,请查看文档

构建应用程序

npm install
dotnet build
mkdir publish
dotnet publish -o publish --self-contained -r linux-arm 

如果您的目标是在 Raspberry Pi 上支持 64 位的发行版,请将linux-arm替换为linux-arm64

打包应用程序

创建. NET 核心应用程序包的最简单方法是使用 Octopus CLI 工具。

创建一个artifacts目录,然后使用octo pack命令创建包:

mkdir artifacts
octo pack --id core4pi --version 1.0.0 --format zip --outFolder artifacts --basePath publish 

再次使用 Octopus CLI,将包推送到服务器:

octo push --server http://octopus/ --apikey API-ABCDEF123456 --package artifacts\core4pi.1.0.0.zip 

构建服务定义

要让应用程序作为服务运行,请参阅微软关于托管的文档。Linux 上的 NET Core

创建一个名为core4pi.service的文件,包含以下文本:

[Unit]
Description=core4pi

[Service]
WorkingDirectory=#{Octopus.Action[deploy web site].Output.Package.InstallationDirectoryPath}
ExecStart=/usr/local/bin/dotnet "#{Octopus.Action[deploy web site].Output.Package.InstallationDirectoryPath}/core4pi.dll"
Restart=always
RestartSec=10
User=pi
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target 

上面的core4pi.service文本中的[deploy web site]字符串表示部署包的步骤的名称。
该输出变量将包含新安装服务的路径。这将确保在安装服务时,它看到的是最新版本。

为服务定义创建一个包,并将其推送到 Octopus 服务器:

octo pack --id core4pi.service --version 1.0.0 --format zip --outFolder artifacts
octo push --server http://octopus/ --apikey API-ABCDEF123456 --package artifacts\core4pi.service.1.0.0.zip 

创建基础设施

如果您还没有为您的 Raspberry Pi 配置 Octopus 环境,请在命令行中创建一个:

octo create-environment --server http://octopus/ --apikey API-ABCDEF123456 --name "Pi Dev" 

或者通过 基础设施使用 web 界面➜环境➜添加环境

接下来,创建一个访问 Pi 的帐户,该帐户可以是用户名/密码帐户,也可以是 Octopus 门户网站中** 基础设施➜帐户* *部分的 SSH 密钥。

最后,在 基础设施➜部署目标下创建一个部署目标➜添加部署目标 作为 SSH 目标

将目标角色设置为代表目标职责的东西,例如PiWeb

添加详细信息(IP 地址或 DNS 名称、SSH 端口和帐户)后,在下。NET 部分,确保选择单声道未安装,并选择linux-armlinux-arm64作为平台。

创建部署项目

通过 Octopus 门户网站的项目部分或命令行创建一个新项目:

octo create-project --server http://octopus/ --apikey API-ABCDEF123456 --name "PiWeb" --projectgroup "All projects" --lifecycle "Default Lifecycle" 

为应用程序创建部署步骤

在新的 PiWeb 项目中,定义您的部署过程。

添加一个部署一个包的步骤,称为deploy web site

此处的步骤名称将允许正确更新服务定义文件中的值。

环境设置为Pi Dev环境。

角色设置为PiWeb角色。

部分,选择您推送给服务器的包:core4pi

我们不需要完成其他选项,因此请保存该步骤。

为服务定义创建部署步骤

添加另一个部署一个包的步骤。这将在目标上安装一个服务来运行应用程序。

对于包选择,从 Octopus 服务器(内置)包馈送中选择core4pi.service包。

对于这一步,您需要Configure Features:

文件中替换变量功能中添加服务定义文件的名称core4pi.service:

【T2

Configuration Scripts特性下,选择 Bash ,将下面的脚本粘贴到部署脚本部分:

#!/bin/bash
if [ -e /lib/systemd/system/core4pi.service ]
then
    echo stopping service
    sudo systemctl stop core4pi.service
fi

echo installing service
sudo cp core4pi.service /lib/systemd/system/
sudo chmod 644 /lib/systemd/system/core4pi.service
sudo systemctl daemon-reload
sudo systemctl enable core4pi.service
echo starting service
sudo systemctl start core4pi.service 

该脚本执行服务安装,并将在步骤执行期间执行。它将要求您用来连接到 Raspberry Pi 的用户拥有sudo权限。

部署它

项目导航菜单中,选择创建发布

创建版本页面将允许您设置版本号,您可以保留默认值。它还允许选择你想要部署的包的版本,默认情况下,它会选择最新的版本。

保存然后按部署到 PI Dev 然后按部署开始部署过程。

创建发布也可以从命令行执行:

octo create-release --server http://octopus/ --apikey API-ABCDEF123456 --project "PiWeb"
octo deploy-release --server http://octopus/ --apikey API-ABCDEF123456 --project "PiWeb" --deployto="Pi Dev" --version "0.0.1" 

第一次部署时,Octopus 服务器将在目标机器上更新 Calamari,这可能需要几分钟。

测试一下

部署完成后,在端口 5000 上导航到您的 Raspberry Pi 的 IP 地址或 DNS 名称,您应该会看到应用程序:

结论

随着许多不同技术的结合,部署。NET 到 Raspberry Pi 的转换是受支持的,而 Octopus Deploy 使它变得不那么痛苦。在这篇文章中,您还看到了许多与 Octopus 服务器集成的不同方式,包括命令行和 web 门户。

使用 Octopus - Octopus Deploy 将应用程序部署到 Kubernetes

原文:https://octopus.com/blog/deploying-applications-to-kubernetes

Octopus steering the Kubernetes ship illustration

Octopus 2018.8 预览了许多新功能,使管理 Kubernetes 部署变得简单。这些 Kubernetes 步骤和目标旨在允许团队利用 Octopus 环境、仪表板、安全性、帐户管理、变量管理以及与其他平台和服务的集成,将应用程序部署到 Kubernetes。

在这篇博文的最后,你将学会如何:

  • 按照最低特权原则配置服务帐户和命名空间
  • 在 Kubernetes 中部署一个正常运行的 web 服务器
  • 使用模拟故障对 Kubernetes 部署执行蓝/绿更新
  • 通过公共网络负载平衡器访问应用程序
  • 通过多个 Nginx 入口控制器引导流量
  • 使用 Helm 部署应用程序
  • 并在开发和生产环境中完成所有这些工作

这将是一篇很长的博文,但它将带您从一个空白的 Kubernetes 集群到一个功能性的多环境集群,该集群使用随着您的团队和应用程序的增长而扩展的模式进行可重复的部署。

Octopus 2018.8 中的 Kubernetes 功能只是预览版。这里讨论的特性可能会在未来的版本中发生变化。

先决条件

要阅读这篇博文,您需要有一个 Octopus 实例,一个已经配置好的 Kubernetes 集群,并且安装了 Helm。这篇博文将使用 Google Cloud 提供的 Kubernetes 服务,但是任何 Kubernetes 集群都可以。

Helm 是 Kubernetes 的一个包管理器,我们将使用它在 Kubernetes 集群中安装一些第三方服务。谷歌云提供了描述如何在他们的云中安装 Helm 的文档,其他云提供商也会提供类似的文档。

刚接触章鱼?查看我们的入门指南了解更多信息。

准备 Octopus 服务器

Octopus 中的 Kubernetes 步骤要求路径上有kubectl可执行文件。同样,掌舵步骤要求路径上有helm可执行文件。

如果你从 Octopus workers 运行 Kubernetes 步骤,你可以使用 Kubernetes 网站上的指令安装kubectl可执行文件,使用 Helm 项目页面上的指令安装helm可执行文件。

因为 Octopus 中的 Kubernetes 功能处于预览状态,所以本文中讨论的步骤需要在Features部分启用。

我们将创造什么

在我们深入部署 Kubernetes 应用程序的细节之前,有必要理解我们试图通过这个例子实现什么。

我们的基础架构有以下要求:

  • 两种环境:开发和生产
  • 一个 Kubernetes 星团
  • 单个应用程序(这里我们部署了 HTTPD Docker 图像作为例子)
  • 该应用程序由一个定制的 URL 路径公开,如 http://myapp/httpd
  • 零停机部署

从高层次来说,这就是我们最终将得到的结果。

Kubernetes Overview

不要担心这个图看起来吓人,因为我们将一步一步地构建这些元素。

饲料

Octopus 中的 Kubernetes 支持依赖于定义一个 Docker 提要。因为我们正在部署的 HTTPD 映像可以在主 Docker 存储库中找到,所以我们将根据https://index.docker.io URL 创建一个提要。

点击了解更多关于 Docker feeds 的信息。

环境

虽然我们列出了两个环境作为需求,但是我们实际上将创建三个。称为Kubernetes Admin的附加环境将是我们运行实用程序脚本来创建用户帐户的地方。

点击了解更多关于环境的信息

Kubernetes Environments

生命周期

Octopus 中的默认生命周期假设所有环境都将依次部署到。对我们来说,情况并非如此。我们有两个不同的生命周期:Development->-Production,以及Kubernetes Admin作为运行实用程序脚本的独立环境。

为了对从DevelopmentProduction的进展进行建模,我们将创建一个名为Application的生命周期。它将包含两个阶段,第一阶段部署到Development环境,第二阶段部署到Production环境。

Application Lifecycle

为了对针对 Kubernetes 集群运行的脚本进行建模,我们将创建一个名为Kubernetes Admin的生命周期。它将包含部署到Kubernetes Admin环境的单一阶段。

Admin Lifecycle

点击了解更多关于生命周期的信息

Kubernetes 管理目标

Octopus 中的 Kubernetes 目标在概念上是 Kubernetes 集群中的权限边界。它使用一个 Kubernetes 名称空间和一个 Kubernetes 帐户来定义这个边界。

随着部署到 Kubernetes 集群的环境、团队、应用程序和服务数量的增长,保持它们的隔离以防止资源被意外覆盖或删除,或者防止 CPU 和内存等资源被恶意部署消耗,这一点非常重要。可以通过将权限和资源限制应用到 Kubernetes 名称空间来实施它们,然后将这些限制应用到该名称空间中的任何部署。

按照最小特权的惯例,每个名称空间都有一个对应的系统帐户,该帐户只对该名称空间拥有特权。

名称空间和仅限于名称空间的服务帐户的组合构成了一个典型的 Octopus Kubernetes 目标。

话虽如此,我们需要一个起点来创建名称空间和服务帐户,为此,我们将创建一个 Kubernetes 目标,它带有部署到Kubernetes Admin环境的管理员凭证。

首先,我们需要创建一个保存管理员用户凭证的帐户。Google Cloud 中的 Kubernetes 集群为一个名为admin的用户提供了一个我们可以使用的随机生成的密码。

Kubernetes cluster details

这些凭证保存在用户名/密码八达通帐户中。

Kubernetes Admin Account

其他云提供商对他们的管理员用户使用不同的认证方案。参见文档了解使用除用户名和密码之外的账户类型的详细信息。

大多数 Kubernetes 集群通过 HTTPS 公开它们的 API,但是通常使用不可信的证书。为了与 Kubernetes 集群通信,我们可以禁用任何证书验证,或者将证书作为 Kubernetes 目标的一部分提供。禁用证书验证被认为不是最佳做法,因此我们将把 Kubernetes 集群证书上传到 Octopus。

证书由 Google 以 PEM 文件的形式提供,如下所示(从Cluster credentials对话框中的Cluster CA certificate字段复制):

-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIQMufY5zcMoXLTKHXY2e5hDTANBgkqhkiG9w0BAQsFADAv
MS0wKwYDVQQDEyQwMjkwMTUzZS05ZGYwLTQzNjAtYmJjMC0xZTFhYjkxMzQwYTgw
HhcNMTgwNzI1MDEyMDAzWhcNMjMwNzI0MDIyMDAzWjAvMS0wKwYDVQQDEyQwMjkw
MTUzZS05ZGYwLTQzNjAtYmJjMC0xZTFhYjkxMzQwYTgwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDDyKNlRbHMvQlh2mvjdBEeIXwAI40t6MBm8K3wGqiF
D/SlY2AsKjsMq/5VR+zlKrbFJUkxQGdN0Dm7tSUpzgkr7DaPTT/FLKPidFNEcG6Z
ZpienqESWLwXT2g8O7yRIfAaFBASzZ60UeUs1VYTLSWdqNSIW96JJD1WzNj7Fwd/
ImlLiZVVlQLN4Yz2yf99wCX4Mg3jCaKLQF4/f7/e+d1PkAROSjG5tRgOpHBDkgqL
ewDBpT5p1tuIBKN6ZyQbMkLRcTU82iFpnDLJwlkXfmhVv3RXBtM/VcK/LD/VuGH+
Rko8xY9+ckrUyYlPU5CxL4WS03pbHFO5JxjPhNeEpfZPAgMBAAGjIzAhMA4GA1Ud
DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAd
0B9H5JuQSW/5O6hoW9bvoMAdga9mwrYjMQ1ErSkHpI94K70CFmnh3vAog6UGkGkb
RN5SOjpqaJiYwAoBuECPwV8VBotsJ17W67B5zl4w37zYDgT6wTlg0s0urdRSvA6s
EHHTTzNaHoeVBArUvFb0NprL7UH3K0QJG+VKhsxSvTYIWddptfpo+Da72OEtGRbs
F1g3GhuAICmyCQnDQ6LqxPRq5/WCCiea43c7hPs2AK3SIAsoA0DTy311gpogqKVn
Cods8yRwx6GPC6l9nmmygAjma0ai06N/oUtWZQhX2oYzAKsgdzu1P+DlQfDbmv5u
Jash2XeDyUqUFUEsH+0+
-----END CERTIFICATE----- 

然后,这些文本被保存到一个名为k8s.pem的文件中,并上传到 Octopus。

点击了解更多关于证书的信息。

Kubernetes Certificate

保存用户帐户和证书后,我们现在可以创建名为Kubernetes Admin的 Kubernetes 目标。

这个目标将被部署到Kubernetes Admin环境中,并承担一个名为 Admin 的角色。该帐户将是我们在上面创建的Kubernetes Admin帐户,集群证书将引用我们在上面保存的证书。

因为这个Kubernetes Admin目标将用于运行实用程序脚本,所以我们不希望它指向 Kubernetes 名称空间,所以该字段留空。

Kubernetes Target

点击了解更多关于 Kubernetes 目标的信息。

我们现在有了一个目标,可以用来为其他名称空间准备服务帐户。

HTTPD 发展服务账户

我们现在有一个 Kubernetes 目标,但是这个目标是用集群管理员帐户配置的。使用管理员帐户来运行部署并不是一个好主意,所以我们需要做的是创建一个名称空间和服务帐户,它将允许我们在 Kubernetes 集群中的一个隔离区域中只部署我们的应用程序所需的资源。

为此,我们需要在 Kubernetes 集群中创建四个资源:一个名称空间、一个服务帐户、一个角色和一个角色绑定。我们已经讨论了名称空间和服务帐户。角色定义了可以应用的操作以及它们可以应用到的资源。角色绑定将服务帐户与角色相关联,授予服务帐户在角色中定义的权限。

Kubernetes 可以将这些资源表示为 YAML,而 YAML 可以在一个文件中表示多个文档,方法是用三重破折号分隔它们。下面的 YAML 文件定义了这四种资源。

---
kind: Namespace
apiVersion: v1
metadata:
  name: httpd-development
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpd-deployer
  namespace: httpd-development
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: httpd-development
  name: httpd-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]  
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: httpd-deployer-binding
  namespace: httpd-development
subjects:
- kind: ServiceAccount
  name: httpd-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: httpd-deployer-role
  apiGroup: "" 

为了创建这些资源,我们需要将 YAML 保存为一个文件,然后使用kubectl在集群中创建它们。为此,我们使用Run a kubectl CLI Script步骤。

Kubernetes Script Step

这一步将瞄准Kubernetes Admin目标,并运行下面的脚本,该脚本将 YAML 保存到一个文件中,然后使用kubectl来应用 YAML。

Set-Content -Path serviceaccount.yml -Value @"
---
kind: Namespace
apiVersion: v1
metadata:
  name: httpd-development
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpd-deployer
  namespace: httpd-development
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: httpd-development
  name: httpd-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]    
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: httpd-deployer-binding
  namespace: httpd-development
subjects:
- kind: ServiceAccount
  name: httpd-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: httpd-deployer-role
  apiGroup: ""
"@

kubectl apply -f serviceaccount.yml 

bash 脚本非常相似。

cat >serviceaccount.yml <<EOL
---
kind: Namespace
apiVersion: v1
metadata:
  name: httpd-development
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpd-deployer
  namespace: httpd-development
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: httpd-development
  name: httpd-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]    
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: httpd-deployer-binding
  namespace: httpd-development
subjects:
- kind: ServiceAccount
  name: httpd-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: httpd-deployer-role
  apiGroup: ""
EOL

kubectl apply -f serviceaccount.yml 

Kubernetes Service Account Script

一旦这个脚本运行,就会创建一个名为httpd-deployer的服务帐户。这个服务帐户被自动分配一个令牌,我们可以使用这个令牌向 Kubernetes 集群进行身份验证。我们可以运行第二个脚本来获取这个令牌。

$user="httpd-deployer"
$namespace="httpd-development"
$data = kubectl get secret $(kubectl get serviceaccount $user -o jsonpath="{.secrets[0].name}" --namespace=$namespace) -o jsonpath="{.data.token}" --namespace=$namespace
[System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($data)) 

使用下面的脚本可以在 bash 中运行相同的功能。

user="httpd-deployer"
namespace="httpd-development"
kubectl get secret $(kubectl get serviceaccount $user -o jsonpath="{.secrets[0].name}" --namespace=$namespace) -o jsonpath="{.data.token}" --namespace=$namespace | base64 --decode 

我们在这里检索令牌作为脚本步骤的一部分,只是为了进行演示。在日志输出中显示令牌有安全风险,应该谨慎操作。相反,这些相同的脚本可以在本地运行,以防止令牌保存在日志文件中。

参见脚本 Kubernetes 目标一节,了解自动创建这些帐户的过程,而不在日志文件中留下令牌的解决方案。

在我们部署脚本之前,我们需要确保项目正在使用Kubernetes Admin生命周期。

Admin Project Lifecycle

我们现在可以运行脚本,它将创建服务帐户并显示令牌。该令牌看起来像这样:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJodHRwZC1kZXZlbG9wbWVudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJodHRwZC1kZXBsb3llci10b2tlbi0ycG1ndCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJodHRwZC1kZXBsb3llciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjliZGQzYWQ0LTk5ZTktMTFlOC04ODdmLTQyMDEwYTgwMDA5MyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpodHRwZC1kZXZlbG9wbWVudDpodHRwZC1kZXBsb3llciJ9.DDiMDOmznf4S8ClHO30RvSZNGHN_7WYk9-FABaLkSC-mIunWtJHiT_lEovbUToogM0fnG1ISueundAZ6tsRRY-eVwefLvhgy1Ync2QlLwaqeoUenGt1d36lH5YFb7gYmon2UD54DGEdYNzafI1TLWi3DS1apjSUc3kWh54HfZXSeQmCE7fGu4wNoJz3WU1MEQZx8KqM9__lVDxtPGmE2pyZX6OYBXoAQv9-cfs_1GP009exfkVWbVYdDFDoEko21KDAORjyKu4ow4KvVXOXzcfgCKe_UlYyuLg0A6NRyc8lDj4D34R1crIPvqWmXVy5BMK4ENchhYEC62nsInptZAg 

Service account token

HTTPD 发展目标

现在,我们已经拥有了创建目标所需的一切,该目标将用于在Development环境中部署 HTTPD 应用程序。

我们首先用上面返回的令牌在 Octopus 中创建一个令牌帐户。

然后我们在一个名为Httpd-Development的新 Kubernetes 目标中使用这个令牌。

注意这里的Target Roles包括一个名为Httpd的角色,它与正在部署的应用程序的名称相匹配,并且Kubernetes namespace被设置为httpd-development。我们创建的服务帐户只有部署到httpd-development名称空间的权限,并且只用于将 HTTPD 应用程序部署到Development环境中。

因此,这个目标代表应用程序和环境的交集,使用一个命名空间和一个有限的服务帐户来实施权限边界。这是我们将在每个应用程序和环境中反复重复的模式。

既然我们有了要部署的目标,让我们部署我们的第一个应用程序吧!

HTTPD 应用程序

Deploy Kubernetes containers步骤提供了将应用程序部署到 Kubernetes 集群的自以为是的过程。这一步实现了一个标准模式,用于创建 Kubernetes 资源的集合,这些资源协同工作以提供可重复的、有弹性的部署。

我们将部署的应用程序是 HTTPD。这是一个来自 Apache 的流行的 web 服务器,虽然我们不会做任何超过显示静态文本作为网页的事情,但 HTTPD 是一个有用的例子,因为大多数部署到 Kubernetes 的应用程序都会像 HTTPD 一样暴露 HTTP 端口。

该步骤被赋予一个名称,并以一个角色为目标。我们这里的目标角色是为了匹配我们正在部署的应用程序的名称而创建的角色。在选择Httpd角色时,我们确保该步骤将使用我们的 Kubernetes 目标,该目标被配置为部署 HTTPD 应用程序。

Deployment部分用于配置将在 Kubernetes 中创建的部署资源的细节。

从现在开始,我将使用术语“资源”(例如,部署资源或 Pod 资源)来区分在 Kubernetes 集群中创建的资源(也就是说,如果您直接使用kubectl工具,您将使用的资源)和 Octopus 概念或一般操作,如部署事物。这可能会导致类似“单击 Deploy 按钮来部署部署资源”这样的句子,但是请不要因此而反对我。

Deployment name字段定义了分配给部署资源的名称。这些名称是命名空间资源中 Kubernetes 资源的唯一标识。这很重要,因为这意味着要在 Kubernetes 中创建新的独特资源,它必须有一个惟一的名称。当我们稍后选择部署策略时,这将非常重要,所以请记住这一点。

Replicas字段定义了这个部署资源将创建多少个 Pod 资源副本。在本例中,我们将保持在1

Progression deadline in seconds字段配置 Kubernetes 等待部署资源完成的时间。如果部署资源在此时间内未完成(这可能是由于 Docker 映像下载缓慢、对 Pod 资源的准备情况检查失败、集群中的资源不足等),则部署资源的部署将被视为失败。

Labels字段允许将通用键/值对分配给该步骤创建的资源。在后台,这些标签将应用于该步骤创建的部署、Pod、服务、入口、配置映射、机密和容器资源。正如我们前面提到的,这一步是固执己见的,其中一个观点是标签应该定义一次,并应用于作为部署的一部分创建的所有资源。

部署策略

Kubernetes 为它管理的资源提供了一个强大的声明性模型。当直接使用kubectl命令时,可以描述资源的期望状态(通常在 YAML)并将该资源“应用”到 Kubernetes 集群中。Kubernetes 然后将资源的期望状态与集群中资源的当前状态进行比较,并进行必要的更改以将集群资源更新到期望状态。

有时,这种更改就像更新标签这样的属性一样简单。但是在其他情况下,期望的状态需要重新部署整个应用程序。

Kubernetes 本身提供了两种部署策略来尽可能平滑地重新部署应用程序:重新创建和滚动更新。

重新创建策略将在创建新的 Pod 资源之前删除任何现有的 Pod 资源。滚动更新策略将逐步替换 Pods 资源。你可以在 Octopus 文档中读到更多关于这些部署策略的信息。

Octopus 提供了第三种部署策略,称为蓝/绿。该策略将为每个部署创建全新的部署资源,当部署资源成功时,流量将被切换。

蓝/绿部署策略为那些负责管理 Kubernetes 部署的人提供了一些有趣的可能性,所以我们将选择这个策略。

卷和配置图

卷为容器资源提供了一种访问外部数据的方式。Kubernetes 为卷提供了很大的灵活性,它们可以是磁盘、网络共享、节点上的目录、GIT 存储库等等。

对于这个例子,我们想要获取存储在 ConfigMap 资源中的数据,并将其公开为容器资源中的一个文件。ConfigMap 资源很方便,因为 Kubernetes 确保了它们的高可用性,它们可以跨容器资源共享,并且易于创建。

因为它们非常方便,所以该步骤可以将 ConfigMap 资源视为部署的一部分。这确保了组成部署的容器资源始终能够访问与其相关联的 ConfigMap 资源。这一点很重要,因为您不希望在应用程序版本 2 正在推出的过程中,应用程序版本 1 引用配置映射资源版本 2。如果这没有多大意义,请不要担心,稍后我们将看到它的实际应用。

这正是我们将为此演示配置的内容。Volume type被设置为Config Map,它被赋予一个Name,我们选择Reference the config map created as port of this step选项来指示稍后将在该步骤中定义的 ConfigMap 资源是卷所指向的。

ConfigMap 卷项目提供了一种将 ConfigMap 资源值映射到文件名的方法。在本例中,我们已经将Key设置为index并将路径设置为index.html,这意味着当该卷被装入容器资源时,我们希望将名为index的 ConfigMap 资源值公开为名为index.html的文件。

集装箱

下一步是配置容器资源。这是我们将配置 HTTPD 应用程序的地方。

我们首先配置容器资源将使用的 Docker 映像。这里,我们从之前创建的 Docker 提要中选择了httpd图像。

为了访问 HTTPD,我们需要公开一个端口。作为 web 服务器,HTTPD 接受端口 80 上的流量。可以对端口进行命名以使它们更容易使用,因此我们将这个端口称为web

最后一项配置是在一个目录中挂载我们之前定义的 ConfigMap 卷。HTTPD Docker 映像已经构建为提供来自/usr/local/apache2/htdocs目录的内容。如果您还记得,我们配置了 ConfigMap 卷,以将名为index的 ConfigMap 资源的值公开为名为index.html的文件。因此,通过在/usr/local/apache2/htdocs目录下挂载这个卷,这个容器资源将拥有一个名为/usr/local/apache2/htdocs/index.html的文件,其中包含 ConfigMap 资源中值的内容。

主步骤 UI 中总结了每个容器的配置,因此您可以一目了然地查看。

配置图

我们已经讨论了很多关于由该步骤创建的 ConfigMap 资源,所以现在是时候配置它了。

Config Map Name部分定义了 ConfigMap 资源的名称(或者,从技术上讲,是名称的一部分——稍后会详细介绍)。Config Map Items定义了构成 ConfigMap 资源的键/值对。

如果您还记得,我们将这个 ConfigMap 资源公开为一个卷,该卷定义了一个项目,该项目将名为index的 ConfigMap 资源值映射到名为index.html的文件。所以这里我们创建了一个名为index的项目,该项目的值就是最终将成为index.html文件内容的内容。

服务

我们现在很快就可以部署和访问应用程序了。因为很高兴看到一些进展,我们将在这里走一点捷径,用我们可用的最快选项向世界展示我们的应用程序。

为了与 HTTPD 应用程序通信,我们需要通过服务资源获取我们在容器资源上公开的端口(端口 80,我们称之为web)。为了从外部访问服务资源,我们将创建一个负载平衡器服务资源。

通过部署负载平衡器服务资源,我们的云提供商将为我们创建一个网络负载平衡器。不同的云提供商创建的网络负载平衡器的类型和配置方式各不相同,但一般来说,默认情况下是创建一个具有公共 IP 地址的网络负载平衡器。

每当您向外界公开应用程序时,都必须考虑增加像防火墙这样的安全性。

Service Name部分定义了服务资源的名称。

Service Type部分是我们将服务资源配置为Load balancer的地方。在此部分中,其他字段可以留空。

Service Ports部分是传入端口映射到容器资源端口的地方。在本例中,我们公开了服务资源上的端口 80,并将其定向到容器资源上的web端口(也是端口 80,但这些值不需要匹配)。

主 UI 中总结了这些端口,以便快速查看。

至此,所有的基础工作都已完成,我们可以部署应用程序了。

第一次部署

当您创建这个项目的部署时,Octopus 允许您定义将要包含的 Docker 映像的版本。如果您回头看看容器资源的配置,您会注意到我们从未指定版本,只指定了图像名称。这是有意的,因为 Octopus 预计大多数部署将涉及新的 Docker 镜像版本,而 Kubernetes 资源的配置将保持静态。

这意味着在日常部署中唯一要做的决定是 Docker 映像的版本,您可以利用 Octopus 的特性,如通道来进一步定制在部署过程中如何选择映像版本。

因此我们的部署成功了。

进入 Google Cloud 控制台,我们可以看到一个名为httpd-deployments-841的部署资源已经创建。该名称是我们在步骤httpd中定义的部署资源名称和deployments-841的 Octopus 部署的唯一标识符的组合。创建这个名称是因为蓝/绿部署策略要求每个部署创建的部署资源是唯一的。

部署还创建了一个名为httpd的服务资源。请注意,它的类型是Load balancer,并且有一个公共 IP 地址。

还创建了名为configmap-deployments-841的配置映射资源。与部署资源一样,ConfigMap 资源的名称是我们在步骤中定义的名称和 Octopus 添加的惟一部署名称的组合。与部署资源不同,由该步骤创建的 ConfigMaps 将始终具有这样的唯一名称(部署资源仅具有针对蓝/绿部署附加的唯一部署名称)。

【T2

所有这些都会导致 HTTPD 将配置映射资源的内容作为服务资源的公共 IP 地址下的网页来提供。

如果你已经做到了这一步,祝贺你!但是您可能想知道为什么我们必须配置这么多东西才能显示一个静态网页。在互联网上阅读任何其他 Kubernetes 教程都会让你在 1000 字前就达到这一点...

在为 Octopus 开发这些 Kubernetes 步骤的过程中,我们发现每个人都喜欢展示如何快速地使用管理帐户将单个应用程序部署到单个环境中,并在专用的负载平衡器上公开所有内容。这很好,但并不代表现实世界部署所面临的那种挑战。

我们在这里实现的是为跨多个环境部署多个应用程序奠定基础,将名称空间和具有有限权限的服务帐户分离开来。

所以,喘口气吧,因为我们只完成了一半。至此,我们已经通过一个负载平衡器将一个应用程序部署到了一个环境中,接下来我们将进行多环境部署。

那么当事情出错时会发生什么呢?

部署有时会失败。这不仅是可以预期的,而且是值得庆祝的,只要它发生在Development环境中。快速失败是稳健的 CD 管道的关键组成部分。

让我们回顾一下我们现在部署了什么。我们有一个指向服务资源的负载平衡器,服务资源又指向部署资源。

【T2

让我们模拟一次失败的部署。我们可以通过配置容器资源就绪探测器来运行一个不存在的命令来做到这一点。Kubernetes 使用就绪探测器来确定容器资源何时准备好开始接受流量,通过故意配置一个不能通过的测试,我们可以模拟一个失败的部署。

作为此失败部署的一部分,我们还将更改 ConfigMap 的值。请记住,这个值就是网页中显示的内容。

不出所料,部署失败了。

那么部署失败意味着什么呢?

因为我们使用蓝/绿部署策略,所以我们现在有两个部署资源。因为最新的一个叫httpd-deployments-842的已经失效,之前的一个叫httpd-deployments-841的还没有移除。

我们还有两个配置映射资源。同样,由于上次部署失败,以前的 ConfigMap 资源尚未删除。

实际上,失败的部署资源及其关联的 ConfigMap 资源是孤立的。它们不能从服务资源中访问,这意味着新部署对外界是不可见的。

因为旧资源在部署期间未被编辑,并且由于部署失败而未被删除,所以我们的上次部署仍然是实时的、可访问的,并且显示与上次成功部署时定义的文本相同的文本。

这也是这一步关于 Kubernetes 部署应该是什么的观点之一。失败的部署不应导致环境崩溃,而是让您有机会解决问题,同时保留以前的部署。

继续并从容器资源中删除不良就绪检查。还要更改 ConfigMap 资源的值以显示新消息。

这一次部署成功了。由于部署成功,以前的部署和 ConfigMap 资源已被清理,新消息显示在网页上。

通过为每个蓝/绿部署创建新的部署资源,并为每个部署创建新的 ConfigMap 资源,我们可以确保我们的 Kubernetes 集群在更新期间或部署失败后不会处于未定义的状态。

我向您承诺了一个多环境部署的示例,所以让我们继续配置我们的Production环境。

首先,为生产环境创建一个服务帐户。这个 YAML 与我们用于为Development环境创建服务帐户的代码相同,只是文本development被替换为production

---
kind: Namespace
apiVersion: v1
metadata:
  name: httpd-production
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpd-deployer
  namespace: httpd-production
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: httpd-production
  name: httpd-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]     
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: httpd-deployer-binding
  namespace: httpd-production
subjects:
- kind: ServiceAccount
  name: httpd-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: httpd-deployer-role
  apiGroup: "" 

同样,获得令牌的 Powershell 是相同的,除了development现在是production

$user="httpd-deployer"
$namespace="httpd-production"
$data = kubectl get secret $(kubectl get serviceaccount $user -o jsonpath="{.secrets[0].name}" --namespace=$namespace) -o jsonpath="{.data.token}" --namespace=$namespace
[System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($data)) 

bash 脚本也是如此。

user="httpd-deployer"
namespace="httpd-production"
kubectl get secret $(kubectl get serviceaccount $user -o jsonpath="{.secrets[0].name}" --namespace=$namespace) -o jsonpath="{.data.token}" --namespace=$namespace | base64 --decode 

我不会重复运行这些脚本、创建令牌帐户或创建目标的细节,所以请回头参考HTTPD 开发服务帐户了解更多细节。

您希望最终配置一个如下所示的目标。

现在继续将 Octopus 部署到Production环境中。

这将导致使用新的公共 IP 地址创建第二个负载平衡器服务资源。

我们的生产实例可以在 web 浏览器中查看。

让我们找点乐子,使用一个变量作为 ConfigMap 资源的值。通过将值设置为变量#{Octopus.Environment.Name},我们将在网页中显示环境名称。

将这一更改应用到生产环境中会导致环境名称显示在页面上。

这是一个微不足道的例子,但它强调了通过配置多环境部署可以获得的强大功能。一旦配置了您的帐户、目标和环境,在环境间移动应用程序就变得简单、安全且高度可配置。

迁移到入口

为了方便起见,我们通过负载平衡器服务资源公开了我们的 HTTPD 应用程序。这是一个快捷的解决方案,因为 Google Cloud 负责构建一个带有公共 IP 地址的网络负载平衡器。

不幸的是,这种解决方案不能扩展到更多的应用。每一个网络负载平衡器都要花钱,而且当涉及到安全和审计时,跟踪多个公共 IP 地址可能是一件痛苦的事情。

解决方案是拥有一个单一的负载平衡器服务,它接受所有传入的请求,然后根据请求将流量定向到适当的 Pod 资源。例如,https://myapp/userservice 流量将被定向到用户微服务,而 https://myapp/cartservice 流量将被定向到 cart 微服务。

这正是入口资源为我们做的事情。单个负载平衡器服务资源会将流量定向到入口控制器资源,入口控制器资源又会将流量定向到不会产生任何额外基础架构成本的其他内部服务资源。

与大多数 Kubernetes 资源不同,入口控制器由第三方提供。一些云提供商有自己的入口控制器,但我们将使用 Nginx 入口控制器,因为它是最受欢迎的,可以在云提供商之间移植。

但是要配置 Nginx Ingress 控制器,我们首先需要设置 Helm。

配置舵

Helm 之于 Kubernetes,就像 Chocolatey 之于 Windows,或者 Apt/Yum 之于 Linux。Helm 提供了一种将简单和复杂的应用程序部署到 Kubernetes 集群的方法,处理所有的依赖关系,公开可用的选项,并提供升级和删除现有部署的命令。

Helm 的伟大之处在于,它已经将大量的应用程序打包到了 Helm 所谓的图表中。我们将使用这些图表中的一个来安装 Nginx 入口控制器。

在 Kubernetes 集群中安装 Helm

Helm 有一个服务器端组件,必须首先安装在 Kubernetes 集群本身上。云提供商有设置服务器端组件的说明,所以点击这些文档以获得使用 Helm 准备 Kubernetes 集群的说明。

舵馈给

为了利用舵,我们需要配置一个舵饲料。因为我们将使用标准的公共 Helm 存储库,所以我们将提要配置为访问https://kubernetes-charts.storage.googleapis.com/

入口控制器和多种环境

此时,我们需要决定如何部署入口控制器资源。

我们可以让一个负载平衡器服务资源将流量定向到一个入口控制器资源,这又可以跨环境定向流量。入口控制器资源可以基于请求的主机名来定向流量,因此发送到 https://myproductionapp/user service 的流量可以发送到Production环境,而 https://mydevelopmentapp/user service 可以发送到Development环境。

另一种选择是每个环境有一个入口控制器资源。在这种情况下,Development环境中的入口控制器资源将只向Development环境中的其他服务发送流量,而Production环境中的入口控制器资源将向Production服务发送流量。

这两种方法都是有效的,各有利弊。对于这个例子,我们将为每个环境部署一个入口控制器资源。

我们将把 Nginx 入口控制器资源视为一个应用部署。这意味着,就像我们在 HTTPD 部署中所做的那样,将为每个环境创建一个服务帐户和目标。

部署掌舵图时,需要调整服务帐户、角色和角色绑定资源。部署 Helm chart 包括在kube-system名称空间中列出和创建资源。为了支持这一点,我们创建了一个具有kube-system名称空间中所需权限的额外角色资源,并用另一个 RoleBinding 资源将该角色资源绑定到服务帐户资源。

这是在nginx-development名称空间中创建nginx-deployer服务帐户资源的 YAML。

---
kind: Namespace
apiVersion: v1
metadata:
  name: nginx-development
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-deployer
  namespace: nginx-development
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: nginx-development
  name: nginx-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]   
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nginx-deployer-binding
  namespace: nginx-development
subjects:
- kind: ServiceAccount
  name: nginx-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: nginx-deployer-role
  apiGroup: ""
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: kube-system
  name: nginx-deployer-role
rules:
- apiGroups: [""]
  resources: ["pods", "pods/portforward"]
  verbs: ["list", "create"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nginx-deployer-development-binding
  namespace: kube-system
subjects:
- kind: ServiceAccount
  name: nginx-deployer
  apiGroup: ""
  namespace: nginx-development
roleRef:
  kind: Role
  name: nginx-deployer-role
  apiGroup: "" 

这是在nginx-production名称空间中创建nginx-deployer服务帐户资源的 YAML。

---
kind: Namespace
apiVersion: v1
metadata:
  name: nginx-production
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-deployer
  namespace: nginx-production
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: nginx-production
  name: nginx-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get"]     
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nginx-deployer-binding
  namespace: nginx-production
subjects:
- kind: ServiceAccount
  name: nginx-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: nginx-deployer-role
  apiGroup: ""
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: kube-system
  name: nginx-deployer-role
rules:
- apiGroups: [""]
  resources: ["pods", "pods/portforward"]
  verbs: ["list", "create"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nginx-deployer-production-binding
  namespace: kube-system
subjects:
- kind: ServiceAccount
  name: nginx-deployer
  apiGroup: ""
  namespace: nginx-production
roleRef:
  kind: Role
  name: nginx-deployer-role
  apiGroup: "" 

为服务帐户获取令牌的过程是相同的,创建令牌 Octopus 帐户和目标也是如此。

创建帐户、名称空间和目标后,我们将在 Octopus 中配置以下目标列表。

配置舵变量

我们可以通过Run a Helm Update步骤部署 Nginx 舵图。

从舵馈入中选择nginx-ingress图表。

Kubernetes Release Name设置为nginx-#{Octopus.Environment.Name | ToLower}。我们已经利用了 Octopus 变量替换来确保 Helm 版本在每个环境中都有一个唯一的名称。

舵图可以用参数定制。Nginx Helm 图表记录了它支持的参数这里。特别是,我们想要定义controller.ingressClass参数,并为每个环境更改它。Ingress 类用于确定哪个 Ingress 控制器将配置哪个规则,我们将使用它来区分Development环境和Production环境中流量的 Ingress 资源规则。

Raw Values YAML部分,添加以下 YAML。请注意,我们再次使用了变量替换来确保每个环境都有一个适用于它的唯一值。

controller:
  ingressClass: "nginx-#{Octopus.Environment.Name | ToLower}" 

保存这些更改,并记住将生命周期更改为Application

现在将舵图部署到Development环境中。

Helm 为我们提供了一个例子,说明如何创建与新部署的入口控制器资源一起工作的入口资源。

The nginx-ingress controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace nginx-development get services -o wide -w nginx-development-nginx-ingress-controller'
An example Ingress that makes use of the controller:
  apiVersion: extensions/v1beta1
  kind: Ingress
  metadata:
    annotations:
      kubernetes.io/ingress.class: nginx-development
    name: example
    namespace: foo
  spec:
    rules:
      - host: www.example.com
        http:
          paths:
            - backend:
                serviceName: exampleService
                servicePort: 80
              path: /
    # This section is only required if TLS is to be enabled for the Ingress
    tls:
        - hosts:
            - www.example.com
          secretName: example-tls 

特别是,注释非常重要。

annotations:
  kubernetes.io/ingress.class: nginx-development 

还记得我们在部署舵图时是如何设置controller.ingressClass参数的吗?这个注释就是那个属性所控制的。这意味着入口资源必须专门设置kubernetes.io/ingress.class: nginx-development注释,以供该入口控制器资源考虑。这就是我们如何区分开发和生产入口控制器资源的规则。

继续将部署推进到Production环境中。

我们现在可以在 Kubernetes 集群中看到 Nginx 部署资源。

这些 Nginx 部署资源可以通过新的负载平衡器服务资源进行访问。

我们现在准备通过入口控制器连接到 HTTPD 应用程序,而不是通过它们自己的网络负载平衡器。

配置入口

回到 HTTPD 容器部署步骤,我们需要将Service TypeLoad balancer更改为Cluster IP。这是因为入口控制器资源可以在内部将流量定向到 HTTPD 服务资源。不再需要公开访问 HTTPD 服务资源,群集 IP 服务资源提供了我们需要的一切。

我们现在需要配置入口资源。

从定义Ingress Name开始。

入口资源支持许多不同的入口控制器,这些控制器可通过注释获得。这些是键/值对,通常包含特定于实现的值。因为我们已经部署了 Nginx 入口控制器,所以我们定义的许多注释都是特定于 Nginx 的。

不过,第一个注释是跨入口控制器资源实现共享的。就是我们之前讲过的kubernetes.io/ingress.class标注。我们将这个注释设置为nginx-#{Octopus.Environment.Name | ToLower}。这意味着当部署在Development环境中时,这个注释将被设置为nginx-development,而当部署到Production环境中时,它将被设置为nginx-production。这就是我们如何针对特定环境的入口控制器资源。

kubernetes.io/ingress.allow-http注释设置为true以允许不安全的 HTTP 流量,将nginx.ingress.kubernetes.io/ssl-redirect设置为false以防止 Nginx 将 HTTP 流量重定向到 HTTPS。

启用 HTTP 流量有安全风险,此处仅用于演示目的。

最后要配置的部分是Ingress Host Rules。这是我们将传入请求映射到公开容器资源的服务资源的地方。在我们的例子中,我们想要公开服务资源端口的/httpd路径,它映射到我们的容器资源上的端口 80。

Host字段留空,这意味着它将捕获所有主机的请求。

继续将它部署到Development环境中。您将得到这样的错误。

The Service "httpd" is invalid: spec.ports[0].nodePort: Invalid value: 30245: may not be used when `type` is 'ClusterIP' 

引发此错误是因为我们将定义了nodePort属性的负载平衡器服务资源更改为不支持nodePort属性的群集 IP 服务资源。Kubernetes 非常善于知道如何更新现有资源以匹配新的配置,但在这种情况下,它不知道如何执行这种更改。

最简单的解决方案是删除服务资源并重新运行部署。因为我们已经在 Octopus 中完全定义了部署过程,所以我们可以安全地删除和重新创建这些资源,因为我们知道没有任何未记录的设置已经应用到我们可能要删除的集群。

这一次部署成功了,我们成功地部署了入口资源。

让我们打开通过入口控制器资源公开的 URL。

我们得到了 404。这里出了什么问题?

管理 URL 映射

这里的问题是,我们打开了一个像 http://35.193.149.6/httpd 这样的 URL,然后通过同样的路径到达 HTTPD 服务。我们的 HTTPD 服务在httpd路径下没有内容可提供。它的根路径中只有从 ConfigMap 资源映射的index.html文件。

幸运的是,这种路径不匹配很容易解决。通过将nginx.ingress.kubernetes.io/rewrite-target注释设置为/,我们可以配置 Nginx 将它在路径/httpd上收到的请求传递到路径/。因此,当我们在浏览器中访问 URLhttp://35.193.149.6/httpd时,HTTPD 服务看到对根路径的请求。

将项目重新部署到Development环境中。一旦部署完成,URLhttp://35.193.149.6/httpd将返回显示环境名称的自定义网页。

既然我们已经让Development环境如我们所期望的那样工作了,那么将部署推送到Production环境(记住删除旧的服务资源,否则将再次抛出nodePort错误)。这一次,部署立即生效。

nginx.ingress.kubernetes.io/rewrite-target注释在简单的情况下有效,但是当返回的内容是链接到 CSS 和 JavaScript 文件的 HTML 页面时,这些链接可能是相对于基本路径的,因为提供内容的应用程序不知道使用的原始路径。

在某些情况下,这可以用nginx.ingress.kubernetes.io/add-base-url: true注释来纠正。这将把一个<base>元素插入到返回的 HTML 的头部。更多信息参见 Nginx 文档

输出变量

使用 Octopus 执行 Kubernetes 部署的一个好处是,您的部署过程可以与更广泛的生态系统集成。这是通过访问为该步骤创建的每个资源生成的输出变量来完成的。这些参数可以在后面的步骤中使用。

通过将 Octopus 项目中的OctopusPrintEvaluatedVariables变量设置为True,可以看到部署期间所有可用的变量。更多细节见文档

在我们的例子中,输出变量是(用步骤的名称替换step name):

  • 章鱼。动作[步骤名称].输出.入口
  • 章鱼。操作[步骤名称].Output.ConfigMap
  • 章鱼。操作[步骤名称].输出.部署
  • 章鱼。动作[步骤名称].输出.服务

这些变量包含创建的 Kubernetes 资源的 JSON 表示。例如,通过在脚本步骤中解析这些 JSON 字符串,我们可以显示一个指向网络负载平衡器的链接,该平衡器公开了我们的 Kubernetes 服务。

$IngressParsed = ConvertFrom-Json -InputObject $OctopusParameters["Octopus.Action[Deploy Httpd].Output.Ingress"]
Write-Host "Access the ingress load balancer at http://$($IngressParsed.status.loadBalancer.ingress.ip)" 

一些有用的提示和技巧

查看资源 YAML

您可能已经注意到 Octopus 步骤确实暴露了可以在部署资源上定义的每一个可能的选项。

如果您需要该步骤没有提供的定制级别,您可以找到在日志文件中创建的资源的 YAML。这些 YAML 文件可以通过Run a kubectl CLI script步骤手动复制、编辑和部署。

即席脚本

管理多个 Kubernetes 帐户和集群的挑战之一是在运行快速查询和一次性维护脚本时不断在它们之间切换。最好的做法是不要用管理员用户运行脚本,但是我想我们都曾以管理员的身份运行过那个卑鄙的命令,只是为了完成工作。而且很多都是用一个有点太宽泛的删除命令烧掉的...

幸运的是,一旦如本文所述在 Octopus 中配置了目标,使用Script Console运行这些局限于单个名称空间的特定脚本就变得很容易了。

您可以访问Script ConsoleTasks -> Script Console。选择反映您正在使用的名称空间的 Kubernetes 目标,并在提供的编辑器中编写一个脚本。

【T2

该脚本将在运行Run a kubectl CLI Script步骤时创建的相同 kubectl 上下文中运行。这意味着您的即席脚本将被包含到目标的名称空间中(当然假设服务帐户具有正确的权限),从而限制了任意命令的潜在损害。

脚本控制台还具有保存谁运行了什么命令的历史记录的优势,为任务关键型系统提供了审计跟踪。

编写 Kubernetes 目标脚本

如果您正在管理一个大型 Kubernetes 集群,那么创建帐户和目标可能会非常耗时。幸运的是,这个过程可以自动化,所以 Kubernetes 名称空间和服务帐户资源以及 Octopus 帐户和目标都是用一个脚本创建的。

创建一个针对现有 Kubernetes 管理目标(即使用 Kubernetes 管理凭证设置的目标)的Run a kubectl CLI Script步骤。

定义以下项目变量:

  • KubernetesUrl-Kubernetes 集群 Url。

将以下代码粘贴为脚本体。

# The account name is the project, environment and tenant
$projectNameSafe = $($OctopusParameters["Octopus.Project.Name"].ToLower() -replace "[^A-Za-z0-9]","")
$accountName = if (![string]::IsNullOrEmpty($OctopusParameters["Octopus.Deployment.Tenant.Id"])) {
    $($OctopusParameters["Octopus.Project.Name"] -replace "[^A-Za-z0-9]","") + "-" + `
    $($OctopusParameters["Octopus.Deployment.Tenant.Name"] -replace "[^A-Za-z0-9]","") + "-" + `
    $($OctopusParameters["Octopus.Environment.Name"] -replace "[^A-Za-z0-9]","")
} else {
    $($OctopusParameters["Octopus.Project.Name"] -replace "[^A-Za-z0-9]","") + "-" + `
    $($OctopusParameters["Octopus.Environment.Name"] -replace "[^A-Za-z0-9]","")
}

# The namespace is the acocunt name, but lowercase
$namespace = $accountName.ToLower()
#Save the namespace for other steps
Set-OctopusVariable -name "Namespace" -value $namespace
Set-OctopusVariable -name "AccountName" -value $accountName

Set-Content -Path serviceaccount.yml -Value @"
---
kind: Namespace
apiVersion: v1
metadata:
  name: $namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: $projectNameSafe-deployer
  namespace: $namespace
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: $namespace
  name: $projectNameSafe-deployer-role
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods", "services", "ingresses", "secrets", "configmaps", "namespaces"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]   
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: $projectNameSafe-deployer-binding
  namespace: $namespace
subjects:
- kind: ServiceAccount
  name: $projectNameSafe-deployer
  apiGroup: ""
roleRef:
  kind: Role
  name: $projectNameSafe-deployer-role
  apiGroup: ""
"@

kubectl apply -f serviceaccount.yml

$data = kubectl get secret $(kubectl get serviceaccount "$projectNameSafe-deployer" -o jsonpath="{.secrets[0].name}" --namespace=$namespace) -o jsonpath="{.data.token}" --namespace=$namespace
$Token = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($data))

New-OctopusTokenAccount `
    -name $accountName `
    -token $Token `
    -updateIfExisting

New-OctopusKubernetesTarget `
    -name $accountName `
    -clusterUrl $KubernetesUrl `
    -octopusRoles "Target Role" `
    -octopusAccountIdOrName $accountName `
    -namespace $namespace `
    -updateIfExisting `
    -skipTlsVerification True 

然后,这个脚本将创建 Kubernetes 资源,获取令牌,并创建 Octopus 令牌帐户和 Kubernetes 目标。

您还可以允许在部署过程中提供项目变量,或者将这个脚本保存为一个步骤模板,以便于重用。

摘要

在本文中,我们看到了如何使用 Octopus 管理 Kubernetes 集群中的多环境部署。在中,每个应用程序和环境都被配置为一个单独的命名空间,具有匹配的服务帐户,该帐户只对该命名空间具有权限。然后将名称空间和服务帐户配置为 Kubernetes 目标,这代表了 Kubernetes 集群中的权限边界。

然后使用蓝/绿策略执行部署,我们看到失败的部署如何将最后一次成功的部署保留在原位,同时可以调试失败的资源。

我们还研究了如何使用 Helm 跨环境部署应用程序,这是通过部署 nginx-ingress 图表实现的。

最终结果是一个可重复的部署过程,该过程强调在Development环境中测试变更,并在准备就绪时将变更推送到Production环境中。

我希望你喜欢这篇博文,如果你对 Kubernetes 的功能有任何建议或意见,请留下评论。这些步骤处于预览状态,因此非常感谢您的反馈。

使用 Octopus Deploy 和 ReadyRoll - Octopus Deploy 部署数据库配置表

原文:https://octopus.com/blog/deploying-database-configuration-tables-with-readyroll

下面是我们的朋友 Daniel Nolan 的客座博文,他是 ReadyRoll 的创始人,ready roll 是一个 Visual Studio 扩展,它使 SQL 数据库的开发和部署变得更加简单。我们认为这项技术很酷,并希望与我们的客户分享,所以请继续阅读!

我最近了解到一个开发团队正在使用 ReadyRoll 和 Octopus Deploy 的组合来驱动配置数据在其每个生命周期环境中的部署。

对于存储在文件中的配置数据,这实际上非常容易实现;毕竟 Octopus 从 1.0 版本开始就内置了对应用配置(Web.config/App.config)变量替换的支持。这个特性允许跨多个环境管理配置数据的键/值对,后来扩展到允许多个项目使用来自可重用库集的变量。

这样做的好处是显而易见的:每当您需要更改服务器名称或更新超时设置时,不必在多个地方更新配置,您只需更新 Octopus 中的一个变量,在下一次部署时,所有相关的系统组件都会发生变化。这意味着,例如,您不需要担心在部署后设置正确的特定于环境的连接字符串,并且您可以避免将服务器名称硬编码到项目源代码中。

到目前为止,一切顺利。

存储在数据库中的配置呢?

对于存储在文件中的配置数据来说,这确实很好,但是如果您的配置存储在数据库表中呢?保持受源代码控制的数据与关系数据库表同步是一项非常困难的任务。当您添加将数据转换成特定于环境的键-值对的复杂性时,很少有团队着手自动化这项任务就不足为奇了。

在这个客户的例子中,他们需要驱动环境敏感的[SSIS Configurations]表的部署——这是一个键值对存储,由他们组织中的多个SQL Server Integration Services包使用。

Octopus+ReadyRoll 提供了解决方案

好消息是 ReadyRoll 支持开箱即用的 Octopus 变量:对于您希望在脚本中使用的每个 Octopus 变量,只需将一个 SQLCMD 变量添加到 ReadyRoll 数据库项目中。在部署时,ReadyRoll 会将存储在 Octopus 中的值传递给部署脚本。

在我们的配置数据部署中,我们将使用一个部署后脚本来实现这个目的,它将在部署的主要部分完成后执行。因为脚本将在每个部署中执行,所以以一种等幂的方式编写它是很重要的,这样它就可以多次运行并得到相同的结果。

首先,我们需要为我们的数据库设置一个基线。在创建新的 ReadyRoll 数据库项目之后,将以下内容添加到新的部署一次脚本中,以生成模式:

CREATE TABLE [dbo].[SSIS Configurations]
(
[ConfigurationId] [int] NOT NULL IDENTITY(1, 1),
[ConfigurationFilter] [nvarchar] (255) NOT NULL,
[ConfiguredValue] [nvarchar] (255) NULL,
[PackagePath] [nvarchar] (255) NOT NULL,
[ConfiguredValueType] [nvarchar] (20) NOT NULL,
CONSTRAINT [PK_SSIS Configurations] PRIMARY KEY CLUSTERED ([ConfigurationId])
);
GO 

下载本脚本

添加等幂数据同步脚本

要填充数据表,通常需要使用一组包含要添加到表中的值的INSERT语句。然而,这不会给我们提供执行增量数据同步所需的“可重新运行性”。

我们需要的是一个可重用的脚本,它足够智能,能够计算出需要对表数据进行哪些更改。实现这一点的关键是 T-SQL MERGE语句,它提供了一种期望状态方法来填充表。

MERGE的伟大之处在于它能够根据需要UPDATEINSERTDELETE目标表中的数据。这基本上意味着,在决定更改表中的配置时,我们不必担心数据的当前状态。

SET IDENTITY_INSERT [dbo].[SSIS Configurations] ON;

MERGE INTO [dbo].[SSIS Configurations] AS Target
USING (VALUES
  (1,'Sales','Server=(local);Database=Customers;Trusted_Connection=True;'
  ,'\Package.Properties[CustomerConnectionString]','Boolean')
 ,(2,'Sales','False','\Package.Properties[SuppressConfigurationWarnings]','Boolean')
 ,(3,'Sales','0','\Package.Properties[LoggingMode]','Object')
 ,(4,'Sales','False','\Package.Properties[FailPackageOnFailure]','Boolean')
 ,(5,'Sales','False','\Package.Properties[Disable]','Boolean')
) AS Source 
([ConfigurationId],[ConfigurationFilter],[ConfiguredValue],[PackagePath],[ConfiguredValueType])
ON (Target.[ConfigurationId] = Source.[ConfigurationId])
WHEN MATCHED THEN
 UPDATE SET
  [ConfigurationFilter] = Source.[ConfigurationFilter], 
  [ConfiguredValue] = Source.[ConfiguredValue], 
  [PackagePath] = Source.[PackagePath], 
  [ConfiguredValueType] = Source.[ConfiguredValueType]
WHEN NOT MATCHED BY TARGET THEN
 INSERT
 ([ConfigurationId],[ConfigurationFilter],[ConfiguredValue],[PackagePath],[ConfiguredValueType])
 VALUES
 (Source.[ConfigurationId],Source.[ConfigurationFilter],Source.[ConfiguredValue]
 ,Source.[PackagePath],Source.[ConfiguredValueType])
WHEN NOT MATCHED BY SOURCE THEN 
 DELETE;

SET IDENTITY_INSERT [dbo].[SSIS Configurations] OFF;
GO 

下载本脚本

如何生成 MERGE 语句

不幸的是,SQL Server 没有提供一种内置的方式来将表数据编写成MERGE语句。因此,我们将使用开源神奇工具 sp_generate_merge ,来完成所有繁重的工作。该工具通过将自身作为全局存储过程安装到[master]数据库中来工作,当执行时,从给定的表中读取数据并输出 T-SQL 语句(访问项目的 Github 页面以了解关于该工具的更多信息)。

将该过程安装到本地 SQL Server 实例中后,在配置数据库的上下文中执行该过程:

EXEC sp_generate_merge 'SSIS Configurations', @schema='dbo'; 

这将生成 MERGE 语句并将结果输出到查询窗口。由于结果包含在一个 Xml 片段中,因此只需删除开头的<?x和结尾的?>字符就可以创建有效的 T-SQL 脚本。将该脚本粘贴到 ReadyRoll 数据库项目中的新部署后脚本中(例如Post-Deployment\01_Deploy_SSIS_Config_Data.sql)。下载输出示例

如何向 T-SQL 脚本添加变量

您可能已经注意到,我们生成的MERGE语句目前包含每个键值对的静态值([ConfigurationValue]列)。我们将在后面的文章中用 SQLCMD 变量替换这些变量,这将与 Octopus 中定义的变量一一对应。首先,我们需要向数据库项目添加一些变量。为了简单起见,我们将为每一个 SSIS 配置键/值对行添加一个 SQLCMD 变量 ,它们将依次映射到 Octopus Deploy 项目中的匹配变量。

在 ReadyRoll 数据库项目设计器中,切换到 SQLCMD Variables 选项卡,为每个键/值对添加一个变量,将上述文字指定为默认值:

接下来,编辑后期部署脚本,用 SQLCMD 变量替换每个[ConfigurationValue]文本值,格式为$(VariableName):

SET IDENTITY_INSERT [dbo].[SSIS Configurations] ON;

MERGE INTO [dbo].[SSIS Configurations] AS Target
USING (VALUES
  (1,'Sales','$(CustomerConnectionString)', '\Package.Properties[CustomerConnectionString]'
   ,'Boolean')
 ,(2,'Sales','$(SuppressConfigurationWarnings)','\Package.Properties[SuppressConfigurationWarnings]'
   ,'Boolean')
 ,(3,'Sales','$(LoggingMode)','\Package.Properties[LoggingMode]','Object')
 ,(4,'Sales','$(FailPackageOnFailure)','\Package.Properties[FailPackageOnFailure]','Boolean')
 ,(5,'Sales','$(Disable)','\Package.Properties[Disable]','Boolean')
) AS Source 
([ConfigurationId],[ConfigurationFilter],[ConfiguredValue],[PackagePath],[ConfiguredValueType])
ON (Target.[ConfigurationId] = Source.[ConfigurationId])
WHEN MATCHED THEN
 UPDATE SET
  [ConfigurationFilter] = Source.[ConfigurationFilter], 
  [ConfiguredValue] = Source.[ConfiguredValue], 
  [PackagePath] = Source.[PackagePath], 
  [ConfiguredValueType] = Source.[ConfiguredValueType]
WHEN NOT MATCHED BY TARGET THEN
 INSERT
 ([ConfigurationId],[ConfigurationFilter],[ConfiguredValue],[PackagePath],[ConfiguredValueType])
 VALUES
 (Source.[ConfigurationId],Source.[ConfigurationFilter],Source.[ConfiguredValue]
 ,Source.[PackagePath],Source.[ConfiguredValueType])
WHEN NOT MATCHED BY SOURCE THEN 
 DELETE;

SET IDENTITY_INSERT [dbo].[SSIS Configurations] OFF;
GO 

下载本脚本

在继续之前,最好通过运行 Build 来测试这个脚本...部署解决方案

如果默认值与表中的当前值匹配,则输出窗口应显示零个受影响的行。

在 Octopus 中设置它

假设你已经通过 Octopus 为部署设置了数据库,最后一步是在 Octopus 项目或者相关的变量库中设置变量。只需向 Octopus 添加与 SQLCMD 变量(在上一步中定义)同名的变量,ReadyRoll 就会在部署时将这些值映射到您的 SQL 脚本。

值得注意的是,在 Octopus 中,由您决定覆盖哪些变量。如果您对数据库项目文件中设置的默认值感到满意,那么您可以选择简单地从 Octopus 配置中省略这些值,默认值将继续用于您的部署中。

要查看所有操作,创建一个发布并部署到一个测试环境中。

请注意,由于这是该项目的第一次发布,因此将创建新表并插入行。

对表数据的快速检查表明,它与我们在 Octopus 中的当前变量配置相匹配:

要测试增量更改是否会成功传播到表中,请打开 Octopus 项目中的变量列表,并将变量FailPackageOnFailure的值调整为True。在更新版本并将其重新部署到测试环境之后,确认[SSIS Configurations]记录的值反映了新的变量值:

结论

通过在 T-SQL 脚本中使用 Octopus 变量,而不是使用文字值,只需更新 Octopus 中的一个变量并重新部署给定的版本,就可以部署配置数据。不需要修改代码,也不需要可怕的硬编码或环境嗅探来确定哪些设置适用于给定的目标环境。

这还意味着,如果生产数据库的备份被恢复到测试 SQL Server 实例上,您需要做的就是在 Octopus 中运行一个部署,以清除表中的任何实时配置数据。

通过在 Octopus 中存储您的所有配置,您可以更进一步为您的所有配置数据创建一个单一的参考点。ReadyRoll 与 Octopus 变量存储的直接集成为简化您的数据库部署流程创造了机会,使您的团队能够更快、更频繁地交付。

不说别的,想想看,一旦您将 DBA 从费力不讨好的任务中解放出来,让他们不必在每个环境中维护所有数据,他们会多么高兴!

下载本项目(。sqlproj,8KB)

使用 Docker、Google、Azure 和 Octopus - Octopus Deploy 构建和部署 Java 应用程序

原文:https://octopus.com/blog/deploying-java-app-docker-google-azure

持续集成(CI)过程通常包括构建映像并将其推送到容器注册中心。然后,持续交付(CD)工具接管并部署到一个端点,如 web 应用程序。在我们的 CI 系列中,我们探索了实现这一目标的各种方法。

为了演示其中一个过程,我构建了一个 Maven Java 项目,并在 Google 容器注册中心(GCR)上托管了这个映像。

您可以通过 Octopus 访问 GCR,并将 Java 应用程序部署到 Azure Kubernetes 服务(AKS)。

先决条件

要跟进这篇文章,你需要:

章鱼水下应用

Octopus underwater 应用程序是用户创建第一个部署的登录页面。它包括帮助您继续 Octopus Deploy 之旅的帖子链接。

你可以在 GitHub 上找到 web 应用库。

对于不同的用例,存储库被分成不同的分支。对于这篇文章,使用水下应用 java 分支。

构建并推送注册中心

您使用命令行构建 Java 项目,并使用 gcloud 将映像推送到 GCR。

首先,配置 gcloud 工具指向您的PROJECT_ID:

gcloud config set project <PROJECT_ID> 

克隆您将用来构建和部署到 Azure 的 Java 项目存储库:

git clone https://github.com/terence-octo/octopus-underwater-app
cd octopus-underwater-app
git checkout underwater-app-java 

使用run命令并访问http://localhost:8080/在本地测试应用程序

chmod +x mvnw
./mvnw spring-boot:run 

当您运行 package 步骤时,它会为应用程序构建可部署的目标 JAR:

./mvnw package 

接下来,启用容器注册表来存储容器映像:

gcloud services enable containerregistry.googleapis.com
export GOOGLE_CLOUD_PROJECT=`gcloud config list --format="value(core.project)"` 

运行以下命令,使用正确的设置创建 config.json:

gcloud auth configure-docker 

jib 工具创建图像并将其推送到容器注册表:

./mvnw com.google.cloud.tools:jib-maven-plugin:build -Dimage=gcr.io/$GOOGLE_CLOUD_PROJECT/octopus-underwater-app:latest 

通过进入集装箱注册主页确认图像存在于 GCR 上。

gcr

正在从 Azure 检索用于 Octopus 部署的凭据

您需要检索一些凭证以传递给 Octopus Deploy。

按照我们文档中的步骤来添加 Azure 服务主体到 Octopus Deploy

创建 Azure Kubernetes 集群

接下来,切换到 Microsoft Azure 来托管您的 Kubernetes 集群。Octopus Deploy 与云无关,因此它可以与跨多个云提供商的部署一起工作。

  1. 通过转到您的资源组并创建一个 Kubernetes 服务来创建一个新的 Kubernetes 集群。
  2. 为集群命名,并接受所有默认选项。

Create Kubernetes Cluster

章鱼步

添加部署目标

  1. 转到基础设施,然后部署目标,然后添加部署目标,然后 Kubernetes 集群
  2. 使用之前设置的 Azure 服务原则填写字段。
  3. 为部署分配唯一的角色。

添加外部源

为了让 Octopus 访问存储在 GCR 的图像,您需要启用 Google 容器注册表提要

设置部署步骤

在您的项目中,添加部署 Kubernetes 容器步骤。

Deploy Kubernetes Containers step

YAML 文件

点击编辑 YAML 框,将下面的 YAML 文件粘贴到框中。YAML 文件填充 Octopus UI 中的各种设置。您必须用您的 google PROJECT_ID 替换 PROJECT_ID。使用前面设置的 Google 外部提要凭证,您还可以使用 UI 手动选择容器图像。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-web-underwater
  labels:
    app: java-web-app
spec:
  selector:
    matchLabels:
      app: java-web-app
  replicas: 1
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: java-web-app
    spec:
      containers:
        - name: java-web-app
          image: gcr.io/<PROJECT_ID>/octopus-underwater-app
          ports:
            - containerPort: 80 

服务

将以下 YAML 粘贴到步骤的服务部分。这将通过 Octopus 客户端创建一个 Azure 服务。

 apiVersion: v1
kind: Service
metadata:
  name: underwater-service
spec:
  type: LoadBalancer
  ports:
    - name: web
      port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: java-web-app 

Kubernetes Service section in Octopus process editor

部署到 Azure

  1. 点击保存
  2. 点击 Create Release ,点击各个步骤,将应用部署到 Azure。

您可以通过 runbooks 设置对 Kubernetes 资源的监控。

转到您的项目仪表板,然后运行手册,然后添加运行手册,然后定义您的运行手册流程,然后添加步骤,然后 Kubernetes - Inspect 资源

Inspect Kubernetes Octopus

分配您为部署目标设置的角色。您可以通过设置资源库对象动词来复制kubectl get service命令。

Get Service

点击保存,然后运行

操作手册现在可以跨团队共享。这意味着监控可以在组织级别完成,而不是在本地机器上单独完成。

转到任务日志,查看您刚刚创建的underwater-service。在外部 IP 下找到 IP 地址。在浏览器地址栏输入这个地址,你就会看到章鱼水下应用。

Octopus Underwater App

结论

在这篇文章中,你构建了章鱼水下应用程序,并将图片推送到 GCR。您使用 Octopus Deploy 来引用这个映像,并将该映像部署到 AKS。

阅读我们的持续集成系列的其余部分。

愉快的部署!

通过 Octopus Deploy - Octopus Deploy 部署带有 MySQL 后端的 Java web 应用程序

原文:https://octopus.com/blog/deploying-java-with-mysql

Deploying a Java web app with a MySQL backend with Octopus Deploy

在本文中,我将介绍如何构建和部署一个使用 MySQL 后端数据库的基于 Java 的 web 应用程序。

设置构建服务器

对于这个演示,我使用 Azure DevOps 作为我的构建服务器。当人们想到 Azure DevOps 时,他们会立即想到。NET/。NET 核心,不是 Java。但是,Microsoft build server 的任务库中内置了 Maven 和 ANT 构建任务。

等等,好像是...太容易了。

您的怀疑是正确的,尽管任务确实存在,但如果没有一点配置,它们实际上是无法工作的。对我们来说幸运的是,这一切都很简单。

构建代理上的 Java

要构建 Java,需要在构建代理上安装 Java 开发工具包(JDK),可以从 OpenJDK 下载。如果您像我一样是 Windows 用户,要使 Java 正常工作,还需要两个额外的步骤:

  • 创建 JAVA_HOME 环境变量,并将其设置为 JAVA 安装的根目录(即 c:\Program Files\Java)。
  • 将\bin 文件夹添加到 Path 环境变量中(即 c:\ Program Files \ Java \ jav exversion \ bin)。

构建代理上的 Maven

我们需要做的下一件事是在我们的构建代理上安装 Maven。Maven 没有安装程序,它是一个. zip 文件,需要提取出来放在构建代理上。与 Java 类似,我们需要配置环境变量:

  • 创建 MAVEN_HOME 并指向提取 MAVEN 的位置(即 c:\maven)。
  • 将\bin 文件夹添加到 Path 环境变量中(即 c:\maven\bin)。

添加 Maven 功能

如果您正在创建一个新的构建代理,这一步可能是不必要的,代理安装的一部分会扫描机器的功能,如果找到的话会自动添加 Maven。如果您使用的是现有的代理,您需要进入 Azure DevOps (ADO)并手动为代理添加功能。

导航到 ADO 的代理池部分。选择您想要修改的代理并点击功能:

点击添加功能按钮,添加以下内容(参见我使用的值的截图):

  • JAVA_HOME
  • 专家
  • MAVEN_HOME

现在,您的 ADO 实例可以构建 Maven 项目。

配置 ADO 以构建 ANT 项目的步骤几乎与此相同。对于构建代理和功能部分,用 ANT 替换 Maven。

示例应用程序

在这个演示中,我使用的是 Pet Clinic 示例应用程序,它最初是为了演示 Spring 框架的功能而开发的。Pet Clinic 已经配置为使用 Maven 构建,并且能够使用 MySQL 作为数据库后端。开箱即用,宠物诊所是完全功能性的,所以我们自然要修改它。

调整 POM

我需要对 POM 做一些调整。XML (Maven 项目对象模型)文件,使其适用于本文:

  • 使用变量使<version>属性动态化。
  • 将活动配置文件切换到 MySQL。
  • 更改 MySQL 概要文件的jdbc.url以使用变量。
  • 改变finalName属性。
  • 更新cssDestinationFolder属性。

使版本号动态化

原始项目中的版本号是硬编码的,但是我想根据内部版本号使它成为一个动态值。这很容易通过在 Maven 构建过程中插入变量来实现。在 POM.xml 文件中找到<version>5.2.1</version>,并将其更改为<version>${project.versionNumber}</version>

变量名project.versionNumber是我选的名字,不过你想怎么命名都行。

将活动数据库配置文件更改为 MySQL

这个报告的作者在使这个应用程序支持多个数据库后端方面做了出色的工作:HyperSQL、MySQL 和 PostgreSQL。默认设置为 HyperSQL 配置文件(HSQLDB)。要将其更改为 MySQL,只需将<activation> XML 节点从 HSQLDB 概要文件移动到 MySQL 概要文件。

为此:

  1. 在 POM 中找到<profiles> XML 节点。XML 文件。
  2. 找到子节点为<id>HSQLDB</id><profile>节点。在<id>节点的正下方是一个<activation>节点。
  3. <activation>节点移动到 MySQL 节点。

生成的 MySQL 节点应该如下所示:

<profile>
    <id>MySQL</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>            
    <properties>
        <db.script>mysql</db.script>
        <jpa.database>MYSQL</jpa.database>
        <jdbc.driverClassName>com.mysql.jdbc.Driver</jdbc.driverClassName>
        <jdbc.url>jdbc:mysql://${databaseServerName}/${databaseName}?useUnicode=true</jdbc.url>
        <jdbc.username>root</jdbc.username>
        <jdbc.password></jdbc.password>
    </properties>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-driver.version}</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</profile> 

更改我的概要文件的 jdbc.url 以使用变量

当编译 Maven 项目时,活动数据库概要文件的属性被复制到结果中的/WEB-INF/classes/spring/datasource-config.xml文件中。战争档案。datasource-config.xml是用于该应用程序数据库连接字符串的文件。在上面的代码示例中,我们将服务器和数据库的连接字符串改为使用变量databaseServerNamedatabaseName:

<jdbc.url>jdbc:mysql://${databaseServerName}/${databaseName}?useUnicode=true</jdbc.url> 

更改 finalName 属性

POM 的finalName属性。XML 是。战争档案将在项目打包时给出。默认finalNamepetclinic:

<finalName>petclinic</finalName> 

构建时,这会产生一个 petclinic.war 的文件名。Octopus Deploy 使用语义版本化,文件名中嵌入了一个版本号。POM 的version属性。XML 文件将非常适合这种情况,因为我们已经将它动态化了。将您的<finalName>属性更新为:

<finalName>petclinic.web.${project.version}</finalName> 

我添加了.web.部分,以便更容易地识别这是 Octopus Deploy 中的哪个组件。

更新 cssDestinationFolder 属性

当我在本地系统上完成这项工作时,当我更改 POM 的finalName属性时,我遇到了令人讨厌的意外;没有一个 css 能成为最终产品。经过一些故障排除后,我发现将 css 复制到应用程序中的操作使用了finalName值作为复制到的路径。为了解决这个问题,我更新了<cssDestinationFolder>属性:

<cssDestinationFolder>${project.build.directory}/petclinic/resources/css</cssDestinationFolder> 

收件人:

<cssDestinationFolder>${project.build.directory}/petclinic.web.${project.version}/resources/css</cssDestinationFolder> 

如果您的应用程序呈现以下图像,则您的<cssDestinationFolder>不正确:

更新 datasource-config.xml

通过这个示例应用程序,我了解到无论何时部署它,它都会运行包含的数据库脚本。经过一番挖掘,我发现我可以在 datasource-config.xml 文件中注释掉一些 XML 来阻止这种情况。我仍然需要数据库脚本,我只是不希望每次部署应用程序时都执行它们。稍后将详细介绍。

导航到/src/main/resources/spring/datasource-config.xml并注释掉数据库初始化器部分。它应该是这样的:

 <!-- Database initializer. If any of the script fails, the initialization stops. -->
    <!-- As an alternative, for embedded databases see <jdbc:embedded-database/>. -->
    <!--
    <jdbc:initialize-database data-source="dataSource">
        <jdbc:script location="${jdbc.initLocation}"/>
        <jdbc:script location="${jdbc.dataLocation}"/>
    </jdbc:initialize-database>
    --> 

添加飞行路线项目

Flyway 是一款基于迁移的数据库部署工具。简而言之,它是一个包含在项目中的命令行实用程序,使用特定的文件夹结构以指定的顺序执行 SQL 脚本。Flyway 下载本质上是您将添加到项目源代码控制中的项目。

添加。到 Flyway 的 sql 脚本

在 Java 应用程序源代码中,复制。位于 Flyway 项目的src/main/resources/db/mysql/sql文件夹的 sql 文件。然后,重命名文件以符合 Flyway 的工作方式。例如:

  • V1__initDb.sql
  • V1_1__populateDb.sql

就是这样!

创建生成定义

现在我们已经完成了在构建代理上安装 Maven 的先决条件工作,调整了几个文件,并添加了 Flyway,我们可以创建我们的构建定义了。

添加 Maven 任务

创建一个新的构建定义,这个演示使用经典编辑器而不是 YAML 方法:

从空工单开始:

添加 Maven 构建任务:

填写任务输入字段:

  • 显示名称:
    • 这个值不重要。
  • Maven POM 文件:
    • 如果您的pom.xml不在根文件夹中,使用省略号(…)找到它。
  • 目标:
    • clean package dependency:purge-local-repository
  • 选项:
    • -Dproject.versionNumber=$(Build.BuildNumber) -DdatabaseServerName=$(DatabaseServerName) -DdatabaseName=$(DatabaseName) -DskipTests=$(SkipTests)

dependency:purge-local-repository 目标可能没有必要,但是我喜欢在构建时清理我的 sources 文件夹。

导航到变量并创建以下内容:

  • 数据库名称:#{Project.MySql.Database.Name}
  • 数据库服务器名称:#{Project.MySql.Database.ServerName}
  • 船长:true

如果您不熟悉变量值使用的#语法,Octopus Deploy 使用它来替换变量。

跳过测试是必要的,因为测试试图连接到数据库后端。因为我们将它们作为部署过程的变量,所以连接尝试将会失败,并使构建完全失败:

您会注意到定义了两个额外的变量;MajorVersionMinorVersion。我使用这些变量在 ADO 中创建我的内部版本号格式。要对此进行设置,单击 Options 选项卡并填写构建号格式,我使用了$(MajorVersion).$(MinorVersion).$(Year:yy)$(DayOfYear).$(Date:Hmmss):

这是 ADO 对$(Build.BuildNumber)的引用值,我们将它输入到传递给 Maven 构建任务的project.VersionNumber变量中。

Maven 任务到此为止。.war是 Octopus Deploy 中内置包存储库支持的文件类型,因此不需要打包 Java 应用程序。

打包飞行路线项目

如前所述,Flyway 是一个命令行实用程序,所以没有构建项目。我们唯一需要做的就是打包它,以便 Octopus Deploy 可以用它做一些事情。为此,我们可以使用任何创建 ZIP 或 NuGet 包的任务。本演示使用 Octopus 部署插件,打包应用程序任务:

  • 包 ID: petclinic.flyway
  • 包装格式:NuPkg
  • 包版本:$(Build.BuildNumber)
  • 来源路径:$(Build.SourcesDirectory)\flyway
  • 输出路径:$(build.stagingdirectory)

推进到章鱼部署

构建过程的最后一部分是将我们创建的包推送到我们的 Octopus Deploy 服务器。添加推送至 Octopus 部署任务:

  • 空间:选择您的空间。
  • 包装:
  • $(Build.SourcesDirectory)\target\*.war
  • $(build.artifactstagingdirectory)\*.nupkg

这就是您的构建定义。

创建 Octopus 部署项目

在 Octopus Deploy web 门户中,单击项目选项卡,然后单击添加项目:

给项目命名,然后点击保存。或者,您可以选择将其放入哪个项目组以及使用哪个生命周期:

变量

点击保存后,我们将直接进入我们全新的项目。单击变量来定义我们的一些变量,因为在我们定义流程之前,我们需要它们先存在:

在变量屏幕上,我们创建以下变量:

  • Project.MySql.Database.Name
  • Project.MySql.Database.ServerName
  • Project.MySql.Database.User.Name
  • Project.MySql.Database.User.Password

为变量命名空间被认为是 Octopus Deploy 的最佳实践;它帮助用户识别变量的来源和用途。

项目。MySql .数据库.名称

这个变量是我们在构建过程中使用#语法设置的变量之一。我们将很快介绍变量是如何被替换的。现在,给这个变量取数据库名的值。在我的例子中,我使用了petclinic

项目。MySql .数据库.服务器名

当您从一个环境转到另一个环境时,数据库服务器通常是不同的。此变量将使用部署到的环境范围内的值。

项目。MySql.Database .用户名

顾名思义,这是数据库连接的用户名。

项目。MySql .数据库.用户.密码

这是数据库连接的用户帐户密码:

过程

定义好变量后,让我们创建流程。点击进程按钮:

添加部署包步骤

在过程屏幕上,点击添加步骤按钮:

选择类别和部署包步骤:

填写步骤的属性:

  • 步骤名称:Deploy Flyway package
  • 关于目标角色:PetClinic-Db(这是我给角色起的名字)。
  • 包装:petclinic.flyway

这是点击保存后的步骤:

变量替换(可选)

如果您在任何。sql 脚本(我做了),您需要配置这个步骤来执行变量替换。

点击配置功能按钮:

选择文件中的替代变量:

点击确定,然后向下滚动并展开文件中的替代变量部分。对于目标文件,输入值sql/*.sql,点击保存:

添加 Flyway 迁移步骤

社区步骤模板库中提供了此步骤。

和前面一样,点击添加步骤按钮。当窗口出现时,键入 flyway 来过滤步骤。鼠标悬停飞行路线迁移并点击安装并添加:

系统会提示您安装并添加,点击保存:

填写步骤模板的必需属性:

  • 关于角色中的目标:PetClinic-Db(这是我给角色起的名字)。
  • 飞行路径包步骤:使用下拉菜单选择部署飞行路径包步骤。
  • 目标网址:jdbc:mysql://#{Project.MySql.Database.ServerName}/#{Project.MySql.Database.Name}?useUnicode=true
  • 目标用户:#{Project.MySql.Database.User.Name}
  • 目标密码:#{Project.MySql.Database.User.Password}

由于这是一个敏感变量,我们需要单击 Bind 图标来设置它使用一个变量:

T14 T16

这一步就这样,点击保存

在撰写本文时,必须在目标上执行部署包步骤和 Flyway 迁移步骤。然而,Flyway 迁移步骤的工人友好版本正在审查中。

部署网站步骤

Octopus Deploy 为两个最流行的基于 Java 的应用程序的 web 服务器 Tomcat 和 JBOSS/wildly 提供了内置步骤。这个演示使用了野花步骤。

像以前一样,点击添加步骤,然后按野花过滤。选择部署到 Wildfly 或 EAP 步骤模板:

填写步骤模板的详细信息:

  • 步骤名称:Deploy Petclinic Web
  • 关于角色中的目标:Petclinic-Web
  • 包 ID: petclinic.web
  • 管理主机或 IP: #{Octopus.Machine.Hostname}(这是一个系统变量,使用要部署到的机器的主机名。)
  • 管理用户:[Your management user]
  • 管理密码:[Password for management account]
  • 部署名称:PetClinic.war

对于 Wildfly 步骤,还有最后一件要配置的事情,替换 datasource-config.xml 中的# variables。单击配置特性按钮并选择替换文件中的变量,就像我们在部署包步骤中所做的那样。在目标文件部分,输入以下值WEB-INF/classes/spring/datasource-config.xml

使用/而不是\是为了支持 Linux 部署目标,并且仍然适用于 Windows 目标。

就是这样。让我们创建我们的第一个版本。

部署时间

我们现在准备部署我们的应用程序。点击创建发布按钮:

点击保存:

点击部署到开发(用您命名的环境替换开发):

点击展开:

T32

完成后,您应该会看到如下所示的屏幕:

注意,web 服务器的部署被部署到两个服务器上,Wildfly1 (Windows)和 Wildfly2 (Linux)。

点击任务日志标签,查看关于我们部署的更多详细信息:

飞行方式:

野花

宠物诊所:

结论

在本文中,我们配置了一个 CI/CD 管道来构建和部署一个基于 Java 的 web 应用程序,使用 Flyway 来管理 MySQL 数据库!

用 Octopus - Octopus Deploy 部署 JavaScript 库项目

原文:https://octopus.com/blog/deploying-javascript-library-project-with-octopus

有一种常见的前端开发模式,它以最好的意图开始,但如果不小心处理,可能会导致痛苦。您看到了跨多个项目重用前端代码的需要,这些项目由使用不同技术的不同团队维护。您创建一个共享的 JavaScript 库项目,它有自己的 repo 和 release 过程。这是一个明智的想法,但提出了一些需要好答案的问题,以阻止我们的快乐成长为一个怪物。

在这篇文章中,我解释了如何管理一个共享 JavaScript 项目的部署过程,这个项目很容易从其他 Octopus 项目中引用。我的例子使用了部署到亚马逊 S3 的 Vue JS 包,但是同样的原则可以应用到前端框架和托管提供商的任何组合。

该过程

我们在 Octopus 中完成的部署过程如下所示:

four step finished deployment process in Octopus

我解释了每一步的原因以及它们是如何工作的。

如果 S3 存储桶不存在,则创建一个

本着把服务器当成牛而不是宠物的精神,除了拥有一个具有适当权限的 AWS 帐户之外,我对我们的部署目标不做太多假设。在一个具体的例子中,我有专门的存储桶用于区域和测试、阶段和生产环境的组合,所以我喜欢这样一个构建过程,它只需要我在限定了作用域的变量中命名存储桶和区域,并在需要时正确地设置它。这是通过运行以下 PowerShell 脚本的 AWS CLI 步骤实现的,该脚本使用 AWS CLI 来查看您在尝试列出 bucket 的内容时是否获得了非错误结果。否则,它会创建 bucket,然后在步骤完成之前轮询以确认 bucket 是否存在。

$bucket = $OctopusParameters["s3-bucket-name"]
$region = $OctopusParameters["s3-region"]
$found = aws s3 ls s3://$bucket/ --recursive --summarize | Select-String -Pattern 'Total Objects:'
if ([string]::IsNullOrWhiteSpace($found)) {
    aws s3api create-bucket --bucket $bucket --region $region
    aws s3api wait bucket-exists --bucket $bucket
} 

制定 S3 CORS 政策

这是另一个 AWS CLI 脚本,内嵌以下 PowerShell:

echo '{"CORSRules": [ { "AllowedOrigins": ["*"], "AllowedHeaders": [],"AllowedMethods": ["GET"] } ] }' | out-file -encoding ASCII cors.json
aws s3api put-bucket-cors --bucket bundle-s3 --cors-configuration file://cors.json 

您可以根据需要更复杂地使用 CORS,但是在我的例子中,我假设我们的包存在于它们自己的专用桶中,所以简单的allow all GET requests是有意义的。

编码步骤很重要,而不是直接回显到文件。我不确定为什么设置 CORS 的 CLI 命令坚持从文件中读取,并且不让你通过命令行传递 JSON。如果您需要更复杂的 CORS 策略,那么选择包中的脚本文件,并在您的 bundle repo 中控制. ps1 和 cors.json 文件的源代码,这比我在这里使用的内联选项更干净。

script options

将包上传到 S3

下一个 AWS CLI 步骤有几个先决条件,在展示如何在 Octopus 中设置之前我会解释一下。我演示了它是如何在 Vue 中实现的。对于其他框架,步骤会有所不同,但是解释会为您指出正确的方向。

一个 JavaScript 文件来管理它们

默认情况下,Vue 会创建一个单独的 CSS 文件、一个生产源映射文件和一个供应商库文件。这是 webpack 执行的一个优化,用于更好地缓存不经常改变的公共依赖项。

这些都是合理的默认设置,但是对于一个不太大的共享 JS 包,您可以从允许消费者引用一个 JS 文件开始,以获得所有的样式和行为。如果需要,您可以在以后引入对优化、源映射和外部 CSS 的支持。

要指示 vue 只构建一个 JavaScript 文件,您可以将以下 vue.config.js 添加到 Vue 项目的根目录下,紧挨着 package.json:

 module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: false
    }
  },
  css: {
    extract: false,
  },
  productionSourceMap: false
} 

单独的 config.json 文件

章鱼变量替换功能强大。为了在您的前端项目中利用它们,您需要告诉 Octopus 我们的包旁边有一个 config.json 文件。

为了让 Vue CLI 将文件包含在其 dist 文件夹中,该文件夹将被压缩以创建发送到 Octopus 的包,您需要在 Vue 启动新项目时生成的 public 文件夹中创建js\config.json

这与针对 React 和 Angular 显示的配置示例类似,只是在完整部署流程的环境中针对 Vue 实施。

现在,您需要运行以下命令:

npm run build 

您可以看到 Vue 已经将 config.json 作为一个单独的文件复制到输出文件夹中。要告诉 Vue 使用它,创建以下助手模块:

const configUrl = document.currentScript.src.substring(0, document.currentScript.src.lastIndexOf('/')) + '/config.json'

module.exports = async function() {
    const response = await fetch(configUrl);
    return await response.json();
}; 

现在,当您调用这个函数时,这个包将获取它部署到的 S3 文件夹中的相邻 config.json。

下面是如何在 Vue 组件中使用它:

<template>
  <div id="app">
    <img alt="Vue logo" :src="`${config.bucketUrl}/assets/logo.png`">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import getConfig from './config.js'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return { config: { } }
  },
  async created () {
    this.config = await getConfig();
  },
}
</script> 

您需要给任何图像或其他外部资产的引用一个基本 URL,类似于 bucketUrl 设置。这是因为 Vue 默认产生的相对路径对 S3 资产的消费者不起作用。

要告诉 Octopus 替换 config.json 文件中的变量,点击配置特性,勾选结构化配置变量

config variables

告诉 Octopus 替换MyBundle\js\config.json中的变量,其中MyBundle是您的包的 ID。

config variables 2

上传您的包

最后,向步骤提供 CLI 命令,将您的包、config.json 和资产上传到以当前版本命名的文件夹中。

aws s3 cp MyBundle s3://#{s3-bucket-name}/release_#{Octopus.Release.Number} --recursive --exclude index.html --acl public-read 

我跳过上传 Vue CLI 生成的 index.html 文件,因为您的捆绑包的传统消费者不能使用该 index.html,而是需要唯一命名的捆绑包文件的 URL。

下一步也是最后一步的重点是向任何需要的项目提供环境的最新包的 URL。

更新变量集中的包 URL

能够通过自动填充的配置设置告诉其他项目从哪里获得缓存不足的共享包是为这种类型的 JavaScript 项目构建 Octopus 流程的一个优势。

破坏缓存的策略多得惊人,根据我的经验,很多都会带来痛苦。对我来说,这种痛苦要么源于消费者对捆绑过程了解太多,要么源于捆绑商对消费者了解太多。理想情况下,任何使用该包的项目只读取带有该包 URL 的配置设置。幸运的是,Octopus 让您在部署时无需太多定制代码就能实现这一点。

部署之后,这个定制脚本步骤更新了其他项目可以引用的库变量集中作用域BundleUrl变量的包 URL。要做到这一点,参考章鱼。客户端在我们的文档中的步骤中获取包。

该步骤还需要对您部署的包的引用,以找到您上传的 JavaScript 文件的名称。然后,它可以运行以下 PowerShell:

Add-Type -Path 'Octopus.Client/lib/net452/Octopus.Client.dll'

$bundle = Get-ChildItem -Path MyBundle/js/*.js | Select-Object -First 1

$endpoint = new-object Octopus.Client.OctopusServerEndpoint $octopusURI,$octopusApiKey
$repository = new-object Octopus.Client.OctopusRepository $endpoint

$scope = New-Object Octopus.Client.Model.ScopeSpecification
$enviornmentName = $OctopusParameters["Octopus.Environment.Name"]
$envID = $repository.Environments.FindByName($enviornmentName).Id
$scope.Add([Octopus.Client.Model.Scopefield]::Environment,(New-Object Octopus.Client.Model.ScopeValue($envID)))

$libraryVariableSetId = $repository.LibraryVariableSets.FindByName('BundleVariables').Id
$libraryVariableSet = $repository.LibraryVariableSets.Get($libraryVariableSetId);
$variables = $repository.VariableSets.Get($libraryVariableSet.VariableSetId);
$releaseId = $OctopusParameters["Octopus.Release.Number"]
$variables.AddOrUpdateVariableValue("BundleUrl", $bucketUrl + 'release_' + $releaseId + '/js/' + $bundle.Name,$scope)
$repository.VariableSets.Modify($variables) 

就是这样!现在,任何数量的其他项目都可以通过包含BundleVariables库变量集来引用您的共享 JavaScript 包,并且可以使用BundleUrl变量。

结论

这篇文章解释了如何应用限定了作用域的变量、作为牲口的服务器和变量集的概念来实现对共享 JavaScript 项目的合理管理。

与管理 JavaScript 项目的其他解决方案相比,我在生产中遵循这一策略取得了良好的结果。我确实发现自己向前端专家解释说,他们需要重新发布消费者项目,以使它自己升级到软件包的最新版本,但在人们掌握了它之后,这是合乎逻辑的。我的团队中的前端专家对这个部署过程模式的工作方式给了我很好的反馈。

愉快的部署!

使用 CloudFormation 部署 Lambda-Octopus 部署

原文:https://octopus.com/blog/deploying-lambda-cloudformation

Lambda 是 AWS 提供的无服务器功能即服务(FaaS)。Lambdas 提供了可伸缩性、高可用性,以及可伸缩至零的能力,从而降低了不常用部署的成本。

像大多数 AWS 资源一样,Lambdas 可以访问 VPC 来与数据库或 EC2 实例等其他资源进行交互。

在本文中,您将部署一个简单的 Lambda,然后在上一篇文章中提供的带有私有和公共子网的 VPC 的基础上,使用 CloudFormation 在一个可以访问互联网的 VPC 中部署一个 Lambda。

一个简单的 Lambda CloudFormation 模板

部署 Lambda 可以简单到创建一个日志组来捕获 Lambda 的输出,通过 IAM 角色授予 Lambda 对日志组的访问权限,然后定义 Lambda 本身。部署这些资源的模板示例如下所示:

Parameters:
  Tag:
    Type: String
  LambdaS3Bucket:
    Type: String
  LambdaS3Key:
    Type: String
  LambdaName:
    Type: String

Resources: 
  AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}"

  IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*"

  MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30 

Lambda 生成的日志放在一个新的日志组中,由 AWS Logs LogGroup 资源表示:

 AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}" 

要授予 Lambda 写入上述日志组的权限,您必须创建一个可由 Lambda 承担的新 IAM 角色,并包括写入日志组的权限:

 IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*" 

最后一步是创建 Lambda 本身,由 AWS Lambda 函数资源表示。

兰达斯部署已经上传到 S3 桶的代码。CloudFormation 不执行文件上传,因此这必须在部署模板之前单独执行。

下面的示例 Lambda 被配置为部署一个本地编译的二进制文件,通常用 Go 之类的语言编写,或者使用 GraalVM 之类的编译器。

其他语言,如 Java、DotNET Core、Python、PHP 和 Node.js,需要它们自己的唯一运行时,这影响了 CloudFormation 模板中的 RuntimeHandler 属性:

MyLambda:
  Type: "AWS::Lambda::Function"
  Properties:
      Code:
        S3Bucket: !Ref "LambdaS3Bucket"
        S3Key: !Ref "LambdaS3Key"
      Description: "My Lambda"
      FunctionName: !Ref "LambdaName"
      Handler: "not.used.in.provided.runtime"
      MemorySize: 256
      PackageType: "Zip"
      Role: !GetAtt "IamRoleLambdaExecution.Arn"
      Runtime: "provided"
      Timeout: 30 

在 VPC 中放置一个λ

在一个更复杂的场景中,您的 Lambda 将被授权访问一个 VPC,以便访问像数据库或 EC2 实例这样的共享资源。

下面的模板建立在前面的示例基础上,演示了一个混合了公共和私有子网的 VPC,然后部署了一个具有 VPC 访问的 Lambda:

Parameters:
  Tag:
    Type: String
  LambdaS3Bucket:
    Type: String
  LambdaS3Key:
    Type: String
  LambdaName:
    Type: String

Resources: 
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      InstanceTenancy: "default"
      Tags:
      - Key: "Name"
        Value: !Ref "Tag"

  InternetGateway:
    Type: "AWS::EC2::InternetGateway"

  VPCGatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref "VPC"
      InternetGatewayId: !Ref "InternetGateway"

  SubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 0
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.0.0/24"

  SubnetB:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 1
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.1.0/24"

  SubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 1
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.2.0/24"

  RouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref "VPC"

  InternetRoute:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTable

  SubnetARouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetA

  EIP:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: "vpc"

  Nat:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt "EIP.AllocationId"
      SubnetId: !Ref "SubnetA"

  NatRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref "VPC"

  NatRoute:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref "Nat"
      RouteTableId: !Ref "NatRouteTable"

  SubnetBRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref NatRouteTable
      SubnetId: !Ref SubnetB

  SubnetCRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref NatRouteTable
      SubnetId: !Ref SubnetC

  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "Lambda Traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0"

  InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup"

  AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}"

  IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*"

  MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30
        VpcConfig:
            SecurityGroupIds:
            - !Ref "InstanceSecurityGroup"
            SubnetIds:
            - !Ref "SubnetB"
            - !Ref "SubnetC"

Outputs:
  VpcId:
    Description: The VPC ID
    Value: !Ref VPC 

本模板的大部分内容定义了构建带有私有和公有子网的 VPC 所需的资源,以及构建网络基础设施(如 internet 网关和 NAT 网关)以提供对 VPC 子网中任何其它资源的 internet 访问。这些资源在我们关于用 CloudFormation 创建混合 AWS VPC 的帖子中有详细介绍。

然后创建一个安全组,由AWSEC2security group资源表示,以定义应用于该 VPC 中资源的网络规则。此示例包括允许所有出站流量的规则:

 InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "Lambda Traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0" 

共享安全组的资源被允许使用以下安全组入口规则相互通信,该规则由AWSEC2SecurityGroupIngress资源表示。这允许相关资源相互访问,而不需要它们具有已知的 IP 地址或被放置在特殊的 CIDR 块中:

 InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup" 

日志组和 IAM 角色与本文开头描述的简单示例相同。

Lambda 略有变化,增加了一个新的VPCConfig属性,允许 Lambda 访问 VPC 内部的资源。

值得注意的是,Lambda 被授权访问私有子网SubnetBSubnetC,这些子网没有连接互联网网关:

 MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30
        VpcConfig:
            SecurityGroupIds:
            - !Ref "InstanceSecurityGroup"
            SubnetIds:
            - !Ref "SubnetB"
            - !Ref "SubnetC" 

结论

Lambda 很容易部署,只需要少量的支持资源,如日志组和 IAM 角色,就可以对 Lambda 进行监控和调试。

对于更复杂的场景,其中 lambda 必须能够访问其他资源,如 VPC 中的数据库或 EC2 实例,可以配置 lambda 对指定子网的网络访问,并使用安全组控制网络流量。

在这篇文章中,您了解了如何执行简单的 Lambda 部署,然后看到了一个更复杂的例子,它在 Lambda 的基础上构建了一个 VPC。

我们有其他关于云形成模板的帖子你可能也会觉得有帮助。

阅读我们的 Runbooks 系列的其余部分。

愉快的部署!

跨环境部署 AWS Lambdas-Octopus Deploy

原文:https://octopus.com/blog/deploying-lambdas

Deploying AWS Lambdas across environments

无服务器是从管理物理机或虚拟机稳步转变的最新版本。术语“无服务器”有点误导,因为仍然有服务器在运行代码。但是无服务器的承诺是你不必再考虑服务器。像 AWS Lambda 这样的无服务器平台为您处理创建、销毁、更新和公开这些服务器,让您专注于运行您的代码。

无服务器模式对于某些类型的工作负载非常有吸引力。不经常运行的代码,比如由文件上传或添加数据库行触发的函数,作为 Lambda 托管非常方便。您甚至会发现,Lambdas 可以以经济高效且可扩展的方式处理更多传统工作负载,如网站托管。

如今,部署无服务器应用程序是微不足道的。CLI 工具和 IDE 插件使您只需一个命令或点击,就可以从代码进入生产。不过,最终这种部署将需要一个更健壮的过程,允许不写代码的团队一起批量修改和验证。满足这些需求的传统解决方案是拥有多个环境,并在进入生产环境之前通过内部测试环境进行部署。

在这篇博文中,我们将深入探讨如何在 CloudFormation 中表达多环境无服务器部署,并以可靠的方式进行。

示例应用程序

在这个例子中,我们将部署两个非常简单的 Lambda 应用程序,它们只是在响应体中返回它们接收到的输入对象。

第一个是用 Go 写的,可以在https://github.com/OctopusSamples/GoLambdaExample找到。

第二个是用 Node.js 写的,可以在https://github.com/OctopusSamples/NodeLambdaExample找到。

独立和分离的部署

在这篇文章中,我们将考虑两种类型的无服务器部署。

自包含风格将所有 Lambda 函数和触发它们的服务(在我们的例子中是 API Gateway)包装成一个单一的 CloudFormation 模板,为每个环境创建独立和隔离的基础设施栈。

独立部署具有以下优势:

  • 万物作为一个整体被创造和毁灭。
  • 部署作为一个组进展到下一个环境。
  • 即使应用程序有多个 Lambdas,也很容易推断已部署应用程序的状态。

虽然自包含部署很容易创建,但它也有缺点,即您不能独立地部署单独的 Lambdas。无服务器平台非常适合微服务,为了充分利用微服务,您必须能够独立开发和部署每个微服务。

分离式风格提供了微服务部署所需的灵活性。在解耦部署中,每个 Lambda 都是独立部署的,同时仍然由单个共享 API 网关公开。

分离部署具有以下优势:

  • 每个 Lambda 管理自己的部署生命周期。
  • 一个共享的 API 网关允许 Lambdas 通过相对 URL 进行交互。
  • 共享主机名使得管理 HTTPS 证书更加容易。

创建自包含部署

自包含部署涉及使用以下资源创建单个 CloudFormation 模板:

AWS::ApiGateway::RestApi资源

AWS::ApiGateway::RestApi资源创建一个 REST API。

API Gateway 提供了多种 API,其中REST API是第一种,也是最可配置的。HTTP API是另一个选项,但是我们在这里不会使用 HTTP API。

下面的代码片段创建了 REST API 资源:

 "RestApi": {
      "Type": "AWS::ApiGateway::RestApi",
      "Properties": {
        "Description": "My API Gateway",
        "Name": "Self-contained deployment",
        "EndpointConfiguration": {
          "Types": [
            "REGIONAL"
          ]
        }
      }
    } 

AWS::Logs::LogGroup资源

为了帮助调试和监控我们的 Lambda 函数,我们创建了一个 CloudWatch 日志组。

日志组的名称基于 Lambda 的名称。这个名称是不可配置的,因此我们从环境名称和服务名称构建日志组名称,这两个名称组合起来创建 Lambda 的名称:

 "AppLogGroupOne": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": { "Fn::Sub": "/aws/lambda/${EnvironmentName}-NodeLambda" }
      }
    } 

AWS::IAM::Role资源

为了让我们的 Lambda 拥有与日志组交互的权限,我们需要一个 IAM 角色来授予访问权限:

 "IamRoleLambdaOneExecution": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": { "Fn::Sub": "${EnvironmentName}-NodeLambda-policy" },
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:CreateLogGroup",
                    "logs:PutLogEvents"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${EnvironmentName}-NodeLambda*:*"
                    }
                  ]
                }
              ]
            }
          }
        ],
        "Path": "/",
        "RoleName": { "Fn::Sub": "${EnvironmentName}-NodeLambda-role" },
      }
    } 

AWS::Lambda::Function资源

这是我们创建 Lambda 本身的地方。

Lambda 示例应用程序已经上传到 S3。如果你正在复制这个模板,那么S3BucketS3Key必须被改变以反映你上传 Lambda 代码的位置。

这个 Lambda 将使用上面创建的 IAM 角色执行:

 "LambdaOne": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Code": {
          "S3Bucket": "deploy-lambda-blog",
          "S3Key": "nodelambdaexample.zip"
        },
        "Environment": {
          "Variables": {}
        },
        "FunctionName": { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
        "Handler": "index.handler",
        "MemorySize": 128,
        "PackageType": "Zip",
        "Role": {
          "Fn::GetAtt": [
            "IamRoleLambdaOneExecution",
            "Arn"
          ]
        },
        "Runtime": "nodejs12.x",
        "Timeout": 20
      }
    } 

AWS::Lambda::Permission资源

为了让 REST API 执行 Lambda,需要授予它访问权限。

有两种方法授予 API 网关对 Lambda 的访问权限: IAM 角色或基于资源的策略。我们在这里选择使用基于资源的策略,因为如果您手动集成两个系统,这就是 API 网关控制台授予自己对 Lambda 的访问权限的方式:

 "LambdaOnePermissions": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Fn::GetAtt": [
            "LambdaOne",
            "Arn"
          ]
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Join": [
            "",
            [
              "arn:",
              {
                "Ref": "AWS::Partition"
              },
              ":execute-api:",
              {
                "Ref": "AWS::Region"
              },
              ":",
              {
                "Ref": "AWS::AccountId"
              },
              ":",
              {"Ref": "RestApi"},
              "/*/*"
            ]
          ]
        }
      }
    } 

AWS::ApiGateway::Resource资源

API 网关公开的路径中的元素称为资源。例如,/vehicles/cars/car1的 URL 路径由三个资源组成:vehiclescarscar1

资源可以用{proxy+}语法匹配整个剩余路径。

下面的模板创建了两个资源,它们组合起来匹配路径/nodefunc/{proxy+}:

 "ResourceOne": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": { "Fn::GetAtt": ["RestApi", "RootResourceId"] },
        "PathPart": "nodefunc"
      }
    },
    "ResourceTwo": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": {
          "Ref": "ResourceOne"
        },
        "PathPart": "{proxy+}"
      }
    } 

AWS::ApiGateway::Method资源

为了响应资源上的 HTTP 请求,我们需要公开一个方法。

当调用 Lambda 时,API Gateway 可以选择使用代理集成

在代理集成选项出现之前,从 API Gateway 调用 Lambda 需要大量的样板文件配置来连接 HTTP 请求和 Lambda 执行。HTTP 请求在请求的 URL、查询字符串、头和 HTTP 主体中公开了一系列信息。然后,HTTP 响应可以包括状态代码、标头和正文。另一方面,我们有一个 Lambda,它接受单个对象作为输入,返回单个对象作为输出。这意味着在调用 Lambda 时,API Gateway 必须配置为将 HTTP 调用中的各种输入编组到单个对象中,并将 Lambda 的响应解组到 HTTP 响应中。在实践中,对每个方法都进行了相同的配置,导致了大量的重复工作。

创建代理集成是为了给这个常见问题提供一个勾选框解决方案。启用代理集成后,API Gateway 将传入的 HTTP 请求整理成一个由 Lambda 使用的标准对象,并期望返回一个具有特定形状的对象,由此生成 HTTP 响应。

更“传统”的方法是将一个 API 网关阶段与一个 Lambda 别名相匹配,阶段和别名都表示环境中的进展。然而,Lambda 别名有很大的局限性,我认为这使得它们根本不适合解决环境发展的常见用例。你可以在博客文章中读到更多关于为什么你不应该使用 Lambda 别名来定义环境的内容。所以我们避免使用别名,并为每个环境部署一个新的 Lambda。

下面是代理集成的两种方法:

 "LambdaOneMethodOne": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {       
        "AuthorizationType": "NONE", 
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceOne"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    },
    "LambdaOneMethodTwo": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {        
        "AuthorizationType": "NONE",
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceTwo"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    } 

AWS::ApiGateway::Deployment资源

上面描述的资源和方法已经被配置在一种工作阶段中。此配置不会暴露给流量,直到在部署中被捕获并升级到某个阶段。

请注意,我们在资源名称上附加了一个随机字符串。部署是不可变的,因此每次这个 CloudFormation 模板被发布到一个堆栈时,我们都会创建一个新的部署资源:

 "Deployment93b7b8be299846a5b609121f6fca4952": {
      "Type": "AWS::ApiGateway::Deployment",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"}
      },
      "DependsOn": [
        "LambdaOneMethodOne",
        "LambdaOneMethodTwo"
      ]
    } 

AWS::ApiGateway::Stage资源

这个过程的最后一步是创建一个阶段,并通过引用部署资源“提升”工作阶段:

 "Stage": {
      "Type": "AWS::ApiGateway::Stage",
      "Properties": {
        "CanarySetting": {
          "DeploymentId": {"Ref": "Deployment93b7b8be299846a5b609121f6fca4952"},
          "PercentTraffic": 0
        },
        "DeploymentId": {"Ref": "Deployment93b7b8be299846a5b609121f6fca4952"},
        "RestApiId": {"Ref": "RestApi"},
        "StageName": {"Fn::Sub": "${EnvironmentName}"}
      }
    } 

完整的模板

部署第二个 Go Lambda 与我们在上面部署的 Node Lambda 非常相似,所以我们不会再讨论所有的资源。

下面的模板是自包含的 CloudFormation 模板的完整副本,其中一个参数定义了构建阶段 URL 的环境名称和输出变量。

环境名称的默认值已使用包含当前环境名称的 Octopus 系统变量进行了配置。这意味着该模板很容易作为 Octopus 中的部署 AWS CloudFormation 模板步骤的一部分进行部署:

{
  "Parameters" : {
    "EnvironmentName" : {
      "Type" : "String",
      "Default" : "#{Octopus.Environment.Name}"
    }
  },
  "Resources": {
    "RestApi": {
      "Type": "AWS::ApiGateway::RestApi",
      "Properties": {
        "Description": "My API Gateway",
        "Name": "Self-contained deployment",
        "EndpointConfiguration": {
          "Types": [
            "REGIONAL"
          ]
        }
      }
    },
    "AppLogGroupOne": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": { "Fn::Sub": "/aws/lambda/${EnvironmentName}-NodeLambda" }
      }
    },
    "IamRoleLambdaOneExecution": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": { "Fn::Sub": "${EnvironmentName}-NodeLambda-policy" },
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:CreateLogGroup",
                    "logs:PutLogEvents"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${EnvironmentName}-NodeLambda*:*"
                    }
                  ]
                }
              ]
            }
          }
        ],
        "Path": "/",
        "RoleName": { "Fn::Sub": "${EnvironmentName}-NodeLambda-role" },
      }
    },
    "LambdaOne": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Code": {
          "S3Bucket": "deploy-lambda-blog",
          "S3Key": "nodelambdaexample.zip"
        },
        "Environment": {
          "Variables": {}
        },
        "FunctionName": { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
        "Handler": "index.handler",
        "MemorySize": 128,
        "PackageType": "Zip",
        "Role": {
          "Fn::GetAtt": [
            "IamRoleLambdaOneExecution",
            "Arn"
          ]
        },
        "Runtime": "nodejs12.x",
        "Timeout": 20
      }
    },
    "LambdaOnePermissions": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Fn::GetAtt": [
            "LambdaOne",
            "Arn"
          ]
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Join": [
            "",
            [
              "arn:",
              {
                "Ref": "AWS::Partition"
              },
              ":execute-api:",
              {
                "Ref": "AWS::Region"
              },
              ":",
              {
                "Ref": "AWS::AccountId"
              },
              ":",
              {"Ref": "RestApi"},
              "/*/*"
            ]
          ]
        }
      }
    },
    "ResourceOne": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": { "Fn::GetAtt": ["RestApi", "RootResourceId"] },
        "PathPart": "nodefunc"
      }
    },
    "ResourceTwo": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": {
          "Ref": "ResourceOne"
        },
        "PathPart": "{proxy+}"
      }
    },
    "LambdaOneMethodOne": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {      
        "AuthorizationType": "NONE",  
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceOne"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    },
    "LambdaOneMethodTwo": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {      
        "AuthorizationType": "NONE",  
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceTwo"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    },
    "AppLogGroupTwo": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": { "Fn::Sub": "/aws/lambda/${EnvironmentName}-GoLambda" }
      }
    },
    "IamRoleLambdaTwoExecution": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": { "Fn::Sub": "${EnvironmentName}-GoLambda-policy" },
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:CreateLogGroup",
                    "logs:PutLogEvents"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${EnvironmentName}-GoLambda*:*"
                    }
                  ]
                }
              ]
            }
          }
        ],
        "Path": "/",
        "RoleName": { "Fn::Sub": "${EnvironmentName}-GoLambda-role" },
      }
    },
    "LambdaTwo": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Code": {
          "S3Bucket": "deploy-lambda-blog",
          "S3Key": "golambdaexample.zip"
        },
        "Environment": {
          "Variables": {}
        },
        "FunctionName": { "Fn::Sub": "${EnvironmentName}-GoLambda" },
        "Handler": "GoLambdaExample",
        "MemorySize": 128,
        "PackageType": "Zip",
        "Role": {
          "Fn::GetAtt": [
            "IamRoleLambdaTwoExecution",
            "Arn"
          ]
        },
        "Runtime": "go1.x",
        "Timeout": 20
      }
    },
    "LambdaTwoPermissions": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Fn::GetAtt": [
            "LambdaTwo",
            "Arn"
          ]
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Join": [
            "",
            [
              "arn:",
              {
                "Ref": "AWS::Partition"
              },
              ":execute-api:",
              {
                "Ref": "AWS::Region"
              },
              ":",
              {
                "Ref": "AWS::AccountId"
              },
              ":",
              {"Ref": "RestApi"},
              "/*/*"
            ]
          ]
        }
      }
    },
    "ResourceThree": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": { "Fn::GetAtt": ["RestApi", "RootResourceId"] },
        "PathPart": "gofunc"
      }
    },
    "ResourceFour": {
      "Type": "AWS::ApiGateway::Resource",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"},
        "ParentId": {
          "Ref": "ResourceThree"
        },
        "PathPart": "{proxy+}"
      }
    },
    "LambdaTwoMethodOne": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {     
        "AuthorizationType": "NONE",   
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-GoLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceThree"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    },
    "LambdaTwoMethodTwo": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {      
        "AuthorizationType": "NONE",  
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambda" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Ref": "ResourceFour"
        },
        "RestApiId": {"Ref": "RestApi"}
      }
    },
    "Deployment93b7b8be299846a5b609121f6fca4952": {
      "Type": "AWS::ApiGateway::Deployment",
      "Properties": {
        "RestApiId": {"Ref": "RestApi"}
      },
      "DependsOn": [
        "LambdaOneMethodOne",
        "LambdaOneMethodTwo"
      ]
    },
    "Stage": {
      "Type": "AWS::ApiGateway::Stage",
      "Properties": {
        "CanarySetting": {
          "DeploymentId": {"Ref": "Deployment93b7b8be299846a5b609121f6fca4952"},
          "PercentTraffic": 0
        },
        "DeploymentId": {"Ref": "Deployment93b7b8be299846a5b609121f6fca4952"},
        "RestApiId": {"Ref": "RestApi"},
        "StageName": {"Fn::Sub": "${EnvironmentName}"}
      }
    }
  },
  "Outputs": {
    "StageURL": {
      "Description": "The url of the stage",
      "Value": {
        "Fn::Join": [
          "",
          [
            "https://",
            {"Ref": "RestApi"},
            ".execute-api.",
            {
              "Ref": "AWS::Region"
            },
            ".amazonaws.com/",
            {
              "Ref": "Stage"
            },
            "/"
          ]
        ]
      }
    }
  }
} 

部署独立的 Lambdas

通过 Octopus 将上面的模板部署到一个名为开发的环境中,会创建一个 API 网关,其中包含一个名为开发的阶段,资源层次会创建路径/gofunc/*nodefunc/*:

我们还有两个 Lambda,称为 Development-GoLambdaDevelopment-NodeLambda :

已经编写了 Lambdas 来返回 API Gateway 在响应体中作为输入传递的对象。这允许我们检查 API Gateway 通过其代理集成构建的对象的详细信息:

如果我们将这个自包含的 Lambda 部署提升到一个新环境,我们将创建第二个 API 网关,它有自己的 stage 和另外两个 Lambda。每个环境都由自己的 CloudFormation 堆栈定义,我们创建的任何资源都不会在环境之间共享。

通过 CloudFormation 模板部署 Lambda 堆栈的一个好处是,它被认为是一个应用:

应用仪表板提供了组成堆栈的各个资源的集中视图:

为了清理环境,我们删除了 CloudFormation 堆栈:

在这篇文章的开头,我们提到了自包含部署的以下好处:

  • 万物作为一个群体被创造和毁灭。
  • 部署作为一个组进展到下一个环境。
  • 即使应用程序有多个 Lambdas,也很容易推断已部署应用程序的状态。

我们现在可以看到将部署堆栈定义为单个自包含的 CloudFormation 模板是如何提供这些好处的。

然而,自包含部署的一个显著缺点是所有 Lambdas 的生命周期彼此紧密耦合。在我们的例子中,您不能独立于 Go Lambda 部署节点 Lambda。当你的栈变得越来越复杂,单个团队开始对每个 Lambda 负责时,这就成了一个问题。

随着您的堆栈发展成为真正的微服务架构,您需要分离每个 Lambda 的部署。一种方法是将每个 Lambda 拆分成它自己的自包含部署。但是,扩展多个自包含部署几乎肯定需要一个服务发现层来应对每个 API 网关实例暴露的独特 URL 的激增。

另一种方法是将每个 Lambda 部署到一个共享 API 网关实例中。这样,每个 Lambda 都可以使用相对 URL 来访问兄弟 Lambda。这就是我们所说的分离部署。

创建分离的部署

分离部署在以下方面不同于自包含部署:

  • API 网关被认为是共享资源,并且是在 Lambdas 的部署之外创建的。
  • API 网关资源(即 URL 中的路径元素)被视为共享资源。例如,你可能有两辆兰博达响应路径/汽车。一个 Lambda 将响应 HTTP POST 方法,第二个响应 HTTP DELETE 方法。在这种情况下,Lambda 都不能声明对资源的独占所有权。
  • 这些阶段被视为共享资源。

让我们看看这在实践中是如何工作的。我们从现有的 API 网关 REST API 开始。我们需要 API ID 和根资源的 ID:

我们需要构建构成 URL 路径的资源。因为这些资源不再只有一个所有者,所以我们不需要在 CloudFormation 模板中表示它们。下面我们创建资源,通过 CLI 公开节点 Lambda:

RESULT=`aws apigateway create-resource --rest-api-id d0oyqaa3l6 --parent-id 6fpwrle83e --path-part nodefunc`
ID=`jq -r  '.id' <<< "${RESULT}"`
aws apigateway create-resource --rest-api-id d0oyqaa3l6 --parent-id $ID --path-part {proxy+} 

然后,我们可以部署 Lambda 并创建附加到上面创建的资源的方法。这里的代码类似于自包含部署,但是通过参数提供了 API 网关和资源 id,因为这些资源是在 CloudFormation 模板之外创建的:

{
  "Parameters" : {
    "EnvironmentName" : {
      "Type" : "String",
      "Default" : "#{Octopus.Environment.Name}"
    },
    "ResourceOne" : {
      "Type" : "String"
    },
    "ResourceTwo" : {
      "Type" : "String"
    },
    "ApiGatewayId" : {
      "Type" : "String"
    }
  },  
  "Resources": {
    "AppLogGroupOne": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": { "Fn::Sub": "/aws/lambda/${EnvironmentName}-NodeLambdaDecoupled" }
      }
    },
    "IamRoleLambdaOneExecution": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": { "Fn::Sub": "${EnvironmentName}-NodeLambdaDecoupled-policy" },
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:CreateLogGroup",
                    "logs:PutLogEvents"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${EnvironmentName}-NodeLambdaDecoupled*:*"
                    }
                  ]
                }
              ]
            }
          }
        ],
        "Path": "/",
        "RoleName": { "Fn::Sub": "${EnvironmentName}-NodeLambdaDecoupled-role" }
      }
    },
    "Lambda": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Description": "Octopus Release #{Octopus.Release.Number}",
        "Code": {
          "S3Bucket": "deploy-lambda-blog",
          "S3Key": "nodelambdaexample.zip"          
        },
        "Environment": {
          "Variables": {}
        },
        "FunctionName": { "Fn::Sub": "${EnvironmentName}-NodeLambdaDecoupled" },
        "Handler": "index.handler",
        "MemorySize": 128,
        "PackageType": "Zip",
        "Role": {
          "Fn::GetAtt": [
            "IamRoleLambdaOneExecution",
            "Arn"
          ]
        },
        "Runtime": "nodejs12.x",
        "Timeout": 20
      }
    },
    "LambdaPermissions": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Fn::GetAtt": [
            "Lambda",
            "Arn"
          ]
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Join": [
            "",
            [
              "arn:",
              {
                "Ref": "AWS::Partition"
              },
              ":execute-api:",
              {
                "Ref": "AWS::Region"
              },
              ":",
              {
                "Ref": "AWS::AccountId"
              },
              ":",
              {
                "Fn::Sub": "${ApiGatewayId}"
              },
              "/*/*"
            ]
          ]
        }
      }
    },
    "LambdaMethodOne": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {      
        "AuthorizationType": "NONE",  
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambdaDecoupled" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Fn::Sub": "${ResourceOne}"
        },
        "RestApiId": {
          "Fn::Sub": "${ApiGatewayId}"
        }
      }
    },
    "LambdaMethodTwo": {
      "Type": "AWS::ApiGateway::Method",
      "Properties": {      
        "AuthorizationType": "NONE",  
        "HttpMethod": "ANY",
        "Integration": {          
          "IntegrationHttpMethod": "POST",          
          "TimeoutInMillis": 20000,
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":apigateway:",
                {
                  "Ref": "AWS::Region"
                },
                ":lambda:path/2015-03-31/functions/",
                "arn:aws:lambda:",
                {
                  "Ref": "AWS::Region"
                },
                ":",
                {
                  "Ref": "AWS::AccountId"
                },
                ":function:",
                { "Fn::Sub": "${EnvironmentName}-NodeLambdaDecoupled" },
                "/invocations"
              ]
            ]
          }
        },        
        "ResourceId": {
          "Fn::Sub": "${ResourceTwo}"
        },
        "RestApiId": {
          "Fn::Sub": "${ApiGatewayId}"
        }
      },
      "DependsOn": [
        "LambdaVersion479fe95fb94b6c89fb86f412be60d8"
      ]
    },
    "Deploymented479fe95fb94b6c89fb86f412be60d8": {
      "Type": "AWS::ApiGateway::Deployment",
      "Properties": {
        "RestApiId": {
          "Fn::Sub": "${ApiGatewayId}"
        },
        "Description": "Octopus Release #{Octopus.Release.Number}"
      },
      "DependsOn": [
        "LambdaMethodOne",
        "LambdaMethodTwo"
      ]
    },
  },
  "Outputs": {
    "DeploymentId": {
      "Description": "The Deployment ID",
      "Value": {
        "Ref": "Deploymented479fe95fb94b6c89fb86f412be60d8"
      }
    }
  }
} 

和以前一样,我们需要创建一个阶段来公开 API 网关配置。然而,这次 stage 是在一个单独的 CloudFormation 模板中创建的。

贡献给共享 API 网关的每个 Lambda 部署将部署一个更新的模板,用一个新的DeploymentId属性定义阶段。这意味着 CloudFormation 堆栈名称必须能够从 API 网关 ID 和阶段名称中重新创建。例如,您可以创建一个名为APIG-d0oyqaa3l 6-开发的堆栈,为 ID 为 d0oyqaa3l6 的 API 网关定义名为开发的阶段。

以下是该阶段的云形成模板:

{
  "Parameters" : {
    "EnvironmentName" : {
      "Type" : "String",
      "Default" : "#{Octopus.Environment.Name}"
    },
    "DeploymentId" : {
      "Type" : "String",
      "Default" : "#{Octopus.Action[Deploy Lambda].Output.AwsOutputs[DeploymentId]}"
    },
    "ApiGatewayId" : {
      "Type" : "String"
    }
  },
  "Resources": {
    "Stage": {
      "Type": "AWS::ApiGateway::Stage",
      "Properties": {
        "DeploymentId": {"Fn::Sub": "${DeploymentId}"},
        "RestApiId": {"Fn::Sub": "${ApiGatewayId}"},
        "StageName": {"Fn::Sub": "${EnvironmentName}"}
      }
    }
  }
} 

部署分离的 Lambdas

在我们研究解耦部署如何工作之前,让我们考虑一个非常重要的问题,即我们期望每个环境有一个 API 网关和阶段。这是类似于 serverless.io 这样的工具所采取的设计决策。

为什么要将我们自己限制在每个环境的一个阶段呢?

工作阶段(我称之为 API Gateway 控制台中的 Resources 视图)累积变更,工作阶段的当前状态基本上是由不可变的AWS::ApiGateway::Deployment资源作为快照捕获的。

当您拥有代表多个环境的多个阶段时,将部署从测试环境推进到生产环境意味着使用分配给测试阶段的AWS::ApiGateway::Deployment资源的 ID 来更新生产阶段。

重要的是,API Gateway 中没有将工作阶段重置为先前部署的状态、进行隔离更改,然后将隔离的更改提升回阶段的概念。这意味着当您有代表多个环境的多个阶段时,您必须假设对工作阶段的每个更改都可以在部署中被捕获,并提升到生产中。

实际上,这也意味着将部署提升到新阶段意味着了解前一阶段或环境,检查前一阶段以找到分配给它的部署,然后用前一阶段的部署 ID 更新下一阶段。

这使得常见的部署场景变得复杂。例如,如何在生产中回滚单个 Lambda?

我们想要做的是用生产阶段的状态重置工作阶段,在工作阶段部署旧的 Lambda 版本,用新的AWS::ApiGateway::Deployment资源拍摄工作阶段的快照,并将该AWS::ApiGateway::Deployment资源提升到生产阶段。对于 GIT 这样的源代码控制工具,这是开发人员想当然的工作流。

然而,因为我们不能用生产阶段的状态来重置工作阶段,所以我们首先必须将旧的 Lambda 版本部署到工作阶段的任何当前状态,用新的AWS::ApiGateway::Deployment资源来快照工作阶段,然后通过开发、测试和生产阶段来促进这种变化。实际上,这意味着我们只是将工作阶段的每个开发 Lambda 版本提升到生产阶段,因为我们试图将单个 Lambda 角色还原。

人们很容易想到,我们可以简单地将旧的AWS::ApiGateway::Deployment资源分配给生产阶段。但是,如果在您意识到您需要回滚 Lambda 之前,其他几个团队已经将他们的 Lambda 版本投入生产了,那该怎么办呢?现在,没有一个AWS::ApiGateway::Deployment资源具有 Lambda 版本和 API 网关设置的正确组合,我们可以回滚到:

特性分支部署也使AWS::ApiGateway::Deployment资源进入生产的过程变得复杂。创建临时 URL 来测试特性分支 Lambda 部署和主线 Lambda 部署是很好的。但是因为任何处于工作状态的东西都可能被快照到一个AWS::ApiGateway::Deployment资源中并提升到生产环境中,所以您很可能会发现您的临时特性分支部署是公开的。

无法将 API Gateway 工作阶段恢复到先前已知的良好状态、进行孤立的更改,以及将该更改升级到某个阶段,这使得回滚或热修复等常见部署模式实际上无法用于多个阶段。此外,工作阶段的每一个变更都是生产部署的候选,这意味着特性分支变得很危险。

通过确保每个环境由具有单个阶段的单个 API 网关表示,我们可以假设工作阶段包含相关公共阶段的最后已知状态。这意味着我们可以通过部署单个 Lambda 的旧版本来回滚单个 Lambda,通过跳过代表早期环境的 API 网关和阶段来执行热修复部署,并通过不将功能分支 Lambda 部署到生产工作阶段来确保功能分支部署不会出现在生产中。

演示分离部署

通过解耦部署,每个单独的 Lambda 部署栈现在都可以将自己插入到一个共享的 API 网关中。最终结果与自包含部署没有区别,这正是我们想要的,因为最终用户不应该看到自包含或解耦部署之间的任何差异。这也意味着分离的部署共享单个域,具有证书等通用设置:

与自包含部署一样,AWS 将通过 CloudFormation 模板创建的资源识别为应用程序。对于分离部署,我们只能看到单独的 Lambda 资源,而看不到 API 网关:

我们还可以通过删除相关的 CloudFormation 堆栈来清理任何资源:

在本文的开头,我们提到了解耦部署的以下优势:

  • 每个 Lambda 管理自己的部署生命周期。
  • 一个共享的 API 网关允许 Lambdas 通过相对 URL 进行交互。
  • 共享主机名使得管理 HTTPS 证书更加容易。

我们现在可以看到解耦部署如何有助于共享 API 网关,给予每个 Lambda 自己的部署生命周期,同时仍然保留共享域名。我们现在有了一个部署策略,允许独立部署微服务,而不改变呈现给最终用户的内容。

结论

无服务器平台的承诺是,您不再需要担心什么服务器或操作系统正在运行您的应用程序。这使得在高度可伸缩和安全的环境中启动和运行您的第一批 Lambdas 变得非常容易。

随着您的 Lambdas 数量和复杂性的增长,常见的部署问题,如环境、热修复、功能分支和独立的微服务生命周期变得至关重要。

不幸的是,AWS 提出的解决方案,特别是 Lambda 别名和 API 网关阶段,并不能很好地解决这些模式。帖子为什么不应该使用 Lambda 别名来定义环境描述了别名如何无法将安全性、性能和日志记录等问题分开,而这篇帖子解释了为什么 API 网关阶段不支持热修复和特性分支。

然而,由于环境与 API 网关和 stage 之间的一对一关系,以及环境与 Lambdas 之间的一对多关系,我们可以设计一个无服务器的部署过程,它可以跨许多环境扩展,同时支持常见的部署模式。

在这篇文章中,我们展示了自包含的、解耦的无服务器部署,并强调了使用 CloudFormation 制作这些部署的许多好处。示例模板为任何希望设计可大规模管理的无服务器部署的人提供了基础。

愉快的部署!

使用 Octopus Deploy - Octopus Deploy 部署 Node.js 应用程序

原文:https://octopus.com/blog/deploying-nodejs

NodeJS logo on a laptop screen in between and connected to MongoDB logo and octopus with glasses.

最初,JavaScript 只在客户端浏览器上执行,做一些像操作文档对象模型(DOM)或执行输入验证这样的事情,以节省到服务器的行程。然而,Node.js 是一种开发人员可以使用 JavaScript 语言编写客户端服务器端代码的技术。

在这篇文章中,我演示了如何使用 Octopus Deploy 部署用 Node.js 编写的应用程序。

示例应用程序

对于这篇文章,我选择了 BestBags 电子商务示例应用程序。这个示例包括快速启动和运行所需的一切,甚至包括创建和播种 MongoDB 数据库(它用作后端)所需的文件。

修改

为了使这个项目更加可配置,我做了一些小的修改。随着应用程序从一个环境转移到另一个环境,原始项目的硬编码值可能会发生变化:

  • 监听的端口
  • 数据库连接字符串

港口

应用程序监听的端口在app.js文件中定义。它检查是否设置了环境变量,如果没有,它使用一个静态值。

我改为使用八进制语法,这样我就可以利用模板中的替代变量特性:

var port = process.env.PORT || #{Project.Nodejs.Port}; 

数据库连接

数据库连接字符串位于config/db.js文件中。与app.js类似,代码测试环境变量,如果没有设置,就使用默认值。

我用 Octostache 替换了默认值,这样我就可以使用模板中的替代变量功能:

const uri = process.env.MONGO_URI || "mongodb://#{MongoDB.Admin.User.Name}:#{MongoDB.Admin.User.Password}@#{MongoDB.Server.Name}:#{MongoDB.Server.Port}/#{Project.Database.Name}?authSource=admin"; 

用于 MongoDB 的 Liquibase changelog

如前所述,示例应用程序在seedDb文件夹中包含一些文件,这些文件将在 MongoDB 中创建集合,并用文档填充集合。虽然可以使用指示Node执行文件的方法来操作数据库,但是它的可伸缩性不是很好。 Liquibase 与 MongoDB 兼容,用于处理数据库部署。我以 Liquibase 期望的格式添加了与seedDb中的文件包含相同信息的dbchangelog.xml

变更日志可以是多种文件格式(XML、JSON 或 YAML)。可能有必要将像&这样的字符 URL 编码为&amp;,以确保它们是有效的。

dbchangelog。XML
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.0.xsd
      http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">

    <changeSet id="1" author="Shawn Sesna">
        <ext:createCollection collectionName="categories">
        </ext:createCollection>
    </changeSet>
    <changeSet id="2" author="Shawn Sesna">
        <ext:createCollection collectionName="products">
        </ext:createCollection>
    </changeSet>
    <changeSet id="3" author="Shawn Sesna">
        <ext:insertMany collectionName="categories">
            <ext:documents>
                [
                    {"_id":{"$oid":"6080461b3167675d2819ad18"},"title":"Backpacks","slug":"backpacks","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad19"},"title":"Briefcases","slug":"briefcases","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad1a"},"title":"Mini Bags","slug":"mini-bags","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad1b"},"title":"Large Handbags","slug":"large-handbags","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad1c"},"title":"Travel","slug":"travel","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad1d"},"title":"Totes","slug":"totes","__v":0},
                    {"_id":{"$oid":"6080461b3167675d2819ad1e"},"title":"Purses","slug":"purses","__v":0}
                ]
            </ext:documents>
        </ext:insertMany>
    </changeSet>
    <changeSet id="4" author="Shawn Sesna">
        <ext:insertMany collectionName="products">
            <ext:documents>
[{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e96"
  },
  "productCode": "2202-3592423009",
  "title": "Classic Blue Backpack",
  "imagePath": "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=334&amp;q=80",
  "description": "Optio accusamus itaque consequuntur eius laudantium laborum nobis. Ratione optio animi corrupti. Sit veniam voluptatem iure.",
  "price": 25,
  "manufacturer": "Kutch and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.056Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e97"
  },
  "productCode": "4085-4774696701",
  "title": "Black Fjallraven Backpack",
  "imagePath": "https://images.unsplash.com/photo-1562546106-b9cb3a76a206?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Repellat at minima labore sapiente. Vitae qui voluptatem fugiat sint hic sapiente et in. Et corporis eius dolor eos velit sed. Placeat incidunt consectetur ducimus optio delectus modi est et sint. Quos quasi quo corporis consequatur fugiat voluptates perferendis.",
  "price": 31,
  "manufacturer": "Swift Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.141Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e98"
  },
  "productCode": "6645-0000839345",
  "title": "Brown and Green Leather Backpack",
  "imagePath": "https://images.unsplash.com/photo-1577733966973-d680bffd2e80?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1500&amp;q=80",
  "description": "Dignissimos aut aut et. Accusantium qui repellendus ut sit sapiente consequatur. Rerum odit expedita hic tempora.",
  "price": 14,
  "manufacturer": "Greenfelder LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.145Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e99"
  },
  "productCode": "3334-9549765845",
  "title": "Grey Stylish Backpack",
  "imagePath": "https://images.unsplash.com/photo-1546938576-6e6a64f317cc?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1400&amp;q=80",
  "description": "Tenetur sed porro magnam a rem deserunt tempore aut. Aspernatur et ex quia laborum magnam temporibus. Eum architecto molestias. Ad voluptas velit sunt.",
  "price": 27,
  "manufacturer": "Kshlerin LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.149Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9a"
  },
  "productCode": "5558-3659233580",
  "title": "Elegant Black Backpack",
  "imagePath": "https://images.unsplash.com/photo-1585916420730-d7f95e942d43?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Voluptatem accusamus sequi quasi atque et molestiae in reiciendis. Quia voluptas quis. Non inventore distinctio molestiae amet maxime fugiat voluptas saepe. Et doloribus ut enim provident cumque quibusdam. Quo mollitia eos voluptatem vero dolores nostrum.",
  "price": 12,
  "manufacturer": "Brekke Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.153Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9b"
  },
  "productCode": "5318-3250673094",
  "title": "Practical Blue Backpack With Leather Straps",
  "imagePath": "https://images.pexels.com/photos/2905238/pexels-photo-2905238.jpeg?auto=compress&amp;cs=tinysrgb&amp;dpr=3&amp;h=750&amp;w=1260",
  "description": "Eos dolores voluptatum voluptates laborum nesciunt aut. Dolorum doloribus iste. Atque sint fugiat qui.",
  "price": 30,
  "manufacturer": "Wisoky and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.156Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9c"
  },
  "productCode": "0362-0259508669",
  "title": "Soft Classic Biege Backpack",
  "imagePath": "https://images.pexels.com/photos/2422476/pexels-photo-2422476.png?auto=compress&amp;cs=tinysrgb&amp;dpr=3&amp;h=750&amp;w=1260",
  "description": "Molestias sed provident voluptatem corporis. Et asperiores neque magnam eaque amet. Consectetur impedit dicta magnam aut nobis hic sed et. Ad nihil aliquid porro.",
  "price": 17,
  "manufacturer": "Jacobi and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.161Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9d"
  },
  "productCode": "3728-3730819531",
  "title": "Practical Durable Backpack",
  "imagePath": "https://images.pexels.com/photos/1545998/pexels-photo-1545998.jpeg?auto=compress&amp;cs=tinysrgb&amp;dpr=3&amp;h=750&amp;w=1260",
  "description": "Et ea ratione qui omnis repellat neque quae. Autem qui officiis. Hic deserunt quia illum ea omnis voluptatem rem.",
  "price": 42,
  "manufacturer": "Gottlieb Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.165Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9e"
  },
  "productCode": "3556-8442757269",
  "title": "Comfortable Laptop Backpack",
  "imagePath": "https://live.staticflickr.com/3428/3361015646_303a2d0571_b.jpg",
  "description": "Corporis omnis omnis dolorem. Eum qui perspiciatis earum. Delectus modi aut hic id laborum adipisci. Eligendi natus neque quis.",
  "price": 32,
  "manufacturer": "Schamberger LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.168Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82e9f"
  },
  "productCode": "1252-2377230172",
  "title": "Extra Large Grey Backpack",
  "imagePath": "https://storage.needpix.com/rsynced_images/backpack-2634622_1280.jpg",
  "description": "Ut vitae sequi consequatur provident esse facere molestiae. Harum nemo quos. Reiciendis voluptatum saepe reiciendis veniam quas. Et sunt nobis aperiam necessitatibus dicta vel placeat nam tenetur. Quae alias quia repudiandae eum fugit.",
  "price": 37,
  "manufacturer": "Balistreri Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad18"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.172Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea0"
  },
  "productCode": "9680-8294649346",
  "title": "Aluminium Metal Suitcase",
  "imagePath": "https://upload.wikimedia.org/wikipedia/commons/6/6d/Aluminium_Briefcase.jpg",
  "description": "Dolor voluptas necessitatibus architecto voluptatem ut voluptate accusantium. Ea ut dolorem voluptatem iusto et vero fuga et. Commodi earum molestiae.",
  "price": 33,
  "manufacturer": "Breitenberg Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad19"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.179Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea1"
  },
  "productCode": "0306-0419633387",
  "title": "Black Leather Durable Suitcase",
  "imagePath": "http://res.freestockphotos.biz/pictures/1/1751-black-leather-briefcase-on-a-white-background-pv.jpg",
  "description": "Iste numquam animi impedit sint. Rerum sed dolore est est esse ut aut omnis sed. Facilis non et. Explicabo quaerat quae facere praesentium ut tempora. Cumque aut natus dolores quia laudantium necessitatibus. Dolores sint reiciendis sit aut occaecati veniam.",
  "price": 10,
  "manufacturer": "Stehr Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad19"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.183Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea2"
  },
  "productCode": "4001-0940276285",
  "title": "Stylish Pastel Pink Travel Bag",
  "imagePath": "https://p1.pxfuel.com/preview/899/786/420/travel-bag-hard-and-bag.jpg",
  "description": "Quos corporis aliquid sit doloremque eos culpa quibusdam dolor sed. Quam sint illo adipisci omnis dignissimos tempora. Commodi libero est repellat quasi non maiores expedita qui ea. Esse molestiae eum odio molestiae repellendus magnam.",
  "price": 43,
  "manufacturer": "Hayes Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.189Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea3"
  },
  "productCode": "8778-9620671594",
  "title": "A Fahionable Set of Two Pink Travel Bags",
  "imagePath": "https://p1.pxfuel.com/preview/479/120/981/luggage-metallic-luguagge-case.jpg",
  "description": "Deleniti rerum voluptas cumque impedit reiciendis officia. Amet ipsam voluptatem aspernatur totam enim culpa doloribus. Et quo temporibus ad amet a alias.",
  "price": 28,
  "manufacturer": "Schneider Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.193Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea4"
  },
  "productCode": "0793-8515385442",
  "title": "White and Black Hard Luggage",
  "imagePath": "https://images.unsplash.com/photo-1565026057447-bc90a3dceb87?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Nisi officiis aut debitis perferendis qui. Dolorem sequi aut ea labore. Eos incidunt autem ea veritatis quo aperiam minima. Consequuntur expedita voluptatibus a perspiciatis dicta nesciunt dolores laboriosam sint. Rem hic alias eaque. Velit repellendus voluptates est quisquam dolore rerum eveniet dicta.",
  "price": 15,
  "manufacturer": "Johns LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.196Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea5"
  },
  "productCode": "4604-6034994643",
  "title": "Rainbow Dotted Duffle Bag Luggage",
  "imagePath": "https://cdn.pixabay.com/photo/2019/06/20/16/10/duffle-bag-4287485_960_720.png",
  "description": "Aperiam fuga illum culpa nesciunt illum. Est animi neque ipsa ut consectetur suscipit. Autem mollitia aut et.",
  "price": 19,
  "manufacturer": "Schiller Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.199Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea6"
  },
  "productCode": "1070-5263957636",
  "title": "Blue and Gray Classic Suitcase",
  "imagePath": "https://p0.pikrepo.com/preview/74/133/blue-and-gray-suede-rolling-luggage-thumbnail.jpg",
  "description": "Aut inventore dolores magnam omnis. Rem eaque qui fugit ratione eaque maiores et qui odio. Ut harum omnis quod ipsum omnis cupiditate mollitia cupiditate nihil. Voluptas aliquam nam soluta incidunt voluptate voluptatem et qui. Optio eos harum accusantium. Quo accusantium molestiae voluptate.",
  "price": 30,
  "manufacturer": "Weber LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.202Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea7"
  },
  "productCode": "6934-6545730771",
  "title": "A Set of Three Hard Durable Suitcases",
  "imagePath": "https://cdn.pixabay.com/photo/2019/01/22/15/53/suitcases-3948389_960_720.png",
  "description": "Cupiditate dolorem illo. Consequuntur qui dignissimos est fuga. Consequatur laudantium qui.",
  "price": 30,
  "manufacturer": "Glover LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.206Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea8"
  },
  "productCode": "2974-9628270546",
  "title": "Light Blue Hard Luggage",
  "imagePath": "https://cdn.pixabay.com/photo/2019/07/09/11/52/travel-bag-4326738_960_720.jpg",
  "description": "Eligendi nihil amet. Id sint voluptatum et omnis quia sapiente. Sed delectus delectus corrupti qui quia est qui qui culpa. Numquam temporibus debitis dolor ut animi molestiae quisquam. Placeat dolores deleniti excepturi possimus est. Facere minus quod saepe distinctio at itaque debitis dignissimos.",
  "price": 27,
  "manufacturer": "Gutkowski Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.209Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ea9"
  },
  "productCode": "7467-1990168016",
  "title": "Black Leather Vintage Suitcase",
  "imagePath": "https://p0.pxfuel.com/preview/942/496/984/various-bag-bags-luggage.jpg",
  "description": "Quam expedita mollitia et commodi quae consequuntur sunt. At at aperiam itaque vel sequi totam. Est a fuga.",
  "price": 16,
  "manufacturer": "Kohler Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.212Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eaa"
  },
  "productCode": "1046-4500322571",
  "title": "A Set of Three Large Travel Bags",
  "imagePath": "https://p0.pxfuel.com/preview/273/580/962/travelvarious-bag-bags-holiday.jpg",
  "description": "Delectus porro et sit aliquid rerum velit. Voluptatem quod fugit pariatur. Ea sed fugiat reprehenderit rerum sed ipsum maiores et omnis. Tenetur possimus autem totam rerum. Animi eaque et maiores cum cupiditate qui odio mollitia et. Ut ut veritatis.",
  "price": 13,
  "manufacturer": "Schmitt Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.216Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eab"
  },
  "productCode": "4356-2358381178",
  "title": "Two Stylish Light Green Travel Bags With Different Sizes",
  "imagePath": "https://p1.pxfuel.com/preview/926/897/247/travel-bag-hard-and-bag.jpg",
  "description": "Quia qui saepe quisquam rerum natus. Vel enim fuga itaque placeat ut odit. Odit cupiditate nemo quos quia non explicabo quam repellat eveniet. Consequatur nulla blanditiis quibusdam et error voluptate rem sed repellat. Unde explicabo harum temporibus.",
  "price": 14,
  "manufacturer": "Deckow and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.220Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eac"
  },
  "productCode": "2683-2539802762",
  "title": "Simple Blue Luggage with Many Compartments",
  "imagePath": "https://p0.pxfuel.com/preview/963/699/697/bag-blue-handbag-white.jpg",
  "description": "Vel sed quo tenetur adipisci delectus dolor porro illum. Quia similique debitis eos voluptatem. Voluptas repudiandae alias dolor quis voluptatem deleniti harum explicabo architecto. Facere nostrum accusantium similique temporibus reiciendis.",
  "price": 17,
  "manufacturer": "Orn Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1c"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.224Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ead"
  },
  "productCode": "1634-4331635389",
  "title": "Pink Leather Crossbody Bag",
  "imagePath": "https://images.unsplash.com/photo-1566150905458-1bf1fc113f0d?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1502&amp;q=80",
  "description": "Libero repellendus sunt eius blanditiis qui. Quibusdam libero et dolore nesciunt aut repellat. Ut placeat quod accusamus ducimus.",
  "price": 37,
  "manufacturer": "Mann Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.229Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eae"
  },
  "productCode": "5023-0310457026",
  "title": "Stylish Pink Crossbody Bag",
  "imagePath": "https://upload.wikimedia.org/wikipedia/commons/b/bc/DKNY_Mini_Flap_Crossbody_W_-_SS_Crossbody_R1513004_Kalbsleder_beige_%281%29_%2816080518124%29.jpg",
  "description": "Molestias et expedita debitis quos ea officiis. Quae reprehenderit earum aut. Praesentium eum neque exercitationem eveniet dolore autem quam reiciendis est.",
  "price": 44,
  "manufacturer": "Bailey Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.233Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eaf"
  },
  "productCode": "4513-1645617349",
  "title": "Mini Black Carra Shoulder Bag",
  "imagePath": "https://p1.pxfuel.com/preview/177/215/691/handbag-bag-today-the-postwoman-fashion-style-skin.jpg",
  "description": "Aut laborum dignissimos. Maxime est voluptatum veritatis qui dolor. Error natus autem molestiae voluptatum quae laudantium dolores. Hic alias eos corporis sit. Dicta voluptas ex est quo consequuntur consequuntur fugit ut. Laborum incidunt eaque reprehenderit.",
  "price": 38,
  "manufacturer": "Morissette Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.236Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb0"
  },
  "productCode": "9364-2868491933",
  "title": "White Leather Mini Bag with Crossbody Strap",
  "imagePath": "https://p2.piqsels.com/preview/392/1016/905/handbags-white-fashion-bag-shoulder-bag.jpg",
  "description": "Nisi hic autem deleniti veniam amet rerum. Ab sit placeat. Dolor explicabo veritatis consectetur quod quasi nulla.",
  "price": 35,
  "manufacturer": "Jast Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.239Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb1"
  },
  "productCode": "0731-7173723339",
  "title": "Blue Jeans Mini Bag",
  "imagePath": "https://c.pxhere.com/photos/37/cb/camera_bag_scene_package_fashion-900156.jpg!d",
  "description": "Voluptatum eaque aut ratione dolor tempora et quis possimus repudiandae. Et id voluptatum nihil. Corrupti cum repellendus qui adipisci quisquam vel. Ab consequatur placeat quia sint provident exercitationem ut ratione non. Alias veritatis provident. Quidem omnis sed sed.",
  "price": 41,
  "manufacturer": "Brakus Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.242Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb2"
  },
  "productCode": "2135-3735671015",
  "title": "Biege Be Dior Mini Bag with Crossbody Strap",
  "imagePath": "https://c.pxhere.com/photos/94/0e/bag_dior_x_n-867928.jpg!d",
  "description": "Eius sunt eius beatae nihil repellat quia pariatur neque est. Rem modi voluptatibus corrupti fugit ullam quis. Ducimus vel quidem et tenetur nihil natus id corporis. Optio quia id deleniti hic.",
  "price": 18,
  "manufacturer": "Jacobson Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.245Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb3"
  },
  "productCode": "7372-3279604268",
  "title": "Red Be Dior Mini Bag with Crossbody Strap",
  "imagePath": "https://c.pxhere.com/photos/92/ad/bag_dior_u-867943.jpg!d",
  "description": "Nemo omnis eos corrupti. Minima laudantium vitae magni non voluptate ut rem. Assumenda id laboriosam ratione at. Sint ut aut numquam libero fugit vel repellendus. Quas est ipsum veniam rerum qui aut perspiciatis ab eius.",
  "price": 42,
  "manufacturer": "Thompson LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.249Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb4"
  },
  "productCode": "9963-0026172561",
  "title": "Light Blue Mini Bag with Golden Strap",
  "imagePath": "https://c.pxhere.com/photos/5b/ea/bag_fashion_style-518819.jpg!d",
  "description": "Impedit iusto fugit nam facilis aut cupiditate est. Totam aut odit alias a possimus at est atque. Soluta veritatis esse dignissimos enim quos eos itaque corporis. Qui dolorem at reprehenderit et quidem.",
  "price": 38,
  "manufacturer": "Torphy and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.252Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb5"
  },
  "productCode": "6113-8986664637",
  "title": "Light Green Mini Bag with Golden Strap",
  "imagePath": "https://c.pxhere.com/photos/19/aa/bag_fashion_style-518820.jpg!d",
  "description": "Minus et non. Fugit facere laborum repudiandae ut quia tempora aliquid et ex. Sunt optio pariatur vel dolorum iusto rem. Sed explicabo ea inventore non nam.",
  "price": 19,
  "manufacturer": "Farrell and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.255Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb6"
  },
  "productCode": "3566-2372429210",
  "title": "Pastel Pink Mini Bag with Golden Strap",
  "imagePath": "https://c.pxhere.com/photos/41/9e/bag_fashion_style-518821.jpg!d",
  "description": "Et laboriosam consectetur ut. Aut iure sit sed eos quasi qui. Neque ut natus ut atque tempore explicabo dolor. Earum a quod ipsam labore nihil eveniet est ullam consequatur. Ipsum ea facilis exercitationem sit vero numquam ea molestias.",
  "price": 22,
  "manufacturer": "Lakin Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.258Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb7"
  },
  "productCode": "5276-5141118088",
  "title": "Biege Leather Crossbody Bag",
  "imagePath": "https://c.pxhere.com/photos/24/f9/bag_fashion_style-518803.jpg!d",
  "description": "Eos ut ut nam voluptatem quibusdam. Hic sed molestias natus. Nostrum minima quia. Quae molestiae in eos asperiores.",
  "price": 43,
  "manufacturer": "Bechtelar Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.261Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb8"
  },
  "productCode": "1552-5848046475",
  "title": "White Leather Crossbody Bag",
  "imagePath": "https://c.pxhere.com/photos/16/e8/bag_fashion_style-518804.jpg!d",
  "description": "Harum corporis quae minus ut cupiditate architecto. Voluptatibus aliquid voluptatem vero ducimus dolor. Modi reiciendis cumque ut et. Facilis iste dolores doloribus laudantium autem aliquid.",
  "price": 36,
  "manufacturer": "Adams LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.266Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eb9"
  },
  "productCode": "2159-7763512374",
  "title": "Elegant White Mini Bag with Silver Strap",
  "imagePath": "https://images.unsplash.com/photo-1564422167509-4f8763ff046e?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Dolores voluptatibus quos aut reiciendis vitae aut molestiae explicabo. Unde ut in voluptatem eum. Autem fugiat corrupti enim a magnam.",
  "price": 25,
  "manufacturer": "Kling and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.272Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eba"
  },
  "productCode": "9565-0822944521",
  "title": "Simple Red Mini Bag",
  "imagePath": "https://c.pxhere.com/photos/87/f0/bag_crimson_product_photos_padlock_bag_women_bags_dot_white-1000331.jpg!d",
  "description": "Voluptate neque quas officiis. Atque voluptates ut voluptatem quos et saepe. Voluptatem cupiditate odit incidunt quia pariatur porro. Debitis voluptas maiores ut.",
  "price": 47,
  "manufacturer": "Anderson Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1a"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.276Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ebb"
  },
  "productCode": "9166-3970912858",
  "title": "Elegant Shiny Brown Leather Handbag",
  "imagePath": "https://c.pxhere.com/photos/a8/b7/handbag_purse_fashion_bag_female_style_women_elegance-703150.jpg!d",
  "description": "Quo consequatur minus totam ut odit officiis omnis. Debitis doloremque consequuntur omnis reiciendis voluptas accusamus fugiat ducimus. Quam iusto labore veniam et in possimus qui eos. Vero quibusdam nobis voluptatum consequatur quia. Et autem quae ut sint maxime quidem. Qui non odit est cum commodi impedit sunt.",
  "price": 20,
  "manufacturer": "Grimes and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.282Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ebc"
  },
  "productCode": "1723-4668870077",
  "title": "Black Leather Handbag with Golden Chains",
  "imagePath": "https://c.pxhere.com/photos/b6/5c/handbag_purse_fashion_bag_female_women_accessory_modern-703145.jpg!d",
  "description": "Animi odit earum corporis facilis dolores quod qui. Ea iusto iste est eveniet perferendis. Sed temporibus quidem possimus.",
  "price": 44,
  "manufacturer": "Hudson Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.286Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ebd"
  },
  "productCode": "0198-1897823728",
  "title": "Elegant Black Leather Handbag",
  "imagePath": "https://c.pxhere.com/photos/4b/82/handbag_purse_fashion_bag_female_style_women_lady-703156.jpg!d",
  "description": "Eos iusto dolore maxime doloremque. Accusantium nisi molestiae vel quos excepturi est harum placeat. Expedita atque ad rerum.",
  "price": 34,
  "manufacturer": "Batz Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.289Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ebe"
  },
  "productCode": "8803-0521316102",
  "title": "Stylish Blue Handbag with its Purse",
  "imagePath": "https://images.unsplash.com/photo-1564422170194-896b89110ef8?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Nisi possimus omnis temporibus et repudiandae doloremque rerum et. Temporibus ut odit minima ipsa incidunt in sapiente aliquid dolorem. Voluptates culpa voluptatem ipsa voluptas dicta reprehenderit aut et quidem.",
  "price": 44,
  "manufacturer": "Predovic Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.293Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ebf"
  },
  "productCode": "4932-1409156275",
  "title": "A set of Two Elegant Handbags",
  "imagePath": "https://images.unsplash.com/photo-1564222256577-45e728f2c611?ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1500&amp;q=80",
  "description": "Sit ut magnam explicabo saepe adipisci est. Nostrum sed eveniet natus. Corporis doloremque assumenda exercitationem ipsa velit vitae doloribus ipsum. Natus delectus voluptas id tempora consequuntur accusamus recusandae molestias. Modi aliquam laboriosam harum.",
  "price": 37,
  "manufacturer": "Funk and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.297Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec0"
  },
  "productCode": "3417-1201909429",
  "title": "Practical Blue Leather Handbag with its Purse",
  "imagePath": "https://p1.pxfuel.com/preview/680/478/429/online-shopping-lisaswardrobe-handbags-shopping.jpg",
  "description": "Illum provident ut. Est asperiores asperiores qui minus voluptatum tempore eaque deserunt cumque. Iusto error accusamus ea. Ratione quisquam debitis incidunt molestiae officia magnam. Quia delectus at ducimus. Dolor ratione non.",
  "price": 25,
  "manufacturer": "Miller LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.300Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec1"
  },
  "productCode": "4217-0275762969",
  "title": "Simple Black Leather Handbag",
  "imagePath": "https://p1.pxfuel.com/preview/762/878/334/handbag-black-gold.jpg",
  "description": "Quibusdam quia porro aliquam id at earum aut. Consequuntur dolores id. Eum non temporibus optio qui consequatur non omnis. Praesentium explicabo eveniet.",
  "price": 12,
  "manufacturer": "Vandervort Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.303Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec2"
  },
  "productCode": "9560-1908876031",
  "title": "Golden Leather Handbag",
  "imagePath": "https://p1.pxfuel.com/preview/550/178/484/bag-handbag-haberdashery.jpg",
  "description": "Accusamus sit qui qui laboriosam et excepturi vero. Ut voluptas voluptas non enim tenetur quia sint qui. Sed iste quia ea voluptates rerum a. Omnis illum sint magni eius sapiente officia. Aut optio officia accusantium.",
  "price": 31,
  "manufacturer": "Morar and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.306Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec3"
  },
  "productCode": "9834-9433988843",
  "title": "Shiny Black Leather Handbag",
  "imagePath": "https://p1.pxfuel.com/preview/5/396/904/package-briefcase-leather-bags.jpg",
  "description": "Repellendus quam minima. In amet eveniet amet dolores distinctio dolores vitae eligendi modi. Cupiditate sit deleniti adipisci quas voluptates nesciunt expedita nam magni.",
  "price": 40,
  "manufacturer": "Bechtelar Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.309Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec4"
  },
  "productCode": "8372-8525525927",
  "title": "Gray and Yellow Flowery Shoulder Bag",
  "imagePath": "https://p1.pxfuel.com/preview/843/210/542/vera-bradley-purse-handbag-shoulder-bag.jpg",
  "description": "Amet eveniet dolores quis earum quaerat. Quibusdam qui repellat qui et odio deserunt optio perferendis officia. Ut officiis consectetur voluptatem eos omnis.",
  "price": 11,
  "manufacturer": "Hirthe and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.312Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec5"
  },
  "productCode": "4032-5897742259",
  "title": "Blue and Brown Leather Handbag with Shoulder Strap",
  "imagePath": "https://p1.pxfuel.com/preview/57/634/392/purse-bag-handbag-fashion.jpg",
  "description": "Et reiciendis ipsum optio perferendis. Assumenda distinctio iure iste accusantium omnis sit sint esse. Mollitia esse iure. Labore dolorum magnam facere similique tempore ut est. Distinctio et et dolore est laborum ducimus.",
  "price": 13,
  "manufacturer": "Boyle Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1b"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.316Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec6"
  },
  "productCode": "1245-0706667105",
  "title": "Hot Pink Leather Purse",
  "imagePath": "https://c.pxhere.com/photos/c2/fc/bag_fashion_style-518806.jpg!d",
  "description": "Sunt et vitae. Ut et natus eaque doloremque aperiam et deleniti laudantium. Sit impedit tempora libero aut corrupti dolores ut. Eligendi culpa suscipit est at non id qui quae dolorum. Temporibus atque ullam eligendi minus blanditiis maxime consequatur.",
  "price": 19,
  "manufacturer": "Becker Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.322Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec7"
  },
  "productCode": "2570-0425995968",
  "title": "Glittery Black Purse with Golden Strap",
  "imagePath": "https://images.unsplash.com/photo-1564222195116-8a74a96b2c8c?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Ut est omnis quod aut impedit nulla rem repudiandae neque. Vero beatae dolore libero. Odio at minima doloribus in ducimus odio a. Voluptatibus et non voluptatum eum recusandae. Saepe quaerat aut ut unde nam consequatur.",
  "price": 27,
  "manufacturer": "Stracke LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.325Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec8"
  },
  "productCode": "9182-0347942666",
  "title": "Practical Black Leather Purse",
  "imagePath": "https://c.pxhere.com/photos/cb/9e/wallet_black_clutch_purse_leather_fashion_style_accessory-952715.jpg!d",
  "description": "Animi reprehenderit aut. Voluptatem pariatur dolores est nesciunt illum quia. Consequuntur quo porro ipsa et qui. Quisquam sequi impedit nihil ut doloremque ea harum. Molestiae consequatur animi odit qui ut hic vel sint dignissimos. Amet reprehenderit eveniet inventore exercitationem maxime.",
  "price": 13,
  "manufacturer": "Will Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.328Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ec9"
  },
  "productCode": "7333-0820395680",
  "title": "Red Leather Pouche with Free Earrings",
  "imagePath": "https://c.pxhere.com/photos/63/90/purse_handbag_fashion_bag_style_design_leather_accessory-780266.jpg!d",
  "description": "Non voluptas incidunt amet quis non. Et voluptas aut. Rem amet eligendi nihil. Dignissimos modi nulla sed enim nulla quae quisquam. Id id molestiae perferendis molestias tempore ipsa et dignissimos.",
  "price": 10,
  "manufacturer": "Romaguera Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.332Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82eca"
  },
  "productCode": "1026-2429105576",
  "title": "Lavender Leather Purse",
  "imagePath": "https://c.pxhere.com/photos/2d/da/wallet_purple_wallet_purple_money_purse_billfold_lavender_fashion-863005.jpg!d",
  "description": "Praesentium iusto quisquam eos voluptatem. Deleniti dolorem non assumenda cum autem blanditiis quasi sunt. Ad repudiandae repellendus natus blanditiis qui natus. Et est at. Dolor voluptas quae iure dolores quisquam totam amet corrupti fuga.",
  "price": 24,
  "manufacturer": "Dicki LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.335Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ecb"
  },
  "productCode": "6343-8178024678",
  "title": "White and Black Snakeskin Purse",
  "imagePath": "https://images.unsplash.com/photo-1563904092230-7ec217b65fe2?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=1534&amp;q=80",
  "description": "Ut fugiat similique cum omnis et dolor. Expedita consequatur illo perferendis at. Velit eius ut est. Et quis veritatis dicta ex. Ut ipsam qui laudantium nihil.",
  "price": 34,
  "manufacturer": "Welch Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.339Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ecc"
  },
  "productCode": "2014-9668734413",
  "title": "Dark Brown Simple Purse",
  "imagePath": "https://www.publicdomainpictures.net/pictures/60000/velka/leather-purse-isolated-background.jpg",
  "description": "Quasi iusto maxime est rem autem aut magni qui aut. Dicta sed praesentium optio quia dolore illum quia. Nisi numquam omnis at amet sapiente quidem. Alias vel totam. Tenetur culpa dolorem impedit ipsam non magnam id.",
  "price": 30,
  "manufacturer": "Considine Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.345Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ecd"
  },
  "productCode": "9631-5237918134",
  "title": "Red Kipling Pouche",
  "imagePath": "https://c.pxhere.com/photos/94/29/bag_handbag_purse_pink_red_fashion_glamour_accessory-952105.jpg!d",
  "description": "Est ducimus consequatur. Qui labore non quam omnis suscipit saepe omnis ipsa. Aut aut provident commodi sed corporis. Rerum aliquam tempora iste facilis necessitatibus laborum quam. Maiores sit hic accusamus velit facilis inventore et occaecati. Est perferendis quaerat voluptas odio praesentium.",
  "price": 15,
  "manufacturer": "Boyer Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.349Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ece"
  },
  "productCode": "3401-7797391101",
  "title": "Biege Kipling Pouche",
  "imagePath": "https://c.pxhere.com/photos/9b/57/bag_purse_handbag_fashion_style_accessory_white-1336949.jpg!d",
  "description": "Eum ratione ut aut blanditiis. Perferendis totam neque aspernatur voluptatum et quidem. Ratione distinctio dolores autem ut dolores ut molestias non rerum. Explicabo aliquid tempore laborum facere sit doloremque aut consequatur. Omnis iure repellat.",
  "price": 42,
  "manufacturer": "Mitchell and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1e"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.353Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ecf"
  },
  "productCode": "2576-0997693699",
  "title": "Plain White Cotton Tote",
  "imagePath": "https://p1.pxfuel.com/preview/1021/986/529/bag-cotton-cotton-bag-textile-wall-white.jpg",
  "description": "Et ea non nostrum et nisi atque similique. Eos quia porro nostrum repellendus. Cumque perspiciatis neque voluptatem autem. Enim minima minima dignissimos labore quod dolorem et eum. Molestias et est commodi dolores quia ut consequatur tempore ipsum.",
  "price": 23,
  "manufacturer": "Bahringer Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.359Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed0"
  },
  "productCode": "7162-3777068614",
  "title": "Elegant Red Leather Tote",
  "imagePath": "https://p1.pxfuel.com/preview/741/996/910/handbag-fashion-fashionable-woman.jpg",
  "description": "Asperiores ullam magni libero nihil cumque veniam corrupti magnam. Delectus inventore perspiciatis in. Sequi dolorem ut expedita id labore reprehenderit. Maxime laborum molestias voluptatem voluptas. Aut et dolores accusamus et quasi omnis rerum.",
  "price": 31,
  "manufacturer": "Yundt and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.362Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed1"
  },
  "productCode": "5513-1178610957",
  "title": "Handmade Embroided White Tote with Red Roses",
  "imagePath": "https://p1.pxfuel.com/preview/58/205/88/shop-bag-bags-sale.jpg",
  "description": "Cumque voluptatem optio. Repellat deserunt eum autem amet. Quae soluta corporis harum culpa fugit sint ipsam architecto. Voluptatum soluta aut.",
  "price": 21,
  "manufacturer": "Hettinger and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.366Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed2"
  },
  "productCode": "3782-4981260945",
  "title": "Multicolored White Tote",
  "imagePath": "https://p1.pxfuel.com/preview/367/279/652/bag-bag-elephant-cloth-bag.jpg",
  "description": "Vitae aut perspiciatis. Ad repellat quae sit. Laborum doloribus est rerum et qui et autem occaecati. Vel eum necessitatibus voluptas voluptatum. Facilis sit qui omnis labore est labore est eaque. Cumque possimus cum.",
  "price": 21,
  "manufacturer": "O'Kon and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.369Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed3"
  },
  "productCode": "7974-9824034609",
  "title": "Owl White Cotton Tote",
  "imagePath": "https://p0.pikrepo.com/preview/627/393/white-blue-and-red-owl-print-tote-bag.jpg",
  "description": "Quibusdam quas autem aut nesciunt sequi incidunt nesciunt doloremque non. Consequatur adipisci repellendus ipsa aut. Quia error et facere numquam est aut aut. Dolor explicabo iure. Fuga qui molestiae vero eius sint reprehenderit quo.",
  "price": 17,
  "manufacturer": "Hauck Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.374Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed4"
  },
  "productCode": "5281-1847423429",
  "title": "Simple Grey Zipped Tote",
  "imagePath": "https://farm5.staticflickr.com/4022/4714518639_8d9e06be13_b.jpg",
  "description": "Quasi similique iste iusto dicta eaque corrupti corporis molestiae sed. Quod beatae eos laborum iusto nobis impedit exercitationem saepe. Error quod in cum occaecati sequi magnam architecto dolorum sequi. Beatae doloremque doloribus facere. Nihil nemo quaerat voluptatem laboriosam sit doloremque.",
  "price": 32,
  "manufacturer": "Hagenes Group",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.377Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed5"
  },
  "productCode": "7977-6231800644",
  "title": "Earth Positive Tote Bag",
  "imagePath": "https://live.staticflickr.com/3538/3674472019_727d8c4669.jpg",
  "description": "Reiciendis perspiciatis similique sint qui. Culpa quo doloribus voluptatem quae at et earum quasi. Id modi rem ipsum ratione nostrum consequatur vel maiores necessitatibus. Ullam accusamus numquam. Explicabo consequatur nam ducimus. Qui et eaque sit cupiditate.",
  "price": 48,
  "manufacturer": "Crona Inc",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.380Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed6"
  },
  "productCode": "3937-4823222947",
  "title": "Deep Purple Handstamped Tote",
  "imagePath": "https://live.staticflickr.com/5161/5342130557_7fa8cc5935_b.jpg",
  "description": "Et dolore doloremque officia et assumenda mollitia dignissimos provident quod. Aspernatur quidem beatae exercitationem rerum et consequuntur et. Aliquid dolores tenetur quam. Qui porro veniam debitis dignissimos nihil temporibus deleniti et sed.",
  "price": 45,
  "manufacturer": "Yundt LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.384Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed7"
  },
  "productCode": "7879-0399481270",
  "title": "White Cotton Tote with Drawings",
  "imagePath": "https://p1.pxfuel.com/preview/368/540/34/bag-cotton-natural-cotton-bag-advertising-royalty-free-thumbnail.jpg",
  "description": "Nisi porro itaque inventore eum neque sint dignissimos doloribus tempore. Dolores quis vel. Excepturi similique asperiores nesciunt optio esse enim et suscipit. Voluptatibus ut nisi cupiditate temporibus error. Iste repellendus autem impedit temporibus qui et aut libero sunt.",
  "price": 45,
  "manufacturer": "Anderson and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.387Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed8"
  },
  "productCode": "5371-4022801793",
  "title": "Grey Wolf Tote",
  "imagePath": "https://p1.pxfuel.com/preview/726/975/813/bag-handbag-womans-bag-sport-bag.jpg",
  "description": "Placeat tenetur perferendis quia ratione error et. Et aut quia eius dicta iusto ex aut omnis qui. Illo id ipsum ut incidunt omnis et earum. Quos soluta nisi quisquam.",
  "price": 24,
  "manufacturer": "Champlin LLC",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.390Z"
  },
  "__v": 0
},{
  "_id": {
    "$oid": "6080462bee0e8361b4a82ed9"
  },
  "productCode": "5842-2034046934",
  "title": "Yellow and Green Bold Tote",
  "imagePath": "https://p1.pxfuel.com/preview/844/198/547/bag-burlap-advertising.jpg",
  "description": "Accusantium sapiente sed reprehenderit saepe laboriosam qui dolorem eligendi. Qui incidunt ut id totam. Qui distinctio perspiciatis sed et ea magnam. Quis assumenda vel. Voluptates rerum necessitatibus nisi neque.",
  "price": 34,
  "manufacturer": "Schmeler and Sons",
  "available": true,
  "category": {
    "$oid": "6080461b3167675d2819ad1d"
  },
  "createdAt": {
    "$date": "2021-04-21T15:35:07.393Z"
  },
  "__v": 0
}]
            </ext:documents>
        </ext:insertMany>
    </changeSet>
</databaseChangeLog> 

您可以在 Bitbucket 上查看修改后的项目。

构建 Node.js 应用程序

Node.js 是一种脚本语言,这意味着它不需要像. NET 或 Java 那样编译。但是,使用构建服务器有一些优势,例如:

  • 在构建时安装依赖项,这样它们就不会存储在源代码控制中。
  • 为 Octopus 部署使用构建服务器集成/插件:
    • 打包应用程序
    • 将包推送到 Octopus Deploy 或第三方存储库
    • 将构建信息推送到 Octopus 部署
    • 创建版本
    • 通过不同的环境部署或推广一个版本

我为这篇文章选择的构建服务器是 Bamboo。我配置了以下步骤:

  • 源代码签出
  • 安装依赖项的 npm
  • 设置版本号的 PowerShell 脚本
  • 注入 Bamboo 变量,以便版本号可用
  • Octopus Deploy: Pack package 来打包 web 前端
  • Octopus 部署:将包打包到包数据库文件
  • Octopus 部署:推送包
  • Octopus 部署:Octopus 构建信息

第一步是不言自明的,所以我不会详细说明这一点。

npm

Bamboo 有一个内置的步骤,可以执行npm命令。选择该步骤并输入install作为要运行的命令。

尽管该步骤存在于 Bamboo 中,但您仍然需要在构建服务器上安装节点可执行文件,并在 Bamboo Administration ➜可执行文件 中定义其位置。

在构建期间安装依赖项的另一种方法是在 web 服务器上运行npm install作为部署后脚本。

PowerShell 脚本

我使用 PowerShell 通过将主要和次要的 Bamboo 变量与日期相结合来创建版本号,因此每个版本都有一个惟一的编号:

$day = ([datetime] $(Get-Date)).DayOfYear
$day = ([string]$day).PadLeft(3, '0')
Add-Content -Path "version.txt" -Value "buildVersion=${bamboo.Major}.${bamboo.Minor}.$(Get-Date -Format "yy")$day.$(Get-Date -format "Hmmss")" 

注入竹子变量

注入 Bamboo 变量步骤获取我们刚刚创建的版本号,并通过指定我们在上面的Add-Content PowerShell 语句中创建的文件(version.txt ),使其可用于构建过程的其余部分。

封装前端

npm install的前一步会将我们项目的package.json文件中定义的依赖项下载并安装到 node_modules 文件夹中。安装完所有必需的模块后,我们可以打包要部署的应用程序:

  • 包 Id: BestBags.Web
  • 版本号:${bamboo.inject.buildVersion}
  • 套餐格式:zip
  • 包基础文件夹:${bamboo.build.working.directory}
  • 包输出文件夹:${bamboo.build.working.directory}\artifacts

与 NPM 类似,您需要在构建服务器上安装 Octopus Deploy CLI,并在 竹管局➜可执行文件 中定义它的位置

打包变更日志文件

要使用 Liquibase 产品,我们需要打包数据库 changelog 文件:

  • 包 Id: BestBags.Db
  • 版本号:${bamboo.inject.buildVersion}
  • 包装格式:zip
  • 包基础文件夹:${bamboo.build.working.directory}/seedDb
  • 包包含路径:dbchangelog.xml
  • 包输出文件夹:${bamboo.build.working.directory}\artifacts

推送包

在我的例子中,我将包推送到本地 Octopus Deploy 存储库:

  • Octopus URL:您的 Octopus 实例的 URL
  • API 密钥:具有足够权限上传包的 API 密钥
  • 空间名称:要上传到的空间的名称,留空将上传到默认空间
  • 包路径:/Artifacts/*.zip

构建信息

该步骤将提交信息上传到 Octopus Deploy,因此它可以包含在发行说明信息中:

  • Octopus URL:您的 Octopus 实例的 URL
  • API 密钥:具有足够权限上传包的 API 密钥
  • 空间名称:要上传到的空间的名称,留空将上传到默认空间
  • 包 id:
  • 版本号:${bamboo.inject.buildVersion}

构建完成后,我们就可以部署应用程序了。

章鱼部署

这篇文章假设您熟悉在 Octopus Deploy 中创建项目,所以我们不会讨论这个问题。如果你不是,请看看我们的入门指南。

我们的部署流程包括以下步骤:

  • Liquibase 应用变更集:这将把我们的变更部署到 MongoDB
  • 部署到 NGINX:我们将使用 NGINX 作为节点应用程序的反向代理

Liquibase:应用变更集

这一步获取我们在构建中打包的 changelog 文件,并将其应用于我们的 MongoDB 数据库服务器。

传统的数据库部署通常要求在尝试部署之前创建数据库。但是,如果引用的数据库不存在,MongoDB 会自动创建它。

该模板也与工人兼容。我选择Run once on a worker作为执行位置

为模板填写以下输入内容:

  • 数据库类型:选择 MongoDB
  • 变更日志文件名:变更日志的文件名,在我们的例子中是dbchangelog.xml
  • 服务器名称:MongoDB 服务器的名称
  • 服务器端口:配置 MongoDB 监听的端口号
  • 数据库名:要执行的数据库的名称
  • 用户名:有足够权限创建和/或更新数据库的用户
  • 密码:用户帐户的密码
  • 连接查询字符串参数:?authSource=admin
  • 下载 Liquibase?:我没有包括 Liquibase 产品,所以我勾选了这个框以便在部署时下载它
  • 变更集包:选择包含变更日志的包

部署到 NGINX

运行 Node.js 应用程序的一种流行方法是在 NGINX 反向代理后面运行它。Octopus Deploy 包含一个内置的部署到 NGINX 的步骤,使得这个步骤变得简单。

将 NGINX 步骤添加到流程中后,单击配置特性按钮,启用定制部署脚本替换模板中的变量特性:

滚动到自定义部署脚本部分,并为后期部署脚本输入以下内容。确保您为语言选择了 Bash :

SYSTEMD_CONF=/etc/systemd/system
SERVICE_USER=$(whoami)
NODEJS=/usr/bin/node

# This is used to generate the systemd filename, so we remove any chars that might be problematic for filenames
APPNAME=#{Octopus.Action[Deploy to NGINX].Package.PackageId | Replace "[^a-zA-Z0-9]" -}
# This path is referenced by the systemd service in multiple places, and systemd treats the % char as special,
# so it is escaped with a second % char
ROOTDIR=#{Octopus.Action[Deploy to NGINX].Output.Package.InstallationDirectoryPath | Replace "%" "%%"}
SYSTEMD_SERVICE_FILE=${SYSTEMD_CONF}/${APPNAME}.service

# Application systemd service configuration
echo "Creating ${APPNAME} systemd service configuration"
cat > "${APPNAME}.service" <<-EOF
 [Unit]
 Description=${APPNAME} service
 After=network.target

 [Service]
 WorkingDirectory=${ROOTDIR}
 User=${SERVICE_USER}
 Group=${SERVICE_USER}
 ExecStart=${NODEJS} ${ROOTDIR}/app.js
 Restart=always
 RestartSec=10
 SyslogIdentifier=${APPNAME}
 [Install]
 WantedBy=multi-user.target
EOF
sudo mv "${APPNAME}.service" ${SYSTEMD_CONF}/${APPNAME}.service

# Any changes to a system file are picked up by reloading the systemd daemon
sudo systemctl daemon-reload
# Enable the service so it starts on boot
sudo systemctl enable "${APPNAME}.service"
# Start or restart the service to pick up any changes
sudo systemctl restart "${APPNAME}.service"
# Print out url for service
projectUrl=$(get_octopusvariable "Project.Url")
write_highlight "[$projectUrl]($projectUrl)" 

滚动到模板中的替代变量部分,并输入以下内容:

app.js
config/db.js 

滚动到 NGINX Web 服务器部分,删除默认绑定,并添加一个带有所需端口的新绑定。我将我的绑定到一个变量上:

点击添加位置:

  • 地点:/
  • 反向代理:选中此框
  • Url:输入节点的 URL 和端口,我将我的绑定到一个变量:

就是这样!我们的流程现在已经完成,可以开始部署了。

部署完成后,我们可以单击显示的链接(部署后脚本的最后一部分)并查看我们的应用程序的运行情况:

结论

在本文中,我演示了如何使用 Octopus Deploy 用 MongoDB 后端部署 Node.js 应用程序。

愉快的部署!

posted @ 2024-11-01 16:33  绝不原创的飞龙  阅读(13)  评论(0编辑  收藏  举报