Flutter 앱의 iOS 배포 자동화를 Fastlane 으로 구성해서 잘 돌아가고 있었는데, 기능을 변경하면서 capability 정보를 포함한 새로운 Provisioning Profile로 교체할 필요가 있었다.
fastlane match 를 사용해 github repository에 보관되어 있는 Provisioning profile를 업데이트하려고 했는데, 어라. 전에 안 보이던 에러가 발생했다.
[09:23:55]: Called from Fastfile at line 67
[09:23:55]:
[09:23:55]: 65: )
[09:23:55]: 66:
[09:23:55]: => 67: match(
[09:23:55]: 68: type: "appstore",
[09:23:55]: 69: force: true,
[09:23:55]: invalid number: -----BEGIN PRIVATE KEY-----
MIGT
github actions를 이용해서 TestFlight 배포를 할 수 있도록 설정해둔 상태였고, 로컬에서도 fastlane을 이용해 배포할 수 있도록
아래와 같이 설정해둔 상태였다.
## {{ProjectRoot}}/.env 파일
# Sentry Info.
SENTRY_DSN=SENTRY_DSN
# 아래는 애플 API 이용을 위한 앱스토어connect api 인증 정보
APP_STORE_CONNECT_API_KEY_ID=YOUR_KEY_ID
APP_STORE_CONNECT_ISSUER_ID=YOUR_ISSUER_ID
APP_STORE_CONNECT_API_KEY_PATH=path/to/AuthKey_YOUR_KEY_ID.p8
# Github Basic Authorization Key
MATCH_GIT_BASIC_AUTHORIZATION=MATCH_GIT_BASIC_AUTHORIZATION
APPLE_ID=APPLE_ID
APPLE_TEAM_ID=APPLE_TEAM_ID
## {{ProjectRoot}}/ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
before_all do
Dotenv.overload("../../.env")
app_store_connect_api_key( # 호출만 하면 만들어진 api_key를 환경변수에 저장하고 이후에 사용할 수 있다.
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_filepath: ENV["APP_STORE_CONNECT_API_KEY_PATH"]
)
end
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
configuration: "Release",
export_options: {
method: "app-store",
signingStyle: "automatic",
teamId: ENV["APPLE_TEAM_ID"]
},
)
match(type: "appstore", readonly: true)
upload_to_testflight()
end
lane :update_certificates do
match(
type: "appstore",
force: true,
readonly: false,
)
end
end
## {{ProjectRoot}}/ios/fastlane/Matchfile
# 별 내용 없다.
git_url("https://github.com/REPOSITORY_URL")
storage_mode("git")
type("appstore")
기존에는 이 설정으로 문제 없이 인증서를 읽고 Profile을 내려받았는데, 갑자기 .p8 인증키에 문제가 생겼다는 메시지가 나온다.
이것저것 다 시도해보다가 결국 문제를 해결했는데, 원인은 Fastlane의 "자동 인증키 로딩 로직" 때문이었다.
Fastlane은 내부적으로 자동 인증 시스템을 갖고 있어서, Fastfile
에서 인증 키를 명시적으로 설정하지 않아도,
일정 조건이 충족되면 알아서 App Store Connect API Key 인증을 시도한다.
다음 환경변수들이 설정되어 있으면 Fastlane은 Spaceship::ConnectAPI::Token.create
메서드를 사용해 자동으로 인증 토큰을 생성하려고 한다:
APP_STORE_CONNECT_API_KEY_ID
APP_STORE_CONNECT_ISSUER_ID
APP_STORE_CONNECT_API_KEY_PATH
Fastlane은 이 세 가지 정보만으로 .p8 키를 읽어서 토큰을 만들려고 하는데, 이때 내부적으로는 다음과 같이 동작한다:
# 내부적으로 호출되는 예시 (spaceship/connect_api/token.rb)
OpenSSL::PKey::EC.new(File.read(key_path))
.env
에 APP_STORE_CONNECT_API_KEY_PATH
환경변수를 설정해두면 Fastlane은 이 값을 경로로 인식하려고 시도하지만,
어떤 환경에서는 문자열 자체를 키 내용으로 오해하는 경우가 있다.
그 결과
APP_STORE_CONNECT_API_KEY_PATH=-----BEGIN PRIVATE KEY-----
MIGT...
이렇게 파일명이 아닌 파일 내용이 APP_STORE_CONNECT_API_KEY_PATH에 할당되고, Fastlane은 이 내용을 경로가 아니라 키 내용 자체로 인식한다.
즉, Fastlane은 문자열이 파일 경로일 거라고 기대했지만, 실제로는 파일 내용이 들어왔기 때문에 에러가 발생하는 것이다.
처음에 기대했던 것처럼 Fastfile
에서 아래처럼 명시적으로 app_store_connect_api_key
를 호출하면,
app_store_connect_api_key(
key_id: ENV["MY_API_KEY_ID"],
issuer_id: ENV["MY_ISSUER_ID"],
key_filepath: ENV["MY_API_KEY_PATH"]
)
Fastlane이 내부에서 .p8
키를 읽고 파싱하는 방식이 명확하게 경로 기반으로 처리된다.
직접 key_filepath:
옵션으로 파일의 경로 문자열을 넘기기 때문에, Fastlane은 이것을 안전하게 파일 경로로 인식하고 처리한다.
결국 의도한 대로 내가 환경변수에 지정한 값을 이용하여 api_key를 만들기 위해서는 A) Fastlane이 자동으로 인증 시도하지 않도록 변경하거나 B) APP_STORE_CONNECT_API_KEY_PATH에 할당되는 값을 변경해줘야 하는 것이다.
그러니 해결 방법은 사실 간단하다.
Apple API Key 관련 환경변수명을 Fastlane이 인식하지 못하도록 변경한다
.env
파일에서 변수명을 바꿔주자:
MY_API_KEY_ID=YOUR_KEY_ID # 기존의 변수명 앞에 "MY_" 등을 추가
MY_ISSUER_ID=YOUR_ISSUER_ID
MY_API_KEY_PATH=./fastlane/AuthKey_YOUR_KEY_ID.p8
Fastfile에서도 변경된 변수명을 이용하여 명시적으로 인증 키를 지정한다:
require 'dotenv'
platform :ios do
before_all do
Dotenv.overload("../../.env")
# Fastlane 자동 인증 방지용. 굳이 넣어주지 않아도 문제 없다.
ENV["APP_STORE_CONNECT_API_KEY_ID"] = nil
ENV["APP_STORE_CONNECT_ISSUER_ID"] = nil
ENV["APP_STORE_CONNECT_API_KEY_PATH"] = nil
ENV["APP_STORE_CONNECT_API_KEY_CONTENT"] = nil
ENV["FASTLANE_APPLE_CONNECT_API_KEY"] = nil
# 명시적 인증
app_store_connect_api_key(
key_id: ENV["MY_API_KEY_ID"],
issuer_id: ENV["MY_ISSUER_ID"],
key_filepath: ENV["MY_API_KEY_PATH"]
)
end
lane :update_certificates do
match(
type: "appstore",
force: true,
readonly: false,
)
end
end
이렇게 하면 끝! 해결은 간단한데, 원인을 찾는데 오래 걸렸다. ㅠㅠ