GitHub Release Asset ๋‹ค์šด๋กœ๋“œ 403 ์ด์Šˆ


- name: Get Assets from GHR
  uri:
	url: "https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}"
	headers:
	  authorization: "Bearer {{ github_token }}"
	return_content: true
  register: response
 
- name: Download Asset from GHR
  get_url:
    url: "https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}"
	# ์œ„ ๋ผ์ธ์€ ์‚ฌ์‹ค url: "{{ response.json.assets[0].url }}"
    headers:
      authorization: "Bearer {{ github_token }}"
      accept: "application/octet-stream"
    dest: "/tmp/{{ asset_filename }}"
    mode: "0644"

EC2 Linux Base Image ๋ฒ ์ดํ‚น์„ ์œ„ํ•ด ์œ„์™€ ๊ฐ™์ด ๊ณตํ†ต ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜๋Š” ๊ณผ์ •์„ Ansible Playbook ์œผ๋กœ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

amazon-ebs.ths: fatal: [default]: FAILED! => {"changed": false, "dest": "/tmp/{asset_filename}", "elapsed": 0, "msg": "Request failed", "response": "HTTP Error 403: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.", "status_code": 403, "url": "https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}"}

์–ด๋А๋‚  ๊ฐ‘์ž‘์Šค๋Ÿฝ๊ฒŒ ์œ„์™€ ๊ฐ™์€ 403 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…ํ•˜๋Š” ๊ณผ์ •์„ ๊ธฐ๋กํ•˜๋ คํ•œ๋‹ค.

GitHub ์—์„œ Release Asset ์„ ๋‹ค์šด๋กœ๋“œํ•˜๋ ค๋ฉด ์šฐ์„  Get a release by tag name API ๋ฅผ ํ†ตํ•ด Asset ์„ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” API ๊ฒฝ๋กœ๋ฅผ JSON response ์—์„œ assets property ์— ๋ช…์‹œ๋œ url ์„ ํ†ตํ•ด ๋ฐ›์•„์™€์•ผํ•œ๋‹ค.

API ๊ฒฝ๋กœ๋Š” Get a release asset ํ˜•์‹์œผ๋กœ ์ฃผ์–ด์ง€๋Š”๋ฐ, ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ์ง์ ‘์ ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ๋ฐ›๊ธฐ ์œ„ํ•ด์„  Accept: application/octet-stream ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค. ์ด๋•Œ GitHub ๋Š” 200 ์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ง์ ‘ stream ์„ ์‹œ๋„ํ•  ์ˆ˜๋„ ์žˆ๊ณ , 302 ์„ ๋ฐ˜ํ™˜ํ•ด stream ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” storage ๊ฒฝ๋กœ๋กœ redirect ํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.

302 Redirect Response ์˜ Location ํ—ค๋”์— ํฌํ•จ๋œ ๊ฒฝ๋กœ๋Š” ๋ณดํ†ต pre-signed URL ๋กœ GitHub ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋‹ค์–‘ํ•œ storage backend ๋กœ ๋ถ€ํ„ฐ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๋‹ค์šด๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. (Related GitHub Discussion)

- name: Get Assets from GHR
  uri:
	url: "https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}"
	headers:
	  authorization: "Bearer {{ github_token }}"
	return_content: true
  register: response
 
- name: Get Asset metadata response
  uri:
    url: "https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}"
    # ์œ„ ๋ผ์ธ์€ ์‚ฌ์‹ค url: "{{ response.json.assets[0].url }}"
    headers:
      authorization: "Bearer {{ github_token }}"
      accept: "application/octet-stream"
    status_code: [200, 302]
    return_content: true
    follow_redirects: no
  register: asset_response
 
- name: Save binary stream to file (200 OK)
  copy:
    content: "{{ asset_response.content }}"
    dest: "/tmp/{{ asset_filename }}"
    mode: '0644'
  when: asset_response.status == 200
 
- name: Download binary from pre-signed URL (302 Redirect)
  get_url:
    url: "{{ asset_response.location }}"
    dest: "/tmp/{{ asset_filename }}"
    mode: '0644'
  when: asset_response.status == 302

์ฒ˜์Œ์—” Ansible ์˜ get_url ๋ชจ๋“ˆ์ด 302 Redirect ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•˜๋Š” ์ค„ ์•Œ์•˜๋‹ค. pre-signed URL ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ถ”๊ฐ€์ ์ธ ์ธ์ฆ์ด ํ•„์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Authorization Header ์™€ ์ถฉ๋Œ๋กœ ์ธํ•œ ์ด์Šˆ๋กœ ์ƒ๊ฐํ–ˆ๋‹ค. ๋•Œ๋ฌธ์— ์œ„์™€ ๊ฐ™์ด uri ๋ชจ๋“ˆ์„ ํ™œ์šฉํ•˜์—ฌ HTTP Response Status Code ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก Ansible Playbook ์„ ์ˆ˜์ •ํ–ˆ๊ณ , 302 Redirect ๋ฅผ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ 403 ์—๋Ÿฌ ์—†์ด ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋‹ค์šด๋ฐ›์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

curl -L \
	-H "Authorization: Bearer {{ github_token }}" \
	https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}
 
curl -v -L \
	-H "Authorization: Bearer {{ github_token }}" \
	-H "Accept: application/octet-stream" \
	-o {{ asset_filename }} \
	https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}

ํ†ต์‹ ๊ณผ์ •์„ ๋” ์ž์„ธํžˆ ๋“ค์—ฌ๋‹ค๋ณด๊ธฐ ์œ„ํ•ด ์œ„์™€ ๊ฐ™์ด curl command ๋ฅผ ํ†ตํ•ด API ๋ฅผ ํ˜ธ์ถœํ•œ ๊ฒฐ๊ณผ, curl command ๋Š” 302 Redirect ๋ฅผ ๋ฌธ์ œ์—†์ด ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ฐ”์ด๋„ˆ๋ฆฌ ์—ญ์‹œ ์„ฑ๊ณต์ ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๋•Œ๋ฌธ์— ๋”๋”์šฑ Ansible ์˜ get_url ๋ชจ๋“ˆ์„ ์˜์‹ฌํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ง์ ‘ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ์ฐพ์•„๋ณด๊ธฐ๋กœํ–ˆ๋‹ค. get_url ๋ชจ๋“ˆ์€ ๋‚ด๋ถ€์ ์œผ๋กœ urls ๋ชจ๋“ˆ์—์„œ ์ œ๊ณตํ•˜๋Š” fetch_url ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ณ , ์ด๋ฏธ ๋‚ด๋ถ€์—” Redirect ๋ฅผ handle ํ•˜๋Š” HTTPRedirectHandler ๊ฐ€ ํฌํ•จ๋˜์–ด์žˆ์—ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด Ansible ์˜ get_url ๋ชจ๋“ˆ์˜ ๋ฌธ์ œ๋Š” ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ๋˜์—ˆ๊ณ , 403 ์—๋Ÿฌ๋ฅผ reproduce ํ•˜๊ธฐ ์œ„ํ•ด Ansible Playbook ์„ ์žฌ์‹คํ–‰ํ•ด๋ณธ ๊ฒฐ๊ณผ ์ด๋ฒˆ์—” ๋ฌธ์ œ์—†์ด ๋ฐ”์ด๋„ˆ๋ฆฌ๊ฐ€ ๋‹ค์šด๋กœ๋“œ ๋˜์—ˆ๋‹ค.

๊ฐ„ํ—์ ์œผ๋กœ 403 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด ์˜์•„ํ•ด์„œ ์‹คํŒจํ–ˆ๋˜ ์‹คํ–‰๊ณผ ์„ฑ๊ณตํ–ˆ๋˜ ์‹คํ–‰์˜ Response ๋ฅผ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ, ์‹คํŒจํ•œ 302 Redirect ์˜ Location Header ๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ objects.githubusercontent.com ์—์„œ ๋ฐ›์•„์˜ค๊ณ  ์žˆ์—ˆ๊ณ , ์„ฑ๊ณตํ•œ ๊ฒƒ์€ release-assets.githubusercontent.com ์—์„œ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๋ฐ›์•„์˜ค๊ณ  ์žˆ์—ˆ๋‹ค.

์•„๋ฌด๋ž˜๋„ GitHub ๊ฐ€ ๋‹ค์–‘ํ•œ storage backend ๋กœ ๋ถ€ํ„ฐ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ streaming ํ•˜๊ธฐ ๋•Œ๋ฌธ์— objects.githubusercontent.com ์„œ๋ฒ„์—์„œ ๋ญ”๊ฐ€ ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๊ฑฐ๋‚˜ ์ธ์ฆ ์ •์ฑ…์ด ๋‹ฌ๋ž๋˜ ๋ชจ์–‘์ด๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋ก  302 Redirect ๋Œ€์ƒ ๋„๋ฉ”์ธ์— ๋”ฐ๋ผ ๊ฐ„ํ—์ ์œผ๋กœ 403 ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ, uri ๋ชจ๋“ˆ์„ ํ†ตํ•ด ์ง์ ‘ 302 Location Header ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ดํ›„์— get_url ๋ชจ๋“ˆ์„ ํ†ตํ•ด ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๋‹ค์šด๋กœ๋“œ๋ฐ›๋Š” ๋ฐฉ์‹์œผ๋กœ 2๋‹จ๊ณ„์— ๊ฑฐ์ณ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์•ˆ์ „ํ•ด๋ณด์ธ๋‹ค.

References