[iOS] 두려움 없이 fastlane, CircleCI 도입하기
플레이키보드 iOS 앱에 CI/CD를 구축해보자.
왜 fastlane을 도입했나요 ?
아픈 얘기지만, 우리는 시간을 낭비했어요.
플레이키보드 iOS 앱은 배포를 손수 해야했습니다. 그리고 나서 이렇게 슬랙에 배포 완료 메세지를 직접 작성하고 멘션을 걸어야 합니다. 이 작업은 몇분이 걸릴까요 ?
⌛ 그럼 우리는 몇 시간을 낭비 했을까요 ?
빌드 넘버를 보았을때 400개가 넘습니다. 하나의 빌드당 15분이 걸리고 테스트 플라이트 업로드까지 15분 정도 걸리니깐, 30분 정도 걸리네요.
30분 X 420 = 12,600분 (210시간)
210시간을 낭비했군요, 하루 8시간 일하는 개발자가 한달동안 배포만 했네요. 하루에 1시간씩 7개월 동안 아침에 더 잘 수 있었겠네요. 😴
fastlane을 도입하는데 걸리는 시간은 몇 시간 일까요 ?
도입은 오래 안걸려요. 처음하는 저는 조금 오래걸렸지만, 현재 상태에서 다시 한다면 1–2 시간이면 충분합니다.
완성도를 높이는건 다음 얘기니깐, 우선 시작해봅시다. 왜냐면 꽤 재미가 쏠쏠하거든요 !
두려움 없이 시작하기.
시작도 전에 겁먹을 필요가 전혀 없답니다. 두려움 없이 시작합시다.
💻 fastlane 설치하기
brew install fastlane
명령어로 fastlane을 다운받아 줍니다. 그리고 gem install bundler
명령어로 fastlane을 업데이트 시켜주는 bundler도 다운받아 줍시다.
🚀 fastlane 시작하기
fastlane init
프로젝트 루트 폴더에서 명령어를 실행하면 Welcome 메세지가 나타나고, 4가지 옵션이 주어지는데 아무거나 눌러도 상관없어요. 4번을 눌러서 진행하도록 해보겠습니다.
fastlane 폴더 아래에 이렇게 Appfile과 Fastfile이 생성된 것을 볼 수 있습니다. 무엇보다도 Fastfile에 집중해야합니다. 왜냐면 fastlane은 Fastfile을 동작시킬 것이기 때문입니다. 나머지 파일들(Appfile, Matchfile …)은 옵션 입니다.
default_platform(:ios)
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
end
이제 이 Fastfile을 수정해서 fastlane을 동작시킬거에요. 우선 잠깐 물을 마시고 오세요. 💦 왜냐면 지금부터 해야할 일이 많거든요.
로컬 환경에서 fastlane 구축하기
우선, 로컬에서 구축한 후에 CI/CD 서버에 올릴거에요.
🔐 .env로 개인 정보를 안전하게 보호하기
dotenv는 사용이 어렵지 않아요.
source "https://rubygems.org"
gem "dotenv"
gem "fastlane"
gem "cocoapods"
우선 Gemfile에 dotenv를 추가해줍니다. 저는 fastlane과 cocoapods도 추가해 두었습니다.
FASTLANE_USER=애플아이디 # 앱스토어 커넥트 API 사용시 필요 없음
FASTLANE_PASSWORD=애플비밀번호
MATCH_PASSWORD=임의설정(팀원 모두 같아야함)
APP_STORE_CONNECT_API_KEY_KEY_ID=바로 아래에서 설명
APP_STORE_CONNECT_API_KEY_ISSUER_ID=바로 아래에서 설명
APP_STORE_CONNECT_API_KEY_KEY=바로 아래에서 설명
이렇게 .env 파일을 추가하면 끝입니다. 그리고 나서는 .gitignore에 추가해주세요. 세상에나 너무 쉽죠 ? (참고-dotenv 깃허브)
🚅 앱스토어커넥트 API로 이중 인증 패싱
이부분은 CI/CD를 구축하기 위해선 필수인데요. 왜냐하면 lane이 앱스토어커넥트에 로그인을 할때 이중인증을 요구해서 lane이 멈추기 때문이에요.
로컬 환경에서는 문자메세지를 받아서 인증번호를 입력하면 되겠지만 GithubAction이나 CircleCI에서 이런일이 발생하면 lane은 영원히 중단될거에요. 이렇게 할수는 없으니 이중 인증을 패싱해야 합니다.
우선, 앱스토어 커넥트에 접속 후에 사용자 및 액세스 탭을 클릭합니다.
상단 탭에 ‘키’가 있을텐데, 만약 ‘키’가 보이지 않는다면, 관리자 계정으로 시도해보세요 !
API 키를 다운로드 받고 안전한 곳에 저장합니다. 키를 잃어버리면 복구할 수 없으니 주의하세요. 다시 API 를 연결하는 것은 꽤 귀찮은 일입니다.
이제 .env에 아래의 변수들을 추가해야 합니다.
- APP_STORE_CONNECT_API_KEY_KEY_ID = 키 ID
- APP_STORE_CONNECT_API_KEY_ISSUER_ID = Issuer ID
- APP_STORE_CONNECT_API_KEY_KEY = 다운받은 키(.p8 파일)의 base64 문자열
openssl base64 < {Key 파일 위치}.p8 | tr -d '\n' | pbcopy
해당 명령어를 사용하면 클립보드에 base64 문자열이 복사됩니다.
app_store_connect_api_key(is_key_content_base64: true, in_house: false)
fastlane에 추가하면 잘 동작할 거에요. 그렇다면 이제 문자메세지 이중 인증은 안해도 되겠네요 😌 (참고-app_store_connect_api_key)
🌈 match로 코드 사이닝 하기
cert, sigh를 사용하는 방식도 있지만, match를 사용하는 이유는 간단합니다. 팀일 경우 인증서를 중앙에서 관리하는 방식으로 효율적입니다. (참고-왜 match를 사용하나요 ?)
우선 깃허브 레포지토리를 하나 생성 합니다. 이름은 적당히 인증센터 느낌을 줄 수 있도록 합니다.
fastlane match init
match를 시작하면 첫번째로 storage mode를 선택하는데, 저희는 깃허브를 사용하기 때문에 1번을 선택하고 아까 만들었던 인증센터 레포 주소를 넣어줍니다.
git_url("your_certificate_center_ssh.git")
storage_mode("git")
type("appstore")
app_identifier(["your_app_identifier1", "your_app_identifier2"])
Matchfile을 이렇게 작성해두면, Fastfile 내에서 match를 사용할때 공통되는 파라미터를 스킵할 수 있어서 편합니다. 하지만 이것은 모두 옵션 사항입니다.
match로 인증서를 생성할텐데, 우선 fastlane match nuke
명령어로 기존 인증서들을 지워줍니다. 우리는 match를 사용하여 코드 사이닝을 진행할 것이기 때문에 기존의 인증서는 이제 필요없습니다.
desc "Generate new certificates"
lane :generate_new_certificates do
sync_code_signing(type: "appstore", force_for_new_devices: true, readonly: false)
end
match는 매번 새롭게 갱신할 필요가 없습니다. 인증서의 만료 기한이 1년이기 때문입니다. 하지만, 갱신하는 코드는 간단하니 Fastfile에 추가해 줍니다. 이 코드가 동작하면, 인증센터 깃헙 레포가 업데이트 됩니다. 아주 간단하죠. 인증서를 가져오는 코드는 더 간단합니다.
sync_code_signing(type: "appstore", readonly: true)
이제 match가 깃허브 저장소에 있는 인증서를 로컬의 키체인으로 가져옵니다.
마지막으로 Xcode 설정을 해주어야합니다. 자동 코드 사이닝을 해제시키고 프로비저닝 프로필을 match로 선택해줍니다. 이부분을 그냥 넘어가면 로컬 키체인에 남아있는 개인용 프로필과 애플 개발자 센터에 있는 프로필이 일치하지 않아서 오류가 발생합니다. 꽤 많이 겪는 오류이고, 저도 엄청 헤맸습니다. 🥲
이제 우리는 코드 사이닝을 직접할 필요가 없어졌습니다. 축하해요. 엄청 많은걸 해냈답니다. 🎉 이제 한숨을 돌려도 돼요. 거의 다왔답니다. (참고 — sync_code_signing)
🏄♂️️ 최신 빌드 넘버를 가져오기
increment_build_number
위의 lane으로 빌드 넘버를 1 증가시킬 수 있습니다. 하지만, 실제 업로드된 앱들의 빌드 번호와 겹치게 되면 테스트플라이트 업로드가 되지 않습니다. 안전하게 하기 위해서 최신 빌드 넘버를 가져와서 1을 증가시키는 lane을 작성할거에요. 😇
increment_build_number({
build_number: latest_testflight_build_number(app_identifier: "your_app_identifier") + 1
})
build_number
파라미터를 주게되면 원하는 빌드넘버로 교체가 가능하고, 이부분에서 latest_testflight_build_number
를 사용하여 최신 빌드넘버를 가져와서 + 1 해주면 끝입니다.
자 이제 우리는 로컬에서 fastlane을 구축하는 것을 끝냈습니다.
📦 추가적인 lane
cocoapods(clean_install: true, use_bundle_exec: false) # 코코아팟 인스톨
build_app(scheme: "your_scheme") # 빌드
upload_to_testflight(skip_waiting_for_build_processing: true) # 테플 업로드
하하. 별거없습니다. 우리 맨날 아카이브 하기전에 코코아팟 인스톨 하고 빌드한 다음 테플에 업로드 하잖아요 🤣
💌 슬랙으로 배포 완료 메세지 보내기
version_number = get_version_number(xcodeproj: "your.xcodeproj", target: "yout_target")
build_number = get_build_number.to_i
slack(
username: "Fastlane",
message: "🎉 배포 완료 🎉",
icon_url: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/App_Store_%28iOS%29.svg/1024px-App_Store_%28iOS%29.svg.png",
slack_url: "슬랙 콜백 url",
payload: { "Version": version_number + "(" + build_number.to_s + ")" }
)
message와 payload를 수정하여 원하는 슬랙 메세지를 보낼 수 있어요 !
크 .. 저는 이제 맥주하러 갑니다. 🍻 는 무슨. 아직 기뻐하긴 일러요.
CircleCI에서 fastlane 구축하기
이제 로컬이 아닌 CI/CD 서버에서 알아서 배포되게 만들거에요.
💸 왜 CircleCI를 사용했나요 ?
저희 팀은 GithubAction을 안드로이드와 서버가 대부분을 사용하고 있어서 iOS까지 끼면 요금제 업그레이드를 해야하는 상황이었습니다. 우선은 추가 요금없이 CI/CD를 구축하고자 무료 서비스를 찾던 중 CircleCI를 선택하게 되었습니다.
⛏️ 프로젝트 설정하기
낯선 화면에 프로젝트 리스트가 뜰거에요. 참고로 회원가입과 로그인을 한 상태입니다. 그러면 Set Up Project를 클릭해서 프로젝트를 팔로우 합니다.
📡 SSH 설정하기
프로젝트 설정에 들어가서 User Key를 추가해줍니다. 저는 깃헙 연동을 해놓으니깐 알아서 Key를 가져오더라고요. 간편했습니다.
🔐 ENV 설정하기
프로젝트 세팅 탭에서 Environment Variables를 클릭하여 환경변수를 추가해줍니다. 로컬에 있는 .env와 동일하게 작성하면 됩니다.
🪄 config.yml
루트 폴더에서 .circleci 폴더를 만들고 바로 아래에 config.yml 파일을 만들어주세요.
# .circleci/config.yml
version: 2.1
jobs:
testflight:
macos:
xcode: 14.2.0
environment:
FL_OUTPUT_DIR: output
FASTLANE_LANE: tf
steps:
- checkout
- run: bundle install
- run:
name: Fastlane
command: bundle exec fastlane $FASTLANE_LANE
- store_artifacts:
path: output
workflows:
build-test-testflight:
jobs:
- testflight:
filters:
branches:
only: develop
놀라울 정도로 간단한 config.yml을 구성하였습니다. develop 브랜치에 푸시 될 때마다 work flow가 동작합니다.
성공하기까지 162번을 실패했었네요 ! 사실.. 퇴근하고도 열심히 돌려봤었답니다. 😈 😈
💫성공💫 저는 이제 진짜 맥주 하러 갑니다. 🍻 🥳
🎉 최종 Fastfile
default_platform(:ios)
platform :ios do
desc "set up circle ci"
before_all do
setup_circle_ci
end
desc "Get certificates"
lane :certificates do
sync_code_signing(type: "appstore", readonly: true)
end
desc "Generate new certificates"
lane :generate_new_certificates do
app_store_connect_api_key(is_key_content_base64: true, in_house: false)
sync_code_signing(type: "appstore", force_for_new_devices: true, readonly: false)
end
desc "Push to TestFlight"
lane :tf do |options|
app_store_connect_api_key(is_key_content_base64: true, in_house: false)
sync_code_signing(type: "appstore", readonly: true)
increment_build_number({
build_number: latest_testflight_build_number(app_identifier: "your_app_identifier") + 1
})
cocoapods(clean_install: true, use_bundle_exec: false)
build_app(scheme: "your_scheme")
upload_to_testflight(skip_waiting_for_build_processing: true)
version_number = get_version_number(xcodeproj: "your.xcodeproj", target: "your_target")
build_number = get_build_number.to_i
slack(
username: "Fastlane",
message: "🎉 CircleCI 구축 성공 🎉",
icon_url: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/App_Store_%28iOS%29.svg/1024px-App_Store_%28iOS%29.svg.png",
slack_url: "your_slack_callback_url",
payload: { "Version": version_number + "(" + build_number.to_s + ")" }
)
end
end
참고자료
하고싶은 말
fastlane을 구축하는 일은 생각만해도 멋진일이었고, 그걸 제가 할 수 있다는 생각에 뿌듯했습니다. 자동 배포가 중요한 이유는 반복 단순 작업에 대한 피로도를 줄일 수 있다는 점입니다.
처음이지만, 마구 찾아보며 노력했던 점은 저에게 칭찬하고 싶네요. CI/CD의 첫 걸음을 내딛었습니다.
여기서 멈추면 안되겠죠. 해야할 일은 많습니다. 테스트 자동화, deliver을 사용한 앱스토어 업로드 자동화, workflow trigger 수정 등 ..
제 소개가 늦었네요. (사실 .. 멋지게 마지막에 하려했습니다. )
저는 플키팀에서 iOS 개발을 담당하고 있는 송영모입니다.
끝까지 읽어주셔서 감사합니다. 🙇♂️🙇♂️