Fly.io 확장(Extensions) 프로비저닝
이 문서는 Fly.io Extensions 프로그램에서 확장 리소스를 프로비저닝하기 위한 REST 기반 API를 설명해요. 공유 시크릿과 SHA256 HMAC으로 서명된 요청, 리소스 프로비저닝 API, 웹훅, SSO 흐름, 고객과의 이메일 커뮤니케이션 지침까지 다룹니다. 파트너 API를 구축하는 개발자를 위한 문서예요.
출처: 문서
본문
이 문서를 파고들기 전에 Extensions 프로그램 개요를 읽어 보아요.
요구사항
다음을 구현하려면 우리가 여러분에게 제공해야 하는 것이 있어요:
- 프로비저닝 요청 서명을 위한 공유 시크릿
- 웹훅 서명을 위한 공유 시크릿
- OAuth 애플리케이션 클라이언트와 시크릿 값
운영 환경을 위한 OAuth 리다이렉트 URL을 우리에게 주어야 해요.
프로비저닝 흐름
Fly.io는 CLI 우선 플랫폼이에요. 그래서 확장 프로비저닝도 CLI에서 시작해요. 앱을 시작할 때, 또는 고객이 명시적으로 확장 프로비저닝을 요청할 때요.
우리는 이 요청을 프로비저닝하는 Fly.io 조직에 대한 세부 사항과 함께 여러분에게 전달해요. 여러분은 프로비저닝된 확장에 대한 세부 사항으로 동기적으로 응답해야 해요. 우리는 이 세부 사항을 대상 애플리케이션에, 해당되는 경우 VM에서 환경 변수로 노출되는 시크릿으로 첨부해요.
참고: 리소스 프로비저닝에 5초 이상 걸리면 flyctl이 리소스 준비 상태를 폴링할 수 있어요.
계정 및 조직 프로비저닝
고객이 Fly.io로 프로비저닝한 계정을 통해 여러분의 플랫폼 기능에 완전히 접근할 수 있길 바래요. 그리고 여러분이 이메일로 고객과 소통할 수 있어야 해요. 이를 위해 모든 프로비저닝 요청에는 다음이 함께 제공돼요:
- 고유한 사용자 ID와 작동하는 이메일 별칭
- 조직 관리자에게 이메일을 보내는 고유한 조직 ID와 작동하는 이메일 별칭
우리의 SSO 흐름은 사용자와 조직 데이터를 가져와 사용자가 주어진 리소스에 접근 권한이 있는지 판단할 수 있게 해 줘요.
최소한 다음을 프로비저닝해야 해요:
- Fly.io를 통해 프로비저닝된 확장을 소유할 계정
- 해당되는 경우 Fly.io 조직과 일치하도록 조직/팀
- 계정 소유자 및 SSO 대상으로서의 하나 이상의 관리 사용자
조직과 사용자를 리소스 프로비저닝 요청과 함께, 또는 아래 설명처럼 SSO 로그인과 함께 멱등(idempotently)하게 프로비저닝할 것을 권장해요. 이렇게 하면 별도의 계정 프로비저닝 API를 구현할 필요가 없어요.
리소스 프로비저닝 API
이 섹션은 개별 확장 리소스를 프로비저닝하기 위한 REST 기반 API를 설명해요.
인증 및 요청 서명
우리는 플랫폼이 부여하는 높은 권한을 고려해 모든 요청을 공유 시크릿과 SHA256 HMAC으로 서명해요. 제공자는 이러한 서명을 검증해야 해요.
모든 요청에는 요청 내용의 HMAC을 담은 X-Signature 헤더가 함께 제공돼요: GET과 DELETE 요청의 경우 쿼리 문자열, POST와 PATCH 요청의 경우 요청 본문이에요.
요청의 진위를 검증하는 데 도움이 되도록 다음 매개변수를 추가해요. 서명된 URL이 요청 URL과 일치하는지 최소한 검증할 것을 권장해요.
timestamp: A UNIX epoch timestamp value
nonce: A random unique string identifying the individual request
url: The full request target URL
요청 기본 URL
파트너 API는 https://logjam.io/flyio 같은 기본 URL 위에 구축할 것을 권장해요. 그런 다음 아래 표시된 경로를 REST 리소스로 추가할 수 있어요.
리소스 프로비저닝
예시 프로비저닝 요청이에요.
POST https://logjam.io/flyio/extensions
{
name: "test",
id: "test",
organization_id: "04La2mblTaz",
organization_name: "High Flyers",
user_email: "v9WvKokd@customer.fly.io",
user_id: "NeBO2G0l0yJ6",
user_role: "admin",
primary_region: "mad",
ip_address: "fdaa:0:47fb:0:1::1d",
read_regions: ["syd", "scl"],
url: "https://logjam.io/flyio/extensions",
nonce: "e3f7b4c8f5ea016d0d2e92048ca0d856",
timestamp: 1685641159
}
매개변수
이 매개변수들은 모든 프로비저닝 요청과 함께 전송돼요.
| 이름 | 타입 | 설명 | 예시 |
|---|---|---|---|
| name | string | Fly.io 플랫폼 전체에서 고유한 확장 이름 | collider-prod |
| id | string | 확장을 나타내는 고유 ID | 30bqn49y2jh |
| organization_id | string | 조직을 나타내는 고유 ID | M03FclA4m |
| organization_name | string | 조직의 표시 이름 | Supercollider Inc |
| user_id | string | 프로비저닝 사용자를 나타내는 고유 ID | M03FclA4m |
| user_email | string | 프로비저닝 사용자에게 라우팅되는 난독화된 이메일 (변하지 않음) | n1l330mao@customer.fly.io |
| user_role | string | 프로비저닝 사용자의 역할 | admin, member |
선택 매개변수
이 매개변수들은 리전 인식이거나 Fly.io에 배포되는 확장에만 전송돼요.
이 매개변수들은 모든 프로비저닝 요청과 함께 전송돼요.
| 이름 | 타입 | 설명 | 예시 |
|---|---|---|---|
| primary_region | string | 대상 앱이 쓰기를 의도하는 세 글자 기본 Fly.io 리전 | mad |
| read_regions | array | 읽기 복제본이 프로비저닝되어야 하는 Fly.io 리전 코드 배열 | ["mad", "ord"] |
| ip_address | string | 이 확장에 할당된 고객 네트워크의 IPv6 주소 | fdaa:0:47fb:0:1::1d |
응답은 연결된 Fly.io 애플리케이션에 설정되어야 하는 시크릿의 키/값 쌍 목록을 포함해야 해요. 이 시크릿들은 환경 변수로 사용할 수 있어요. 애플리케이션 VM 밖에서는 보이지 않아요.
서비스가 Fly에 배포된다면, 응답은 프라이빗 IP 주소에 대한 대상 Fly.io 애플리케이션의 세부 사항도 포함해야 해요. 자세한 내용은 다음 섹션을 참고해요.
프로비저닝 응답
{
"id": "432cb1c9-4d06-4a91-95dc-bc7aa27b896d",
"config": {
"LOGJAM_URL": "https://user:password@test.logjam.io"
},
"name": "logjam-1bd03ba",
"fly_app_name": "logjam-production"
}
응답은 고유한 ID를 반환해야 해요. 여러분의 것일 수도, 우리가 보낸 것과 같은 것일 수도 있어요. 우리는 이 ID를 항상 여러분의 API에 요청할 때 사용해요. 또한 프로비저닝이 설정해야 하는 환경 변수를 담은 config 객체를 반환해야 해요.
선택적으로 다음을 보낼 수 있어요:
- 확장에 프로비저닝된 IP 주소에서 Flycast 트래픽의 대상으로서의
fly_app_name. - 고유성 제약이나 다른 이유로 우리가 제공한 이름을 바꿔야 한다면 확장 리소스의 업데이트된
name.
| 이름 | 타입 | 설명 | 예시 |
|---|---|---|---|
| id | string | 제공자 플랫폼에서 리소스의 고유 식별자 | 432cb1c9-4d06-4a91-95dc-bc7aa27b896d |
| fly_app_name | string | 내부 트래픽의 대상인 Fly.io 애플리케이션 이름 | logjam-production |
| name | string | 내부 트래픽의 대상인 Fly.io 애플리케이션 이름 | logjam-1bd03ba |
고객에게 서비스에 대한 프라이빗 접근 제공
Fly.io에 배포한다면 서비스는 공개 인터넷에 노출하지 않고 고객이 접근할 수 있어야 해요. 그렇게 할 수 없거나 원하지 않는 경우, 서비스는 기본적으로 인터넷 접근을 방지하는 네트워크 제한을 설정할 수 있어요.
Flycast로 프라이빗 트래픽 라우팅
우리의 Flycast 내부 로드 밸런싱 기능을 사용하면 고객 네트워크의 IPv6 주소에서 여러분의 Fly.io 애플리케이션 중 하나로 트래픽을 라우팅할 수 있어요.
위의 ip_address 필드는 이 기능을 가리켜요. 프로비저닝 시에 우리는 여러분을 위해 고객 네트워크의 주소를 할당해요. 그런 다음 여러분의 응답에는 그 IP 트래픽을 라우팅하고 싶은 대상 Fly.io 애플리케이션이 포함되어야 해요.
기본적으로 서비스가 구성될 때까지 앱으로 라우팅되는 트래픽은 없어요. 자세한 내용은 Machines API와 fly.toml 문서를 참고해요.
선택적으로 서비스에 프록시 프로토콜 핸들러를 추가할 수 있어요. 우리는 클라이언트 연결 시점에 고객 네트워크에 할당된 IP 주소를 서비스가 알 수 있도록 프록시 프로토콜을 이용해요. 이 IP는 사용자/비밀번호 자격증명이나 토큰 외에 추가 보안 검사를 수행하는 데 유용해요.
DNS 레코드
좋은 개발자 경험을 위해, 이전 섹션에서 언급한 Flycast IP 주소로 해석되는 내부 서비스의 공개 DNS 레코드를 만들어야 해요. 이 주소는 프로비저닝 응답의 구성 변수에 사용되어야 해요. 즉:
{
"config": {
"LOGJAM_URL": "https://user:password@logjam-instance-01.logjam.io"
},
"fly_app_name": "logjam-instance-01",
"id": "432cb1c9-4d06-4a91-95dc-bc7aa27b896d"
}
확장 상태 가져오기
대부분의 경우 특정 확장 리소스에 대한 정보를 가져와야 해요. 예를 들어 현재 status를 가져오거나 사용량 정보를 표시하는 것 등을 위해요.
또한 리소스를 동기적으로 프로비저닝할 수 없다면, 준비 상태를 폴링하기 위해 이러한 엔드포인트가 필요해요.
확장 업데이트
고객은 flyctl에서 직접 일부 확장을 업데이트할 수 있어야 해요. 예를 들어 데이터베이스에 읽기 복제본을 추가/제거하는 것 등이에요.
여러분의 API는 위의 POST 요청과 같은 매개변수를 사용한 업데이트를 다음에서 지원해야 해요:
PATCH {base_url}/extensions/{extension_id}
단일 로그인(SSO) 흐름
flyctl에서 직접 최대한 마찰 없이 사용자를 여러분의 플랫폼에 데려오고 싶어요. 예시:
flyctl ext logjam dashboard -a myapp
이 명령은 고객을 확장 상세 화면에서 여러분의 UI에 로그인시켜야 해요.
CLI가 사용자의 브라우저를 열고 GET 요청을 보내요.
GET {base_url}/extensions/{extension_id}/sso
사용자가 이미 로그인되어 있다면 확장 상세 화면으로 리다이렉트해야 해요. 아니라면 Fly.io에 대한 OAuth 인가 요청을 시작해 다음으로 리다이렉트해야 해요:
GET https://api.fly.io/oauth/authorize?client_id=123&response_type=code&redirect_uri=https://logjam.io/flyio/callback&scope=read
조직 ID와 원하는 권한 범위를 전달해야 해요. 현재 read 범위만 지원돼요.
사용자를 인증하면 OAuth redirect_uri로 인가 코드와 함께 리다이렉트하는데, 이 코드를 POST 요청으로 액세스 토큰과 교환할 수 있어요.
POST https://api.fly.io/oauth/token
client_id=logjam
client_secret=123
grant_type=authorization_code
code=myauthcode
redirect_uri=https://logjam.io/flyio/callback
JSON 응답:
{
"access_token"=>"fo1__034hk03k4mhjea0l4224hk",
"token_type"=>"Bearer",
"expires_in"=>7200,
"refresh_token"=>"j-elry40hpy05m2qbaptr",
"scope"=>"read",
"created_at"=>1683733170
}
마지막으로 이 액세스 토큰을 사용해 토큰 인트로스펙션 엔드포인트에 대한 인라인 요청을 발행해요. 응답은 다음을 할 수 있는 충분한 정보를 줘요:
- 사용자가 시스템에서 올바른 부모 조직에 속해 있다면 인가하기
- 사용자를 프로비저닝하고 이 조직에 추가하기
- Fly.io의
admin또는member역할에 매핑되는 역할 할당하기 - 이메일 별칭을 통해 사용자에게 이메일 보내기
GET https://api.fly.io/oauth/token/info
Authorization: Bearer fo1__0...24hk
JSON 응답:
{
"resource_owner_id"=>"rzjkdw3g0ypx061q",
"user_id"=>"rzjkdw3g0ypx061q",
"user_name": "Gabriella Fern",
"email": "5mrgw4xzkxoz02yk@customer.fly.io",
"organizations"=> [
{
id: "zd3e5wvkjel6pgqw",
role: "admin"
},
{
id: "zgyep87w1m4q06d4",
role: "member"
}
],
"scope"=>["read"],
"expires_in"=>7200,
"application"=>{"uid"=>"logjam"},
"created_at"=>1683740928
}
수신 웹훅: 확장 리소스 변경을 Fly.io에 알리기
제공자는 리소스에 변경이 있을 때 Fly.io에 웹훅을 보내야 해요. 예를 들면:
- 데이터베이스가
ready에서sleeping으로 전환될 때 - 데이터베이스가 제공자 UI를 통해 제거될 때
- 고객이 리소스 또는 조직의 결제 플랜을 바꿀 때
웹훅 URL과 형식
각 확장 제공자는 다음과 같은 자체 웹훅 URL을 받아요:
https://api.fly.io/api/hooks/extensions/logjam
이 엔드포인트는 application/json 타입의 POST 요청을 받아들여요.
웹훅 인증 및 요청 서명
웹훅 요청은 프로비저닝 요청을 서명하는 것과 같은 방식으로 서명되어야 하지만, 다른 공유 시크릿을 사용해요.
웹훅 요청 본문
요청에는 UNIX timestamp, action 문자열, resource 객체가 포함되어야 해요. 예시:
{
"timestamp": "1693513586",
"action": "resource.updated",
"resource": {
"plan": "scaler_pro",
"id": "5lgmabb3y30",
"status": "ready"
}
}
지원되는 액션은:
resource.updated
resource.deleted
resource.created
resource.created의 경우 요청 본문에 리소스를 프로비저닝한 Fly.io organization_id와 user_id가 포함되어야 해요.
{
"timestamp": "1693513586",
"action": "resource.created",
"resource": {
"plan": "scaler_pro",
"name": "myprod-db"
"organization_id": "kg032ljbmqs0j",
"user_id": "nh0kweyt23jyhl",
"id": "5lgmabb3y30",
"status": "ready"
}
}
참고: resource는 개별 리소스에 대한 GET 엔드포인트가 제공하는 것과 같은 매개변수를 포함해야 해요.
발신 웹훅: 프로비저닝된 계정과 리소스의 변경을 알림받기
우리는 시스템 변경에 대해 서비스에 알리려고 해요. 예를 들면:
- 조직에서 프로비저닝된 사용자의 추가 또는 제거
- 프로비저닝된 사용자 역할 변경
- 여러분의 배포에 영향을 주는 호스트 문제
- 개별 머신 이벤트 (중지, 시작, 크래시 등)
현재 우리는 단일 Fly.io 조직에서만 머신 이벤트를 보내요.
서비스가 Fly.io에 배포된다면, 중지와 시작 같은 머신 이벤트에 대한 웹훅을 각각의 원인과 근거에 대한 세부 사항과 함께 보낼 수 있어요.
이 웹훅은 CloudEvents 명세를 준수해요. 명세는 ID, source, type, timestamp 같은 공통 속성을 HTTP 헤더로 인코딩해요. 여기에서 원하는 SDK를 찾아보세요.
Type은 io.fly.machine.starting 같은 형식으로 인코딩돼요. data의 내용은 이벤트에 따라 달라져요. 이러한 타입과 데이터는 결국 문서화될 거예요. 지금으로서는 내용이 자명하지 않다면 건별로 우리에게 문의해 주세요.
위에서 언급한 필드를 뺀 샘플 페이로드예요.
POST https://logjam.io/flyio/extensions/events
{
"machine_id": "5683977ad31768",
"status": "failed",
"data": {
"Error": "image not found",
"Transition": "prepareImage"
}
}
고객과의 이메일 커뮤니케이션
Fly.io를 통해 일어나는 자동 가입 과정은 제공자 웹사이트를 통해 하는 가입보다 덜 의도적이에요. 그렇기 때문에 사용자가 의도한 목표(서비스 프로비저닝)와 무관한 메시지로 사용자를 압도하지 않도록 하고 싶어요.
다음은 건별로 논의할 수 있는 지침이에요.
제공자는 나중에 놀라움을 피하기 위해 원하는 이메일 주기(cadence)를 과정 초기에 Fly.io 고객과 논의해야 해요.
제공자는 새 사용자에게 이메일 확인을 요구하지 않아야 해요. 이메일 확인은 우리 플랫폼 사용의 요구사항이므로, 이메일은 미리 확인된 것으로 간주할 수 있어요.
제공자는 대부분 리소스 자체나 결제에 대한 트랜잭션 메시지를 보내야 해요. 예를 들어 데이터베이스 유휴화, 시스템 유지보수, 무료 티어 한도 초과에 대한 알림 등이에요.
제공자는 개발자가 중복 이메일을 받지 않도록 주의해야 해요. 예를 들어 사용자가 옵트아웃 이메일 캠페인에 배치되면 다음을 확인해야 해요:
같은 사용자가 둘 이상의 Fly.io 조직에서 리소스를 프로비저닝했다고 해서 동일한 이메일을 여러 번 받아서는 안 돼요. 조직이 생산과 스테이징 환경을 분리하는 데 사용되므로 이는 꽤 흔한 일이에요.
조직의 한 사용자가 이메일 캠페인에 배치된 후에는, 같은 조직의 다른 사용자는 단지 SSO 로그인을 사용했다는 이유만으로 비슷한 캠페인에 배치되어서는 안 돼요.
Fly.io에서 서비스 배포
데이터베이스 같은 지연 시간에 민감한 서비스를 운영한다면, 공식 확장으로 간주되려면 서비스를 Fly.io에서 실행해야 해요.
Fly.io 플랫폼에서 서비스 배포에 대해 extensions@fly.io로 문의해 주세요.
Fly.io를 통해 고객에게 청구
서비스는 비용을 가져와 고객에게 전달하기 위한 최소한의 결제 API를 제공해야 해요. 우리는 이 API를 하루에 최소 한 번 조회하고 싶어요. Fly.io는 서비스에 대해 고객에게 청구하고, 매출 공유를 뺀 뒤 지급해야 할 금액을 지불해요.
우리는 간단한 종량제(pay-as-you-go) 가격 모델을 제공할 수 있는 서비스를 선호해요.
또한 결제, 지원 플랜 등의 변경이 제공자 UI 내부에서 일어나는 것을 선호해요.
지원
Extensions의 지원 프로세스는 진화 중이며 건별로 논의되어야 해요.
제한 사항
Fly.io에서 확장을 실행하는 것을 고려할 때 다음을 기억해 두세요.
Extensions 프로그램은 하지 않는 것:
- 확장에 대한 결제 로직을 실행하지 않아요 — 편의를 위해 결제 데이터를 인보이스를 통해 전달해요.
- 밑바탕의 Fly.io 서비스를 넘어 여러분의 서비스를 관리하지 않아요.
- 서비스에 대한 지원을 실행하지 않아요 — 하지만 지원 요청은 여러분에게 전달해요.
- 서비스를 자동으로 배포하는 워크플로우를 실행하지 않아요 — 제어 플레인(control plane)이 필요해요.