CI/CD 구현
도입 이유
lean startup methodology를 따라서 개발, 배포, 가설 검증, 피드백 등을 진행하면서 빠른 배포의 필요성이 대두되었다.
병렬적으로 배포를 올리고 다른 태스크를 쳐내면서 업무의 효율성을 올리기 위해서 CI/CD 도입을 하게 되었다.
또한 sprint #2 부터 배포를 진행하면서 빠르게 사용자들의 피드백을 받고싶었고 테스터들을 점점 늘리면서 지속적인 피드백을 가지고 가고 싶었다.
현재 서비스의 문제점
한 번 빌드를 올리기 위해서는
- 현재 모든 기능들이 정상적으로 작동하는지 확인하고 ( 테스트 )
- provisioning profile, certificate 설정을 해주고
- 빌드 번호 체크를 한 뒤
- testflight에 배포
위의 과정을 따라야 하는데, 일주일에 약 2번 정도 배포를 진행하면서 배포에 들어가는 시간 또한 무시할 수 없어졌다.
구현 과정
설계
먼저 두가지의 Actions를 구현하려고 생각하였다.
- develop 브랜치에 pr을 날릴 때
- 프로젝트가 빌드가 되는지 확인한다.
- 테스트를 돌리고 통과하는지 확인한다.
- main 브랜치에 push 될 때 (머지되었을 때)
- 프로젝트 빌드
- testflight로 테스터들에게 배포
사실 위의 기능들은 간단하게 개발이 가능하였으나, Tuist를 사용하면서 pbxproj을 gitignore처리를 해두었고, tuist를 통해서 가상 환경에서 프로젝트를 만들고, 해당 프로젝트의 설정을 섬세하게 넣어주는 작업이 필요했다.
develop 브랜치에 pr을 날릴 때
사실 이 부분은 간단하게 처리가 가능했다.
# .github/workflows/****-develop.yml
# name: ****-develop
on:
pull_request:
branches: [ develop ]
jobs:
build:
name: ****-develop
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Access Available
run: chmod +x Scripts/onboarding.sh
- name: Install Setup Script and Fetch Dependencies
run: ./Scripts/onboarding.sh
- name: Build and Test
run: tuist test ****-Workspace
- os를 macos로 설정을 해준다.
- onboarding이라는 스크립트에 실행 권한을 준다
- 스크립트를 실행한다.
- 스크립트는 간단하게 tuist를 다운로드하고, dependency를 fetch하고, 프로젝트 파일을 생성하는 명령어로 구성되어 있다.
curl -Ls https://install.tuist.io | bash
tuist fetch
tuist generate
- 앞에서 만들 프로젝트 파일의
****-Workspace
스키마의 test를 돌린다.
위와 같은 순서로 아주 간단하게 구현할 수 있었다.
대부분의 빌드 세팅은 Tuist를 통해서 진행했는데, 해당 부분은
이전의 다른 글을 통해서 자세하게 설명해두었다.
main 브랜치에 push 될 때 (머지되었을 때)
이 부분이 문제가 아주 많았다….
# .github/workflows/****-testflight.yml
# name: ****-testflight
on:
push:
branches: [ main ]
jobs:
build:
name: ****-testflight
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Access Available
run: chmod +x Scripts/onboarding.sh
- name: Install Setup Script and Fetch Dependencies
run: ./Scripts/onboarding.sh
- name: Install Bundler
run: |
gem install bundler
bundle install --without=documentation --path vendor/bundle
- uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: Beta Release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
run: bundle exec fastlane ios remote
- os를 macos로 설정해준다
- onboarding 스크립트를 통해 tuist를 설치하고, dependency를 fetch해온 후 프로젝트 파일을 생성한다.
- gemfile을 통해서 fastlane 관련된 것들을 다운로드 받는다.
- github에 접근하기 위해서 ssh key를 통해서 세션을 유지시켜준다
- remote lane을 실행시킨다.
fastlane remote
default_platform(:ios)
platform :ios do
desc "Push a new beta build to fastlane"
lane :remote do
setup_ci if ENV['CI']
create_keychain(
name: ENV["KEYCHAIN_NAME"],
password: ENV["KEYCHAIN_PASSWORD"],
timeout: 1800,
default_keychain: true,
unlock: true,
lock_when_sleeps: false
)
app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
duration: 500,
in_house: false
)
match(
git_url: "git@github.com:****/****-match.git",
storage_mode: "git",
type: "appstore",
app_identifier: "com.****.***.****",
readonly: false,
keychain_name: ENV["KEYCHAIN_NAME"],
keychain_password: ENV["KEYCHAIN_PASSWORD"]
)
increment_build_number({
build_number: latest_testflight_build_number + 1
})
update_code_signing_settings(
use_automatic_signing: false,
team_id: "*****",
path: "*****.xcodeproj"
)
build_app(
workspace: "****.xcworkspace",
scheme: "****",
configuration: "Release",
export_method: "app-store",
codesigning_identity: "Apple Distribution: ****",
export_options: {
method: "app-store",
provisioningProfiles: {
"com.****.****.****" => 'match AppStore com.****.****.**** ***'
}
}
)
upload_to_testflight(skip_waiting_for_build_processing: true)
end
end
- ci환경을 세팅해준다
- keychain을 세팅해준다.
- provisioning profile과 certificate가 keychain에 저장되고
- 저장된 provisioning profile과 certificate를 사용해야 하기 때문에 설정해주어야 한다.
app_store_connect_api_key
를 통해서 appstore connect에 접속한다.- 만약 app_store_connect_api_key를 사용하지 않으면 이중 로그인을 진행해야 하기 때문에 ci환경에서 불편함이 생긴다.
- app_store_connect_api_key를 사용하면 이중 로그인 없이 appstore connect에 접속 가능하다.
match
를 통해서 provisioning profile과 certificate를 만들어둔 keychain에 저장한다.- match 통해서 provisioning profile, certificate 원격으로 받아오기
- match를 통해서 private repository에 provisioning profile과 certificate를 올려두었다.
- 이제 가상 환경에서 match를 통해서 repository에 접근하기 위해서 ssh key를 발급해줘야 한다.
- ssh-keygen 명령어를 통해서 sshkey를 발급받고, 해당 key를 복사해서 사용하기 위해서 pbcopy를 사용해서 복사해준다.
- match를 통해서 private repository에 provisioning profile과 certificate를 올려두었다.
- match 통해서 provisioning profile, certificate 원격으로 받아오기
increment_build_number
를 통해서 빌드 넘버를 올려준다update_code_signing_settings
를 통해서 code signing을 수동으로 변경하고 뒤에서 직접 설정해주도록 한다.- 멀티 프로젝트, 멀티 타겟으로 앱이 구성되어 있기 때문에 각각의 모듈에 대해서 provisioning profile과 certificate이 삽입되게 된다.
- 메인 프로젝트를 제외한 부분들은 provisioning profile과 certificate를 제거해줘야 하기 때문에 수동으로 변경한다.
build_app
을 통해서 scheme, code-signing-identity, export options 등을 설정해준다.- export_options를 통해서 메인 프로젝트의 번들 id에 해당하는 부분만 provisioning profile을 설정해준다.
upload_to_testflight
를 통해서 build_app을 통해 만들어진 ipa 파일을 testflight에 업로드한다.
결과
CI/CD를 통해서 서비스를 사용자들에게 지속적으로 업데이트해서 보여줄 수 있는 파이프라인을 빠르게 구축할 수 있었고, 빌드 → 테스트 → 배포의 과정을 간단하게 진행을 하면서 리소스를 아껴 다른 곳에서 사용할 수 있었다.
결과적으로 14번의 배포를 진행할 수 있었고, 한 번의 스프린트에 평균 약 2번 정도의 배포를 진행할 수 있었다.
이를 통해서 사용자들에게 다양한 피드백을 받을 수 있었고, lean하게 개선해야 할 부분과 추가해야 할 부분들을 파악하고 개선할 수 있었다.
- 올린 빌드들