<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yogae 블로그</title>
    <link>https://yogae.tistory.com/</link>
    <description>개발을 하면서 공부한 자료를 수집하고 정리한다.
일상에서 느끼는 것과 책을 읽고 생각나는 것을 기록한다.</description>
    <language>ko</language>
    <pubDate>Fri, 26 Jun 2026 16:48:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yogae</managingEditor>
    <image>
      <title>yogae 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3611171/attach/7851653c2ed447a6a9b8f8dfff03a46c</url>
      <link>https://yogae.tistory.com</link>
    </image>
    <item>
      <title>Elasticsearch 비용 절약하기</title>
      <link>https://yogae.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아무것도 모르는 상태라서 Elasitcsearch Cloud를 사용했지만 규모가 커지면서 비용이 많이 발생하기 시작했습니다. 규모가 커지면서 비용문제가 지속되었고 비용을 줄이기 위해 조치했던 내용을 정리해보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Elasticsearch Cloud에서 AWS Self Managed로 Migration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;처음 Elasticsearch를 구성할 때는 Cloud를 사용하여 구성했습니다.&lt;span&gt; 빠르게 production에 적용을 진행해야해서 Elasticsearch Cloud를 사용하여 운영을 시작했습니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Elasticsearch&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Cloud를 사용할 때는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Elasticsearch&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Cloud에서 자동으로 진행되는 것들이 많았습니다. version upgrade나 설정 변경 시 자동으로 rolling update를 진행하여 클릭만 하면 되었습니다. AWS Self Managed로 운영하면서 이렇게 자동으로 진행되는 것을 하나하나 수동으로 진행해야한다는 사실을 알게되었습니다. Elasticsearch를 운영하면서 시간이 오래 걸리는 작업이 상당히 많다는 것을 알게되었습니다. 가능하다면 운영에 대한 준비를 많이 해야 시간을 절약할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많아지면서 비용이 정말 빠르게 증가했고 Elasticsearch 운영 비용을 절약하기 위해 AWS에 EC2위에 Elasticsearch Cluster 구성을 진행했습니다. 데이터 Migration을 진행은 Elasticsearch Snapshot을 활용하여 Migration을 진행했습니다. Migration을 진행할때 CCR, CCS를 활용하는 방법도 확인해보았지만 License가 필요한 부분이 있어서 사용하지 못했고 Snapshot만을 활용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; snapshot을 활용한 migration 진행 시 꼭 호환되는 Elasticsearch version을 확인해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html#snapshot-index-compatibility&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html#snapshot-index-compatibility&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Instance Type 선택 시 비용 고려사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch Cluster를 AWS EC2로 Migration을 진행하면서 가장 고민했던 부분은 AWS EC2 Type을 선택하는 것이었습니다. EC2를 기본적으로 선택하면 &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EBS 최적화 인스턴스를 사용할 것입니다. &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EBS 최적화 인스턴스 선택하기 전에 EC2 Instance Storage를 지원하는 instance type을 사용하는 것을 고려해봐야한다. &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EC2 Instance Storage는 EC2 인스턴스에 직접 연결된 스토리지입니다. DB와 같은 장비를 구성할 때 IOS를 많이 사용하는 경우가 많지만 EBS를 사용하는 경우 IOS 추가적으로 구성하면 비용이 발생합니다. &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EC2 Instance Storage를 사용하는 매우 빠른 I/O 성능을 제공하여 IOS에 대한 추가적인 비용 발생을 고려하지 않아도 됩니다. 단점은 instace stop시 데이터가 사라진다는 것입니다. &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EC2 Instance Storage를 사용하는 경우 장애상황을 대비하여 HA를 구성해야하며, 가능하다면 추가적인 replica shard를 구성하여 운영하는 해야합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;2개의 tier로 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;Lifecycle 정책을 분리하여 관리하고 있습니다. tier에 따라서 EC2 Instance Storage를 선택하여 사용하고 있습니다. Hot tier의 경우 indexing을 담당하고 가장 많은 access를 보장하도록 구성했습니다. SSD &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EC2 Instance Storage인 i4i type을 선택하여 hot tier를 구성했습니다. warm는 과거 데이터를 저장하여 hot tier에 비해 access가 적었고 HDD &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;EC2 Instance Storage인 d2 type을 사용하여 구성했습니다. &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;i4i 장비는 2년 동안 hardware retirement event가 한 번도 발생하지 않았습니다. 하지만 d2 type의 장비의 경우 1년에 2번에서 3번은 발생하는 것으로 보입니다. ap-northeast-2에서는 &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;d2 type이 부족한 현상이 발생하여서 &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;d2 type 선택 시 주의해야합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;Instance Type 선택 시 추가적인 고려해야할 사항들은 아래 정리해보았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292e33; text-align: start;&quot;&gt;* he exact number of shards per 1 GB of memory depends on the use case, with the best practice of 1 GB of memory for every 20 shards on disk.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292e33; text-align: start;&quot;&gt;* &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: left;&quot;&gt;There are no hard limits on shard size, but experience shows that shards&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;between 10GB and 50GB&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;typically work well for logs and time series data. You may be able to use larger shards depending on your network and use case.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292e33; text-align: start;&quot;&gt;* &lt;b&gt;적당한 Heap메모리&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;는 전체 메모리의 절반 정도 수준에서 전체 메모리가 크다면 32GB보다 작은 값으로 설정합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Data Transfer 비용도 고려해야합니다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 Elasticsearch Cluster를 운영하면서 데이터가 수백 TB가 넘어가면서 Data Transfer 비용이 무시 못할 비용이 발생하게 되었습니다. AWS에서는 동일한 리전 내 동일 가용 영역(AZ) 내에서의 데이터 전송은 무료이지만 같은 region이라도 다른 가용 영역(AZ) data가 이동하면 비용이 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGlNEX/btsJGKrEzcV/18kAOc10eeGn8jvxENxoD1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGlNEX/btsJGKrEzcV/18kAOc10eeGn8jvxENxoD1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGlNEX/btsJGKrEzcV/18kAOc10eeGn8jvxENxoD1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGlNEX%2FbtsJGKrEzcV%2F18kAOc10eeGn8jvxENxoD1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;640&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch Cluster를 같은 AZ에 구성할 수 없어서 여러 AZ에 걸쳐서 shard를 배치시킵니다. Search query 요청이 들어오면 Elasticsearch의 동작 방식이 분산되어 있는 데이터를 찾기 위해서는 여러 node에 데이터를 query하고 해당 데이터를 반환하도록 구성되어 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726753729752&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Elasticsearch&amp;rsquo;s Distributed Search: Query and Fetch Phases&quot; data-og-description=&quot;To understand Elasticsearch&amp;rsquo;s distributed search, let&amp;rsquo;s take a moment to understand how querying and fetching work. Unlike simple CRUD&amp;hellip;&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&quot; data-og-url=&quot;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pTMfS/hyW6EdqXci/Iw3rxlX6n4gJyDaujpY311/img.jpg?width=1000&amp;amp;height=667&amp;amp;face=0_0_1000_667,https://scrap.kakaocdn.net/dn/bDiThK/hyW6Cta0J4/LUgehn6i5dHsq3xndDleKk/img.jpg?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/bhCtIP/hyW2RldK2X/IfkSAFqv9KlygeHh6jC0S1/img.jpg?width=1000&amp;amp;height=667&amp;amp;face=0_0_1000_667&quot;&gt;&lt;a href=&quot;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@musabdogan/elasticsearchs-distributed-search-query-and-fetch-phases-df869d35f4b3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pTMfS/hyW6EdqXci/Iw3rxlX6n4gJyDaujpY311/img.jpg?width=1000&amp;amp;height=667&amp;amp;face=0_0_1000_667,https://scrap.kakaocdn.net/dn/bDiThK/hyW6Cta0J4/LUgehn6i5dHsq3xndDleKk/img.jpg?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/bhCtIP/hyW2RldK2X/IfkSAFqv9KlygeHh6jC0S1/img.jpg?width=1000&amp;amp;height=667&amp;amp;face=0_0_1000_667');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Elasticsearch&amp;rsquo;s Distributed Search: Query and Fetch Phases&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;To understand Elasticsearch&amp;rsquo;s distributed search, let&amp;rsquo;s take a moment to understand how querying and fetching work. Unlike simple CRUD&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Data Transfer를 줄이기 위해서는 한 번에 search query에 참여하는 shard의 개수를 줄일 수 있도록 구성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Data Transfer간 전송되는 데이터 사이즈를 줄이기 위해서 compression 설정을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Search 요청 시 _source를 지정하여 필요한 데이터만 반환하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. threshold rebalancing을 줄입니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>develop</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/50</guid>
      <comments>https://yogae.tistory.com/50#entry50comment</comments>
      <pubDate>Thu, 19 Sep 2024 23:02:17 +0900</pubDate>
    </item>
    <item>
      <title>Github Actions 관리하기(Organization secrets, Reusable workflows)</title>
      <link>https://yogae.tistory.com/48</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;code-5290465_1920.jpg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcEOmN/btrGfPc1HiK/i78pcvK5LwKjPdTTEaKdq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcEOmN/btrGfPc1HiK/i78pcvK5LwKjPdTTEaKdq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcEOmN/btrGfPc1HiK/i78pcvK5LwKjPdTTEaKdq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcEOmN%2FbtrGfPc1HiK%2Fi78pcvK5LwKjPdTTEaKdq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1280&quot; data-filename=&quot;code-5290465_1920.jpg&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 service를 만들때마다 workflow yml 파일을 복사 붙여넣기하여 사용하고 있었다. reusing workflows를 사용하여 workflow를 재사용할 수 있도록 구성했다. workflow를 구성하면서 손이가는 작업이 github actions의 secret를 설정하는  작업이었다. 이부분 또한 organization secret을 사용하여 정리해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✏️ Organization secrets&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gihub Organization을 사용하고 있다면 Organization 안에서  사용할 수 있는 github actions secrets을 설정할 수 있다. Organization secrets을 사용하지 않았을 때는 repository별로 secrets를 설정해야했다. repository별로  다른 secrects를 설정해야한다면 문제 없겠지만 모든 repository에서  같은 secrets을 사용하는 경우가 많았다. repository를 새로 생성해야하면 secrets를 매번 새롭게 설정해야했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Organization의 admin만이 organization secrets을 설정할 수 있다. admin으로 접속하여 organization page에서 settings -&amp;gt; actions -&amp;gt; secrets으로 이동하여 organization secrets을 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HjJCg/btrGdtva8ae/bjTSgR4DrcvsYKqxgNg9EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HjJCg/btrGdtva8ae/bjTSgR4DrcvsYKqxgNg9EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HjJCg/btrGdtva8ae/bjTSgR4DrcvsYKqxgNg9EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHjJCg%2FbtrGdtva8ae%2FbjTSgR4DrcvsYKqxgNg9EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;418&quot; height=&quot;380&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;organization secrets은 repository별로 설정이 가능하다. repository를 다양한 사용자가 생성하고 사용하고 있어서 특정 repository에만 secrets에 접근할 수 있는 권한을 부여했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;organization secrets은 개별 repository에서 설정한 secrects과 동일한 방법으로 접근이 가능하다. &lt;i&gt;ORG_ &lt;/i&gt;prefix를 설정하여 개별 repository secrets과 분리했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1656664035471&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
jobs:
    deploy:
        runs-on: ubuntu-latest
        steps:
        	...
            - name: deploy
              run: |
                  yarn deploy
              env:
                  npm_token: ${{ secrets.ORG_NPM_TOKEN }}
            	  aws_access_key_id: ${{ secrets.ORG_ACTION_ACCESS_KEY_ID }}
                  aws_secret_access_key: ${{ secrets.ORG_ACTION_SECRET_ACCESS_KEY }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secrets에 문제가 있거나 주기적으로 key를 변경해야하는 경우 한 번의 설정으로 여러 repository에 변경사항을 반영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⭐️ Reusable workflows&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;reusing workflows 공식문서&lt;/a&gt; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reusing workflows의 yml은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1656492655810&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: node deploy reusable workflow

on:
    workflow_call:
        inputs:
            project_name:
                required: true
                type: string
            stage:
            	default: dev
                required: false
                type: string
        secrets:
            npm_token:
                required: true
            aws_access_key_id:
                required: true
            aws_secret_access_key:
                required: true
jobs:
    deploy:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3
            - name: Setup Node.js
              uses: actions/setup-node@v3
              with:
                  node-version-file: &quot;.nvmrc&quot;
                  cache: &quot;yarn&quot;
                  registry-url: &quot;https://registry.npmjs.org&quot;
            - name: Get yarn cache directory path
              id: yarn-cache-dir-path
              run: echo &quot;::set-output name=dir::$(yarn cache dir)&quot;
            - name: Cache node modules
              uses: actions/cache@v3
              with:
                  path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
                  key: ${{ runner.os }}-yarn-${{ env.CACHE_NAME }}-${{ env.STAGE }}-${{ hashFiles('**/yarn.lock') }}
                  restore-keys: ${{ runner.os }}-yarn-${{ env.CACHE_NAME }}-${{ env.STAGE }}-
              env:
                  CACHE_NAME: ${{ inputs.project_name }}
                  STAGE: ${{ inputs.stage }}
            - name: yarn install local dependency
              run: yarn install
              env:
                  NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
            - name: deploy
              run: |
                  yarn deploy
              env:
                  NODE_ENV: ${{ inputs.stage }}
                  DEPLOY_ALIAS: ${{ inputs.deploy_alias }}
                  AWS_ACCESS_KEY_ID: ${{ secrets.aws_access_key_id }}
                  AWS_SECRET_ACCESS_KEY: ${{ secrets.aws_secret_access_key }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reusing workflows로 값을 전달할 수 있다. 암호화가 필요하지 않은 inputs과 암호화가 필요한 secret이 존재한다. jobs는 기존의 workflow를 사용하는 방법과 동일하게 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  reusing workflows 제한사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- reusing workflows는 다른 reusing workflows를 호출할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- reusing workflows을 private repository에 저장한다면 같은 repository안에서만 reusing workflows를 호출하여 사용하야한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✏️ Reusing workflows 호출하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reusing workflows 설정 시 private repository 사용한다면 주의해야하는 제한은 같은 repository에 Reusable workflows를 저장하여 사용해야한다는 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Reusable workflows stored within a private repository can only be used by workflows within the same repository.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 repository에서 사용할 수 있도록 구성해야 했기에 private repository로 설정할 수 없었고 public repository에 &lt;span&gt;reusing workflows를 저장해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;private repository에서 reusing workflows&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reusing workflows를 private repository에 저장하는 경우 같은 repository안에 있는 reusing workflows만 호출이 가능하다. 같은 repository에 있는 경우 아래와 같이 상대경로를 통해 reusing workflows를 호출할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1656493527855&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: test
on:
    pull_request:
        branches:
            - develop
jobs:
    node-deploy:
        uses: ./.github/workflows/{filename}@master
        with:
            project_name: test-project
            stage: test
        secrets:
            npm_token: ${{ secrets.ORG_NPM_TOKEN }}
            aws_access_key_id: ${{ secrets.ORG_ACTION_ACCESS_KEY_ID }}
            aws_secret_access_key: ${{ secrets.ORG_ACTION_SECRET_ACCESS_KEY }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reusing workflows를 &lt;span&gt;private repository에 생성하면 같은 respository에서만 호출하여 사용할 수 있어서 여러 repository를 관리해야한다면 public으로 변경하여 아래와 같은 방식의 설정이 필요한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;public repository에서 reusing workflows&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 repository에 저장되어 있는 reusing workflows를 호출하고자 한다면 {owner}/{repo}/.github/workflows/{filename}@{ref} 방식으로 호출할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1656493356811&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: test
on:
    pull_request:
        branches:
            - develop
jobs:
    node-deploy:
        uses: {owner}/{repo}/.github/workflows/{filename}@{ref}
        with:
            project_name: test-project
            stage: test
        secrets:
            npm_token: ${{ secrets.ORG_NPM_TOKEN }}
            aws_access_key_id: ${{ secrets.ORG_ACTION_ACCESS_KEY_ID }}
            aws_secret_access_key: ${{ secrets.ORG_ACTION_SECRET_ACCESS_KEY }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img style=&quot;text-align: center; caret-color: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dn/bLZSZ0/btrF4wrKeAo/Kne2c3qthLX5vQQSLMYra1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;704&quot; data-filename=&quot;스크린샷 2022-06-29 오후 3.36.56.png&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>develop</category>
      <category>Github Actions</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/48</guid>
      <comments>https://yogae.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 29 Jun 2022 18:09:42 +0900</pubDate>
    </item>
    <item>
      <title>lambda@edge를 사용한 요청 header 처리(user-agent, accept-language)</title>
      <link>https://yogae.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accept-language에 따라 다른 언어의 html을 보여주어야했습니다. accept-language header를 cloudfront whitelist에 추가하여 origin에 header를 전달하도록 구성했습니다. 하지만 accept-language header의 형태가 다양하여 cloudfront cache의 효율이 좋지 않았고 지원하지 않는 언어가 들어오는 경우 default language 설정이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user-agent header를 parsing하여 IE 브라우저로 접속 시 redirect 처리하고 있습니다. user-agent header 또한 cloudfront whitelist에 추가하여 origin으로 header를 전달하고 있었지만 cache 효율이 좋지 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client 요청 시 header에 관련된 처리를 하는 &lt;code&gt;lambda@edge&lt;/code&gt;를 생성하여 cache 효율을 높이고자 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lambda@Edge&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;545&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HQbbX/btrp0aqSoZi/Kq1UHcphEELerQtvYlzN0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HQbbX/btrp0aqSoZi/Kq1UHcphEELerQtvYlzN0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HQbbX/btrp0aqSoZi/Kq1UHcphEELerQtvYlzN0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHQbbX%2Fbtrp0aqSoZi%2FKq1UHcphEELerQtvYlzN0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;545&quot; height=&quot;194&quot; data-origin-width=&quot;545&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda@edge 함수는 viewer request, viewer response, origin request, origin response 중 선택하여 동작할 수 있습니다. 모든 요청에 대한 cache key를 변경하려면 viewer request를 사용해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-how-to-choose-event.html&quot;&gt;Lambda 함수를 트리거하는 데 사용할 CloudFront 이벤트를 결정하는 방법&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lambda@Edge 제한사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda@edge는 기본 lamdba function을 사용하는 것보다 많은 제약사항이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;$LATEST&lt;/code&gt; 또는 별칭이 아니라 번호가 매겨진 Lambda 함수 버전을 사용해야합니다.&lt;/li&gt;
&lt;li&gt;Lambda 함수는 미국 동부 리전에 있아야 합니다.&lt;/li&gt;
&lt;li&gt;Viewer request와 viewer response의 timetout 제한 5초, Origin request와 origin response timeout 제한 30초 입니다.&lt;/li&gt;
&lt;li&gt;Viewer request와 viewer response의 package size 제한 1MB, Origin request와 origin response의 package size 제한 50MB 제한이 있습니다.&lt;/li&gt;
&lt;li&gt;환경변수를 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;header parsing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 시 lambda@edge의 1MB 제한사항 때문에 module 선택 시 size를 꼭 확인해야합니다. accept-language header를 &lt;code&gt;accept-language-parser&lt;/code&gt; npm module을 사용하여 정형화 했습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { pick } from &quot;accept-language-parser&quot;;

export class AcceptLanguageParser {
    private supportedLanguages: string[];
    private defaultLanguage: string;
    constructor(supportedLanguages: string[], defaultLanguage: string) {
        this.supportedLanguages = supportedLanguages;
        this.defaultLanguage = defaultLanguage;
    }

    public pickLanguage(acceptLanguage: string): string {
        const lang = pick(this.supportedLanguages, acceptLanguage, {
            loose: true,
        });
        if (lang) return lang;
        return this.defaultLanguage;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const acceptLanguageParser = new AcceptLanguageParser([&quot;en&quot;, &quot;ko&quot;], &quot;ko&quot;);
const lang1 = acceptLanguageParser.pickLanguage(&quot;en-GB,en;q=0.8&quot;); // en
const lang2 = acceptLanguageParser.pickLanguage(&quot;fr-CA', 'fr-FR', 'fr&quot;); // ko&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정형화한 값을 cloudfront origin으로 전달할 때는 querystring으로 전달하도록 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { parse, stringify } from &quot;querystring&quot;;
import * as ua from &quot;useragent&quot;;

const acceptLanguageParser = new AcceptLanguageParser([&quot;en&quot;, &quot;ko&quot;], &quot;ko&quot;);

export function handler(
    event: CloudFrontRequestEvent,
    context: Context,
    callback: Callback
) {
    const request = Records[0].cf.request;
      let userAgent;
      if (
        request.headers[&quot;accept-language&quot;] &amp;amp;&amp;amp;
        request.headers[&quot;accept-language&quot;][0]
    ) {
        const acceptLanguage = request.headers[&quot;accept-language&quot;][0].value;
        delete request.headers[&quot;accept-language&quot;]; // cloudfront cache 생성 시 accept-language header에 따라 cache가 생성되는 것을 방지하기 위해 삭제
          const lang = acceptLanguageParser.pick(acceptLanguage)
        request.querystring = stringify({ lang });
      }

      if (request.headers[&quot;user-agent&quot;] &amp;amp;&amp;amp; request.headers[&quot;user-agent&quot;][0]) {
        userAgent = request.headers[&quot;user-agent&quot;][0].value;
        delete request.headers[&quot;user-agent&quot;];  // cloudfront cache 생성 시 user-agent header에 따라 cache가 생성되는 것을 방지하기 위해 삭제              
    }

      if(ua.is(userAgent).ie) callback(null, {
            status: &quot;301&quot;,
            statusDescription: &quot;Permanently Moved&quot;,
            headers: {
                location: [
                    {
                        key: &quot;Location&quot;,
                        value: &quot;{{url}}&quot;,
                    },
                ],
            },
        }); // ie 브라우저로 접근 시 redirect
     else callback(null, request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 스크립트 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 스크립트는 node.js code를 작성하고 작성한 code를 packaging하여 s3에 upload하는 것까지 작성했습니다. s3에 올라간 package는 terraform으로 lambda@edge, cloudfront 설정을 했습니다. 아래 코드는 s3에 upload하는 shell 스크립트입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;    npm install &amp;amp;&amp;amp; tsc
    npm pack --json | jq '.[0].filename' | xargs -I {} mv {} &quot;$OUTPUT_FILENAME&quot;

    mkdir &quot;$TMP_FOLDER&quot;
    mv &quot;$OUTPUT_FILENAME&quot; &quot;$TMP_FOLDER&quot;/&quot;$OUTPUT_FILENAME&quot;

    cd $TMP_FOLDER
    tar -xvzf &quot;$OUTPUT_FILENAME&quot;
    cd package &amp;amp;&amp;amp; zip -r &quot;../$OUTPUT_ZIP&quot; . &amp;amp;&amp;amp; cd ..
    ENC_METADATA=`openssl dgst -sha256 -binary $OUTPUT_ZIP | openssl enc -base64`
    aws s3 cp ./$OUTPUT_ZIP s3://{{s3 upload 위치}} --metadata sha256=$ENC_METADATA&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 시 어려웠던 점은 node.js application package하는 것과 새로운 package를 s3 upload 시에만 terraform으 변경사항을 감지하고 배포할 수 있도록 구성하는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. node.js application package&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda@edge의 1MB 제한사항 때문에 devdependency가 들어가지 않도록 해야합니다. 이를 위해 &lt;code&gt;package.json&lt;/code&gt;에 &lt;code&gt;bundleDependencies&lt;/code&gt;를 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
 ...
    &quot;dependencies&quot;: {
        &quot;accept-language-parser&quot;: &quot;^1.5.0&quot;,
        &quot;useragent&quot;: &quot;^2.3.0&quot;
    },
    &quot;bundleDependencies&quot;: [
        &quot;accept-language-parser&quot;,
        &quot;useragent&quot;
    ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;bundleDependencies&lt;/code&gt;를 사용하면 &lt;code&gt;npm pack&lt;/code&gt; 실행 시 &lt;code&gt;bundleDependencies&lt;/code&gt;에 추가되어 있는 module만 &lt;code&gt;node_modules&lt;/code&gt;에 추가하게 됩니다. 기존에는 yarn을 사용했지만 &lt;code&gt;bundleDependencies&lt;/code&gt;설정이 &lt;code&gt;yarn pack&lt;/code&gt;으로는 제대로 실행되지 않는 것을 확인되어 &lt;code&gt;npm pack&lt;/code&gt;으로 실행하는 스크립트를 작성했습니다. &lt;code&gt;.npmignore&lt;/code&gt;를 사용하여 ts 파일과 log file등 packaging에 필요 없는 file을 제거 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;npm pack&lt;/code&gt;실행의 결과물은 &lt;code&gt;*.tgz&lt;/code&gt;입니다. tgz를 그대로 s3 upload할 수 없습니다. lambda에서 사용하는 file은 zip 형식이므로 tgz를 zip으로 변환해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 새로운 package를 s3 upload 시에만 terraform 변경사항 감지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform에서는 변경사항이 있을 때만 배포되기 때문에 metadata를 사용하여 &lt;code&gt;aws_lambda_function&lt;/code&gt;의 &lt;code&gt;source_code_hash&lt;/code&gt;에 맞는 방식으로 encode한 hash 값을 설정해야 했습니다. &lt;code&gt;aws_lambda_function&lt;/code&gt;의 &lt;code&gt;source_code_hash&lt;/code&gt;에 잘못된 값을 넣으면 terraform에서 apply 때마다 변경사항이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;source_code_hash&lt;/code&gt;을 사용하는 방식 이외에도 s3 object를 versioning할 수 있도록 설정하여 version id를 &lt;code&gt;aws_lambda_function&lt;/code&gt;에 설정하는 방식도 있습니다. s3 object를 versioning하는 방식을 사용하지 않고 있어 &lt;code&gt;source_code_hash&lt;/code&gt;를 사용하는 방식으로 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;data &quot;aws_s3_bucket_object&quot; &quot;header_parsing_lambda_edge_artifact&quot; {
  bucket = &quot;${node package를 upload한 s3 bucket}&quot;
  key    = &quot;${node package를 upload한 s3 key}&quot;
}

data &quot;aws_iam_policy_document&quot; &quot;assume_role_policy_doc&quot; {
  statement {
    sid    = &quot;AllowAwsToAssumeRole&quot;
    effect = &quot;Allow&quot;

    actions = [&quot;sts:AssumeRole&quot;]

    principals {
      type = &quot;Service&quot;

      identifiers = [
        &quot;edgelambda.amazonaws.com&quot;,
        &quot;lambda.amazonaws.com&quot;,
      ]
    }
  }
}

resource &quot;aws_iam_role&quot; &quot;lambda_at_edge&quot; {
  name               = &quot;lambda-edge-role&quot;
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy_doc.json
}

data &quot;aws_iam_policy_document&quot; &quot;lambda_logs_policy_doc&quot; {
  statement {
    effect    = &quot;Allow&quot;
    resources = [&quot;*&quot;]
    actions = [
      &quot;logs:CreateLogStream&quot;,
      &quot;logs:PutLogEvents&quot;,
      &quot;logs:CreateLogGroup&quot;,
    ]
  }
}

resource &quot;aws_iam_role_policy&quot; &quot;logs_role_policy&quot; {
  name   = &quot;log-policy&quot;
  role   = aws_iam_role.lambda_at_edge.id
  policy = data.aws_iam_policy_document.lambda_logs_policy_doc.json
}

resource &quot;aws_lambda_function&quot; &quot;header_parsing_lambda_edge&quot; {
  function_name = &quot;lambda-edge&quot;

  # Find the file from S3
  s3_bucket         = &quot;${node package를 upload한 s3 bucket}&quot;
  s3_key            = &quot;${node package를 upload한 s3 key}&quot;
  source_code_hash  = chomp(data.aws_s3_bucket_object.header_parsing_lambda_edge_artifact.metadata[&quot;Sha256&quot;])
  provider          = aws.aws_cloudfront

  publish = true
  handler = &quot;dist/app.handler&quot;
  runtime = &quot;nodejs14.x&quot;
  role    = aws_iam_role.lambda_at_edge.arn
}

resource &quot;aws_cloudfront_distribution&quot; &quot;static_distribution&quot; {
    ...

    ordered_cache_behavior {
        lambda_function_association {
            event_type = &quot;viewer-request&quot;
            include_body = false
            lambda_arn = aws_lambda_function.header_parsing_lambda_edge.qualified_arn
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cloudfront를 적극적으로 사용하고 있습니다. 하지만 &lt;code&gt;accept-language&lt;/code&gt;, &lt;code&gt;user-agent&lt;/code&gt;와 같이 형태가 다양한 header를 cloudfront origin으로 전달할 경우 cache hit 효율이 떨어집니다. &lt;code&gt;lambda@edge&lt;/code&gt;를 사용하는 방법이 이러한 문제를 해결할 수 있는 방법이 될 수 있습니다. 하지만 &lt;code&gt;lambda@edge&lt;/code&gt; 도입에 장벽으로 느껴지는 부분이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 장벽은 &lt;code&gt;lambda@edge&lt;/code&gt;의 제약사항이 많다는 것이었습니다. lambda packaging 시 size 제한으로 code 개발 시 항상 사용하는 module을 전부 제거해야만 했습니다. lambda timeout 제한이 있어 외부 요청이나 db 연결 시 timeout이 발생하지 않도록 주의해야 합니다. 이러한 제한 사항으로 설계시 어려움이 많이 느껴졌습니다. 두번째 장벽은 기존에 사용하는 lambda 배포 pipeline을 같이 사용할 수 없다는 것이었습니다. 기존에 serverless framework로 lambda를 관리하고 lambda 이외의 infra는 terraform으로 관리하고 있었습니다. cloudfront를 terraform으로 관리하고 있었고 serverless framework로 배포한 &lt;code&gt;lambda@edge&lt;/code&gt;를 terraform에 연결하는 작업이 어려움이 있다고 판단되었습니다. 기존에 사용하는 serverless framework배포 방식을 선택하지 않고 terraform으로만 관리하는 방법으로 새롭게 구성해야했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lambda@edge&lt;/code&gt;을 도입하면서 cloudfront origin으로 설정한 application에서 header를 처리하는 로직을 제거할 수 있었습니다. header를 처리하는 로직을 분리하여 application에는 core 로직에만 중집할 수 있게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://gavinlewis.medium.com/localizing-content-with-lambda-edge-fefb12aa6199&quot;&gt;https://gavinlewis.medium.com/localizing-content-with-lambda-edge-fefb12aa6199&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-how-to-choose-event.html&quot;&gt;lambda 함수를 트리거하는 데 사용할 cloudfornt 이벤트를 결정하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Bramzor/lambda-edge-locale&quot;&gt;https://github.com/Bramzor/lambda-edge-locale&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>develop</category>
      <category>aws</category>
      <category>Cloudfront</category>
      <category>lambda@edge</category>
      <category>Terraform</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/43</guid>
      <comments>https://yogae.tistory.com/43#entry43comment</comments>
      <pubDate>Wed, 5 Jan 2022 17:39:14 +0900</pubDate>
    </item>
    <item>
      <title>python 절대경로 / 상대경로</title>
      <link>https://yogae.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;unittest code를 작성하고 test code에서 module을 import할 때 너무나 많은 error가 발생했습니다. module을 찾을 수 없는 error와 &lt;code&gt;attempted relative import with no known parent package&lt;/code&gt; error 등의 error가 발생했습니다. 이러한 error가 발생한 원인을 정확하게 파악하려 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;__name__&lt;/code&gt;의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python의 &lt;code&gt;__name__&lt;/code&gt;은 모듈이 저장되는 변수이며 import로 모듈을 가져왔을 때 모듈의 이름이 들어갑니다. 파이썬 인터프리터를 통해 파이썬파일을 직접실행할 경우에는 파이썬에서 알아서 그파일의 name은 &lt;code&gt;__main__&lt;/code&gt;이 됩니다. 파이썬 모듈을 import해서 사용할 경우에는 name은 원래 모듈 이름으로 설정됩니다. 그러므로 만약 해당 파일이 직접 실행시킬 때에만 실행되도록 설정하고 싶다면 &lt;code&gt;if __name__ == '__main__':&lt;/code&gt;로 실행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대경로와 상대경로&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDZVTo/btrp7ia0zML/IBgIAvJogDMcgDDB9A8AC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDZVTo/btrp7ia0zML/IBgIAvJogDMcgDDB9A8AC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDZVTo/btrp7ia0zML/IBgIAvJogDMcgDDB9A8AC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDZVTo%2Fbtrp7ia0zML%2FIBgIAvJogDMcgDDB9A8AC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;187&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 구조의 프로젝트가 있다고 한다고 합니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# add_two_numbers.py

from .lib.linked_list import LinkedList&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대경로는 내위치를 중심으로 표현한 경로를 말합니다. &lt;code&gt;.&lt;/code&gt;은 현재위치를 &lt;code&gt;..&lt;/code&gt;은 상위 디렉터리를 나타냅니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# add_two_numbers.py

from lib.linked_list import LinkedList&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대경로는 절대경로는 변하지않는 고유경로입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대경로로 import하여 &lt;code&gt;add_two_numbers.py&lt;/code&gt;을 직접 실행하는 경우 &lt;code&gt;attempted relative import with no known parent package&lt;/code&gt; error가 발생합니다. 실행 후 상대경로를 통해 다른 모듈을 import 할때, 파이썬은 모듈의 이름 &lt;code&gt;__name__&lt;/code&gt;에 기반을 두고 현재모듈의 위치를 찾는다. &lt;code&gt;__name__&lt;/code&gt;이 직접 실행하는 경우 &lt;code&gt;__main__&lt;/code&gt;으로 변경되어 위치를 찾을 수가 없게 됩니다. 직접 실행하는 python file의 경우 항상 절대경로를 사용해야한다고 적혀있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/2/tutorial/modules.html#intra-package-references&quot;&gt;Intra-package References&lt;/a&gt;: Note that relative imports are based on the name of the current module. Since the name of the main module is always &quot;main&quot;, modules intended for use as the main module of a Python application must always use absolute imports.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;sys.path&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sys.path&lt;/code&gt;는 디렉터리의 경로가 기록된 문자열 리스트입니다. 이 리스트에 경로를 추가하면 해당 경로에 있는 파이썬 파일을 import할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pip install &amp;lt;package&amp;gt;&lt;/code&gt; 명령을 실행하면 &lt;code&gt;site-packages&lt;/code&gt; folder에 package가 설치됩니다. &lt;code&gt;sys.path&lt;/code&gt;에 &lt;code&gt;site-packages&lt;/code&gt; folder의 path가 등록되어 있어 다운 받은 package를 절대경로로 import하여 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sys.path&lt;/code&gt;에는 기본적으로 몇 가지 경로가 미리 추가되어 있습니다. 직접 실행한 python file이 속한 folder의 위치가 추가되어 있습니다. 또한 위에서 언급한 &lt;code&gt;site-packages&lt;/code&gt;의 위치, python 인터프이터의 위치 등이 추가되어 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;unittest 사용하면서 경로 설정 문제점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 방법에 따라 다른 경로 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unittest의 테스트 케이스를 작성은 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;# tests/test.py
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트 케이스를 실행하는 방법은 &lt;code&gt;python -m unittest test.py&lt;/code&gt;로 실행합니다. 이와 같은 방법으로 실행하여 &lt;code&gt;__name__&lt;/code&gt;의 값을 확인하면 &lt;code&gt;test&lt;/code&gt;가 반환되는 것을 확인할 수 있습니다. &lt;code&gt;__name__&lt;/code&gt;에서 &lt;code&gt;__main__&lt;/code&gt;이 반환되지 않는 것으로 보아 직접실행되는 형태가 아닙니다. 직접실행하는 방법은 &lt;code&gt;python test.py&lt;/code&gt;로 실행하는 것입니다. &lt;code&gt;python test.py&lt;/code&gt;로 실행해야만 &lt;code&gt;unittest.main()&lt;/code&gt;이 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;python -m&lt;/code&gt;의 실행은 모듈을 스크립트로 수행할 때 쓰는 옵션이라고 합니다. &lt;code&gt;python -m unittest&lt;/code&gt;로 실행하면 unittest module이 스크립트로 실행되고 &lt;code&gt;test.py&lt;/code&gt;는 unittest에 인자로 전달되어 import 형식으로 &lt;code&gt;test.py&lt;/code&gt;가 실행되는 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;python -m unittest test.py&lt;/code&gt;로 실행하는 방법과 &lt;code&gt;python test.py&lt;/code&gt;로 실행하는 방법이 import하는 경로의 차이가 발생합니다. &lt;code&gt;python test.py&lt;/code&gt;로 직접 실행하는 경우 &lt;code&gt;sys.path&lt;/code&gt;의 경로에 &lt;code&gt;tests&lt;/code&gt; folder가 포함되지만 &lt;code&gt;python -m unittest test.py&lt;/code&gt;로 실해하는 경우 shell의 현재 위치가 &lt;code&gt;sys.path&lt;/code&gt;에 추가되어 있습니다. 따라서 경우 2가지 실행 방법에서 절대 경로를 설정한다면 다른 import 경로를 설정해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;통합으로 test case를 실행할 때 다른 경로 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unittest로 test code를 작성할 때는 주로 하나의 파일만 실행합니다. test case를 작성을 완료하면 모든 test code를 한 번에 실행해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9SoEB/btrp54j406P/wQbfK8drNjqcrykCQ30OJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9SoEB/btrp54j406P/wQbfK8drNjqcrykCQ30OJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9SoEB/btrp54j406P/wQbfK8drNjqcrykCQ30OJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9SoEB%2Fbtrp54j406P%2FwQbfK8drNjqcrykCQ30OJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;571&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;# test_attendace_service.py
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
from flaskr.service import attendance_service
from flaskr.model import AttendanceModel, TopicModel, UserModel

.....

if __name__ == '__main__':
    unittest.main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대경로로 module을 import하여 &lt;code&gt;test_attendace_service.py&lt;/code&gt; file 하나만 실행하는 경우 import 경로에 문제가 없이 동작하게 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;# all_tests.py
import glob
import unittest
import sys
import os
testdir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(
    os.path.join(testdir, &quot;../../des_api_lambda&quot;)))

if __name__ == '__main__':
    loader = unittest.TestLoader()
    start_dir = 'tests'
    suite = loader.discover(start_dir)

    runner = unittest.TextTestRunner()
    runner.run(suite)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;all_test.py&lt;/code&gt;에서는 &lt;code&gt;sys.path&lt;/code&gt;에 project root folder의 위치를 추가하여 import 경로에 문제가 발생하지 않도록 구성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.potados.com/dev/python3-import/&quot;&gt;https://blog.potados.com/dev/python3-import/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>develop</category>
      <category>Python</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/41</guid>
      <comments>https://yogae.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 4 Jan 2022 17:59:06 +0900</pubDate>
    </item>
    <item>
      <title>DAS / NAS / SAN</title>
      <link>https://yogae.tistory.com/37</link>
      <description>&lt;p&gt;DAS, NAS, SAN는 스토리지의 종류이고, 연결방식의 차이가 있다.&lt;/p&gt;
&lt;h2&gt;DAS(Direct Attached Storage)&lt;/h2&gt;
&lt;p&gt;시스템에 직접 붙이는 외장 스토리지이다. PC나 노트북에 외장형 하드를 붙이는 방식이 DAS이다. 서버와 하드웨어를 1:1로 연결한다. 서버에 직접 외장 스토리지를 연결하므로 속도는 빠르고 확장은 쉽지만 연결 수에 한계가 있다.&lt;/p&gt;
&lt;h2&gt;NAS(Networt Attached Storage)&lt;/h2&gt;
&lt;p&gt;서버와 저장 장치가 이더텟 등의 LAN 방식의 네트워크에 연결된 방식이다. LAN은 TCP/IP 프로토콜을 기반이고 저장장치는 SCSI를 사용하므로 이들간의 통신을 휘해 중계 역할을 하는 파일 서버가 필요하다. DAS와 달리 PORT수 제한이 없어 확장성과 유연성이 뛰어나다. 하지만 접속 증가 시 성능의 저하가 있고 파일 전송 속도는 DAS보다 느리다. 파일시스템을 공유하기 때문에 보안에 비교적 취약하다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SCSI: 컴퓨터에 주변기기를 연결할 때 직렬 방식으로 연결하기 위한 표준을 일컫는다. SATA와 비교되며, SCSI가 진화된 것이 SAS이다. 무엇보다 안정성이 높은 것이 최대의 장점이지만 가격이 매우 비싸다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;SAN(Storage Area Network)&lt;/h2&gt;
&lt;p&gt;SAN은 여러 기기로 이루어진 로컬 네트워크인 반면, NAS는 LAN(Local Area Network)에 연결되는 단일 스토리지 디바이스이다. SAN 기술의 근간은 FC(Fiber Channel 광 채널)에 두고 있으며 NAS는 TCP/IP에 기반을 두고 있다. 단점은 SAN 구축 시 많은 비용을 투자 해야한다.&lt;/p&gt;</description>
      <category>develop</category>
      <category>storage</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/37</guid>
      <comments>https://yogae.tistory.com/37#entry37comment</comments>
      <pubDate>Fri, 29 Oct 2021 20:11:42 +0900</pubDate>
    </item>
    <item>
      <title>3 계층 architecture</title>
      <link>https://yogae.tistory.com/36</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;3 계층 architecture란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 플랫폼을 3계층으로 나누어 별도의 놀리적 물리적인 장치에 구축 및 운영하는 형태를 말한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계층과 레이어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어와 계층이 혼동되어 사용될 때가 있다. 레이어는 소프트웨어의 기능적 분할을 의미하지만, 계층은 이프라에서 실행되는 소프트웨어의 기능적 분할을 의미한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계층을 분리하여 사용하는 목적&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층을 담당하는 팀들을 구성하여 업무 분담이 가능해지므로 업무 효율성이 증가할 수 있다. 여러 대의 서버로 나누어 각 계층이 동작하므로 서버의 부하를 줄여줄 수 있다. 부하가 발생하는 특정 계층의 서버에 대해서만 스케일업을 고려할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3 계층 구조 구성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;191&quot; data-filename=&quot;3tier web (2).jpg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFa2za/btrjbMX2WIY/vH6u7T9loPGokFsptz6Itk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFa2za/btrjbMX2WIY/vH6u7T9loPGokFsptz6Itk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFa2za/btrjbMX2WIY/vH6u7T9loPGokFsptz6Itk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFa2za%2FbtrjbMX2WIY%2FvH6u7T9loPGokFsptz6Itk%2Fimg.jpg&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;191&quot; data-filename=&quot;3tier web (2).jpg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프레젠터이션 계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보를 표시하고 사용자로부터 정보를 수집한다. 애플리케이션의 사용자 인터페이스를 지원한다. 주로 웹 서버를 뜻한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;애플리케이션 계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리 계층 또는 중간 계층이라고도 하는 애플리케이션 계층은 애플리케이션의 핵심이다. 이 계층에서는 프리젠테이션 계층에서 수집된 정보를 처리한다. 데이터 계층의 데이터를 추가, 삭제 또는 수정할 수도 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 액세스 계층 또는 백엔드라고도 불리는 데이터 계층은 애플리케이션이 처리하는 정보가 저장 및 관리되는 곳이다. 주로 DBMS(Database Management System)이 이 계층에 해당한다. Python, Ruby 또는 PHP를 사용하여 개발되며, 예를 들어 e Django, Rails, Symphony 또는 ASP.NET 등의 프레임워크를 실행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Web server vs WAS(Web Application Server)&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Web server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 request가 오면 요청에 대한 처리를 담당하며 정적문서를 요청하는 경우 요청에 응답한다. 정적 컨텐트가 아닐 경우 WAS로 처리를 위임하여 WAS에서 반환된 값을 응답한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WAS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 조회나 다양한 로직 처리를 요구하는 동적인 컨텐츠를 제공하기 위해 만들어진 서버이다. HTTP를 통해 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어(소프트웨어 엔진)이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Web server와 WAS 를 분리하는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS는 DB 조회나 다양한 로직을 처리하느라 바쁘기 때문에 단순한 정적 컨텐츠는 Web server에서 빠르게 클라이언트에 제공하여 서버 부하를 방지한다. 만약 정적 컨텐츠 요청까지 WAS가 처리한다면 정적 데이터 처리로 인해 부하가 커지게 되고, 동적 컨텐츠의 처리가 지연됨에 따라 수행 속도가 느려진다. 자원 이용의 효율성 및 장애 극복, 배포 및 유지보수의 편의성 을 위해 Web Server와 WAS를 분리한다.&lt;/p&gt;</description>
      <category>develop</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/36</guid>
      <comments>https://yogae.tistory.com/36#entry36comment</comments>
      <pubDate>Fri, 29 Oct 2021 07:15:29 +0900</pubDate>
    </item>
    <item>
      <title>AWS KMS</title>
      <link>https://yogae.tistory.com/35</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS에 데이터 저장 시 암호화 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측 암호화와 서버 측 암호화를 구분하는 기준은 어디서 암호화를 하느냐는 것이다. 클라이언트에서 암호화를 암호화하는 경우 CSE이고 AWS내의 서버에서 암호화를 하는 경우를 SSE라고 한다. S3에서 object 저장 시 암호화하여 저장하는 방식이 SSE에 해당한다. S3에 object를 업로드하기 전 object를 암호화하여 s3에 저장하는 방식은 CSE에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 측 암호화(Client-Side Encrytion)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 전송하기 전에 암호화 수행&lt;/li&gt;
&lt;li&gt;고객이 직접 암호화 키를 마련하고 직접 관리하거나, AWS KMS / CloudHSM내에 보관 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 측 암호화(Server-Side Encryption)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS가 전송된 데이터에 대해 고객 대신 버서 측에서 암호화 작업 수행&lt;/li&gt;
&lt;li&gt;총 58개 서비스 연동: S3, EBS ....&lt;/li&gt;
&lt;li&gt;고객 관리 통제 하에 AWS KMS에 암호화키 보관&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CMK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMS를 조사하면서 혼동되었던 부분이 CMK와 데이터 키이다. CMK는 KMS에 내부에 저장되어 외부유출을 방지한다. AWS KMS는 데이터 키를 저장, 관리 또는 추적하지 않는다. Aws-sdk에서 특정 CMK를 지정하려면 keyid를 사용한다. CMK를 사용하여 데이터키를 생성하며 데이터키로 암/복호화를 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CMK의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CMK은 절대 평문 형태로 HSM을 벗어나지 않는다.&lt;/li&gt;
&lt;li&gt;CMK는 HSM에 저장된 도메인 키로 암호화되며 암호화된 상태로 KMS 내부의 별도 저장공간(KMS Host)에 저장된다.&lt;/li&gt;
&lt;li&gt;데이터키는 KMS 내부의 HSM에서 생성된다.&lt;/li&gt;
&lt;li&gt;생성된 데이터키는 CMK로 암호화하여 평문 데이터키와 함께 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CMK 종류&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관리 주체에 따른 CMK 종류&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;customer managed key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성, 소유 관리할 수 있는 CMK이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AWS managed key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS KMS와 통합된 AWS service에 의해 생성, 괸리, 사용되는 CMK이다. 사용자가 직접 관리하거나 변경할 수 없다.&lt;/li&gt;
&lt;li&gt;특정 AWS service에서는 AWS managed key만을 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AWS owned key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;multiple AWS account에서 사용하기 위해 AWS service에서 소유하고 관리하는 CMK이다.&lt;/li&gt;
&lt;li&gt;사용자가 생성, 관리할 필요없다. 하지만 사용자가 사용, CloudTrail 통한 사용 기록을 호가인할 수 없다.&lt;/li&gt;
&lt;li&gt;key의 사용 요금이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대칭키 / 비대칭키&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS KMS key는 default로 대칭키를 사용한다. AWS KMS와 연동된 AWS service에서는 데이터를 암화하기 위해 대칭키를 사용한다(비대칭키 사용을 제공하지 않음).&lt;br /&gt;비태칭키는 공개키와 비밀키로 구성된다. 비밀키는 암호화되지 않은 형태로 AWS KMS 외부로 전송되지 않는다. 비밀키를 사용하기 위해서는 AWS KMS API를 요출해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터키&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터키를 생성할 때 평문 데이터 키와 암호화된 데이터 키가 반환된다. 암호화된 데이터 키는 복호화에 사용되며, 데이터와 함께 저장해도 안전하다. 데이터 키는 AWS KMS에서 저장, 관리되지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;봉투암호화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;봉투 암호화 동작 방식(대칭키 사용)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 키 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;var params = {
  KeyId: &quot;alias/ExampleAlias&quot;, // The identifier of the KMS key to use to encrypt the data key. You can use the key ID or Amazon Resource Name (ARN) of the KMS key, or the name or ARN of an alias that refers to the KMS key.
  KeySpec: &quot;AES_256&quot;// Specifies the type of data key to return.
 };
 kms.generateDataKey(params, function(err, data) {
   if (err) console.log(err, err.stack); // an error occurred
   else     console.log(data);           // successful response
   /*
   data = {
    CiphertextBlob: &amp;lt;Binary String&amp;gt;, // 암호화된 데이터 키 - 데이터 복호화를 위해 사용
    KeyId: &quot;arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab&quot;, // The ARN of the KMS key that was used to encrypt the data key.
    Plaintext: &amp;lt;Binary String&amp;gt;// 데이터 키 - 데이터 암호화를 위해 사용하며 암호화에 사용하고 따로 저장하지 않고 삭제해야한다.
   }
   */
 });&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평문 데이터 키를 이용하여 평문을 암호화합니다. 암호화가 완료되면 평문 데이터 키는 삭제합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평문을 암호화하기위해서는 암호화 module을 사용해야한다(&lt;a href=&quot;https://github.com/aws/aws-encryption-sdk-javascript/tree/master/modules/client-node&quot;&gt;AWS Encryption SDK&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/crypto-js&quot;&gt;crypto-js&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/ncrypt-js&quot;&gt;ncrypt-js&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 예시
var encrypted = CryptoJS.AES.encrypt(&quot;Message&quot;, &quot;&amp;lt;1번에서 반환된 Plaintext - 데이터 키&amp;gt;&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;3&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;암호화된 문자열과 암호화된 Data Key를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;봉투 복호화 동작 방식(대칭키 사용)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;암호화된 데이터 키 복호화&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt; var params = {
  CiphertextBlob: &amp;lt;Binary String&amp;gt;, // The encrypted data (ciphertext).
  KeyId: &quot;arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab&quot;// A key identifier for the KMS key to use to decrypt the data.
 };
 kms.decrypt(params, function(err, data) {
   if (err) console.log(err, err.stack); // an error occurred
   else     console.log(data);           // successful response
   /*
   data = {
    KeyId: &quot;arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab&quot;, // The Amazon Resource Name (ARN) of the KMS key that was used to decrypt the data.
    Plaintext: &amp;lt;Binary String&amp;gt;// The decrypted (plaintext) data.
   }
   */
 });&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; start=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평문 Data Key를 이용하여 암호화된 문자열을 복호화하여 반환합니다. 복호화가 완료된 평문 Data Key는 삭제합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 예시
var decrypted = CryptoJS.AES.decrypt(encrypted, &quot;&amp;lt;1번에서 반환된 Plaintext - 데이터 키&amp;gt;&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/security/how-to-encrypt-and-decrypt-your-data-with-the-aws-encryption-cli/&quot;&gt;https://aws.amazon.com/ko/blogs/security/how-to-encrypt-and-decrypt-your-data-with-the-aws-encryption-cli/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/2518/&quot;&gt;https://techblog.woowahan.com/2518/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aws/aws-encryption-sdk-javascript/tree/master/modules/client-node&quot;&gt;https://github.com/aws/aws-encryption-sdk-javascript/tree/master/modules/client-node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=B7JTWT3vfis&quot;&gt;https://www.youtube.com/watch?v=B7JTWT3vfis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>develop</category>
      <category>aws</category>
      <category>KMS</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/35</guid>
      <comments>https://yogae.tistory.com/35#entry35comment</comments>
      <pubDate>Sun, 17 Oct 2021 22:20:36 +0900</pubDate>
    </item>
    <item>
      <title>node.js multi processing, threading</title>
      <link>https://yogae.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js로 multi threading, multi processing을 필요로하는 작업을 진행하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 multi threading을 위해서는 &lt;code&gt;worker_thread&lt;/code&gt;(node version 10부터 사용가능)를 사용한다. Multi processing을 위해서는 &lt;code&gt;child_process&lt;/code&gt; 또는 &lt;code&gt;cluster&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process를 단순하게 병렬로 실행하는 것은 &lt;code&gt;child_process&lt;/code&gt;로 가능하고, 로드밸런싱과 포트 공유가 필요하다면 &lt;code&gt;cluster&lt;/code&gt;를 사용하는 것이 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;multi threading과 multi processing 중 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multi threading, multi processing을 사용하여 모두 구현은 가능하지만 두가지의 장단점이 다르기 때문에 요구사항에 따라 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multi threading을 사용하는 것은 multi processing을 사용하는 것 보다 병렬처리를 가볍게 처리하는 방식이다. memory를 효율적으로 공유하고 적은 공간을 사용할 수 있다. Multi processing으로 구현하는 경우 메모리를 공유할 수 없고 통신(IPC)을 통해 데이터를 주고 받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multi processing은 CPU-intensive한 작업을 처리할 때 선택하는 것이 좋고 multi threading은 I/O-intensive한 작업을 처리할 때 선택하는 것이 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://www.telerik.com/blogs/exploring-the-worker-thread-api-in-node&quot;&gt;Exploring the Worker Thread API in Node&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;worker_thread 사용 예시&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const { Worker } = require('worker_threads')

const runService = (workerData) =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    const worker = new Worker(__filename,{ workerData });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) =&amp;gt; {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    })
  })
}

const run = async () =&amp;gt; {
  const result = await runService('test')
}

run().catch(err =&amp;gt; console.error(err))&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;cluster 사용 예시&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
// main process가 실행
  console.log(`Master ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i &amp;lt; numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) =&amp;gt; {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
// worker process가 실행
  http.createServer((req, res) =&amp;gt; {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Javascript 파일을 처음 실행하는 process가 마스터가 된다. 마스터에서는 &lt;code&gt;fork()&lt;/code&gt; 메서드를 통해서 새로운 worker를 생성한다.&lt;/p&gt;</description>
      <category>develop</category>
      <category>node</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/34</guid>
      <comments>https://yogae.tistory.com/34#entry34comment</comments>
      <pubDate>Fri, 2 Apr 2021 19:02:48 +0900</pubDate>
    </item>
    <item>
      <title>VPC AWS ElasticSearch Service로 Cognito 인증 접근</title>
      <link>https://yogae.tistory.com/33</link>
      <description>&lt;p&gt;VPC내에 ES를 사용하고 있다. VPC내 ES로 접근하기 위해서는 ssh tunneling을 사용하거나 proxy 서버를 구성하여 접근 해야한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ES를 보고서를 작성할 때만 사용하고 있어서 많은 설정이 필요 없는 ssh tunneling으로 사용하고 있었다. 하지만 점점 사용 빈도가 늘어나면서 매번 tunneling을 하는 것이 귀찮아졌다. kibana에 개발자가 아닌 사람이 접속할 상황이 생기고 점점 tunneling을 관리하기가 어려워졌다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-ssh-elasticsearch/&quot;&gt;Amazon Cognito 인증으로 Kibana에 액세스하기 위해 SSH 터널 사용&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lY3a2/btqV35RTe4g/BSOGKkJwmoJekVQaBbBW8K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lY3a2/btqV35RTe4g/BSOGKkJwmoJekVQaBbBW8K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lY3a2/btqV35RTe4g/BSOGKkJwmoJekVQaBbBW8K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlY3a2%2FbtqV35RTe4g%2FBSOGKkJwmoJekVQaBbBW8K%2Fimg.jpg&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위의 그림 처럼 tunneling을 사지 않고 proxy 서버를 통해 traffic이 VPC 내부에 있는 ES로 접근할 수 있도록 구성했다. ES에 접근 시 cognito의 login을 하여 인증된 사용자에게만 권한을 부여하고 ES를 사용할 수 있도록 구성했다.&lt;/p&gt;
&lt;h2&gt;Nginx Proxy Server 설정&lt;/h2&gt;
&lt;p&gt;기존에 ssh tunneling으로 사용하는 EC2 instance는 spot instance였다. Spot instance는 싸지만 언제든지 새로운 instance로 교체될 수 있다. 지속적으로 사용해야하는 경우 on-demand 또는 reserved instance를 생성해야한다.&lt;/p&gt;
&lt;h3&gt;Let&amp;rsquo;s encrypt&lt;/h3&gt;
&lt;p&gt;ALB를 사용하면 ACM 서비스를 이용하여 SSL 인증서를 관리할 수 있고 확장성이 용이한 구성을 할 수 있다. 하지만 ALB사용 시 유지비용이 추가된다. 회사 내부에서만 ES에 접근하는 상황이라 EC2 instance 하나만 사용했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ALB나 Cloudfront를 사용하지 않고 EC2 instance 만 사용하는 경우 ACM을 사용할 수 없다. SSL 인증서를 발급받기 위해 let&amp;rsquo;s encrypt를 사용했다. Amazon Linux2를 사용하는 경우 Let&amp;rsquo;s encrypt를 사용하는 방법은 아래 링크에 자세하게 설명되어 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#letsencrypt&quot;&gt;자습서: Amazon Linux 2에서 SSL/TLS 구성 - Amazon Elastic Compute Cloud&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1612533391263&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;자습서: Amazon Linux 2에서 SSL/TLS 구성 - Amazon Elastic Compute Cloud&quot; data-og-description=&quot;여러 가지 방법으로 사용자 지정 키를 EC2 인스턴스에 업로드할 수 있지만, 가장 간편하고 유익한 방법은 텍스트 편집기(예: vi, nano, 메모장)를 로컬 컴퓨터와 인스턴스에 모두 열고 두 편집기 간&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#letsencrypt&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#letsencrypt&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#letsencrypt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/SSL-on-amazon-linux-2.html#letsencrypt&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;자습서: Amazon Linux 2에서 SSL/TLS 구성 - Amazon Elastic Compute Cloud&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;여러 가지 방법으로 사용자 지정 키를 EC2 인스턴스에 업로드할 수 있지만, 가장 간편하고 유익한 방법은 텍스트 편집기(예: vi, nano, 메모장)를 로컬 컴퓨터와 인스턴스에 모두 열고 두 편집기 간&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;docs.aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;Certbot으로 인증서를 발급받을 때 주의할 사항은 먼저 route53에 EC2의 public ip에 특정 도메인을 먼저 연결하고 EC2 instance의 Security Group 80 port를 열어야한다.&lt;/p&gt;
&lt;h3&gt;nginx proxy 설정&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resolver 10.0.0.2 ipv6=off;
# 80 -&amp;gt; 443
server {
    listen 80;
    server_name $host;
    set $es_endpoint &amp;lt;ES_ENDPOINT&amp;gt;;
    return 301 https://&amp;lt;ES_ENDPOINT&amp;gt;;
}

server {
    listen 443 ssl;
    server_name $host;
    rewrite ^/$ https://$host/_plugin/kibana redirect;

    ssl_certificate /etc/nginx/cert.crt;
    ssl_certificate_key /etc/nginx/cert.key;

    ssl on;
    ssl_session_cache builtin:1000 shared:SSL:10m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
    ssl_prefer_server_ciphers on;

    set $es_endpoint &amp;lt;ES_ENDPOINT&amp;gt;;
    set $cognito_endpoint &amp;lt;COGNITO_ENDPOINT&amp;gt;;

    location ^~ /_plugin/kibana {
        # Forward requests to Kibana
        proxy_pass https://$es_endpoint;

        # Handle redirects to Amazon Cognito
        proxy_redirect https://$cognito_endpoint https://$host;

        # Update cookie domain and path
        proxy_cookie_domain $es_endpoint $host;

        # Response buffer settings
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }

    location ~ \/(log|sign|error|fav|forgot|change|confirm) {
        # Forward requests to Cognito
        proxy_pass https://$cognito_endpoint;

        # Handle redirects to Kibana
        proxy_redirect https://$es_endpoint https://$host;

        # Handle redirects to Amazon Cognito
        proxy_redirect https://$cognito_endpoint https://$host;

        # Update cookie domain
        proxy_cookie_domain $cognito_endpoint $host;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 내용을 &lt;code&gt;/etc/nginx/conf.d/default.conf&lt;/code&gt;에 작성하여 nginx server를 재실행한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-nginx-elasticsearch/&quot;&gt;Amazon Cognito 인증으로 Kibana에 액세스하기 위해 NGINX 사용&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1612533404995&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;company&quot; data-og-title=&quot;Amazon Cognito 인증으로 Kibana에 액세스하기 위해 NGINX 사용&quot; data-og-description=&quot;Amazon Elasticsearch Service(Amazon ES) 클러스터가 Virtual Private Cloud(VPC)에서 실행됩니다. Amazon Cognito 인증으로 VPC 외부에서 Kibana에 액세스하기 위해 NGINX 프록시를 사용하려고 합니다.&quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-nginx-elasticsearch/&quot; data-og-url=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-nginx-elasticsearch/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pgu8k/hyJakgZ9P6/up5W6rWxszsBKQJxwAZFZ1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/fTvhH/hyJbsYMVk9/POBSgRVDlC1KR9vm2B18PK/img.png?width=179&amp;amp;height=109&amp;amp;face=0_0_179_109&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-nginx-elasticsearch/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/kibana-outside-vpc-nginx-elasticsearch/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pgu8k/hyJakgZ9P6/up5W6rWxszsBKQJxwAZFZ1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/fTvhH/hyJbsYMVk9/POBSgRVDlC1KR9vm2B18PK/img.png?width=179&amp;amp;height=109&amp;amp;face=0_0_179_109');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Amazon Cognito 인증으로 Kibana에 액세스하기 위해 NGINX 사용&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Amazon Elasticsearch Service(Amazon ES) 클러스터가 Virtual Private Cloud(VPC)에서 실행됩니다. Amazon Cognito 인증으로 VPC 외부에서 Kibana에 액세스하기 위해 NGINX 프록시를 사용하려고 합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2&gt;ElasticSearch Service 설정&lt;/h2&gt;
&lt;p&gt;ElasticSearch Service에 cognito 인증을 설정하기 위해서 &lt;code&gt;인증 수정&lt;/code&gt; 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CMAnL/btqVSvLWNfz/1QbsMaYNaxIvnQyXTkEXYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CMAnL/btqVSvLWNfz/1QbsMaYNaxIvnQyXTkEXYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CMAnL/btqVSvLWNfz/1QbsMaYNaxIvnQyXTkEXYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCMAnL%2FbtqVSvLWNfz%2F1QbsMaYNaxIvnQyXTkEXYK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;인증 수정에서 사용할 user pool과 identity pool을 설정한다. console에서 설정하면 자동으로 앱클라이언트 설정과 앱클라이언트와 연결된 identity pool을 설정해준다.(자동으로 앱클라이언트가 생성된다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ES와 cognito를 연결했다면 권한을 설정해야한다. (위의 과정만 진행하면 kibana endpoint로 접근 시 로그인 페이지가 나오지 않고 바로 kibana로 접속된다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QlIAk/btqV08Pnf9e/ReWKGK7NDeRgQklftKDDzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QlIAk/btqV08Pnf9e/ReWKGK7NDeRgQklftKDDzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QlIAk/btqV08Pnf9e/ReWKGK7NDeRgQklftKDDzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQlIAk%2FbtqV08Pnf9e%2FReWKGK7NDeRgQklftKDDzK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;AWS&quot;: &quot;arn:aws:iam::&amp;lt;account id&amp;gt;:role/&amp;lt;인증된 사용자 역할&amp;gt;&quot;
      },
      &quot;Action&quot;: &quot;es:ESHttp*&quot;,
      &quot;Resource&quot;: &quot;arn:aws:es:&amp;lt;region&amp;gt;:&amp;lt;account id&amp;gt;:domain/&amp;lt;es domain name&amp;gt;/*&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;identity pool의 설정에서 인증된 사용자 역할을 위의 json에 채운다.&lt;/p&gt;</description>
      <category>develop</category>
      <category>Cognito</category>
      <category>Elasticsearch</category>
      <category>nginx</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/33</guid>
      <comments>https://yogae.tistory.com/33#entry33comment</comments>
      <pubDate>Fri, 5 Feb 2021 22:55:53 +0900</pubDate>
    </item>
    <item>
      <title>Airflow 기본 시용법 정리</title>
      <link>https://yogae.tistory.com/32</link>
      <description>&lt;p&gt;Python을 사용하여 행동 로그를 분석하고 있다. 스크립트를 작성하여 필요할 때마다 실행하여 분석 데이터를 수집하였다. 로그 데이터를 분석을 시작하니 정말 다양한 요구사항이 있었다. 이러한 요구사항에 따라 script를 작성하다보니 script가 많아졌고 많은 script를 관리해야하는 문제가 생겼다. 또한, script간의 순서가 생기면서 workflow를 정의하고 관리하는 일이 많아졌다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/introducing-amazon-managed-workflows-for-apache-airflow-mwaa/&quot;&gt;AWS에서 airflow를 관리형 서비스로 출시&lt;/a&gt;했다는 소식을 듣게 되었다. AWS의 관리형 airflow를 사용하면 좋지만 현재 python script를 관리의 목적 및 분석 workflow구성을 위해서만 사용하고 있어서 필요할 때마다 airflow를 local 환경에서 실행하여 사용하기로 했다. script를 주기적으로 실행해야하는 경우나 여러사람이 공유해야한다면 MWAA(&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Amazon Managed Workflows for Apache Airflow&lt;/span&gt;)를 찾아보면 좋을 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://airflow.apache.org/docs/apache-airflow/stable/start.html#basic-airflow-architecture&quot;&gt;Basic Airflow architecture&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Airflow 설치&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export AIRFLOW_HOME=~/airflow

pip install apache-airflow&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;airflow의 설치는 위와같이 쉽게 할 수 있다. &lt;code&gt;$AIRFLOW_HOME&lt;/code&gt;에 설정한 폴더에 airflow 설정 file &lt;code&gt;airflow.cfg&lt;/code&gt;, sqlite db file &lt;code&gt;airflow.db&lt;/code&gt;이 생성된다.&lt;/p&gt;
&lt;h2&gt;데이터 베이스&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;airflow db init

airflow users create \
    --username admin \
    --firstname Peter \
    --lastname Parker \
    --role Admin \
    --email spiderman@superhero.org&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;db를 초기화 하고 사용자를 생성한다.&lt;/p&gt;
&lt;p&gt;airflow를 상태를 저장하기 위해 database를 사용한다. 기본으로 sqlite를 사용한다. 동시에 여러 task를 실행하기 위해서는 MySQL 또는 PostgreSQL을 사용해야한다. sqlite를 사용하는 경우 병렬로 task를 실행하여도 하나씩 처리하게 된다.&lt;/p&gt;
&lt;h2&gt;Airflow web server 실행&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# start the web server, default port is 8080
airflow webserver --port 8080

# start the scheduler
# open a new terminal or else run webserver with ``-D`` option to run it as a daemon
airflow scheduler&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Airflow Webserver와 Scheduler는 독립된 process에서 실행된다. Webserver만 실행하고 &lt;code&gt;0.0.0.0:8080&lt;/code&gt;으로 접속하면 Airflow Web page를 볼 수 있다. 하지만 DAG를 실행하게 되면 Task가 실행중으로 남게 된다. Task가 계속 실행 중으로 남아있다면 Airflow scheduler를 확인해보자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br9K5o/btqUkZOpXdb/CRBqt7UCHktHpRXR9fpNqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br9K5o/btqUkZOpXdb/CRBqt7UCHktHpRXR9fpNqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br9K5o/btqUkZOpXdb/CRBqt7UCHktHpRXR9fpNqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr9K5o%2FbtqUkZOpXdb%2FCRBqt7UCHktHpRXR9fpNqK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Airflow web server를 처음 실행하면 위의 사진과 다르게 여러개의 예제 DAGs를 볼수 있다. 예제 DAGs를 삭제하려면 &lt;code&gt;airflow.cfg&lt;/code&gt;의 &lt;code&gt;load_examples&lt;/code&gt;을 False로 설정해하고 DB를 초기화 해야한다. &lt;code&gt;load_examples&lt;/code&gt;값만 False로 변경하면 예제 DAGs가 그대로 남아 있다.&lt;/p&gt;
&lt;p&gt;위쪽 끝에 위치한 pause 토글 버튼이 처음 생성되었을 때는 pause로 되어 있다. DAG가 pause되어 있어도 DAG를 실행할 수는 있지만 task가 실행되지 않는다. 실행전에 pause 버튼을 확인하기 바란다.&lt;/p&gt;
&lt;h2&gt;DAG 작성&lt;/h2&gt;
&lt;p&gt;Airflow의 DAG 작성은 Python으로 작성해야한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from datetime import timedelta

# The DAG object; we'll need this to instantiate a DAG
from airflow import DAG

# Operators; we need this to operate!
from airflow.operators.bash import BashOperator
from airflow.utils.dates import days_ago
# These args will get passed on to each operator
# You can override them on a per-task basis during operator initialization
default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'email': ['airflow@example.com'],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': timedelta(minutes=5),
    # 'queue': 'bash_queue',
    # 'pool': 'backfill',
    # 'priority_weight': 10,
    # 'end_date': datetime(2016, 1, 1),
    # 'wait_for_downstream': False,
    # 'dag': dag,
    # 'sla': timedelta(hours=2),
    # 'execution_timeout': timedelta(seconds=300),
    # 'on_failure_callback': some_function,
    # 'on_success_callback': some_other_function,
    # 'on_retry_callback': another_function,
    # 'sla_miss_callback': yet_another_function,
    # 'trigger_rule': 'all_success'
}
dag = DAG(
    'tutorial',
    default_args=default_args,
    description='A simple tutorial DAG',
    schedule_interval=timedelta(days=1),
    start_date=days_ago(2),
    tags=['example'],
)

# t1, t2 and t3 are examples of tasks created by instantiating operators
t1 = BashOperator(
    task_id='print_date',
    bash_command='date',
    dag=dag,
)

t2 = BashOperator(
    task_id='sleep',
    depends_on_past=False,
    bash_command='sleep 5',
    retries=3,
    dag=dag,
)
dag.doc_md = __doc__

t1.doc_md = &quot;&quot;&quot;\
#### Task Documentation
You can document your task using the attributes `doc_md` (markdown),
`doc` (plain text), `doc_rst`, `doc_json`, `doc_yaml` which gets
rendered in the UI's Task Instance Details page.
![img](http://montcs.bloomu.edu/~bobmon/Semesters/2012-01/491/import%20soul.png)
&quot;&quot;&quot;
templated_command = &quot;&quot;&quot;
{% for i in range(5) %}
    echo &quot;{{ ds }}&quot;
    echo &quot;{{ macros.ds_add(ds, 7)}}&quot;
    echo &quot;{{ params.my_param }}&quot;
{% endfor %}
&quot;&quot;&quot;

t3 = BashOperator(
    task_id='templated',
    depends_on_past=False,
    bash_command=templated_command,
    params={'my_param': 'Parameter I passed in'},
    dag=dag,
)

t1 &amp;gt;&amp;gt; [t2, t3]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Task를 정의할 때는 operator를 사용한다. Airflow에서 제공하는 operator는 여러가지가 있다. 위의 예제에서와 같은 Bash을 실행하는 BashOperator와 python을 실행하는 PythonOperator가 있다. DB와 aws s3 등 저장소 ETL 작업과 관련된 operator 등 다양한 operator를 제공한다. operator를 custom하게 생성하여 사용할 수도 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://airflow.apache.org/docs/apache-airflow/stable/howto/custom-operator.html?highlight=custom&quot;&gt;Creating a custom Operator &amp;mdash; Airflow Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Airflow params 전달&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd8pih/btqUkZHMnjX/6Xxxw3aQrwqVM0DHCvYTVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd8pih/btqUkZHMnjX/6Xxxw3aQrwqVM0DHCvYTVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd8pih/btqUkZHMnjX/6Xxxw3aQrwqVM0DHCvYTVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd8pih%2FbtqUkZHMnjX%2F6Xxxw3aQrwqVM0DHCvYTVk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Trigger DAG버튼을 클릭하면 위와 같은 json 입력창이 보인다. json형식으로 Task에 전달할 params를 작성하고 Trigger하면 params를 전달할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from airflow import DAG
from airflow.operators.python import PythonOperator

args = {
    'owner': 'airflow',
}

dag = DAG(
    dag_id='analysis_dag',
    default_args=args,
    schedule_interval=None,
    start_date=days_ago(2),
    tags=['example'],
)

def process_func(ds, **kwargs):
    params = kwargs['params']
    START_TIMESTAMP = params[&quot;START_TIMESTAMP&quot;]
    END_TIMESTAMP = params[&quot;END_TIMESTAMP&quot;]
      ...

PythonOperator(
  task_id='process_task',
  python_callable= process_func,
  dag=dag,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sh로 실행하는 경우&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;airflow dags trigger {dag_id} -c '{&quot;START_TIMESTAMP&quot;: &quot;START_TIMESTAMP&quot;, &quot;END_TIMESTAMP&quot;: &quot;END_TIMESTAMP&quot;}'

airflow tags test {dag_id} {execution_date} -t '{&quot;START_TIMESTAMP&quot;: &quot;START_TIMESTAMP&quot;, &quot;END_TIMESTAMP&quot;: &quot;END_TIMESTAMP&quot;}'&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Variables&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0IuAe/btqUmKiZXY4/1PKNFQwbY5KC5YnlqOaO50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0IuAe/btqUmKiZXY4/1PKNFQwbY5KC5YnlqOaO50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0IuAe/btqUmKiZXY4/1PKNFQwbY5KC5YnlqOaO50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0IuAe%2FbtqUmKiZXY4%2F1PKNFQwbY5KC5YnlqOaO50%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DB url과 같은 보안이 필요한 값은 &lt;code&gt;Admin &amp;gt; Variables&lt;/code&gt;에 등록하여 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from airflow.models import Variable

def example_func(ds, **kwargs):
    DB = Variable.get(&quot;DB_LOCAL&quot;)
      ....

PythonOperator(
    task_id='example_task',
    python_callable=example_func,
    dag=dag,
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Task간 message 전송&lt;/h2&gt;
&lt;p&gt;Task간 message를 전송하는 방법이 조금 까다롭다. Task간에 message가 작은 경우(48KM 이하) XCOM을 사용하여 message를 전달할 수 있다. message 크기 제한 때문에 가능한 다른 저장소를 사용하고 XCOM에는 간단한 key 값이나 path값만 전달하여 사용하는 방식이 맞다고 생각된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from airflow.operators.python import PythonOperator

def example_xcom_push(ds, **kwargs):
     kwargs['ti'].xcom_push(
      key=&quot;example_xcom&quot;,value=str(1))

def example_xcom_pull(ds, **kwargs):
     pull_value = kwargs['ti'].xcom_pull(
        task_ids='', key='example_xcom')
     print(pull_value)    

push_task = PythonOperator(
   task_id='example_xcom_push_task',
   python_callable= example_xcom_push,
   dag=dag,
)

pull_task = PythonOperator(
   task_id='example_xcom_pull_task',
   python_callable= example_xcom_pull,
   dag=dag,
)

push_task &amp;gt;&amp;gt; pull_task&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 사이즈 message 경우 local filesystem을 사용하거나 다른 저장소를 사용하여 전달해야 한다.&lt;/p&gt;
&lt;h2&gt;AWS configuration&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://airflow.apache.org/docs/apache-airflow-providers-amazon/stable/connections/aws.html&quot;&gt;Amazon Web Services Connection &amp;mdash; apache-airflow-providers-amazon Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aldente0630.github.io/data-engineering/2018/06/17/developing-workflows-with-apache-airflow.html&quot;&gt;아파치 에어플로우로 작업흐름 개발해보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mk.kakaocdn.net/dn/if-kakao/conf2019/%EB%B0%9C%ED%91%9C%EC%9E%90%EB%A3%8C_2019/T03-S04.pdf&quot;&gt;https://mk.kakaocdn.net/dn/if-kakao/conf2019/%EB%B0%9C%ED%91%9C%EC%9E%90%EB%A3%8C_2019/T03-S04.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bcho.tistory.com/1184&quot;&gt;조대협의 블로그 :: 데이타 워크플로우 관리를 위한 Apache Airflow #1 - 소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://louisdev.tistory.com/22&quot;&gt;Airflow Local 개발환경 설정(1)_설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://airflow.apache.org/docs/apache-airflow/stable/concepts.html#taskflow-api&quot;&gt;Concepts &amp;mdash; Airflow Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>develop</category>
      <category>Airflow</category>
      <author>yogae</author>
      <guid isPermaLink="true">https://yogae.tistory.com/32</guid>
      <comments>https://yogae.tistory.com/32#entry32comment</comments>
      <pubDate>Fri, 22 Jan 2021 16:53:50 +0900</pubDate>
    </item>
  </channel>
</rss>