亚马逊AWS官方博客

AWS CodeBuild for macOS 现已支持 Fastlane

我很高兴地宣布,您现在可以在 AWS CodeBuild for macOS 环境中使用 Fastlane。AWS CodeBuild 是一项完全托管的持续集成服务,可编译源代码、运行测试并生成可随时部署的软件包。

Fastlane 是一个开源工具套件,旨在实现移动应用程序开发各个环节的自动化。它为移动应用程序开发人员提供了一组集中式工具,用于管理代码签名、屏幕截图生成、测试版分发和应用商店提交等任务。它与热门的持续集成和持续部署(CI/CD)平台集成,同时支持 iOS 和 Android 开发工作流程。尽管 Fastlane 具备强大的自动化功能,但开发人员在设置和维护过程中可能会遇到挑战。Fastlane 的配置可能较为复杂,对于不熟悉 Ruby 语法和包管理系统的团队来说更是如此。由于移动平台或第三方服务的更新可能需要对现有工作流程进行调整,因此要让 Fastlane 及其依赖项保持最新状态,需要持续付出努力。

2024 年 8 月,我们推出了 AWS CodeBuild for macOS。当时我们就了解到,在构建环境中安装和维护 Fastlane 是您面临的挑战之一。尽管可以在自定义构建环境中手动安装 Fastlane,但在 AWS,我们致力于为您消除基础设施方面无差别的繁重工作,让您能够将更多时间花在对业务真正重要的方面。从今天起,Fastlane 将默认安装,您可以在 buildspec.yaml 文件中使用熟悉的 fastlane build 命令。

Fastlane 和代码签名
为了在 App Store 上发布应用程序,开发人员必须使用在 Apple 开发人员门户上生成的私钥对其二进制文件进行签名。在构建过程中,必须能够获取该私钥以及用于验证它的证书。这对开发团队来说可能是一项挑战,因为他们需要在团队成员之间共享开发私钥(用于在选定的测试设备上进行部署)。此外,在将二进制文件上传到 App Store 之前的签名过程中,必须要有发布私钥(用于在 App Store 上发布应用程序)。

Fastlane 是一个多功能的构建系统,它还能帮助开发人员管理开发和发布密钥及证书。开发人员可以使用 fastlane match 在团队中共享签名材料,以便在开发人员的个人计算机和持续集成(CI)环境中都能安全且便捷地获取这些材料。match 允许将私钥、证书和移动配置文件存储在安全的共享存储中。它确保本地构建环境,无论是开发人员的笔记本电脑还是云端的服务器,都能与共享存储保持同步。在构建时,它会安全地下载为应用程序签名所需的证书,并对构建计算机进行配置,以便 codesign 工具能够获取这些证书。

match 支持通过 GitHub、GitLab、Google Cloud Storage、Azure DevOps 和 Amazon Simple Storage Service(Amazon S3)共享签名密钥。

如果您已经在使用上述某一种方式,并且正在将项目迁移到 CodeBuild,那么久无需执行太多额外工作。只需确保 CodeBuild 构建环境能够访问共享存储即可(详见演示中的步骤 3)。

下面我们来看看它的工作原理
如果您是刚刚接触 Fastlane 或 CodeBuild,就让我们来看看它的工作原理。

在这个演示中,我从一个现有的 iOS 项目入手。该项目已配置为在 CodeBuild 上构建。可以参考我之前的博客文章使用 AWS CodeBuild 将 macOS 添加到您的持续集成管道中,以了解更多详细信息。

我将通过三个步骤向您展示如何开始:

  • 将现有的签名材料导入到共享的私有 GitHub 存储库
  • 配置 fastlane 来构建项目并为其签名
  • 在 CodeBuild 中使用 fastlane

第 1 步:导入签名材料

我读过的大多数 fastlane 文档都在讲解如何创建新的密钥对和证书来启动项目。虽然对于新项目来说确实如此,但在实际情况中,您可能已经有了项目以及签名密钥。因此,第一步是导入这些现有的签名材料。

Apple App Store 针对开发和发布使用不同的密钥和证书(此外还有临时和企业证书,但这些不在本文讨论范围内)。对于每种用途,您都必须拥有三个文件(总共六个文件):

  • 一个 .mobileprovision 文件,您可以从 Apple 开发人员控制台创建并下载。预置文件会将您的身份、应用程序标识以及应用程序可能拥有的权限关联起来。
  • 一个 .cer 文件,这是 Apple 发出的用于验证您的私钥的证书。您可以从 Apple 开发人员门户下载该文件。选择证书,然后选择下载
  • 一个 .p12 文件,其中包含您的私钥。在 Apple 开发人员门户创建密钥时,您可以下载密钥。如果您当时没有下载,但密钥已在您的计算机上,则您可以从 Apple Keychain 应用程序中导出。请注意,在 macOS 15.x 系统中,KeyChain.app 是隐藏的。您可以通过 open /System/Library/CoreServices/Applications/Keychain\ Access.app 打开该应用程序。选择要导出的密钥,右键单击以选择导出
从 Keychain 中导出 p12 文件

获取这些文件后,创建一个 fastlane/Matchfile 文件,其中包含以下内容:

git_url("https://github.com/sebsto/secret.git")
storage_mode("git")
type("development")
# or use appstore to use the distribution signing key and certificate
# type("appstore")
Ruby

请务必替换您的 GitHub 存储库的 URL,并确保该存储库是私有的。它将作为您的签名密钥和证书的存储位置。

然后,我使用 fastlane match import --type appstore 命令导入现有文件。我针对每个环境(appstoredevelopment)重复执行此命令。

第一次执行时,fastlane 会提示我输入 Apple ID 用户名和密码。它会连接到 App Store Connect 来验证证书的有效性,必要时还会创建新证书。会话 cookie 存储在 ~/.fastlane/spaceship/<您的 apple 用户 ID>/cookie 中。

fastlane match 还会要求输入密码。它会使用这个密码生成一个密钥,用于加密存储中的签名材料。请务必记住这个密码,因为在构建时需要用它在构建计算机上导入签名材料。

以下是完整的命令及其输出:

 fastlane match import --type appstore

[] 🚀
[16:43:54]: Successfully loaded '~/amplify-ios-getting-started/code/fastlane/Matchfile' 📄

+-----------------------------------------------------+
| Detected Values from './fastlane/Matchfile'         |
+--------------+--------------------------------------+
| git_url.     | https://github.com/sebsto/secret.git |
| storage_mode | git                                  |
| type         | development                          |
+--------------+--------------------------------------+

[16:43:54]: Certificate (.cer) path:
./secrets/sebsto-apple-dist.cer
[16:44:07]: Private key (.p12) path:
./secrets/sebsto-apple-dist.p12
[16:44:12]: Provisioning profile (.mobileprovision or .provisionprofile) path or leave empty to skip
this file:
./secrets/amplifyiosgettingstarteddist.mobileprovision
[16:44:25]: Cloning remote git repo...
[16:44:25]: If cloning the repo takes too long, you can use the `clone_branch_directly` option in match.
[16:44:27]: Checking out branch master...
[16:44:27]: Enter the passphrase that should be used to encrypt/decrypt your certificates
[16:44:27]: This passphrase is specific per repository and will be stored in your local keychain
[16:44:27]: Make sure to remember the password, as you'll need it when you run match on a different machine
[16:44:27]: Passphrase for Match storage: ********
[16:44:30]: Type passphrase again: ********
security: SecKeychainAddInternetPassword <NULL>: The specified item already exists in the keychain.
[16:44:31]: 🔓 Successfully decrypted certificates repo
[16:44:31]: Repo is at: '/var/folders/14/nwpsn4b504gfp02_mrbyd2jr0000gr/T/d20250131-41830-z7b4ic'
[16:44:31]: Login to App Store Connect (sebsto@mac.com)
[16:44:33]: Enter the passphrase that should be used to encrypt/decrypt your certificates
[16:44:33]: This passphrase is specific per repository and will be stored in your local keychain
[16:44:33]: Make sure to remember the password, as you'll need it when you run match on a different machine
[16:44:33]: Passphrase for Match storage: ********
[16:44:37]: Type passphrase again: ********
security: SecKeychainAddInternetPassword <NULL>: The specified item already exists in the keychain.
[16:44:39]: 🔒 Successfully encrypted certificates repo
[16:44:39]: Pushing changes to remote git repo...
[16:44:40]: Finished uploading files to Git Repo [https://github.com/sebsto/secret.git]
Bash

我确认 Fastlane 已将我的签名材料导入到我的 Git 存储库中。

Fastlane match — 导入后的 github 存储库

我还可以配置本地机器,以便在下一次构建时使用这些签名材料:

» fastlane match appstore 

[✔] 🚀 
[17:39:08]: Successfully loaded '~/amplify-ios-getting-started/code/fastlane/Matchfile' 📄

+-----------------------------------------------------+
|   Detected Values from './fastlane/Matchfile'       |
+--------------+--------------------------------------+
| git_url      | https://github.com/sebsto/secret.git |
| storage_mode | git                                  |
| type         | development                          |
+--------------+--------------------------------------+


+-------------------------------------------------------------------------------------------+
|                                 Summary for match 2.226.0                                 |
+----------------------------------------+--------------------------------------------------+
| type                                   | appstore                                         |
| readonly                               | false                                            |
| generate_apple_certs                   | true                                             |
| skip_provisioning_profiles             | false                                            |
| app_identifier                         | ["com.amazonaws.amplify.mobile.getting-started"] |
| username                               | xxxx@xxxxxxxxx                                   |
| team_id                                | XXXXXXXXXX                                       |
| storage_mode                           | git                                              |
| git_url                                | https://github.com/sebsto/secret.git             |
| git_branch                             | master                                           |
| shallow_clone                          | false                                            |
| clone_branch_directly                  | false                                            |
| skip_google_cloud_account_confirmation | false                                            |
| s3_skip_encryption                     | false                                            |
| gitlab_host                            | https://gitlab.com                               |
| keychain_name                          | login.keychain                                   |
| force                                  | false                                            |
| force_for_new_devices                  | false                                            |
| include_mac_in_profiles                | false                                            |
| include_all_certificates               | false                                            |
| force_for_new_certificates             | false                                            |
| skip_confirmation                      | false                                            |
| safe_remove_certs                      | false                                            |
| skip_docs                              | false                                            |
| platform                               | ios                                              |
| derive_catalyst_app_identifier         | false                                            |
| fail_on_name_taken                     | false                                            |
| skip_certificate_matching              | false                                            |
| skip_set_partition_list                | false                                            |
| force_legacy_encryption                | false                                            |
| verbose                                | false                                            |
+----------------------------------------+--------------------------------------------------+

[17:39:08]: Cloning remote git repo...
[17:39:08]: If cloning the repo takes too long, you can use the `clone_branch_directly` option in match.
[17:39:10]: Checking out branch master...
[17:39:10]: Enter the passphrase that should be used to encrypt/decrypt your certificates
[17:39:10]: This passphrase is specific per repository and will be stored in your local keychain
[17:39:10]: Make sure to remember the password, as you'll need it when you run match on a different machine
[17:39:10]: Passphrase for Match storage: ********
[17:39:13]: Type passphrase again: ********
security: SecKeychainAddInternetPassword <NULL>: The specified item already exists in the keychain.
[17:39:15]: 🔓  Successfully decrypted certificates repo
[17:39:15]: Verifying that the certificate and profile are still valid on the Dev Portal...
[17:39:17]: Installing certificate...

+-------------------------------------------------------------------------+
|                          Installed Certificate                          |
+-------------------+-----------------------------------------------------+
| User ID           | XXXXXXXXXX                                          |
| Common Name       | Apple Distribution: Sebastien Stormacq (XXXXXXXXXX) |
| Organisation Unit | XXXXXXXXXX                                          |
| Organisation      | Sebastien Stormacq                                  |
| Country           | US                                                  |
| Start Datetime    | 2024-10-29 09:55:43 UTC                             |
| End Datetime      | 2025-10-29 09:55:42 UTC                             |
+-------------------+-----------------------------------------------------+

[17:39:18]: Installing provisioning profile...

+-------------------------------------------------------------------------------------------------------------------+
|                                          Installed Provisioning Profile                                           |
+---------------------+----------------------------------------------+----------------------------------------------+
| Parameter           | Environment Variable                         | Value                                        |
+---------------------+----------------------------------------------+----------------------------------------------+
| App Identifier      |                                              | com.amazonaws.amplify.mobile.getting-starte  |
|                     |                                              | d                                            |
| Type                |                                              | appstore                                     |
| Platform            |                                              | ios                                          |
| Profile UUID        | sigh_com.amazonaws.amplify.mobile.getting-s  | 4e497882-d80f-4684-945a-8bfec1b310b9         |
|                     | tarted_appstore                              |                                              |
| Profile Name        | sigh_com.amazonaws.amplify.mobile.getting-s  | amplify-ios-getting-started-dist             |
|                     | tarted_appstore_profile-name                 |                                              |
| Profile Path        | sigh_com.amazonaws.amplify.mobile.getting-s  | /Users/stormacq/Library/MobileDevice/Provis  |
|                     | tarted_appstore_profile-path                 | ioning                                       |
|                     |                                              | Profiles/4e497882-d80f-4684-945a-8bfec1b310  |
|                     |                                              | b9.mobileprovision                           |
| Development Team ID | sigh_com.amazonaws.amplify.mobile.getting-s  | XXXXXXXXXX                                   |
|                     | tarted_appstore_team-id                      |                                              |
| Certificate Name    | sigh_com.amazonaws.amplify.mobile.getting-s  | Apple Distribution: Sebastien Stormacq       |
|                     | tarted_appstore_certificate-name             | (XXXXXXXXXX)                                 |
+---------------------+----------------------------------------------+----------------------------------------------+

[17:39:18]: All required keys, certificates and provisioning profiles are installed 🙌
Plain text

Step 2:配置 Fastlane 以对项目签名

我在 fastlane/Fastfile 中创建了一个 Fastlane 构建配置文件(您可以使用 fastlane init 命令来开始):

default_platform(:ios)

platform :ios do
  before_all do
    setup_ci
  end

  desc "Build and Sign the binary"
  lane :build do
    match(type: "appstore", readonly: true)
    gym(
      scheme: "getting started",
      export_method: "app-store"
    )
  end
end
Ruby

确保将 setup_ci 操作添加到 Fastfilebefore_all 部分,以便 match 操作能正常运行。此操作会创建一个具有正确权限的临时 Fastlane 密钥链。如果没有这一步,您可能会遇到构建失败或结果不一致的问题。

然后,我使用 fastlane build 命令测试本地构建。我输入导入密钥和证书时使用的密码,然后让系统构建我的项目并为其签名。一切配置正确时,它会产生类似的输出。

...
[17:58:33]: Successfully exported and compressed dSYM file
[17:58:33]: Successfully exported and signed the ipa file:
[17:58:33]: ~/amplify-ios-getting-started/code/getting started.ipa

+---------------------------------------+
|           fastlane summary            |
+------+------------------+-------------+
| Step | Action           | Time (in s) |
+------+------------------+-------------+
| 1    | default_platform | 0           |
| 2    | setup_ci         | 0           |
| 3    | match            | 36          |
| 4    | gym              | 151         |
+------+------------------+-------------+

[17:58:33]: fastlane.tools finished successfully 🎉
Plain text

第 3 步:将 CodeBuild 配置为使用 Fastlane

接下来,我在 CodeBuild 上创建一个项目。这里,我不会详细介绍具体步骤。您可以参考我之前的帖子CodeBuild 文档

只有一项与 Fastlane 特定相关的配置。为了访问签名材料,Fastlane 需要获取三个秘密值,我将以环境变量的形式传递这些值:

  • MATCH_PASSWORD,导入签名材料时输入的密码。Fastlane 使用这个密码来解密 GitHub 仓库中加密的文件。
  • FASTLANE_SESSION,FASTLANE_SESSION:Apple ID 会话 cookie 的值,位于 ~/.fastlane/spaceship/<您的 apple 用户 ID>/cookie。会话有效期从几个小时到数天不等。会话过期时,从您的笔记本电脑上使用 fastlane spaceauth 命令重新进行身份验证,并使用新的 cookie 值更新 FASTLANE_SESSION 的值。
  • MATCH_GIT_BASIC_AUTHORIZATION:您的 GitHub 用户名进行 base 64 编码后,加上一个冒号,再加上用于访问私有 GitHub 存储的个人访问令牌(PAT)。您可以在 GitHub 控制台的“您的个人资料”>“设置”>“开发人员设置”>“个人访问令牌”中生成 PAT。我使用以下命令来生成该环境变量的值:echo -n my_git_username:my_git_pat | base64

请注意,对于这三个值,我既可以在 AWS Secrets Manager 中输入密钥的 Amazon 资源名称(ARN),也可以输入纯文本值。我们强烈建议使用 Secrets Manager 来存储对安全敏感的值

我是一个注重安全的用户,因此使用以下命令将这三个密钥存储在 Secrets Manager 中:

aws --region $REGION secretsmanager create-secret --name /CodeBuild/MATCH_PASSWORD --secret-string MySuperSecretPassword
aws --region $REGION secretsmanager create-secret --name /CodeBuild/FASTLANE_SESSION --secret-string $(cat ~/.fastlane/spaceship/my_appleid_username/cookie)
aws --region $REGION secretsmanager create-secret --name /CodeBuild/MATCH_GIT_BASIC_AUTHORIZATION --secret-string $(echo -n my_git_username:my_git_pat | base64)

如果您的构建项目引用了存储在 Secrets Manager 中的密钥,构建项目的服务角色必须允许 secretsmanager:GetSecretValue 操作。如果您在创建项目时选择了新建服务角色,CodeBuild 会将此操作包含在构建项目的默认服务角色中。但是,如果您选择了现有服务角色,则必须单独将此操作添加到您的服务角色中。

在本次演示中,我使用以下 AWS Identity and Access Management(IAM)策略:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"secretsmanager:GetSecretValue"
			],
			"Resource": [
				"arn:aws:secretsmanager:us-east-2:012345678912:secret:/CodeBuild/*"
			]
		}
	]
}
JSON

AWS 管理控制台的 CodeBuild 部分创建项目后,我输入了三个环境变量。请注意,该值是 Secrets Manager 中密钥的名称。

Codebuild — Fastlane match 的环境变量

您也可以在 buildspec.yaml 文件中定义环境变量及其在 Secrets Manager 中的密钥名称。

接下来,我修改项目根目录下的 buildspec.yaml 文件,以便使用 fastlane 构建二进制文件并为其签名。现在,我的 buildspec.yaml 文件如下所示:

# buildspec.yml
version: 0.2
phases:
  install:
    commands:
      - code/ci_actions/00_install_rosetta.sh
  pre_build:
    commands:
      - code/ci_actions/02_amplify.sh
  build:
    commands:
      - (cd code && fastlane build)
artifacts:
  name: getting-started-$(date +%Y-%m-%d).ipa
  files:
    - 'getting started.ipa'
  base-directory: 'code'
YAML

需要 Rosetta 和 Amplify 脚本才能接受后端的 Amplify 配置。如果您的项目中未使用 AWS Amplify,则无需这些脚本。

请注意,构建文件中没有任何内容用于在构建环境中下载签名密钥或准备密钥链,fastlane match 会为我完成这些操作。

我将新的 buildspec.yaml 文件和我的 ./fastlane 目录添加到 Git 中。我提交并推送这些文件。git commit -m "add fastlane support" && git push

如果一切顺利,我就能看到 CodeBuild 上的构建任务正在运行,并收到成功消息。

Codebuild — 成功消息

定价和可用性
现在,在所有支持 CodeBuild for macOS 的区域,CodeBuild 使用的所有 macOS 映像中都预先安装了 Fastlane,无需额外费用。在撰写本文时,这些区域包括美国东部(俄亥俄州、北弗吉尼亚州)、美国西部(俄勒冈州)、亚太地区(悉尼)和欧洲地区(法兰克福)。

根据我的经验,正确配置 fastlane match 会花费一些时间。配置完成后,让它在 CodeBuild 上运行就非常简单了。在 CodeBuild 上试用之前,请确保它在您的本地计算机上能够正常运行。如果 CodeBuild 出现问题,请仔细检查环境变量的值,并确保 CodeBuild 可以访问 AWS Secrets Manager 中的密钥。

现在就开始(在 macOS 上)构建吧!