본문 바로가기

iOS/iOS (응용)

[iOS] 앱 배포 자동화: Slack 명령어로 TestFlight 업로드 (Flask, Bitbucket Pipelines, Fastlane 활용)

👋 개요


해당 포스팅은 Slack에서 한 줄의 명령어로 iOS앱을 배포하는 방법에 대해서 설명합니다.

동작 원리는 Slack에서 Slash Command를 사용하면 요청한 URL에 접근하여 POST요청을 하게 되고, Flask 웹앱이 전달받아 Bitbucket Pipeline을 Trigger 요청하는 REST API를 호출합니다. 성공하면 Pipeline이 실행되어 Fastlane 명령어를 실행하게 되고 TestFlight에 배포가 된 후 Slack Webhook을 통해 알림을 받을 수 있습니다.

설명드린 동작 원리의 순서대로 의존성을 가지게 되기 때문에 역순으로 구현방법에 대해서 설명하겠습니다.

 

 

⚙️ TestFlight


iOS앱개발자는 직접 만든 앱을 배포 및 출시 하거나 테스터들에게 테스트할 수 있도록 제공하기 위해 TestFlight를 활용할 수 있습니다.

TestFlight에 앱을 업로드하기 위해서는 Archive파일을 Xcode의 Organizer를 이용하여 업로드하거나, ipa파일을 Command Line Tool로 ipa를 업로드하는 방법이 있습니다.

 

 

🚀 Fastlane


Fastlane이란? iOS 앱 배포 단순화를 목표로 하는 오픈 소스 플랫폼입니다.

Fastlane은 여러가지 기능들을 제공하고, 해당 기능들을 활용하여 배포작업(TestFlight에 앱을 업로드하는 작업)시간을 단축시키거나 간단하게 처리할 수 있습니다.

Fastlane의 기본동작 원리는 lane이라는 워크플로를 작성한 후 실행하는 방식입니다.

 

 

1️⃣ Fastlane에 필요한 툴 설치

Fastlane을 사용하는 경우 의존성을 가지게되는 툴이 존재하기 때문에 사용하기 전 미리 설치할 필요가 있습니다.

  1. Xcode command line tool
  2. Homebrew
  3. Ruby & Git

 

Xcode command line tool

Xcode Command Line Tool은 터미널에서 명령어를 통해 Xcode를 빌드하고 실행할 수 있도록 도와주는 도구입니다.

Xcode를 설치하면 함께 설치되지만, 터미널에서 다음 명령어를 실행하여 설치 여부를 확인할 수 있습니다.

 

Xcode command line tool 설치방법
$ xcode-select --install
  • 이 명령어를 실행했을 때 Xcode Command Line Tool이 이미 설치되어 있다면 오류 메시지가 표시되고, 설치되어 있지 않다면 설치가 진행됩니다.

 

Homebrew

Homebrew는 macOS의 패키지 관리자입니다. 유틸리티를 설치하고 해당 유틸리티에 있을 수 있는 모든 종속성을 관리하여 도와줍니다.

 

Homebrew 설치방법
/bin/bash -c "$(curl -fsSL <https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh>)"

 

Ruby & Git

ruby는 맥에 기본 설치되어 있을 수 있습니다. 하지만 2.5미만 버전에서는 Fastlane이 지원되지 않을 수 있기 때문에 2.5이상 버전을 새로 설치해야 할 수 있습니다.

ruby를 설치하는 경우 여러 버전을 관리할 수 있는 rbenv를 설치하여 관리하는 것을 추천합니다.

 

rbenv를 이용한 ruby설치방법

$ brew install rbenv

$ rbenv init

# oh-my-zsh을 사용하는 경우 .zshrc파일에서 아래 코드를 작성합니다.
# 기본 Bash를 사용한다면 .bash_profile에 작성합니다.
eval "$(rbenv init -)"

$ rbenv install 3.3.0

# 전역 설정 및 로컬 설정
$ rbenv global 3.3.0
$ rbenv local 3.3.0

# 3.3.0버전이 잘 선택되었는지 확인합니다.
$ rbenv versions

 

 

git 설치방법

$ brew install git

 

2️⃣ Fastlane설치

🔗 Fastlane을 설치하는 방법은 여러가지가 있습니다.

  1. Bundler를 통한 설치
  2. Homebrew를 통한 설치
  3. Ruby를 통한 설치

3가지 방법 중 가장 선호되는 방법은 Bundler를 사용하는 것이고 Ruby를 사용하는 것은 종속성을 관리하기 어렵고 충돌을 일으킬 수 있으므로 권장되지 않습니다.

 

저는 간편한 Homebrew를 통한 설치를 진행했습니다. 참고로 Hombrew로 설치를 진행하면 Ruby를 설치하는 과정을 생략할 수 있습니다.

 

Hombrew를 통한 fastlane 설치방법

$ brew install fastlane

# 설치가 잘 되었는지 확인합니다.
$ fastlane -v

 

3️⃣ Fastlane 적용

Fastlane 설정

 

Fastllane 초기화

Fastlane은 각각의 프로젝트에서 초기화되기 때문에 프로젝트의 루트디렉토리로 이동하여 초기화를 진행합니다.

$ fastlane init

초기화명령어를 실행하면 4개의 템플릿 옵션을 제공해주는데 4번(수동옵션)을 선택합니다.

완료되면 프로젝트에 새로운 파일이 추가되는데, Gemfile(Fastlane 구성을 저장하는 파일)과 lane을 작성하기 위한 Fastfile을 포함하는 fastlane폴더가 생성됩니다.

 

Appfile 작성

fastlane폴더의 Appfile에 자주 사용되는 환경변수들을 저장합니다.

app_identifier("[번들 ID]")
apple_id("[Apple 이메일 주소]")
team_id("[팀 ID]")

 

 

CodeSigning

fastlane에서 🔗 code signing을 하기위해 여러 방법이 있지만 공식문서에서 권장하는 방법은 Cert & Sigh를 이용하거나 Match를 이용할 수 있습니다.

 

Cert & Sigh를 이용한 인증방법

Cert는 인증서 요청을 자동화하고 유효한 iOS 인증서, 개인 키가 있는지 확인한 후 있다면 로컬에 설치합니다.

없다면, 새로운 개인키를 생성하여 키체인에 저장합니다. 그리고나서 인증서 요청을 자동화한 후 설치합니다.

Sigh는 프로비저닝 프로필 관리를 간단하게 해줍니다. 프로필 생성, 갱신, 다운로드 같은 작업을 처리합니다.

 

저장을 완료한 후, Cert와 Sigh가 동작하는 custom lane을 생성합니다.

fastlane폴더의 Fastfile에 platform do ~ end 구문에 custom lane을 작성합니다.

default_platform(:ios)

platform :ios do
  
  desc "Cert & Sigh를 이용한 인증, 빌드"
  lane :certSighBuild do
    get_certificates           
    get_provisioning_profile
    build_app
  end

end
  • 기본적인 lane작성방법은 desc는 lane에 대한 설명이 들어갑니다. 원하는 lane명을 지정해준 후 do ~ end 구문에 실행시킬 코드를 작성합니다. 위의 코드는 cert와 sigh를 이용하여 인증 후 빌드하는 custom lane을 작성했습니다.

 

AppService 인증

작성을 모두 완료한 후 터미널로 이동하여 작성한 lane을 실행합니다.

$ fastlane certSighBuild

 

작성한 lane을 실행하면 AppstoreConnect의 이메일주소와 앰암호를 물어보는데 해당 값들을 입력한 후 인증코드를 입력합니다.

 

 

Apple 앱 암호 발급

위의 앱암호를 입력하려면 🔗 AppleID로 이동하여 앱암호를 발급받아야 합니다.

 

로그인 → 로그인 및 보안 메뉴 → 앱 암호 선택

 

위의 과정을 하는 이유는 fastlane은 Apple서비스에 접근하기 위해 인증과정이 필요하기 때문에 인증을 하는 과정입니다. 인증하는 방법은 다양하게 있어서 아래 링크를 참고할 수 있습니다.

 

TestFlight 업로드 Lane 작성

lane이 정상적으로 끝났다면 다시 fastlane/Fastfile로 들어가서 Testflight에 업로드하는 코드를 추가합니다.

default_platform(:ios)

platform :ios do

  desc "Cert & Sigh를 이용한 인증, 빌드"
  lane :certSighBuild do
  	get_certificates           
	get_provisioning_profile
	build_app
  end
	
  desc "TestFlight Beta 업로드"
  lane :beta do
  	certSighBuild
	build_app(configuration: "Debug")
	upload_to_testflight
  end

  desc "TestFlight Release 업로드"
  lane :release do
    certSighBuild
    build_app(configuration: "Release")
    upload_to_testflight
  end
	
end

 

작성을 모두 완료한 후 터미널로 이동하여 작성한 lane을 실행합니다.

$ fastlane beta

 

성공적으로 TestFlight에 올라갔는지 확인합니다.

 

⛓️ Bitbucket PipeLines


Bitbucket Pipelines는 Bitbucket Cloud에 내장된 통합 CI/CD 툴입니다.

해당 툴을 사용하여 TestFlight에 업로드하는 lane을 터미널이 아닌 Bitbucket Cloud에서 동작시킬 수 있습니다.

Pipeline을 작성한 후, 실행하면 Bitbucket 서버에서 동작하게 됩니다.

하지만, Bitbucket Pipeline은 빌드시간에 따라 가격정책이 달라지기 때문에 🔗 가격표를 확인해봐야 합니다.

빌드시간이 오래걸리는 경우에 요금이 많이 나올 수 있기 때문에 무료로 사용할 수 있는 Self Hosted Runner 기능이 있습니다.

 

1️⃣ Self Hosted Runner

로컬 컴퓨터에서 Pipeline을 통해 빌드를 실행할 수 있는 기능입니다.

Runner를 실행하기 위해서는 Java를 설치해야합니다.

 

Java 설치

$ brew install openjdk@11

# oh-my-zsh을 사용하는 경우 .zshrc파일에서 아래 코드를 작성합니다.
# 기본 Bash를 사용한다면 .bash_profile에 작성합니다.
echo 'export PATH="/opt/homebrew/opt/openjdk@11/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc

$ java --version

 

Bitbucket 저장소의 메뉴 중 Repository settings를 선택합니다.

 

Runners 메뉴 → Add runner 선택합니다.

 

System and architecture를 MacOS로 선택한 후 Runner name을 원하는 이름으로 작성합니다.

 

제공하는 명령어들을 Runner를 실행할 로컬컴퓨터에서 화살표에 표시된 복사버튼을 기준으로 차례대로 입력해줍니다.

 

 

정상적으로 완료되면 ONLINE 상태가 표시됩니다.

 

2️⃣ Pipeline 작성

파이프라인 → Select를 선택하여 pipeline.yml파일을 작성합니다.

 

 

pipelines:
  custom:
    upload_testFlight:
      - step:
          runs-on:
            - self.hosted
            - macos
          script:
            - fastlane beta
    upload_testFlight_release:
      - step:
          runs-on:
            - self.hosted
            - macos
          script:
            - fastlane release
  • self hosted runner이기 때문에 명시를 해줍니다.
  • script에 실행할 명령어를 추가해줍니다.

작성을 모두완료한 후 커밋을 완료하면 파이프라인을 실행할 수 있습니다. 실행시킬 브랜치와 만들어준 upload_testFlight 파이프라인을 실행합니다.

 

 

🍸 Flask


 

API요청을 받았을 때 Pipeline을 자동으로 실행해주기 위한 Flask웹앱을 작성합니다.

Flask를 선택한 이유는 파이썬을 이용하여 간단하게 구축할 수 있기 때문에 최소한의 필요한 기능만 구현할 예정입니다.

Flask웹앱을 원격에 배포하기 위해 조건부무료인 🔗pythonanywhere를 사용할 수 있습니다.

 

1️⃣ Pythonanywhere 설정

pythonanywhere로 들어가서 회원가입 후 로그인을 해주세요.

로그인을 하게 되면 Dashboard가 표시되는데 Web을 선택합니다.

Add a new web app을 선택합니다. Flask웹앱을 만들 예정이기 때문에 Flask를 선택하여 앱을 생성합니다.

 

웹앱 추가가 모두 완료되었다면 코드를 작성해줘야 합니다.

Source cods가 작성되어 있는 파일로 이동 (Go to directory 선택) → flask_app.py 선택합니다.

 

 

2️⃣ Bitbucket Pipeline Trigger 코드 구현

이제부터 flask_app.py에 Bitbucket Pipeline을 trigger하는 요청을 보내는 코드를 작성합니다.

🔗 Bitbucket Cloud REST API - Trigger a Pipeline for a branch

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello from Flask!'

@app.route('/upload_testFlight', methods=['POST'])
def upload_testFlight():
    data = ' { "target": { "ref_type": "branch", "type": "pipeline_ref_target", "ref_name": "main", "selector": { "type": "custom", "pattern": "upload_testFlight" } } }'
    response = requests.post('<https://api.bitbucket.org/2.0/repositories/sookim-1/distributiontest/pipelines/>', headers={'Content-Type': 'application/json',}, data=data, auth=("sookim-1", "앱암호"))

    return jsonify(
        text='Upload TestFlight Post Success',
    )

 

Bitbucket 앱암호 발급

위의 API를 요청할 때 인증을 하는 경우 Bitbucket 앱암호가 필요합니다.

  • 앱암호 써있는 부분 → Bitbucket 앱암호 만드는 방법
  • Bitbucket의 Personal settings 이동 → 앱 비밀번호 → 앱 비밀번호 생성하기

  • 앱 비밀번호 권한 설정 - 파이프라인 필수

 

 

3️⃣ Pythonanywhere 환경변수 설정

발급받은 앱암호는 중요한 정보이기 때문에 바로 코드에 작성하기 보다는 pythonanywhere의 환경변수 설정을 통해 지정해주는 것을 권장합니다.

 

Dashboard → Bash Shell을 실행해줍니다.

 

.env 작성

 

앱이 존재하고 있는 폴더로 이동합니다. 저의 경우는 mysite폴더에 존재합니다.

해당폴더에서 .env 파일을 작성하고 Bitbucket에서 발급한 앰암호를 입력합니다.

# python-dotenv 패키지 버전 0.8 이상은 export를 인식가능합니다.
$ echo "export SECRET_KEY=[Bitbucket 앱암호]" >> .env

# python-dotenv 패키지 버전 0.8미만
$ echo "SECRET_KEY=[Bitbucket 앱암호]" >> .env

# 주의 : Flask WebApp python버전으로 설치
$ pip3.7 install --user python-dotenv

 

 

WSGI 파일 설정

 

WSGI파일에서 작성한 .env파일이 로드되도록 설정합니다.

import os
from dotenv import load_dotenv
project_folder = os.path.expanduser('~/[앱이 존재하는 폴더]')
load_dotenv(os.path.join(project_folder, '.env'))

 

위의 로드되는 코드를 추가한 WSGI파일코드

import sys
import os
from dotenv import load_dotenv

# project_home 설정
project_home = '/home/scstnghks/mysite'

# .env 파일 불러오기 (절대 경로 사용)
load_dotenv(os.path.join(project_home, '.env'))  # .env 파일 경로

# sys.path에 project_home 추가 (선택 사항)
if project_home not in sys.path:
    sys.path.insert(0, project_home)

# Flask 앱 import 및 이름 변경
from flask_app import app as application

 

 

flask_app.py에서 앱암호 사용 부분 환경변수로 변경

from flask import Flask
import os

app = Flask(__name__)
SECRET_KEY = os.getenv("SECRET_KEY")

@app.route('/')
def hello_world():
    return 'Hello from Flask!'

@app.route('/upload_testFlight', methods=['POST'])
def upload_testFlight():
    data = ' { "target": { "ref_type": "branch", "type": "pipeline_ref_target", "ref_name": "main", "selector": { "type": "custom", "pattern": "upload_testFlight" } } }'
    response = requests.post('<https://api.bitbucket.org/2.0/repositories/sookim-1/distributiontest/pipelines/>', headers={'Content-Type': 'application/json',}, data=data, auth=("sookim-1", SECRET_KEY))

    return jsonify(
        text='Upload TestFlight Post Success',
    )

 

 

💬 Slack Slash Command


1️⃣ Slack App 생성

Slack 앱은 Slack 플랫폼 위에서 동작하는 소프트웨어를 의미합니다. 이 앱은 단순한 메시징 기능을 넘어 팀의 생산성 향상, 업무 자동화, 외부 서비스 연동 등 다양한 목적을 위해 개발될 수 있습니다. Slack 앱은 크게 다음 두 가지 유형으로 나뉩니다:

  • 내부 통합 앱: 특정 회사나 팀의 요구사항에 맞춰 개발되는 앱으로, 외부에 공개되지 않습니다.
  • Slack 앱 디렉토리 앱: Slack 앱 디렉토리에 등록되어 모든 Slack 사용자가 이용할 수 있는 앱입니다.

내부 통합 앱을 만드는 방법에 대해서 설명하겠습니다.

 

Slack App 생성페이지로 이동한 후, Create an App 버튼을 클릭합니다.

 

 

앱설정 옵션을 From scratch를 설정합니다. 이미지에서와 같이 수동 및 자동의 차이입니다.

 

원하는 앱이름을 작성한 후 사용할 워크스페이스를 지정합니다.

 

앱을 워크스페이스에 설치하기 위해서는 이미지의 경고문처럼 커스텀앱 권한 범위를 지정해줘야합니다. 따라서 Permission 메뉴로 이동하여 범위를 지정해줍니다.

 

 

 

다시 BasicInfomation메뉴에서 Install to Workspace를 선택하여 앱 설치를 진행합니다.

 

 

2️⃣ Slash Command 구현

🔗 Slash commands

Slash Command는 Slack 메시지 입력창에 "/"를 입력하여 실행하는 명령어입니다.

예를 들어, /apps-deploy news release develop 이렇게 작성하면 Slash Command는 apps-deploy이고 news, release, develop이라는 매개변수들이 POST로 포장되어 전송됩니다.

Slack앱 고유의 암호로 Slack에서 요청이 왔는지 확인합니다.

Slack이 보내는 각각의 HTTP요청에는 X-Slack_Signature HTTP header가 추가됩니다.

 

 

 

 

  • Command - 명령어 실행할 이름작성
  • Request URL - 사용자가 명령을 호출할 때 페이로드를 보낼 URL
  • Short Description - 간략한 설명
  • Usage Hint - 필요한 파라미터 힌트     

 

Slash Command 파라미터 처리

SlackSlashCommand기능 중에 필요한 파라미터를 text매개변수에 순서대로 담아 전달할 수 있습니다.

이 부분을 활용하여 branchName과 build Type을 전달받아 특정 branch에서 원하는 custom Lane을 실행하도록 Flask웹앱을 일부 수정할 수 있습니다.

 

from flask import Flask, jsonify, request
import os
import requests

app = Flask(__name__)

# .env 파일 또는 환경 변수에서 SECRET_KEY 가져오기
SECRET_KEY = os.getenv("SECRET_KEY")

# Bitbucket API 엔드포인트 확인
BITBUCKET_API_ENDPOINT = '<https://api.bitbucket.org/2.0/repositories/sookim-1/distributiontest/pipelines/>'

@app.route('/')
def hello_world():
    return 'Hello from Flask!'

@app.route('/upload_testFlight', methods=['POST'])
def upload_testFlight():
    # Slack Slash Command로부터 전달된 text 파라미터 가져오기
    text = request.form.get('text', '')
    params = text.split()

    # 파라미터 유효성 검사
    if len(params) < 2:
        return jsonify(text='Error: Two parameters are required - branch name and build type.'), 400

    branch_name = params[0]
    build_type = params[1]

    # 파이프라인 패턴 설정
    pipeline_pattern = "upload_testFlight"
    if build_type == 'release':
        pipeline_pattern += "_release"

    # 요청 데이터 생성
    data = {
        "target": {
            "ref_type": "branch",
            "type": "pipeline_ref_target",
            "ref_name": branch_name,  # 파라미터로 받은 branch 이름 사용
            "selector": {
                "type": "custom",
                "pattern": pipeline_pattern  # 조건에 따라 파이프라인 패턴 설정
            }
        }
    }

    # Bitbucket API 요청
    response = requests.post(BITBUCKET_API_ENDPOINT, headers={'Content-Type': 'application/json'}, json=data, auth=("sookim-1", SECRET_KEY))

    # 응답 확인 및 결과 반환 (필요에 따라 수정)
    if response.status_code == 201:
        return jsonify(text='Upload TestFlight Post Success')
    else:
        return jsonify(text='Upload TestFlight Post Failed', error=response.json()), response.status_code

 

 

 

모두 완료된 후 / 를 하면 해당 명령어가 표시됩니다.

/ios라이더앱배포 subTest release 라고 입력하면 subTest 브랜치의 release custom Lane을 실행하게됩니다.

3️⃣ Slack Workflow Builder 구현

Slack 워크플로는 특정 작업을 자동화하는 기능입니다.

해당 기능을 활용하여 Slash Command를 입력할 명령어를 안내하도록 할 예정입니다. 참고하실 점은 Slack 워크플로는 유료Plan에서만 이용가능합니다.

 

 

 

 

🧑‍🎓 정리


글을 정리하며 느낀점은 배포 자동화를 구현할 때 중간마다 다른 툴을 사용할 수 있는 부분들이 많았습니다. 예를 들어, Bitbucket Pipeline을 GithubAction을 활용할 수 도 있고, Flask웹앱이 아닌 모든 다른 REST API를 처리할 수 있는 서버만 구축한다면 여러가지로 대체될 수도 있다고 생각합니다.

 

피드백, 질문등은 자유롭게 댓글로 작성해주세요~!

🙇‍♂️ 읽어주셔서 감사합니다 🙇‍♂️

 

 

🔗 참고링크


'iOS > iOS (응용)' 카테고리의 다른 글

[iOS] SwiftPM - PackageCollection  (0) 2024.08.21
[iOS] SwiftPM - SwiftPackage  (0) 2024.08.21
[iOS] URL및 endpoint관리하기  (0) 2024.04.19