① 배운 것
지난번에 깃헙액션을 이용해 flutter ios를 app distribution에 자동배포했음
근데 깃헙액션만을 이용하면 👇 이 과정을 직접 해야한다.
ios 배포시 필요한 인증서와 profile을 모두 apple developer에서 다운 -> base64로 인코딩 -> 그것을 github secrets에 올림 -> workflow에서 secrets를 참고해 다시 디코딩 -> 원래 profile이 있어야 하는 위치에 파일을 만듦
애플 같은 경우 developer profile 타입으로 아카이브를 하면 그 profile에 등록된 애플기기에서만 앱을 다운받을 수 있다.
즉, 테스터가 한명 추가될때마다 profile에 새로운 기기를 등록해야하고 profile이 변경되었으니 다시 'apple developer에서 profile 다운 -> base64로 인코딩 -> github secrets에 올리는' 과정을 해야한다.
이렇게 개발자가 직접 profile과 cert를 관리하는 것이 너무 비효율적이라 생각되었는데 fastlane의 match를 사용하면 간편하게 cert, profile을 관리할 수 있다고 함 + 평소에 fastlane이 대체 뭔지 궁금했던 참이라 이번에는 fastlane으로 ios 자동배포를 해보기로 했다.
자세한 설명은 여기 참고
match를 사용하면 apple developer에서 개발자가 직접 cert,profile을 다운받아 관리하지 않아도 됨.
match가 필요한 cert,profile을 apple developer에 자동으로 만들어 주고(사진참고)
그걸 인코딩해서 git repo에 올려준다.
인코딩할때 비밀번호를 입력 받고 그 비밀번호를 활용해 인코딩 해서 비밀번호가 유출되지 않는 이상 디코딩을 하기 어렵겠지만 이 repo는 private으로 관리하는걸 추천함.
그리고 fastlane을 이용해 앱을 빌드할때는 그 레포에서 cert,profile을 가져오고 디코딩해서 사이닝에 관련된 사항들을 알아서 세팅해준다.

ios 자동배포 항상 젤 어려웠던 부분이 인증서,profile 부분이였는데 xCode에서 automatic signing을 이용하는 것 처럼 사이닝 관련해서 개발자가 아무것도 신경쓸 필요가 없다니 이건 정말 혁명이다.. 🍯
자동배포를 검색하면 깃헙액션과 Fastlane이 둘다 나와서 무슨 차이인지 궁금했는데 이제 확실히 알게 되었다.
지피티의 도움을 받아 이해한 바를 정리해봄.
Fastlane은 모바일 앱 배포 자동화에 특화된 도구인 반면, GitHub Actions은 더 넓은 범위의 자동화 플랫폼입니다.
GitHub Actions은 Git 이벤트를 기반으로 모든 종류의 작업을 자동화할 수 있어서 이론적으로는 Fastlane의 기능도 구현할 수 있지만, 모바일 앱 배포와 관련해서는 인증서 관리나 앱스토어 배포 같은 특화 기능을 제공하는 Fastlane을 사용하는 것이 더 효율적입니다.
더 쉽게 정리하자면,
깃헙액션과 fastlane 둘다 자동으로 앱 빌드/배포를 구현 할 수 있지만
깃헙 액션은 이것만 할 수 있는게 아니라 GitHub의 실행 환경 내에서 허용된 모든 shell 명령어를 실행할 수 있는 범용 자동화 도구임.
반면, Fastlane은 모바일 앱의 빌드, 테스트, 배포 및 인증서 관리 같은 모바일 앱 개발 워크플로우에 특화된 자동화 도구임
| 특징 | Fastlane | GitHub Actions |
|---|---|---|
| 주요 용도 | 모바일 앱 배포 자동화에 특화 | 일반적인 CI/CD 및 자동화 플랫폼 |
| 범위 | iOS/Android 앱 빌드, 테스트, 배포 | 모든 종류의 소프트웨어 개발 워크플로우 |
| 특화 기능 | • iOS 인증서/프로필 관리(match) • 스크린샷 자동화 • 앱스토어 메타데이터 관리 • TestFlight/Firebase 배포 통합 | • 코드 리뷰 자동화 • 이슈/PR 관리 • 다양한 클라우드 서비스 통합 • 커스텀 워크플로우 생성 |
| 트리거 | 주로 수동 실행 또는 CI/CD 파이프라인의 일부로 실행 | Git 이벤트(push, PR 등) 또는 수동 트리거 |
| 실행 환경 | 로컬 머신 또는 CI 서버 | GitHub의 클라우드 환경 |
| 학습 곡선 | 모바일 앱 배포에 특화된 간단한 문법 | YAML 기반의 범용적인 워크플로우 설정 |
| 통합성 | 모바일 개발 도구들과의 긴밀한 통합 | 광범위한 개발 도구 및 서비스와 통합 |
| 사용 사례 | • 앱스토어 배포 • 베타 테스트 배포 • 인증서 관리 • 앱 서명 | • 코드 테스트 • 빌드 자동화 • 배포 자동화 • 코드 품질 검사 • 알림 자동화 |
결론 :
모바일 앱 배포 자동화는 깃헙액션을 이용해 특정 깃헙 이벤트를 캐치해서 깃헙에서 제공하는 runner에 fastlane을 실행하기 위해 필요한 것들(ruby, fastlane, flutter 등)을 설치하고, fastlane을 실행해 앱 빌드/배포는 fastlane에서 진행되도록 하는것이 가장 좋다.
그래서 어떻게 하는건데!
나는 처음에 내 로컬 컴에서 fastlane을 직접 실행시켜서 테스트 해봤기 때문에 로컬컴에 fastlane 환경을 설치,구축 하는 것도 진행했지만 이 포스팅에서는 로컬컴이 아니라 runner에 fastlane을 설치, 구축하는 것을 진행할것 이기 때문에 이부분은 따로 설명 x
필요하다면 [이 블로그](여러 블로그에서 )에 설명이 잘 나와있어서 따라하면됨
[GithubAction workflow.yaml]
name: TestFlight Release
on:
push:
tags:
- "v*" #v로 시작하는 태그가 푸시될때 트리거됨
jobs:
release:
runs-on: macos-latest
steps:
# 1. 코드 체크아웃
- name: Checkout repository
uses: actions/checkout@v3
# 2. SSH 키 설정 - fastLane match에서 사용
# github ssh키를 생성하고 그것을 깃헙 secrets에 넣어서 사용
# https://velog.io/@skyepodium/Github-SSH-Key-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0
# fastlane match는 private 깃헙 레포에 저장된 ios certification, profile을 가져와서
# ios 사이닝을 자동으로 해준다.
# 이때 private repo에 접근하기 위해서 ssh키가 필요한것임
- name: Setup SSH
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
# 3. Ruby 설정 및 캐싱 - fastlane은 루비언어로 되어있기 때문에 설치 필요
- name: Setup Ruby with caching
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0
bundler-cache: true
# 4. Fastlane 캐싱
# Gemfile.lock이 변경되기 전까지 캐싱됨
- name: Cache Fastlane
uses: actions/cache@v3
with:
path: |
~/.bundle
~/.fastlane
key: ${{ runner.os }}-fastlane-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-fastlane-
# 5. Install Fastlane
# 플러터 프로젝트를 다루고 있기 때문에 cd ios를 해서 ios 디렉토리로 이동해줘야함
- name: Install Fastlane
run: |
cd ios
gem install bundler
bundle install
# pubspec.lock 파일이 바뀌기 전까지는 이전 플러터 디펜던시 활용
- name: Cache Flutter dependencies
uses: actions/cache@v3
with:
path: /Users/runner/hostedtoolcache/flutter
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-flutter-
# 6. Flutter 설치
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: 'stable'
#7. private github repo에서 가져오는 dependency에 접근하기위한 설정
# 특정 라이브러리를 fork 떠서 private 레포에서 수정해서 사용하는게 있어서 이부분이 필요
- name: Configure Git
run: |
git config --global url."https://${{ vars.USER_NAME }}:${{ secrets.TOKEN_GITHUB }}@github.com/".insteadOf "https://github.com/"
# 8. Flutter 의존성 설치
- name: Install Dependencies
run: flutter pub get
# 9. config.dart 생성
# 빌드에 필요한데 gitIgnore되고있는 파일이 있다면
# 파일을 github secrets에 등록 후 빌드 전에 원래 위치에 파일을 만들어줘야함
# 내 프로젝트는 dev,prod에 따라 환경설정을 config.dart에서 하고 있었고
# 여기는 키 값 같은 것들이 있어서 gitIgnore 시켜놓았음
- name: Generate Config
run: |
echo '${{ secrets.CONFIG }}' | base64 --d >> ./lib/common/config.dart
# 10. Fastlane 실행
# 플러터 프로젝트를 다루고 있기 때문에 cd ios를 해서 ios 디렉토리로 이동해줘야함
# fastlane ${lane이름} < 이렇게 하면 특정 lane이 실행됨
# env로 fastlane에 필요한 값들을 넘겨줌
- name: Build and Upload to TestFlight
run: |
cd ios
fastlane release_to_testflight
env:
CI: true
BUNDLE_ID: ${{ secrets.BUNDLE_ID }}
# 개발자 애플 계정이 아니라 apple store connect에서 확인할 수 있는 앱에 할당된 apple Id
# '앱스토어커넥트 -> 앱 -> 일반정보 -> 앱정보'에서 확인 가능
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
# 'fastlane match appstore'를 이용해 math가 자동으로 만든 인증서를 인코딩할 때 입력했던 비밀번호
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
# https://velog.io/@king/slack-incoming-webhook
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# https://github.com/fastlane/fastlane/issues/21060#issuecomment-1772362069
# 내 애플개발자 계정 과 비밀번호. 설정방법은 위의 주소 참고
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
# 애플개발자 계정 비밀번호 - 위에서 암호화 한거 말고 원래 비밀번호
# fastlane produce를 할때 앱스토어커넥트에 애플계정으로 로그인 하려면 비밀번호를 전달해 주면 된다.
# 근데 이중인증을 어떻게 해결하는지 몰라서 그냥 produce는 로컬에서 하고
# runner에서는 produce하지 않고 match만 하기로함
# 그래서 이 env는 사실상 필요 없음
# FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
[fastFile]
default_platform(:ios)
platform :ios do
desc "Push to TestFlight"
lane :release_to_testflight do
# CI 환경 설정 (GitHub Actions 등)
setup_ci if ENV['CI']
# 인증서 및 프로비저닝 프로파일 설정
# 내 프로젝트는 NotificationServiceExtension을 활용하기 때문에 NotificationServiceExtension을 사용하기 위한 profile도 만들어야했음
# 위에서 말한 이중인증 이슈 해결 못함으로 인해 cert, profile 만드는 것은 로컬에서 진행
# runner에서는 따로 procude하지 않고 match만 시켜줌
# produce(
# username: ENV["FASTLANE_USER"],
# app_identifier: "co.example.example.NotificationServiceExtension",
# app_name: "AppName NotificationServiceExtension",
# skip_itc: true,
#)
# match를 활용해 cert, profile 가져옴
# workflow 'Setup SSH'에서 ssh 키를 설정해 준 이유가 이 부분 때문임
# match가 인증서를 저장한 private 레포에 접근하기 위해 ssh키와 git_url 활용
# readonly : 기존 인증서 및 프로필만 가져오고 새 인증서를 생성하지 않습니다.
# 따라서 처음 match를 시행할때는 false로 해놓고 이후부터는 true로 설정해야함
match(
git_url: "git@github.com:...", #ssh url활용
app_identifier: ["co.example.example", "co.example.example.NotificationServiceExtension"],
type: "appstore",
readonly: true
)
# 버전 관리
pubspec = YAML.load_file("../../pubspec.yaml")
version = pubspec["version"].split('+')
increment_version_number(
version_number: version[0],
xcodeproj: "Runner.xcodeproj"
)
increment_build_number(
build_number: version[1],
xcodeproj: "Runner.xcodeproj"
)
# 코코아팟 업데이트 및 빌드
cocoapods(
repo_update: true,
clean_install: true,
use_bundle_exec: false
)
# flutter build
# # match가 서명할 것이므로 no-codesign 추가
sh("flutter build ios --release --flavor prod --no-codesign")
#앱 빌드
build_app(
workspace: "Runner.xcworkspace",
scheme: "prod", #flavor를 활용하고 있었기 때문에 해당되는 flavor를 scheme으로 넣어줌
export_method: "app-store",
export_options: {
# NotificationServiceExtension도 따로 만들어줘서 그런지 이부분을 안넣으면 자꾸 에러가남
# bundle과 match가 자동으로 만들어준 Profile을 매칭시켜줌
provisioningProfiles: {
"co.example.example" => "match AppStore co.hiing.hiing",
"co.example.example.NotificationServiceExtension" => "match AppStore co.example.example.NotificationServiceExtension"
},
}
)
# TestFlight 업로드
upload_to_testflight(
#업로드 후에 App Store Connect 에 올라가기 전까지 시간이 걸리는데 이걸 기다리고 싶지 않다면 true 로 설정.
skip_waiting_for_build_processing: true,
apple_id: ENV["APPLE_ID"],
team_id: ENV["TEAM_ID"]
)
# Slack 알림
slack(
message: "TestFlight 배포 완료 🎉",
slack_url: ENV["SLACK_WEBHOOK_URL"]
)
end
error do |lane, exception, options|
# 배포 실패 시 Slack 알림
slack(
message: "TestFlight 배포 실패 🥲\n #{exception}",
success: false,
slack_url: ENV["SLACK_WEBHOOK_URL"]
)
end
end
AppFile, MatchFile은 처음 fastlane이랑 match세팅할때 자동으로 만들어진것에서 변경한것없음
[GemFile]
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods" //cocoapods을 넣어줘야함
//Gemfile은 Ruby 프로젝트의 의존성을 관리하는 파일임
//gem "cocoapods"는 이 프로젝트가 CocoaPods를 의존성으로 사용한다는 것을 명시
[Xcode Signing&Capabilites 설정]

② 회고 (restropective)
이걸 할수있을까 했는데 내가해냄✌️
항상 궁금했던 깃헙액션과 fastlane의 차이점에 대해 알게되어서 굿
그리고 fastlane의 match를 활용하면 ios 사이닝에 관해 일절 관련하지 않아도 되서 너무 혁명쓰..👏
③ 개선을 위한 방법
안드로이드 & app distribution에 배포 되는 것 까지 하나의 워크플로우에서 자동으로 트리거되도록 완성하는게 목표임