<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://euclid1990.github.io/atom.xml" rel="self" type="application/atom+xml" /><link href="https://euclid1990.github.io/" rel="alternate" type="text/html" /><updated>2026-11-02T16:17:04+00:00</updated><id>https://euclid1990.github.io/atom.xml</id><title type="html">Cùi Bắp</title><subtitle>&quot;Everyday, I learn something new.&quot;</subtitle><author><name>Cùi Bắp</name></author><entry><title type="html">Bypass internet packages với SSL/TLS Tunnel và SNI/DNS giả</title><link href="https://euclid1990.github.io/til/bypassing-content-based-internet-packages-with-an-ssltls-tunnel-sni-spoofing-and-dns-spoofing/" rel="alternate" type="text/html" title="Bypass internet packages với SSL/TLS Tunnel và SNI/DNS giả" /><published>2024-05-31T02:00:00+00:00</published><updated>2024-05-31T02:00:00+00:00</updated><id>https://euclid1990.github.io/til/bypassing-content-based-internet-packages-with-an-ssltls-tunnel-sni-spoofing-and-dns-spoofing</id><content type="html" xml:base="https://euclid1990.github.io/til/bypassing-content-based-internet-packages-with-an-ssltls-tunnel-sni-spoofing-and-dns-spoofing/"><![CDATA[<p>Gần đây mình có nhận được một số quảng cáo về việc hack 4G data, ban đầu mình cũng không tin việc đó, vì đơn giản cũng làm trong nghề, nên không nghĩ là cả một nhà mạng với bao nhiêu con người tài năng lại dễ dàng để xảy ra lỗ hổng như vậy. Mặt khác vào mấy trang bán gói 4G giá siêu rẻ chỉ 10/20K 1 tháng cho mình cảm giác như kiểu web lập ra để scam người dùng nạp tiền 😂</p>

<p>Tuy nhiên sau khi tìm hiểu về cơ chế thì mình thấy việc này không hề giả trân 😆 bạn có thể tham khảo paper sau <a href="https://arxiv.org/abs/2212.05447">Bypassing Content-based internet packages with an SSL/TLS Tunnel, SNI Spoofing, and DNS spoofing</a> được submit vào tháng 12 năm 2022.</p>

<embed src="/assets/img/posts/2024-05-31-bypassing-content-based-internet-packages-with-an-ssltls-tunnel-sni-spoofing-and-dns-spoofing/2212.05447v1.pdf" type="application/pdf" width="100%" height="350" />

<p><strong>Cơ chế đơn giản như sau</strong></p>

<p>Chuyển hướng toàn bộ lưu lượng web đến một VPS cá nhân, giả mạo thông tin kết nối để ISP tin rằng bạn đang truy cập một máy chủ hợp pháp như máy chủ zoom.us, tiktok.com. Sau khi lưu lượng được chuyển hướng đến VPS, nó sẽ được chuyển đến các điểm đích thông qua một máy chủ proxy hoặc VPN cài đặt trên VPS. Điều này cho phép người dùng sử dụng internet chùa mà không bị hạn chế. :blush:</p>

<p>Về cơ bản thì nhà mạng vẫn làm ngơ thôi, còn nếu muốn chặn thì mình nghĩ chắc vẫn có cơ chế block 🥲</p>]]></content><author><name>Cùi Bắp</name></author><category term="TIL" /><category term="Research" /><summary type="html"><![CDATA[Gần đây mình có nhận được một số quảng cáo về việc hack 4G data, ban đầu mình cũng không tin việc đó, vì đơn giản cũng làm trong nghề, nên không nghĩ là cả một nhà mạng với bao nhiêu con người tài năng lại dễ dàng để xảy ra lỗ hổng như vậy. Mặt khác vào mấy trang bán gói 4G giá siêu rẻ chỉ 10/20K 1 tháng cho mình cảm giác như kiểu web lập ra để scam người dùng nạp tiền 😂]]></summary></entry><entry><title type="html">Làm thế nào để override 3rd typescript types được khai báo bởi install package trong node modules</title><link href="https://euclid1990.github.io/tips/override-third-party-typescript-types-in-node-modules/" rel="alternate" type="text/html" title="Làm thế nào để override 3rd typescript types được khai báo bởi install package trong node modules" /><published>2024-05-09T02:00:00+00:00</published><updated>2024-05-09T02:00:00+00:00</updated><id>https://euclid1990.github.io/tips/override-third-party-typescript-types-in-node-modules</id><content type="html" xml:base="https://euclid1990.github.io/tips/override-third-party-typescript-types-in-node-modules/"><![CDATA[<p>Hiện tại mình đang sử dụng thư viện <a href="https://github.com/openai/openai-node">openai-node</a>. Tuy nhiên khi implement code theo sample trong Document (<a href="https://platform.openai.com/docs/guides/batch/2-uploading-your-batch-input-file">Uploading Your Batch Input File</a>) của thư viện thì phát sinh lỗi:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">TSError</span><span class="p">:</span> <span class="err">⨯</span> <span class="nx">Unable</span> <span class="nx">to</span> <span class="nx">compile</span> <span class="nx">TypeScript</span><span class="p">:</span>
<span class="nx">worker</span><span class="o">/</span><span class="nx">generate</span><span class="p">.</span><span class="nx">ts</span><span class="p">:</span><span class="mi">18</span><span class="p">:</span><span class="mi">4</span> <span class="o">-</span> <span class="nx">error</span> <span class="nx">TS2322</span><span class="p">:</span> <span class="nx">Type</span> <span class="dl">'</span><span class="s1">"batch"</span><span class="dl">'</span> <span class="nx">is</span> <span class="nx">not</span> <span class="nx">assignable</span> <span class="nx">to</span> <span class="nx">type</span> <span class="dl">'</span><span class="s1">"fine-tune" | "assistants"</span><span class="dl">'</span><span class="p">.</span>

<span class="mi">18</span>    <span class="nx">purpose</span><span class="p">:</span> <span class="dl">"</span><span class="s2">batch</span><span class="dl">"</span><span class="p">,</span>
      <span class="o">~~~~~~~</span>

  <span class="nx">node_modules</span><span class="o">/</span><span class="nx">openai</span><span class="o">/</span><span class="nx">resources</span><span class="o">/</span><span class="nx">files</span><span class="p">.</span><span class="nx">d</span><span class="p">.</span><span class="nx">ts</span><span class="p">:</span><span class="mi">119</span><span class="p">:</span><span class="mi">5</span>
    <span class="mi">119</span>     <span class="nx">purpose</span><span class="p">:</span> <span class="dl">'</span><span class="s1">fine-tune</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">assistants</span><span class="dl">'</span><span class="p">;</span>
            <span class="o">~~~~~~~</span>
    <span class="nx">The</span> <span class="nx">expected</span> <span class="nx">type</span> <span class="nx">comes</span> <span class="k">from</span> <span class="nx">property</span> <span class="dl">'</span><span class="s1">purpose</span><span class="dl">'</span> <span class="nx">which</span> <span class="nx">is</span> <span class="nx">declared</span> <span class="nx">here</span> <span class="nx">on</span> <span class="nx">type</span> <span class="dl">'</span><span class="s1">FileCreateParams</span><span class="dl">'</span>
</code></pre></div></div>

<p>Đi vào trong source code của node_modules pakage này mình phát hiện một điều, trong file <a href="https://github.com/openai/openai-node/blob/7196ac9310d58d057fb2a575e60c1718bf6341a2/src/resources/files.ts#L185">openai-node/src/resources/files.ts - L185</a>. Property <code class="language-plaintext highlighter-rouge">purpose</code> thiếu mất <code class="language-plaintext highlighter-rouge">type</code> là <strong>batch</strong>.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">purpose</span><span class="p">:</span> <span class="dl">'</span><span class="s1">fine-tune</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">assistants</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>Trong thời gian chờ team dev của OpenAI fix lỗi này (Mình đã log 1 issue tại <a href="https://github.com/openai/openai-node/issues/830">đây</a>). Để có thể dùng được thư viện, chúng ta cần override lại <strong>FileCreateParams Interface</strong>.</p>

<p>Có khá nhiều cách, trong bài viết này mình sử dụng <code class="language-plaintext highlighter-rouge">Remap paths trong tsconfig</code>. Theo nghĩa đen, chỉ cần sao chép, dán nguyên văn nội dung file từ node_modules và áp dụng các bản sửa lỗi của riêng bạn.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">tsconfig.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"paths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"openai/resources/*"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"worker/declarations/openai/resources/*"</span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Trong thư mục <code class="language-plaintext highlighter-rouge">worker/declarations/openai/resources/</code> mình tạo 1 file <code class="language-plaintext highlighter-rouge">index.d.ts</code> với nội dung sửa lại <code class="language-plaintext highlighter-rouge">purpose</code> như sau:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">FileCreateParams</span> <span class="p">{</span>
	<span class="cm">/**
	 * The File object (not file name) to be uploaded.
	 */</span>
	<span class="nl">file</span><span class="p">:</span> <span class="nx">Uploadable</span><span class="p">;</span>
	<span class="cm">/**
	 * The intended purpose of the uploaded file.
	 *
	 * Use "fine-tune" for
	 * [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) and
	 * "assistants" for
	 * [Assistants](https://platform.openai.com/docs/api-reference/assistants) and
	 * [Messages](https://platform.openai.com/docs/api-reference/messages). This allows
	 * us to validate the format of the uploaded file is correct for fine-tuning.
	 */</span>
	<span class="nl">purpose</span><span class="p">:</span> <span class="dl">"</span><span class="s2">assistants</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">batch</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">fine-tune</span><span class="dl">"</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">FileListParams</span> <span class="p">{</span>
	<span class="cm">/**
	 * Only return files with the given purpose.
	 */</span>
	<span class="nl">purpose</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">...</span>
<span class="c1">//# sourceMappingURL=files.d.ts.map</span>

</code></pre></div></div>

<p>Ngoài ra nếu bạn không muốn phải chỉnh sửa <code class="language-plaintext highlighter-rouge">compilerOptions</code> trong <code class="language-plaintext highlighter-rouge">tsconfig.json</code>. Tạo thư mục <code class="language-plaintext highlighter-rouge">node_modules</code> trong <code class="language-plaintext highlighter-rouge">src</code> của bạn, sau đó đặt typings của modules bạn muốn ghi đè vào bên trong:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── node_modules
│   └── ...
│
└── src
    ├── index.ts
    ├── ... your codes ...
    │
    └── node_modules
        └── &lt;module-to-be-overwritten&gt;
            └── index.d.ts
</code></pre></div></div>

<p>Tham khảo thêm tại <a href="https://www.typescriptlang.org/docs/handbook/module-resolution.html">How TypeScript resolves modules section</a>.</p>

<ul>
  <li>Stackoverflow
    <ul>
      <li><a href="https://stackoverflow.com/questions/67701504/best-way-to-override-third-party-typescript-types-in-node-modules">Best way to <em>override</em> third party TypeScript types in node_modules</a></li>
      <li><a href="https://stackoverflow.com/questions/41627631/exclude-overwrite-npm-provided-typings">Exclude/overwrite npm-provided typings</a></li>
    </ul>
  </li>
</ul>

<p>Cảm ơn bạn đã quan tâm tới tận cuối bài viết 😆</p>]]></content><author><name>Cùi Bắp</name></author><category term="Tips" /><category term="Typescript" /><summary type="html"><![CDATA[Hiện tại mình đang sử dụng thư viện openai-node. Tuy nhiên khi implement code theo sample trong Document (Uploading Your Batch Input File) của thư viện thì phát sinh lỗi:]]></summary></entry><entry><title type="html">Xây dựng serverless data stream pipeline bằng AWS Kinesis, Lambda, S3 cho Snowflake</title><link href="https://euclid1990.github.io/data-pipeline/build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/" rel="alternate" type="text/html" title="Xây dựng serverless data stream pipeline bằng AWS Kinesis, Lambda, S3 cho Snowflake" /><published>2024-01-26T03:00:00+00:00</published><updated>2024-01-26T03:00:00+00:00</updated><id>https://euclid1990.github.io/data-pipeline/build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake</id><content type="html" xml:base="https://euclid1990.github.io/data-pipeline/build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/"><![CDATA[<p>Trong bài viết này mình sẽ chia sẻ về một mô hình mà mình sử dụng để đề xuất khách hàng trong quá trình thực hiện estimate một dự án trong bộ phận, mà trên thực tế bản thân cũng chưa từng sử dụng (liều ăn nhiều :meat_on_bone:) nên có thể sẽ phát sinh khá nhiều issue 😂.</p>

<p>Ban đầu cá nhân mình cũng rất mong muốn được một Data Engineer có nhiều kinh nghiệm chinh chiến trong nghề giúp sức, nhưng tìm hoài trong công ty không có vị trí này và cũng chưa thấy có ai đã từng vận hành hệ thống kiểu này cho khách, nên thôi tự mình thử implement coi sao 🫣, nếu bạn cũng có kinh nghiệm với các hệ thống xây dựng data pipeline kiểu này, hãy để lại comment góp ý nhé 🚀</p>

<h2 id="bối-cảnh">Bối cảnh</h2>

<p>Hệ thống của khách hàng có 3 services chính, phục vụ end-user, mỗi service lại sử dụng các 3rd-party khác nhau để thu thập metrics nhằm theo dõi, đo lường, phân tích hành vi người sử dụng.</p>

<p>Mục đích cuối cùng là để có những bản báo cáo, phân tích, giúp bộ phận Marketing có sự đánh giá chính xác hơn về tình hình kinh doanh online và có cơ sở thực tế để thực hiện các chiến dịch Marketing, quảng cáo hiệu quả hơn.</p>

<p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/current-system.png" alt="" /></p>

<p>Hệ thống gồm 3 services, và các event logs được gửi tới nhiều 3rd services phục vụ cho việc lưu trữ. Các event logs này sau đó được biến đổi cấu trúc và lưu lại vào mấy dịch vụ OLAP (Online Analytical Processing) serverless query service đại loại như <code class="language-plaintext highlighter-rouge">GCP BigQuery</code>, <code class="language-plaintext highlighter-rouge">AWS Athena</code>.</p>

<p>Dữ liệu từ các OLAP services sẽ được query và thực hiện insert vào <code class="language-plaintext highlighter-rouge">AWS DynamoDB</code> bởi một <em>Batch Process</em> đã được lập lịch.</p>

<p>Để quản trị viên có thể dễ dàng phân tích số liệu qua biểu đồ, phía khách hàng đã build một ứng dụng riêng, quẩy dữ liệu sau cùng từ <code class="language-plaintext highlighter-rouge">AWS DynamoDB</code> và hiển thị cho quản trị viên, nhân viên bán hàng, …</p>

<h2 id="vấn-đề">Vấn đề</h2>

<p>Hệ thống trên đang tồn tại một số vấn đề như:</p>

<ul>
  <li>Độ trễ trong việc hiển thị dữ liệu mới nhất, không phù hợp ở thời điểm cần phản hồi nhanh.</li>
  <li>Cấu trúc / Format dữ liệu không đồng nhất giữa các services</li>
  <li>Cần nhiều tài nguyên: thời gian xử lý, cấu hình máy chủ do dữ liệu ứ đọng lớn</li>
  <li>Gánh nặng quản trị hệ thống khi xảy ra lỗi một thành phần nào đó trong hệ thống</li>
</ul>

<p>Trước những vấn đề đó, khách hàng tìm tới chúng tôi mong muốn có giải pháp khắc phục hiện trạng này. Như vậy cần cải thiện hệ thống để khắc phục các nhược điểm trên.</p>

<p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/new-system.png" alt="" /></p>

<h2 id="giải-pháp">Giải pháp</h2>

<p>Để tiết kiệm tiền cho khách, đội dự án đập toàn bộ những phần cũ đi và xậy lại một Data Stream Pipeline 😆</p>

<ul>
  <li><strong>Thời Gian Thực (Real-time)</strong>: Cho phép xử lý dữ liệu ngay lập tức khi nó được tạo ra,thông tin mới nhất sẽ có sẵn ngay khi nó xuất hiện.</li>
  <li><strong>Khả Năng Mở Rộng (Scalability)</strong>: Thích ứng với các biến động trong khối lượng dữ liệu.</li>
  <li><strong>Phản Hồi Nhanh (Quick Feedback)</strong>: Với khả năng xử lý dữ liệu ngay lập tức, data stream pipeline cung cấp phản hồi nhanh chóng, giúp người dùng có thể đưa ra quyết định dựa trên thông tin mới ngay khi nó có sẵn.</li>
  <li><strong>Xử Lý Dữ Liệu Liên tục (Continuous Data Processing)</strong>: Không chờ đến khi có một lô dữ liệu đầy đủ như trong batch processing.</li>
  <li><strong>Tính Linh Hoạt (Flexibility)</strong>: Data stream pipeline thường linh hoạt hơn với khả năng xử lý dữ liệu đa dạng từ nhiều nguồn và định dạng khác nhau. Việc thêm mới và cập nhật dữ liệu có thể được thực hiện dễ dàng hơn.</li>
  <li><strong>Tiết Kiệm Tài Nguyên (Resource Efficiency)</strong>: Do data stream pipeline thường chỉ xử lý dữ liệu cần thiết và không đợi đến khi có lượng dữ liệu lớn, nó có thể tiết kiệm tài nguyên so với việc xử lý theo batch.</li>
</ul>

<h3 id="kiến-trúc-pipeline">Kiến trúc Pipeline</h3>

<p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/data-stream-pipeline-architect.png" alt="" /></p>

<p>Trong kiến trúc trên thì các services lần lượt đóng vai trò nhiệm vụ sau:</p>

<ul>
  <li><strong>AWS Kinesis Data Streams</strong>: <code class="language-plaintext highlighter-rouge">Source Stream</code></li>
  <li><strong>AWS Kinesis Data Firehose</strong>: <code class="language-plaintext highlighter-rouge">Delivery Stream</code></li>
  <li><strong>AWS Lambda</strong>: <code class="language-plaintext highlighter-rouge">Transformer Function</code></li>
  <li><strong>AWS S3</strong>: <code class="language-plaintext highlighter-rouge">Data Lake</code></li>
  <li><strong>Snowflake</strong>: <code class="language-plaintext highlighter-rouge">Data Warehouse</code></li>
</ul>

<p>Tất cả đều là <strong>Fully Managed Services</strong> giúp giảm gánh nặng quản trị hệ thống cho đội ngũ kỹ sư hạ tầng bên khách.</p>

<h4 id="data-flow">Data Flow</h4>

<ul>
  <li>① Các services hiện tại sẽ tích hợp Kinesis Client và thực hiện <code class="language-plaintext highlighter-rouge">PUT event</code> vào <strong>Source Stream</strong> (<code class="language-plaintext highlighter-rouge">AWS Kinesis Data Streams</code>).</li>
  <li>② Sau đó event sẽ được chuyển tới <strong>Delivery Stream</strong> (<code class="language-plaintext highlighter-rouge">AWS Kinesis Data Firehose</code>)
    <ul>
      <li>③ Tại đây event sẽ được phân loại, validate, transform thông qua <strong>Transformer Function</strong> (<code class="language-plaintext highlighter-rouge">Lambda Function</code>).
        <ul>
          <li>Trong trường hợp này chỉ đơn thuần là convert từ <code class="language-plaintext highlighter-rouge">JSON</code> dang <code class="language-plaintext highlighter-rouge">CSV</code> format (<a href="https://www.snowflake.com/blog/how-to-load-terabytes-into-snowflake-speeds-feeds-and-techniques/"><em>Loading data into Snowflake is fast and flexible. You get the greatest speed when working with CSV files</em></a>)</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>④ Dữ liệu sau xử lý sẽ đi vào <strong>Data Lake</strong> (<code class="language-plaintext highlighter-rouge">AWS S3</code>)</li>
  <li>⑤ Từ <code class="language-plaintext highlighter-rouge">AWS S3</code>, thông qua Snow Pipe Auto-ingestion, dữ liệu được đồng bộ tới <code class="language-plaintext highlighter-rouge">Database &gt; Table</code> trên <strong>Data Warehouse</strong> (<code class="language-plaintext highlighter-rouge">Snowflake</code>)</li>
</ul>

<h4 id="monitoring">Monitoring</h4>

<p>… To be define :relieved:</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>Toàn bộ code thực hiện demo từ khâu triển khai xây dựng hạ tầng trên AWS = CDK, Cấu hình Snowflake, Thực hiện <code class="language-plaintext highlighter-rouge">PUT</code> <strong>dummy Orders data</strong> tới Source Stream đều đã được public lên Github cá nhân mình:</p>

<p>⭐ 🌟 <strong><a href="https://github.com/euclid1990/data-stream-pipeline">https://github.com/euclid1990/data-stream-pipeline</a></strong> :bell:</p>

<h3 id="1-chuẩn-bị-tài-khoản">1. Chuẩn bị tài khoản</h3>

<ul>
  <li>Đăng ký tài khoản <strong>AWS</strong>: <a href="https://portal.aws.amazon.com/billing/signup">https://portal.aws.amazon.com/billing/signup</a></li>
  <li>Đăng ký tài khoản <strong>Snowflake</strong>: <a href="https://signup.snowflake.com">https://signup.snowflake.com</a>
    <ul>
      <li>Sau khi hoàn tất đăng ký bạn sẽ có 400$ credit / Trial trong 1 tháng
<img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-signup-bonus.png" alt="" /></li>
    </ul>
  </li>
</ul>

<h3 id="2-cấu-hình-chung">2. Cấu hình chung</h3>

<p>Trước hết chúng ta cần có source code trên máy</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:euclid1990/data-stream-pipeline.git
<span class="nb">cd </span>data-stream-pipeline
</code></pre></div></div>

<p>Chỉnh sửa tệp tin <strong>.env</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cp</span> .env.example .env
<span class="nv">$ </span>code .env

<span class="nv">CDK_STACK</span><span class="o">=</span><span class="s2">"PipelineStack"</span>   <span class="o">&gt;&gt;&gt;</span>  Tên stack sẽ tạo trên AWS
<span class="nv">CDK_ACCOUNT</span><span class="o">=</span><span class="s2">"123456789012"</span>  <span class="o">&gt;&gt;&gt;</span>  <span class="k">*</span>AWS Account ID<span class="k">*</span> của bạn / Bắt buộc chỉnh sửa
<span class="nv">CDK_REGION</span><span class="o">=</span><span class="s2">"us-east-1"</span>      <span class="o">&gt;&gt;&gt;</span>  Region sẽ thực hiện deploy stack trên
<span class="nv">CDK_USER_NAME</span><span class="o">=</span><span class="s2">"cdk"</span>         <span class="o">&gt;&gt;&gt;</span>  Tên user name tạo mới để chạy CDK, tách biệt với default account
<span class="nv">CDK_ROLE_NAME</span><span class="o">=</span><span class="s2">"cdk-role"</span>    <span class="o">&gt;&gt;&gt;</span>  Role mà CDK_USER_NAME sẽ assume để có permission cần thiết provision AWS resources
<span class="nv">CDK_USER_POLICY_NAME</span><span class="o">=</span><span class="s2">"cdk-inline-policy"</span>   <span class="o">&gt;&gt;&gt;</span>  Tên user name tạo mới để chạy CDK, tách biệt với default account
</code></pre></div></div>

<p>Thêm quyền execute cho toàn bộ Bash script trong thư mục <code class="language-plaintext highlighter-rouge">/scripts</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x ./scripts/<span class="k">*</span>/<span class="k">**</span>/<span class="k">*</span>.sh
</code></pre></div></div>

<h3 id="3-tạo-aws-userrole-chạy-cdk-app">3. Tạo AWS User/Role chạy CDK app</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/create_iam_user_role.sh
</code></pre></div></div>

<h3 id="4-thực-hiện-deploy-aws-stack">4. Thực hiện deploy AWS Stack</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/cdk.sh deploy <span class="nt">--outputs-file</span> ./outputs.json
</code></pre></div></div>

<p>Kết quả</p>

<p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/cdk-outputs.png" alt="" /></p>

<p>Toàn bộ các Outputs sẽ được save ở file <code class="language-plaintext highlighter-rouge">./outputs.json</code> phục vụ cho việc setup <strong>Snowflake</strong> cũng như <strong>Produce event</strong> tới <strong>Kinesis Data Streams</strong></p>

<h3 id="5-cấu-hình-snowflake-auto-ingest-s3-data">5. Cấu hình Snowflake Auto-ingest S3 Data</h3>

<p>Chi tiết bước này đã được phía <strong>Snowflake</strong> hướng dẫn rất kỹ tại đây</p>

<ul>
  <li><a href="https://docs.snowflake.com/en/user-guide/tables-external-s3">Refreshing External Tables Automatically for Amazon S3</a></li>
</ul>

<p>Mình đã re-write lại các step = <strong>bash script</strong> cho tiện</p>

<ul>
  <li>
    <h4 id="51-giới-hạn-quyền-truy-cập-tới-aws-s3-bucket-theo-vpc-id-của-snowflake-account">5.1. Giới hạn quyền truy cập tới <strong>AWS S3 Bucket</strong> theo <strong>VPC ID</strong> của Snowflake account</h4>

    <ul>
      <li>Truy vấn thông tin <strong>VPC ID</strong> trên <strong>Snowflake</strong> nơi đặt tài khoản Snowflake của bạn trên AWS
        <ul>
          <li>Nội dung <code class="language-plaintext highlighter-rouge">SQL Query</code>: <code class="language-plaintext highlighter-rouge">./scripts/snowflake/01-allow-vpc-id/retrieve_snowflake_aws_vpc_id.sql</code></li>
          <li>Kết quả
<img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-account-vpc-id.png" alt="" /></li>
        </ul>
      </li>
      <li>
        <p>Thiết lập Bucket Policy tương ứng</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/snowflake/01-allow-vpc-id/restrict_s3_access_to_vpc.sh &lt;VPC_ID&gt;
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>
    <h4 id="52-cấu-hình-quyền-truy-cập-từ-snowflake-tới-aws-s3">5.2. Cấu hình quyền truy cập từ <strong>Snowflake</strong> tới <strong>AWS S3</strong></h4>

    <ul>
      <li>
        <p>Tạo mới IAM Policy</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/snowflake/02-aws-snowflake-s3-access-policy/create.sh
</code></pre></div>        </div>

        <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-s3-access-policy.png" alt="" /></p>
      </li>
      <li>
        <p>Tạo mới IAM Role trên AWS</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/snowflake/03-aws-snowflake-iam-role/create.sh<span class="sb">`</span>
</code></pre></div>        </div>

        <p>Output của việc thực thi script</p>

        <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-query-instructions.png" alt="" /></p>

        <p>Kết quả trên <strong>AWS IAM Roles</strong></p>

        <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-role.png" alt="" /></p>

        <p>Ở bước này, toàn bộ các câu <strong>SQL</strong> cần dùng để thực thi trong các step sắp tới đã được generate thành file <strong><code class="language-plaintext highlighter-rouge">./scripts/snowflake/03-aws-snowflake-iam-role/integration.sql</code></strong>.</p>

        <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-auto-generated-sql.png" alt="" /></p>

        <p>Hãy copy &amp; paste sang <strong><em>SQL Worksheet</em></strong> trên <strong>Snowflake</strong>.</p>

        <ul>
          <li>Tạo Database <strong>s3_to_snowflake</strong></li>
          <li>Tạo S3 Storage Integration <strong>s3_int</strong></li>
          <li>
            <p>Thực hiện truy xuất ARN của AWS IAM user được tạo tự động cho tài khoản Snowflake của bạn</p>

            <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-desc-s3-integration.png" alt="" /></p>

            <blockquote>
              <ul>
                <li>STORAGE_AWS_IAM_USER_ARN: AWS IAM user được tạo cho tài khoản Snowflake của bạn. Tất cả các tích hợp lưu trữ S3 đều sử dụng IAM user đó.</li>
                <li>STORAGE_AWS_EXTERNAL_ID: External ID cần thiết để thiết lập mối quan hệ tin cậy trust relationship.</li>
              </ul>
            </blockquote>
          </li>
        </ul>

        <p>Thực thi theo các bước hướng dẫn từ Output của script trên.</p>

        <ul>
          <li>
            <p>Cho phép Snowflake IAM User quyền được truy cập tới Bucket Objects</p>

            <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/snowflake/03-aws-snowflake-iam-role/update.sh &lt;STORAGE_AWS_IAM_USER_ARN&gt; &lt;STORAGE_AWS_EXTERNAL_ID&gt;
</code></pre></div>            </div>

            <p>Kết quả trên AWS, <strong>Trust relationships</strong> đã được cập nhật</p>

            <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-role-trust-relationship.png" alt="" /></p>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <h4 id="53-làm-mới-dữ-liệu-của-external-table-với-amazon-sqs-notifications-cho-s3-bucket">5.3. Làm mới dữ liệu của external table với Amazon SQS notifications cho S3 bucket</h4>

    <ul>
      <li>Sử dụng nội dung trong <strong><code class="language-plaintext highlighter-rouge">./scripts/snowflake/03-aws-snowflake-iam-role/integration.sql</code></strong> để tạo mới
        <ul>
          <li><strong>Stage</strong></li>
          <li><strong>CSV Format</strong></li>
          <li><strong>External Table</strong></li>
        </ul>
      </li>
      <li>
        <p>Sau đó chúng ta có thể truy xuất thông tin tới ARN của SQS queue cho External table trong cột notification_channel
<img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-notification-channel.png" alt="" /></p>
      </li>
      <li>Thực hiện cấu hình Event Notifications cho <strong>S3 Bucket</strong> với giá trị cột <strong>notification_channel</strong>
        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/snowflake/04-aws-s3-event-notification/create.sh &lt;notification_channel_arn&gt;
</code></pre></div>        </div>

        <p>Kết quả trên AWS, <strong>S3 Event Notification</strong> đã được cập nhật</p>

        <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/aws-s3-event-notification.png" alt="" /></p>
      </li>
    </ul>
  </li>
</ul>

<h3 id="6-kiểm-tra-việc-sync-dữ-liệu--auto-ingest-sang-snowflake">6. Kiểm tra việc Sync dữ liệu / Auto-ingest sang Snowflake</h3>

<h4 id="before">Before</h4>

<ul>
  <li>
    <p>Dữ liệu trên <strong>AWS S3</strong> và <strong>Snowflake</strong> đều chưa có gì 🚫</p>

    <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/aws-s3-bucket-empty.png" alt="" /></p>

    <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-table-empty.png" alt="" /></p>
  </li>
</ul>

<h4 id="after">After</h4>

<ul>
  <li>Thực hiện stream 1,800 records tới <strong>AWS Kinesis Data Streams</strong></li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/produce.sh 1800
</code></pre></div></div>

<p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/produce-1800-records-to-kinesis.png" alt="" /></p>

<ul>
  <li>
    <p>Dữ liệu xuất hiện tại <strong>S3 Desitnation Stream</strong></p>

    <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/aws-s3-bucket-stream-data.png" alt="" /></p>
  </li>
  <li>
    <p>Dữ liệu được auto-ingest tới <strong>Snowflake Table</strong></p>

    <p>Record ID = 1,800 đã có mặt 😆</p>

    <p><img src="/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/snowflake-table-data.png" alt="" /></p>
  </li>
</ul>

<p>🍀 Như vậy chúng ta có thể xác nhận luồng dữ liệu hoạt động như mong đợi. Kiểm chứng đề xuất có khả năng hiện thực hoá 💯</p>

<p>Cảm ơn bạn đã quan tâm tới tận cuối bài viết 🚀</p>]]></content><author><name>Cùi Bắp</name></author><category term="Data-Pipeline" /><category term="AWS" /><category term="Kinesis Data Streams" /><category term="Data Pipeline" /><category term="Snowflake" /><summary type="html"><![CDATA[Trong bài viết này mình sẽ chia sẻ về một mô hình mà mình sử dụng để đề xuất khách hàng trong quá trình thực hiện estimate một dự án trong bộ phận, mà trên thực tế bản thân cũng chưa từng sử dụng (liều ăn nhiều :meat_on_bone:) nên có thể sẽ phát sinh khá nhiều issue 😂.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2024-01-26-build-serverless-data-stream-pipeline-by-using-kinesis-lambda-s3-for-snowflake/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ngăn chặn truy cập tới ứng dụng web đặt sau AWS CloudFront theo Client IP</title><link href="https://euclid1990.github.io/cloud/blocking-access-to-application-behind-cloudfront-by-client-ip/" rel="alternate" type="text/html" title="Ngăn chặn truy cập tới ứng dụng web đặt sau AWS CloudFront theo Client IP" /><published>2023-12-26T03:00:00+00:00</published><updated>2023-12-26T03:00:00+00:00</updated><id>https://euclid1990.github.io/cloud/blocking-access-to-application-behind-cloudfront-by-client-ip</id><content type="html" xml:base="https://euclid1990.github.io/cloud/blocking-access-to-application-behind-cloudfront-by-client-ip/"><![CDATA[<p>Trong bài viết này mình mô tả một sự cố gần đây xảy ra trong hệ thống mà mình gặp phải khiến RDS Database CPU/Memory luôn gặp phải tình trạng quá tải, ảnh hưởng tới trải nghiệm người dùng. Đồng thời chia sẻ về các giải pháp khả thi mà các thành viên trong đội dự án đã thử triển khai thành công. Thực tế, còn có nhiều giải pháp xịn hơn, nếu bạn đã từng gặp phải vấn đề này, hãy để lại comment chia sẻ nhé 🚀</p>

<p>Hệ thống của mình là một web app thông thường được host trên AWS với mô hình giản lược sau:</p>

<p><img src="/assets/img/posts/2023-12-26-blocking-access-to-application-behind-cloudfront-by-real-client-ip/web-architect.png" alt="" /></p>

<p>Toàn bộ Traffic sẽ đi qua các thành phần <code class="language-plaintext highlighter-rouge">CloudFront</code>, <code class="language-plaintext highlighter-rouge">ALB</code> trươc khi tới <code class="language-plaintext highlighter-rouge">ECS Cluster</code>.</p>

<p>Cụm <code class="language-plaintext highlighter-rouge">ECS</code> gồm có 3 thành phần Container chính</p>

<ul>
  <li>NGINX: Đóng vai trò reverse proxy nhận request</li>
  <li>Web Application: Dynamic web app nhận request từ Nginx</li>
  <li>Fluentd: Push log từ <code class="language-plaintext highlighter-rouge">NGINX</code> và <code class="language-plaintext highlighter-rouge">Web</code> Container sang <code class="language-plaintext highlighter-rouge">Firehorse</code> &gt; <code class="language-plaintext highlighter-rouge">Opensearch</code> &gt; <code class="language-plaintext highlighter-rouge">Kibana</code> phục vụ cho việc phân tích log. (<a href="https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_firelens.html">Custom log routing
</a> / <a href="https://faun.pub/streaming-aws-ecs-logs-via-fluentd-fluent-bit-using-aws-firelense-log-driver-14f2782fc885">Streaming AWS ECS logs via Fluentd using AWS FireLense</a>)</li>
</ul>

<h2 id="vấn-đề">Vấn đề</h2>

<p>Vào một ngày bình thường như bao ngày, AWS liên tục gửi alert về việc % sử dụng CPU của RDS Database liên tục trên ngưỡng 90% đáng báo động 😢</p>

<p>Trước hoàn cảnh đó, cả team cần gấp rút tìm ra nguyên nhân tại sao lại xảy ra hiện tượng như vậy. Chúng ta hãy sang phần tiếp theo xem đội dự án đã xử lý ra sao.</p>

<h2 id="quá-trình-điều-tra">Quá trình điều tra</h2>

<h3 id="giai-đoạn-1">Giai đoạn 1</h3>

<p>Thuận tự nhiên, lỗi ở RDS thì engineer vào AWS <strong>RDS Performance Insight</strong> &gt; <strong>TOP SQL</strong>. Phát hiện ra câu query chính khiến tải RDS tăng.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="nv">`articles`</span><span class="p">.</span><span class="nv">`id`</span>
<span class="k">FROM</span> <span class="nv">`articles`</span>
<span class="k">WHERE</span> <span class="nv">`articles`</span><span class="p">.</span><span class="nv">`deleted_at`</span> <span class="k">IS</span> <span class="k">NULL</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="nv">`articles`</span><span class="p">.</span><span class="nv">`id`</span>
<span class="k">DESC</span> <span class="k">LIMIT</span> <span class="mi">8</span> <span class="k">OFFSET</span> <span class="mi">357678</span>
<span class="cm">/*controller:article,action:index*/</span>
</code></pre></div></div>
<p><em>Câu query thực gây ra việc tăng tải của hệ thống đã được thay đổi</em></p>

<p>Hàng loạt các câu query tương tự như trên xuất hiện trong bảng xếp hạng TOP những câu query chiếm tài nguyên của RDS.</p>

<p>Ở giai đoạn này chúng tôi phán đoán ở <code class="language-plaintext highlighter-rouge">ArticleController</code> &gt; <code class="language-plaintext highlighter-rouge">Index</code> action đang phát sinh nhiều request hơn thực tế.</p>

<h3 id="giai-đoạn-2">Giai đoạn 2</h3>

<p>Điều tra log người dùng truy cập hệ thống xem bất thường gì không.</p>

<p>Với thiết kế ban đầu, hệ thống sẽ lưu toàn bộ dữ liệu log người dùng truy cập để phục vụ cho <em>Marketing Insight</em> và <em>Debug</em>. Logs được đẩy vào <strong>Opensearch</strong> và Visualize trên <strong>Kibana</strong>.</p>

<p>Filter log truy cập url <code class="language-plaintext highlighter-rouge">article</code> bởi <code class="language-plaintext highlighter-rouge">NGINX</code> theo khoảng thời gian, team phát hiện số lượng request tăng đột biết từ 10~15k request trong khoảng thời gian phát sinh sự cố. Đặc biết khi chú ý tới giá trị <code class="language-plaintext highlighter-rouge">OFFSET</code> câu query <code class="language-plaintext highlighter-rouge">SQL</code> ở giai đoạn 1, và query parameter phân trang <code class="language-plaintext highlighter-rouge">?page=</code>. Nhận thấy các số trong page đều chạy từ nhỏ đến rất lớn (&gt;= 10,000), hành vi này không giống với thực tế end-user sử dụng mà thay vào đó khá giống từ <strong>Crawler Bot</strong> đi thu thập dữ liệu.</p>

<p>Trích xuất thông tin từ dữ liệu log, đội dự án thu được 2 thông tin chính:</p>

<ul>
  <li>Dải IP của BOT (Check thông tin tại <a href="https://ipinfo.io/123.100.101.123">ipinfo</a>)
    <ul>
      <li>123.100.101.0/24</li>
      <li>123.100.102.0/24</li>
      <li>…</li>
      <li><em>Đã chỉnh sửa khác với giá trị thực tế</em></li>
      <li>
        <p>Để get network IP / CIDR 24 từ danh sách các IP có thể dụng đoạn code <code class="language-plaintext highlighter-rouge">Javascript/NodeJS</code> sau:</p>

        <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ipList</span> <span class="o">=</span> <span class="p">[</span>
  <span class="dl">'</span><span class="s1">123.100.101.1</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">123.100.101.2</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">123.100.102.5</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">...</span>
<span class="p">];</span>

<span class="kd">function</span> <span class="nx">getCIDR</span><span class="p">(</span><span class="nx">ip</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">ipParts</span> <span class="o">=</span> <span class="nx">ip</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">ipParts</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">.0/24</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">cidrArray</span> <span class="o">=</span> <span class="nx">ipList</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">getCIDR</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">uniqueCIDRs</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Set</span><span class="p">(</span><span class="nx">cidrArray</span><span class="p">));</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">uniqueCIDRs</span><span class="p">);</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>
    <p>Header <code class="language-plaintext highlighter-rouge">User-Agent</code> của BOT</p>

    <div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Mozilla</span>/<span class="m">5</span>.<span class="m">0</span> (<span class="n">Macintosh</span>; <span class="n">Intel</span> <span class="n">Mac</span> <span class="n">OS</span> <span class="n">X</span> <span class="m">10</span><span class="err">_</span><span class="m">15</span><span class="err">_</span><span class="m">7</span>) <span class="n">AppleWebKit</span>/<span class="m">537</span>.<span class="m">36</span> (<span class="n">KHTML</span>, <span class="n">like</span> <span class="n">Gecko</span>) <span class="n">Chrome</span>/<span class="m">123</span>.<span class="m">0</span>.<span class="m">0</span>.<span class="m">0</span> <span class="n">Safari</span>/<span class="m">537</span>.<span class="m">36</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="giải-pháp">Giải pháp</h3>

<p>Kết thúc giai đoạn 2, đội dự án phán đoán nguyên nhân RDS tăng tải do xuất hiện nhiều request từ <strong>Crawler Bot</strong>.</p>

<p>Đầu tiên cần xác định quan điểm chặn, phạm vi ảnh hưởng tới người dùng thực:</p>

<ul>
  <li>Chặn những page mà chỉ có Bot muốn vào, người dùng hiếm vào (Hành vi)</li>
  <li>Chặn những page có performance thấp, ảnh hưởng RDS nếu tần suất truy cập nhiều</li>
</ul>

<p>Từ đó đưa ra các giải pháp block access / chặn truy cập từ Crawler Bot như.</p>

<ul>
  <li>Chặn truy cập theo header trên <strong>ALB listener rules</strong>
    <ul>
      <li>Condition match header của Bot</li>
      <li>Action return fixed response 503</li>
    </ul>
  </li>
  <li>Chặn truy cập từ các client IP thuộc dải IP của Bot / 1 trong 2 cách
    <ul>
      <li>Chặn truy cập trên <strong>WAF</strong> &gt; <strong>WebACLs</strong>
        <ul>
          <li>Sử dụng <code class="language-plaintext highlighter-rouge">Rule builder</code> &gt; Block hoặc Rate Limit các truy cập từ IP set của Bot
            <ul>
              <li><a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based.html">Rate-based rule statement</a></li>
              <li>Cấu hình các quy tắc - <a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based-aggregation-options.html">Aggregation options and keys</a></li>
              <li>
                <p>Ví dụ:</p>

                <p><img src="/assets/img/posts/2023-12-26-blocking-access-to-application-behind-cloudfront-by-real-client-ip/waf-webacls-rule.png" alt="" /></p>

                <p>Mặc định mỗi <strong>WebACLs</strong> sẽ có 5000 WCU (WAF web ACL capacity units). 1 Rule trên sẽ mất 7 WCU. Nếu dùng quá 1500 WCU thì bạn cần lưu ý thêm <strong>Price</strong> của AWS WAF tại <a href="https://aws.amazon.com/waf/pricing/">đây</a>, bởi lúc đó AWS sẽ charge phí sử dụng.</p>
              </li>
            </ul>
          </li>
          <li>Có thể sử dụng thêm Match regex URI / Query string với condition <code class="language-plaintext highlighter-rouge">?page=</code> để giảm thiểu độ ảnh hưởng</li>
        </ul>
      </li>
      <li>Chặn truy cập ở <strong>NGINX Layer</strong>
        <ul>
          <li>Sử config của NGINX bằng cách thêm phần <strong>map</strong> là IP range của Bot. ($http_cf_client_ip là real client IP được thêm vào header từ CloudFront)</li>
        </ul>

        <div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">http</span> {
  <span class="n">map</span> $<span class="n">http_cf_client_ip</span> $<span class="n">block_access</span> {
    ~*^<span class="m">123</span>\.<span class="m">100</span>\.<span class="m">101</span>\..*$   <span class="m">1</span>;
    ~*^<span class="m">123</span>\.<span class="m">100</span>\.<span class="m">102</span>\..*$   <span class="m">1</span>;
    <span class="n">default</span>                 <span class="m">0</span>;
  }

  <span class="n">server</span> {

    <span class="n">location</span> / {
      <span class="n">if</span> ($<span class="n">block_access</span>) {
        <span class="n">return</span> <span class="m">403</span>;
      }
      <span class="n">try_files</span> $<span class="n">uri</span> @<span class="n">app</span>;
    }

    <span class="n">location</span> @<span class="n">app</span> {
      <span class="n">proxy_pass</span> <span class="n">http</span>://<span class="n">app_service</span>;
      ...
    }
  }
}
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<p>Sau nhiều cân nhắc thì kết quả đội dự án quyết định sử dụng giải pháp <strong>AWS WAF - WebACLs</strong> với khả năng block request linh hoạt và hiệu quả.</p>

<h3 id="kiểm-tra">Kiểm tra</h3>

<p>Sử dụng conditions với điều kiện hẹp hơn, và các công cụ benchmark request như <strong><a href="https://httpd.apache.org/docs/2.4/programs/ab.html">ApacheBench</a></strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ab <span class="nt">-A</span> basicAuthUsername:basicAuthPassword <span class="nt">-n</span> 100 <span class="nt">-c</span> 10 <span class="s2">"https://example.com/"</span>

<span class="nt">-n</span> requests: Số lượng request được gửi
<span class="nt">-c</span> concurrency: Số lượng request được thực hiện 1 thời điểm
</code></pre></div></div>

<hr />

<p><strong>Bổ sung kiến thức:</strong> <code class="language-plaintext highlighter-rouge">CloudFront</code> <strong>không</strong> pass <code class="language-plaintext highlighter-rouge">Client IP</code> sang <code class="language-plaintext highlighter-rouge">ALB</code> và <code class="language-plaintext highlighter-rouge">NGINX</code>, vậy làm thế nào để <strong>có thể</strong> get được <code class="language-plaintext highlighter-rouge">Client IP</code> trên các Layer này ??</p>

<p><img src="/assets/img/posts/2023-12-26-blocking-access-to-application-behind-cloudfront-by-real-client-ip/web-architect.png" alt="" /></p>

<p>Trên lý thuyết AWS cung cấp header “CloudFront-Viewer-Address” - <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html">Adding CloudFront request headers
</a>. Tuy nhiên muốn pass header này qua <strong>origin* cần **disable caching</strong> với Cache Policy.</p>

<p>Xem lại sơ đồ kiến trúc hệ thống, chúng ta thấy CloudFront module có trigger <strong>Lambda@Edge Function</strong> mỗi khi có request tới (Event Type = Origin-request). Về cơ bản để thêm header thì chỉ cần một đoạn code Javascript như sau:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">exports</span><span class="p">.</span><span class="nx">handler</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Records</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">cf</span><span class="p">.</span><span class="nx">request</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">clientIp</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Records</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">cf</span><span class="p">.</span><span class="nx">request</span><span class="p">[</span><span class="dl">'</span><span class="s1">clientIp</span><span class="dl">'</span><span class="p">];</span>
    <span class="kd">const</span> <span class="nx">headerName</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">CF-Client-IP</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">headerName</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()]</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">key</span><span class="p">:</span> <span class="nx">headerName</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">clientIp</span> <span class="p">}];</span>
    <span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">request</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Tham khảo thêm về <a href="https://aws.amazon.com/blogs/networking-and-content-delivery/adding-http-security-headers-using-lambdaedge-and-amazon-cloudfront/">Adding HTTP Security Headers Using Lambda@Edge and Amazon CloudFront</a></p>

<p>Trên <strong>NGINX</strong> layer config như sau để log ra <strong>Client IP</strong></p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log_format</span> <span class="n">main</span> <span class="n">escape</span>=<span class="n">json</span>
  <span class="s1">'{'</span>
    ...
    <span class="s1">'"request":"$request",'</span>
    <span class="s1">'"request_method":"$request_method",'</span>
    <span class="s1">'"host":"$host",'</span>
    <span class="s1">'"uri":"$uri",'</span>
    <span class="s1">'"status":"$status",'</span>
    <span class="s1">'"http_cf_client_ip":"$http_cf_client_ip",'</span>
    ...
  <span class="s1">'}'</span>;

<span class="n">access_log</span>  /<span class="n">var</span>/<span class="n">log</span>/<span class="n">nginx</span>/<span class="n">access</span>.<span class="n">log</span>  <span class="n">main</span>;
</code></pre></div></div>

<p>Cảm ơn bạn đã quan tâm tới tận cuối bài viết 😆</p>]]></content><author><name>Cùi Bắp</name></author><category term="Cloud" /><category term="AWS" /><category term="CloudFront" /><category term="WAF" /><category term="Nginx" /><category term="DDos" /><summary type="html"><![CDATA[Trong bài viết này mình mô tả một sự cố gần đây xảy ra trong hệ thống mà mình gặp phải khiến RDS Database CPU/Memory luôn gặp phải tình trạng quá tải, ảnh hưởng tới trải nghiệm người dùng. Đồng thời chia sẻ về các giải pháp khả thi mà các thành viên trong đội dự án đã thử triển khai thành công. Thực tế, còn có nhiều giải pháp xịn hơn, nếu bạn đã từng gặp phải vấn đề này, hãy để lại comment chia sẻ nhé 🚀]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2023-12-26-blocking-access-to-application-behind-cloudfront-by-real-client-ip/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2023-12-26-blocking-access-to-application-behind-cloudfront-by-real-client-ip/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tại sao nên dùng String Union Types thay cho String Enums trong Typescript</title><link href="https://euclid1990.github.io/tips/why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/" rel="alternate" type="text/html" title="Tại sao nên dùng String Union Types thay cho String Enums trong Typescript" /><published>2023-10-30T05:00:00+00:00</published><updated>2023-10-30T05:00:00+00:00</updated><id>https://euclid1990.github.io/tips/why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript</id><content type="html" xml:base="https://euclid1990.github.io/tips/why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/"><![CDATA[<p>Khi sử dụng TypeScript, chúng ta thường gặp phải trường hợp xác định kiểu của một biến mà giá trị của nó rơi vào một hay nhiều khả năng khác nhau.</p>

<p>Ví dụ, status của một button có thể là: <code class="language-plaintext highlighter-rouge">"hidden"</code>, <code class="language-plaintext highlighter-rouge">"enabled"</code>, <code class="language-plaintext highlighter-rouge">"disabled"</code>.</p>

<p>Với Typescript chúng ta có ít nhất hai cách</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Kiểu string enums</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">ButtonStatus</span> <span class="p">{</span>
  <span class="nx">HIDDEN</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">ENABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">DISABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="c1">// Kiểu string literal union types</span>
<span class="kd">type</span> <span class="nx">ButtonStatus</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>Có cũng có kiểu enum cơ bản (không có phần <code class="language-plaintext highlighter-rouge">= 'HIDDEN'</code>), khi đó các giá trị sẽ chạy từ 0 → 2, nhưng ta sẽ không bàn về nó trong bài viết so sánh này.</p>

<p>Nào, hãy bắt đầu so sánh coi 🥰</p>

<h2 id="declaring--accessing">Declaring &amp; Accessing</h2>

<ul>
  <li>Tính ngắn gọn trong khai báo
    <ul>
      <li>[✔] Union Type</li>
      <li>[✘] String Enums</li>
    </ul>
  </li>
  <li>Autocompletion trên Editor/IDE
    <ul>
      <li>[✔] Union Type
        <ul>
          <li>Tự động nhận trên <strong>VsCode</strong>
<img src="/assets/img/posts/2023-10-30-why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/vscode-auto-completion.gif" alt="" /></li>
        </ul>
      </li>
      <li>[✘] String Enums
        <ul>
          <li>Cần exported và imported mới hoạt động</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><strong>Chiến thắng với 2 points cho Union types!</strong></p>

<table>
  <thead>
    <tr>
      <th>Union Type</th>
      <th>String Enums</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<h2 id="extending">Extending</h2>
<ul>
  <li>[✔] Union Type
    <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">DynamicButtonStatus</span> <span class="o">=</span> <span class="nx">ButtonStatus</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">LOADING</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
  <li>[✘] String Enums
    <ul>
      <li>Sử dụng trick <a href="https://github.com/Microsoft/TypeScript/issues/17592#issuecomment-320028196">spread operator</a>
        <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">enum</span> <span class="nx">ButtonStatus</span> <span class="p">{</span>
  <span class="nx">HIDDEN</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">ENABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">DISABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="c1">// extend enum using "spread"</span>
<span class="kd">const</span> <span class="nx">NewButtonStatus</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">...</span><span class="nx">ButtonStatus</span><span class="p">,</span>
    <span class="na">SHOW</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHOW</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">BLINK</span><span class="p">:</span> <span class="dl">'</span><span class="s1">BLINK</span><span class="dl">'</span>
<span class="p">};</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<p><strong>Thêm 1 point cho Union types!</strong></p>

<table>
  <thead>
    <tr>
      <th>Union Type</th>
      <th>String Enums</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<h2 id="typeerror">TypeError</h2>

<ul>
  <li>[✔] Union Type
    <ul>
      <li><a href="https://www.typescriptlang.org/play?#code/PTAEHUEsBcAtQK4DtIHsmmgTwA4FMBnUVAM1AOgCdIkBzUAGxj0oEMGCAobfUAIQTRo6AMrRW0BEQC8oAOQAJAJIARFQFEAcnNAAfeVoCCfADLqVO-XJVKRxsxYDcnTiWQBjaGgwAVQtABVFHQfXDwACgoJKQAufkFhJDFoggBKUABvTlByAHcYd3hI8Uk0zOyc0HdWAjx5ZTUtOTjKPElKDABGZ0qqmrq5I1NzZtBW9owAJh7K6tr5Gzthixa2hA7QAGYZnJBQTVQvdzq4CUxYFjw5IiRUUAATPBJWBAZoPtqKgF9OH849gDCqAAtjhIAw6gQEO5jgQiJAyDgakQAG7sSD3cglKSgfJwUBBbygUL4TjudAEVAQgB0DFQtHCfgohJCYXCilUGm0qVSzn+YCBoPBdRYlFQlFACNASLhkqQaKYmKipVxMHgLN8YTJFKpeFp9MZ-g1JIichECgA8uA5DzHEA">Playground Link</a>
        <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// With union types of string literals</span>
<span class="kd">type</span> <span class="nx">ButtonStatus</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">TestUnionType</span><span class="p">(</span><span class="nx">status</span><span class="p">:</span> <span class="nx">ButtonStatus</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">:</span> <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span><span class="p">:</span> <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">:</span> <span class="k">return</span> <span class="mi">3</span><span class="p">;</span>
    <span class="c1">// Notice that there's no default case</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Compile success if pass valid status with Union Type</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">TestUnionType</span><span class="p">(</span><span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">));</span>

<span class="c1">// Compile error if pass invalid status with Union Type</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">TestUnionType</span><span class="p">(</span><span class="dl">'</span><span class="s1">SHOW</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div>        </div>
      </li>
      <li>Error được phát hiện từ bước <strong>Transpile</strong>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Argument of type '"SHOW"' is not assignable to parameter of type 'ButtonStatus'.
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>[✘] String Enums
    <ul>
      <li><a href="https://www.typescriptlang.org/play?#code/PTAEHUEsBcAtQM7QE6QHYHNQFM0FcBbBAKF0NACE9poB7NAZWgENo8FQBvY0UACQCSAESEBRAHKgAvKADkgkRNkAaHqAkBBCgBlRQ6XM069KtUIEMtu-TNnnLxobOIBfANzFPAMzxoAxtCQ9KAAKthITKiYovhEABRIrOwAXJTUdIwsbAgAlFxqCADuMH7wCVnsedy8vH7MCNhpNPRMSQgAdApi4qnI2GzIaKAAjB41oHUNTRmt2e1G1r39eIOgAExjNZONVM2Zbe32VnpLA0MAzJu8ACbYXsx4ADbQpytDACybLq6efvRIiAqCGGBnkwm6slA9WmLSBHmIIFAAGFaAQAA6QR6NBB4Px+cIcSBeUBo+ocABuzEekGugLaoGKcFAkXQWBihBIfzQCFoWPaj1oGDiYQiKFZ7PiiWywxyOXhXIBUvYa1BDD4AHlwJDobsZnDPIjxOrkaiMVicMhkLRkKAiSSyba0JTqbSlRxGfAWZh1LFOf9edh+YLheFoF6MBKEOU2mtZW4gA">Playground Link</a>
        <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// With string enums</span>
<span class="kr">enum</span> <span class="nx">ButtonStatus</span> <span class="p">{</span>
  <span class="nx">HIDDEN</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">ENABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">DISABLED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span>
<span class="p">};</span>


<span class="kd">function</span> <span class="nx">TestStringEnums</span><span class="p">(</span><span class="nx">status</span><span class="p">:</span> <span class="nx">ButtonStatus</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="nx">ButtonStatus</span><span class="p">.</span><span class="nx">HIDDEN</span><span class="p">:</span> <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">case</span> <span class="nx">ButtonStatus</span><span class="p">.</span><span class="nx">ENABLED</span><span class="p">:</span> <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">case</span> <span class="nx">ButtonStatus</span><span class="p">.</span><span class="nx">DISABLED</span><span class="p">:</span> <span class="k">return</span> <span class="mi">3</span><span class="p">;</span>
    <span class="nl">default</span><span class="p">:</span> <span class="k">return</span> <span class="mi">4</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">status1</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span> <span class="k">as</span> <span class="nx">ButtonStatus</span><span class="p">;</span>

<span class="c1">// Compile success if pass valid status with String Enums</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">TestStringEnums</span><span class="p">(</span><span class="nx">status1</span><span class="p">));</span>

<span class="kd">const</span> <span class="nx">status2</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">SHOW</span><span class="dl">'</span> <span class="k">as</span> <span class="nx">ButtonStatus</span><span class="p">;</span>

<span class="c1">// NO Compile error if pass invalid status with String Enums</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">TestStringEnums</span><span class="p">(</span><span class="nx">status2</span><span class="p">));</span>
</code></pre></div>        </div>
      </li>
      <li>Truyền sai giá trị <code class="language-plaintext highlighter-rouge">status2</code> nhưng chương trình vẫn được transpiled và run bình thường.</li>
    </ul>
  </li>
</ul>

<p><em>Chỉ có 1 cách duy nhất để sử dụng <strong>union types</strong>, trong khi với <strong>enums</strong> bạn có thể sử dụng <code class="language-plaintext highlighter-rouge">ButtonStatus.HIDDEN</code> hoặc giá trị trực tiếp <code class="language-plaintext highlighter-rouge">'HIDDEN'</code> mà không gặp phải compiler complaining.</em></p>

<p><strong>Thêm 1 point về Typesafe cho Union types!</strong></p>

<table>
  <thead>
    <tr>
      <th>Union Type</th>
      <th>String Enums</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<h2 id="iterating">Iterating</h2>

<ul>
  <li>[✘] Union Type
    <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Union types</span>
<span class="c1">// ? Can't iterate over a type, defining an array is needed</span>
<span class="kd">const</span> <span class="nx">ButtonStatus</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">HIDDEN</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ENABLED</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">DISABLED</span><span class="dl">'</span><span class="p">]</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span>
<span class="c1">// Notice that the array and type can have the same name if you want</span>
<span class="kd">type</span> <span class="nx">ButtonStatus</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">buttonStatuses</span><span class="p">[</span><span class="kr">number</span><span class="p">];</span>

<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="nx">status</span> <span class="k">of</span> <span class="nx">buttonStatus</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// ? the type of status here is ButtonStatus</span>
  <span class="c1">// ? always iterate in order</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>[✔] String Enums
    <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// String enums</span>
<span class="c1">// ? Just do it</span>
<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="nx">status</span> <span class="k">in</span> <span class="nx">ButtonStatus</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// ? The type of status here is string</span>
  <span class="c1">// ? iteration order not guaranteed with "in"</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p><strong>Trả lại 1 point cho String Enums</strong></p>

<table>
  <thead>
    <tr>
      <th>Union Type</th>
      <th>String Enums</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<h2 id="renaming">Renaming</h2>

<ul>
  <li>[✘] Union Type
    <ul>
      <li><strong>Find and replace</strong> toàn Project</li>
    </ul>
  </li>
  <li>[✔] String Enums
    <ul>
      <li><strong>Right click &gt; Rename symbol</strong>
<img src="/assets/img/posts/2023-10-30-why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/rename-symbol.gif" alt="" /></li>
    </ul>
  </li>
</ul>

<p><strong>Rõ ràng String Enums tiện hơn, trả thêm 1 point cho String Enums</strong></p>

<table>
  <thead>
    <tr>
      <th>Union Type</th>
      <th>String Enums</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<h2 id="tổng-kết">Tổng kết</h2>

<p>Trong bài viết so sánh trên quan điểm cá nhân mình thì <strong>Union Type</strong> nhận được <strong>4 điểm</strong> và <strong>String Enums</strong> nhận được <strong>2 điểm</strong>!</p>

<p>Hãy sử dụng <strong>Union types by default</strong> :laughing:</p>

<p>Nếu bạn có quan điểm khác, hãy để lại comment dưới bài viết này, xin chân thành cảm ơn các góp ý từ bạn đọc 🌺</p>]]></content><author><name>Cùi Bắp</name></author><category term="Tips" /><category term="Typescript" /><summary type="html"><![CDATA[Khi sử dụng TypeScript, chúng ta thường gặp phải trường hợp xác định kiểu của một biến mà giá trị của nó rơi vào một hay nhiều khả năng khác nhau.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2023-10-30-why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2023-10-30-why-should-you-use-string-literal-union-types-instead-of-string-enums-in-typescript/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mở ứng dụng Local trên Internet bằng tính năng VSCode - Local Port Forwarding</title><link href="https://euclid1990.github.io/tips/access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/" rel="alternate" type="text/html" title="Mở ứng dụng Local trên Internet bằng tính năng VSCode - Local Port Forwarding" /><published>2023-09-11T08:00:00+00:00</published><updated>2023-09-11T08:00:00+00:00</updated><id>https://euclid1990.github.io/tips/access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature</id><content type="html" xml:base="https://euclid1990.github.io/tips/access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/"><![CDATA[<p>Trong quá trình phát triển ứng dụng, web/app chắc hẳn mỗi chúng ta đều đã từng gặp bài toán cần chia sẻ ứng dụng của mình cho một bên khác thông qua internet. Ví dụ:</p>
<ul>
  <li>Bạn muốn demo website đang code cho khách hàng truy cập từ xa</li>
  <li>Bạn cần trỏ mobile app &gt; backend API endpoint tới server trên máy cá nhân cho tiện debug</li>
  <li>Bạn muốn test tính năng webhook từ một service bên thứ 3 tới endpoint trỏ tới server trên localhost</li>
  <li>…</li>
</ul>

<p><em>Việc bạn công khai ứng dụng lên internet sẽ không được khuyến khích vì lý do bảo mật, vì vậy hãy cân nhắc nhiều cách khác nhau, trước khi thực hiện điều đó.</em></p>

<p>Trước đây để thực hiện việc export ứng dụng ra ngoài Internet có rất nhiều công cụ hỗ trợ như <strong><a href="https://ngrok.com/">Ngrok</a></strong>, <strong><a href="https://theboroer.github.io/localtunnel-www/">Localtunnel</a></strong>, …</p>

<p>Trong bài viết này mình xin giới thiệu một công cụ mới, hỗ trợ Developer có thể làm việc này rất dễ dàng và hoàn toàn miễn phí, được cung cấp bởi <strong><a href="https://code.visualstudio.com/">VSCode</a></strong> ➡︎ <strong><a href="https://code.visualstudio.com/docs/editor/port-forwarding">Local Port Forwarding</a></strong>. Tính năng <strong><a href="https://code.visualstudio.com/updates/v1_82">Built-in port forwarding - Forward local server ports from within VS Code</a></strong> được xuất hiện trên <strong><a href="https://code.visualstudio.com/">VSCode</a></strong> từ phiên bản <strong>1.82.0</strong>.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/release_note_1.82.0.png" alt="" class="align-center" /></p>

<h2 id="các-bước-thực-hiện">Các bước thực hiện</h2>

<h3 id="bước-1">Bước 1</h3>

<p>Chuẩn bị ứng dụng, trong bài viết này mình sử dụng một web server chạy trên <strong>Localhost</strong> dùng <strong>Port 3000</strong></p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/access_local_address.png" alt="" class="align-center" /></p>

<p>Tất nhiên hãy để ý, phiên bản <strong>VSCode</strong> mà bạn đang sử dụng phải là phiên bản <strong>≥ 1.82.0</strong>.</p>

<h3 id="bước-2">Bước 2</h3>

<p>Sau khi đã khởi chạy server trên máy cá nhân, bạn tiến hành mở Project bằng <strong>VSCode</strong>, sau đó nhấn tổ hợp phím Ctr + Shift + P (Windows/Linux) hoặc Cmd + Shift + P (MacOS), tiếp đó tìm tới <strong>Forward a Port</strong>.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/command_palette.png" alt="" class="align-center" /></p>

<p>Bấm vào <strong>Forward A Port</strong> button</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/forward_port.png" alt="" class="align-center" /></p>

<h3 id="bước-3">Bước 3</h3>

<p>Nhập <strong>Port</strong> của server trên <strong>Local</strong>. Ở đây Bun server mình đang khởi động chạy trên cổng 3000</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/enter_local_port.png" alt="" class="align-center" /></p>

<p>Sau đó nhấn <strong>Enter</strong> và chọn <strong>Allow</strong></p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/allow_local_tunnel_port_forwarding.png" alt="" class="align-center" /></p>

<p><strong>VSCode</strong> sẽ yêu cầu bạn tiến hành cấp quyền cho ứng dụng có thể truy cập tài khoản <strong>Github</strong> quyền <em>read-only</em></p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/vscode_authorize.png" alt="" class="align-center" /></p>

<p>Chọn <strong>Authorize Visual-Studio-Code</strong> và đợi một chút, bên ứng dụng <strong>VSCode</strong> sẽ hiện Logs khởi tạo địa chỉ cho ứng dụng của bạn trên Internet.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/port_forwarding_logs.png" alt="" class="align-center" /></p>

<p>Chúng ta có thể tìm được địa chỉ <strong>Domain Address</strong> tại đây:</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/forwarded_address.png" alt="" class="align-center" /></p>

<h3 id="bước-4">Bước 4</h3>

<p>Truy cập thử <strong>Endpoint</strong> mà <strong>Port Forwarding</strong> cung cấp.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/ms_dev_tunnels_authorize.png" alt="" class="align-center" /></p>

<p>Sau khi cấp quyền cho <strong>Dev Tunnels</strong> của <strong>Microsoft</strong>, chúng ta đã có thể truy cập ứng dụng từ Internet.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/access_forwarded_address.png" alt="" class="align-center" /></p>

<p>:heart_eyes:</p>

<p><strong>Mặc định</strong> địa chỉ này sẽ Private, tức là chỉ có bạn mới có thể truy cập được, nếu muốn công khai cho người ngoài truy cập thì hãy thiết lập <strong>Public Address</strong> trong cột <strong>Visibility</strong> như hình bên dưới.</p>

<p><img src="/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/public_adress.png" alt="" class="align-center" /></p>

<h2 id="tổng-kết">Tổng kết</h2>

<p>Trên đây mình đã giới thiệu tới bạn đọc cách chuyển tiếp Port của ứng dụng trên Local lên Internet với <strong>VSCode</strong>, hy vọng sẽ giúp ích cho bạn đọc.</p>

<blockquote>
  <p>Ngoài lề, forwarded ports được bảo mật thế nào ?</p>

  <p>Mặc định, cả việc hosting và connecting tới tunnel đều yêu cầu authentication từ cùng một GitHub và Microsoft account.</p>

  <p>VSCode không thiết lập bất kỳ listener nào ngoài việc kết nối ra tới services được hosted trên Azure.</p>

  <p>Tuy nhiên, một khi <strong>Public Port</strong> (Thay vì <strong>Private</strong>), bất kỳ người dùng nào cũng có thể truy cập được. Cần cẩn thận để tránh lưu trữ bất kỳ thông tin bí mật hoặc dịch vụ không an toàn nào trên các cổng đó.</p>

  <p>Nếu một tổ chức muốn kiểm soát quyền truy cập vào <strong>Forward Port</strong>, bạn có thể cho phép hoặc từ chối quyền truy cập vào miền <code class="language-plaintext highlighter-rouge">global.rel.tunnels.api.visualstudio.com</code></p>
</blockquote>

<blockquote>
  <p>Tham khảo thêm các giới hạn về Port Forwarding tại <strong><a href="https://code.visualstudio.com/docs/remote/tunnels#_are-there-usage-limits-for-the-tunneling-service">đây</a></strong></p>
</blockquote>]]></content><author><name>Cùi Bắp</name></author><category term="Tips" /><category term="Debug" /><category term="Vscode" /><category term="Port Forwarding" /><summary type="html"><![CDATA[Trong quá trình phát triển ứng dụng, web/app chắc hẳn mỗi chúng ta đều đã từng gặp bài toán cần chia sẻ ứng dụng của mình cho một bên khác thông qua internet. Ví dụ: Bạn muốn demo website đang code cho khách hàng truy cập từ xa Bạn cần trỏ mobile app &gt; backend API endpoint tới server trên máy cá nhân cho tiện debug Bạn muốn test tính năng webhook từ một service bên thứ 3 tới endpoint trỏ tới server trên localhost …]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2023-09-11-access-localhost-from-the-internet-through-vscode-local-port-forwarding-feature/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Nâng cấp phiên bản AWS RDS MySQL từ 5.7 lên 8.0</title><link href="https://euclid1990.github.io/migration/upgrade-aws-rds-mysql-from-57-to-80/" rel="alternate" type="text/html" title="Nâng cấp phiên bản AWS RDS MySQL từ 5.7 lên 8.0" /><published>2022-09-09T00:49:04+00:00</published><updated>2022-09-09T00:49:04+00:00</updated><id>https://euclid1990.github.io/migration/upgrade-aws-rds-mysql-from-57-to-80</id><content type="html" xml:base="https://euclid1990.github.io/migration/upgrade-aws-rds-mysql-from-57-to-80/"><![CDATA[<p>Đã hơn 5 tháng trời trôi qua không viết một bài nào trên Blog, và mình đã quay trở lại rồi đây 😂</p>

<p>Mình sẽ khởi động một chuỗi các bài viết mang tính thực chiến hơn là hướng dẫn chi tiết. Dù cho phương pháp trong bài viết có thể không là tối ưu hay cao siêu nhất … nhưng chắc chắn không phải kiểu bài viết chưa xài trên môi trường thực tế đã đem đi khè người nghe, khiến người đọc bị Bias theo suy nghĩ của tác giả 🤣</p>

<p>Nội dung bài viết có thể không phải 100% là của mình mà là của anh em đồng nghiệp, bạn bè gần xa để lại. Và toàn bộ kiến thức này được sử dụng trong dự án thực (<em>nghe vậy thôi chứ anh em cũng đừng tin tưởng 100%</em> 😆)</p>

<p>Để mở đầu, mình sẽ chia sẻ tới các bạn một issue mà bạn <a href="https://ttuan.xyz/">Trần Văn Tuấn</a> đồng nghiệp của mình đã điều tra cùng khách nhằm giải quyết bài toán nâng cấp phiên bản <strong>MySQL</strong> từ <strong>5.7</strong> lên <strong>8.0</strong> trên Production (sản phẩm hơn 30 triệu end-users) sử dụng <strong>Amazon Relational Database Service (RDS)</strong>. Nội dung bài viết này mình đã Remix lại một chút :joy:</p>

<hr />

<h2 id="1-vấn-đề">1. Vấn đề</h2>

<p>Hệ thống hiện tại sử dụng Amazon Relational Database Service (RDS) phiên bản MySQL 5.7 và Khách hàng mong muốn nâng cấp lên phiên bản MySQL 8.0</p>

<p>Mỗi phiên bản của không chỉ <strong>MySQL</strong> mà đa số các phần mềm đều có định dạng như sau <code class="language-plaintext highlighter-rouge">MAJOR.MINOR.PATCH</code>, do đó việc upgrade MySQL <code class="language-plaintext highlighter-rouge">5.7</code> → <code class="language-plaintext highlighter-rouge">8.0</code>, đồng nghĩa với việc bạn thực hiện một MAJOR upgrade. Dĩ nhiên theo như tài liệu từ chính chủ thì:</p>

<blockquote>
  <p>Major version upgrades can contain database changes that are not backward-compatible with existing applications. As a result, you must manually perform major version upgrades of your DB instances. <a href="#references">[1]</a></p>
</blockquote>

<p>→ Tin buồn, bạn buộc phải upgrade bằng tay - upgrade manually ✋</p>

<blockquote>
  <p>For major version upgrades, you must manually modify the DB engine version through the AWS Management Console, AWS CLI, or RDS API. <a href="#references">[2]</a></p>
</blockquote>

<p>→ Và nếu như đang sử dụng AWS RDS, sẽ có những công cụ sau để nâng cấp phiên bản <strong>MySQL</strong>: AWS Console, AWS CLI hoặc RDS API. Và để giảm thiểu rủi ro thì việc sử dụng AWS CLI được khuyến khích hơn cả.</p>

<p>Cho dù dùng công cụ nào bạn cũng sẽ phải đương đầu với bài toán <strong>Downtime</strong> trên <strong>Production</strong>, như vậy nhiệm vụ của chúng ta ngoài việc đảm bảo hệ thống chạy ổn định trên phiên bản mới, còn phải giảm thiểu việc dịch vụ ngừng hoạt động khi đang tiến hành nâng cấp hệ thống.</p>

<h2 id="2-giải-pháp">2. Giải pháp</h2>

<p>Có 2 hướng tiếp cận để nâng cấp MySQL engine version trên AWS RDS:</p>

<h3 id="hướng-1">Hướng 1</h3>

<p><strong>Upgrade Directly</strong>, nâng cấp trực tiếp DB instance hiện tại từ MySQL v5.7 sang v8.0</p>

<h3 id="hướng-2">Hướng 2</h3>

<p><strong>Use a Read Replica</strong> để nâng cấp, mục tiêu giảm Downtime của MySQL Database:</p>

<ul>
  <li>Tạo một read replica từ master DB instance.</li>
  <li>Upgrade MySQL engine version của read replica instance.</li>
  <li>Promote read replica instance sang standalone DB instance → <strong>New</strong> master DB instance.</li>
  <li>Đổi DNS endpoint từ RDS endpoint hiện tại sang master DB instance’s endpoint mới.</li>
</ul>

<p>Cho dù chọn cách nào thì bạn cũng cần chuẩn bị và diễn tập trước khi thao tác trên môi trường người dùng 😎</p>

<h2 id="3-chuẩn-bị">3. Chuẩn bị</h2>

<h3 id="31-prechecks-trước-khi-nâng-cấp-mysql-57-lên-80">3.1. Prechecks trước khi nâng cấp MySQL 5.7 lên 8.0</h3>

<blockquote>
  <p>MySQL 8.0 includes a number of incompatibilities with MySQL 5.7. These incompatibilities can cause problems during an upgrade from MySQL 5.7 to MySQL 8.0. So, some preparation might be required on your database for the upgrade to be successful. <a href="#references">[1]</a></p>
</blockquote>

<p>Để đảm bảo việc nâng cấp suôn sẻ thì bạn cần đảm bảo các yêu cầu sau:</p>

<ul>
  <li>Không có tables nào sử dụng các kiểu dữ liệu và hàm đã lỗi thời. Nếu có thì bạn cần sử dụng <code class="language-plaintext highlighter-rouge">REPAIR TABLE</code> trước khi nâng cấp sang bản 8.0.</li>
  <li>Không tồn tại các orphan <code class="language-plaintext highlighter-rouge">*.frm</code> files, keyword violations</li>
  <li>Trigger nếu có phải đảm bảo không bị thiếu</li>
  <li>Chi tiết hơn anh em xem tại <a href="https://dev.mysql.com/doc/refman/8.0/en/upgrade-prerequisites.html">đây</a>.</li>
</ul>

<p>Khi chúng ta bắt đầu upgrade thì AWS RDS sẽ thực hiện việc prechecks tự động để xác định các không tương thích. Tất nhiên nếu tồn tại, thì RDS sẽ ngăn ngừa việc upgrade và cho chúng ta một mả log để điều tra.</p>

<p>Cách chuẩn bị tốt nhất là chúng ta nên tạo test instance từ staging db snapshot, sau đó thử tiến hành upgrade, get log file xem có chỗ nào không tương thích không và tiến hành fix trên local trước.</p>

<p>MySQL cũng có sẵn các hướng dẫn xử lý vấn đề mà chúng ta có thể tham khảo:</p>

<ul>
  <li><a href="https://dev.mysql.com/doc/refman/8.0/en/upgrading.html">Upgrading MySQL</a></li>
  <li><a href="https://dev.mysql.com/blog-archive/upgrading-to-mysql-8-0-here-is-what-you-need-to-know/">Upgrading to MySQL 8.0? Here is what you need to know</a></li>
</ul>

<h3 id="32-bật-sao-lưu-dự-phòng">3.2. Bật sao lưu dự phòng</h3>

<p>Hãy đảm bảo rằng tính năng Auto backup được kích hoạt (hoặc giá trị của <code class="language-plaintext highlighter-rouge">backup retention period</code> ≥ 0).</p>

<blockquote>
  <p>Amazon RDS takes two DB snapshots during the upgrade process. The first DB snapshot is of the DB instance before any upgrade changes have been made. If the upgrade doesn’t work for your databases, you can restore this snapshot to create a DB instance running the old version. The second DB snapshot is taken when the upgrade completes.</p>
</blockquote>

<blockquote>
  <p>Amazon RDS only takes DB snapshots if you have set the backup retention period for your DB instance to a number greater than 0. To change your backup retention period, see <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html">Modifying an Amazon RDS DB instance</a>. <a href="#references">[1]</a></p>
</blockquote>

<p>Nếu option này không được kích hoạt thì chúng ta sẽ cần tạo snapshot thủ công.</p>

<blockquote>
  <p>A snapshot is taken (if backups are enabled) while the instance is still running on the previous version. If Amazon RDS doesn’t find any recent backups, a full snapshot is taken during the upgrade process, which can impact the overall upgrade time. <a href="#references">[3]</a></p>
</blockquote>

<p>Việc chủ động tạo một bản snapshot trước khi upgrade sẽ giúp giảm thời gian upgrade đi đáng kể.</p>

<p>Chúng ta có thể dùng aws-cli với câu lệnh sau:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws rds create-db-snapshot <span class="se">\</span>
    <span class="nt">--db-instance-identifier</span> <span class="nv">$db_instance_name</span> <span class="se">\</span>
    <span class="nt">--db-snapshot-identifier</span> <span class="nv">$db_snapshot_name</span>
</code></pre></div></div>

<h3 id="33-một-số-điểm-cần-lưu-ý-khác">3.3. Một số điểm cần lưu ý khác</h3>

<ol>
  <li>Upgrade DB instance OS (Optional)</li>
</ol>

<blockquote>
  <p>To perform a major version upgrade for a MySQL version 5.6 DB instance on Amazon RDS to MySQL version 5.7 or later, first perform any available OS updates. After OS updates are complete, upgrade to each major version: 5.6 to 5.7 and then 5.7 to 8.0. MySQL DB instances created before April 24, 2014, show an available OS update until the update has been applied. For more information on OS updates, see <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Maintenance.html#USER_UpgradeDBInstance.OSUpgrades">Applying updates for a DB instance</a>. <a href="#references">[1]</a></p>
</blockquote>

<p>Sẽ tốt hơn nếu hệ điều hành của DB instance được cập nhật.</p>

<ol>
  <li>Recheck targets which are needed to be upgraded
    <blockquote>
      <p>If your MySQL DB instance is using read replicas, you must upgrade all of the read replicas before upgrading the source instance.</p>

      <p>If your DB instance is in a Multi-AZ deployment, both the primary and standby replicas are upgraded. Your DB instance will not be available until the upgrade is complete.</p>
    </blockquote>
  </li>
</ol>

<p>Chúng ta sẽ cần phải nâng cấp Read Replicas instances và DB instances ở cả các AZ khác.</p>

<h2 id="4-tiến-hành-upgrade">4. Tiến hành Upgrade</h2>

<p><strong>Highlight Recommend: <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.MySQL.html#USER_UpgradeDBInstance.MySQL.UpgradeTesting">Testing an upgrade</a> trước khi bạn quyết định sử dụng phương pháp nào</strong></p>

<h3 id="41-upgrade-directly">4.1. Upgrade Directly</h3>

<ul>
  <li>
    <p>Sử dụng command sau:</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws rds modify-db-instance <span class="se">\</span>
    <span class="nt">--db-instance-identifier</span> <span class="nv">$db_instance_name</span> <span class="se">\</span>
    <span class="nt">--engine-version</span> <span class="nv">$new_mysql_version</span> <span class="se">\</span>
    <span class="nt">--allow-major-version-upgrade</span> <span class="se">\</span>
    <span class="nt">--apply-immediately</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Thời gian Downtime</p>

    <blockquote>
      <p>Database engine upgrades require downtime. The duration of the downtime varies based on the size of your DB instance.</p>

      <p>MySQL major version upgrades typically complete in about 10 minutes. Some upgrades might take longer because of the DB instance class size or because the instance doesn’t follow certain operational guidelines in Best practices for Amazon RDS. <a href="#references">[1]</a></p>
    </blockquote>

    <p>Chạy thử với free-tier db instance <code class="language-plaintext highlighter-rouge">db.t3.micro</code>, mất khoảng ~12 minutes để upgrade thành công.</p>

    <p>Tuy nhiên trong quá trình upgrading, db instance sẽ bị shuts down ở một chế độ đặc biệt có tên là <code class="language-plaintext highlighter-rouge">a slow shutdown</code> nhằm đảm bảo tính nhất quán (consistency). Chính vì lý do đó mà trong 5 phút đầu của quá trình upgrade, ứng dụng của chúng ta vẫn có thể kết nối được tới DB.</p>
  </li>
  <li>
    <p>Xác nhận</p>

    <p>Để đảm bảo DB instance đã được upgrade lên đúng phiên bản mong muốn, sử dụng lệnh sau:</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws rds describe-db-instances <span class="se">\</span>
  <span class="nt">--db-instance-identifier</span> <span class="nv">$db_instance_name</span> <span class="se">\</span>
  <span class="nt">--query</span> <span class="s1">'DBInstances[*].{DBInstanceIdentifier:DBInstanceIdentifier,DBInstanceStatus:DBInstanceStatus,EngineVersion:EngineVersion}'</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="42-using-a-read-replica-to-reduce-downtime">4.2. Using a read replica to reduce downtime</h3>

<p>Hướng tiếp cận này đòi hỏi DB instance phải được bật tính năng <code class="language-plaintext highlighter-rouge">Auto Backup</code>.</p>

<p>Các bước được dùng trong upgrade bạn có thể tham khảo (<a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.MySQL.html#USER_UpgradeDBInstance.MySQL.ReducedDowntime">chi tiết tại đây</a>):</p>

<ul>
  <li>
    <p>Để xây dựng môi trường test chúng ta có thể làm theo các bước sau</p>

    <ul>
      <li>
        <p>Create a read replica of your MySQL 5.7 instance.</p>

        <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">read_replica_db_instance_name</span><span class="o">=</span><span class="s2">"read-replica-rds-upgrade-testing"</span>

<span class="c"># Create read replica</span>
aws rds create-db-instance-read-replica <span class="se">\</span>
    <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span> <span class="se">\</span>
    <span class="nt">--source-db-instance-identifier</span> <span class="nv">$db_instance_name</span>

<span class="c"># Wait util read replica db instance becomes available (~8 minutes)</span>
aws rds <span class="nb">wait </span>db-instance-available <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span>
</code></pre></div>        </div>
      </li>
      <li>
        <p>(Không bắt buộc) Khi read replica được khởi tạo và <code class="language-plaintext highlighter-rouge">Status</code> hiển thị <code class="language-plaintext highlighter-rouge">Available</code>, chuyển đổi read replica sang Multi-AZ deployment và bật tính năng backups.</p>
      </li>
      <li>
        <p>Khi read replica <code class="language-plaintext highlighter-rouge">Status</code> hiển thị <code class="language-plaintext highlighter-rouge">Available</code>, Nâng cấp read replica sang MySQL 8.0.</p>

        <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Upgrade</span>
aws rds modify-db-instance <span class="se">\</span>
    <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span> <span class="se">\</span>
    <span class="nt">--engine-version</span> <span class="nv">$new_mysql_version</span> <span class="se">\</span>
    <span class="nt">--allow-major-version-upgrade</span> <span class="se">\</span>
    <span class="nt">--apply-immediately</span>

<span class="c"># Wait util read replica db instance becomes available (~13 minutes)</span>
aws rds <span class="nb">wait </span>db-instance-available <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span>

<span class="c"># Ensure that upgraded read replica has latest MySQL engine version</span>
aws rds describe-db-instances <span class="se">\</span>
  <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span> <span class="se">\</span>
  <span class="nt">--query</span> <span class="s1">'DBInstances[*].{DBInstanceIdentifier:DBInstanceIdentifier,DBInstanceStatus:DBInstanceStatus,EngineVersion:EngineVersion}'</span>

</code></pre></div>        </div>
      </li>
      <li>
        <p>Khi quá trình upgrade hoàn thành <code class="language-plaintext highlighter-rouge">Status</code> hiển thị <code class="language-plaintext highlighter-rouge">Available</code> hãy xác nhận lại xem read replica có được cập nhật với phiên bản MySQL 5.7 DB chưa.</p>
      </li>
      <li>
        <p>(Không bắt buộc) Bạn có thể tạo thêm read replica để tăng HA.</p>
      </li>
      <li>
        <p>(Không bắt buộc) Cấu hình các custom DB parameter group cho read replica.</p>
      </li>
      <li>
        <p>Promote read replica lên standalone DB instance</p>

        <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Promote read replica</span>
aws rds promote-read-replica <span class="se">\</span>
    <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span>

<span class="c"># Wait util read replica db instance become available (~5 minutes)</span>
aws rds <span class="nb">wait </span>db-instance-available <span class="nt">--db-instance-identifier</span> <span class="nv">$read_replica_db_instance_name</span>

</code></pre></div>        </div>

        <p>Tuy nhiên bạn cần lưu ý sau</p>
        <blockquote>
          <p>When you promote your MySQL 8.0 read replica to a standalone DB instance, it no longer is a replica of your MySQL 5.7 DB instance. We recommend that you promote your MySQL 8.0 read replica during a maintenance window when your source MySQL 5.7 DB instance is in read-only mode and all write operations are suspended. When the promotion is completed, you can direct your write operations to the upgraded MySQL 8.0 DB instance to ensure that no write operations are lost.</p>
        </blockquote>

        <blockquote>
          <p>In addition, we recommend that, before promoting your MySQL 8.0 read replica, you perform all necessary data definition language (DDL) operations on your MySQL 8.0 read replica. An example is creating indexes. This approach avoids negative effects on the performance of the MySQL 8.0 read replica after it has been promoted. <a href="#references">[1]</a></p>
        </blockquote>
      </li>
      <li>
        <p>Đổi kết nối của ứng dụng sang new MySQL 8.0 DB instance. (Update DNS endpoint or DB setting in app)</p>
      </li>
    </ul>
  </li>
  <li>
    <p>Thời gian Downtime</p>
    <ul>
      <li>Short downtime: Trong khi read replica instance tiến hành upgraded, ứng dụng của chúng ta vẫn có thể truy cập vào <code class="language-plaintext highlighter-rouge">primary</code> db instance. Sau quá trình upgrade tất cả các dữ liệu sẽ được đồng bộ từ <code class="language-plaintext highlighter-rouge">primary</code> sang <code class="language-plaintext highlighter-rouge">read replica</code> instance.</li>
    </ul>
  </li>
</ul>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">Downtime = Time to promote read replica to standalone instance + Time to change DNS endpoint/DB setting app</code>.</p>
</blockquote>

<p>Thực tế với free-tier db instance, sẽ mất khoảng ~5 phút để promote read replica instance.</p>

<ul>
  <li>
    <p>Lợi ích</p>

    <p>Easy to rollback: Nếu chẳng may có bất kỳ vấn đề nào xảy ra trong quá trình nâng cấp, chúng ta có thể dễ dàng rollbackk lại bằng cách thay đổi DNS endpoint/DB endpoint sang <code class="language-plaintext highlighter-rouge">v5.7</code> DB instance.</p>
  </li>
</ul>

<h4 id="tham-khảo-thêm-các-bài-viết-về-hướng-tiếp-cận-này">Tham khảo thêm các bài viết về hướng tiếp cận này</h4>

<ul>
  <li><a href="https://blogs.halodoc.io/upgrading-to-rds-mysql-8-0-with-minimum-downtime/">Upgrading to RDS MySQL 8.0 with minimum downtime</a></li>
  <li><a href="https://technologytickles.com/2019/03/24/upgrading-a-mysql-db-on-aws-rds/">Upgrading a MySQL DB on AWS RDS</a></li>
  <li><a href="https://medium.com/99dotco/our-mysql-rds-upgrade-journey-cutting-down-downtime-by-11200-and-lessons-learned-1fa828e6009c">Our MySQL RDS Upgrade Journey: Cutting Down Downtime by 11,200% and Lessons Learned</a></li>
  <li><a href="https://workmarket.tech/zero-downtime-maintenances-on-mysql-rds-ba13b51103c2">Zero Downtime Maintenances on MySQL RDS</a></li>
</ul>

<h2 id="5-hậu-nâng-cấp">5. Hậu nâng cấp</h2>

<h3 id="51-kiểm-tra-lại-tiến-trình-nâng-cấp-đã-hoàn-thành-chưa">5.1. Kiểm tra lại tiến trình nâng cấp đã hoàn thành chưa</h3>

<p>Chúng ta có thể sử dụng câu lệnh sau để kiểm tra xem trạng thái của DB instance đã sẵn sàng chưa</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws rds wait db-instance-available --db-instance-identifier $db_instance_name
</code></pre></div></div>

<h3 id="52-retest-the-system">5.2. Retest the system</h3>

<p>Kết nối ứng dụng vào new DB instance, và thực hiện retest hành vi hệ thống.</p>

<h3 id="53-rollback-trong-trường-hợp-upgrade-failed">5.3. Rollback trong trường hợp upgrade failed</h3>

<h4 id="531-trường-hợp-upgrade-directly">5.3.1. Trường hợp Upgrade directly</h4>

<blockquote>
  <p>After the upgrade is complete, you can’t revert to the previous version of the database engine. If you want to return to the previous version, restore the first DB snapshot taken to create a new DB instance. <a href="#references">[1]</a></p>
</blockquote>

<ul>
  <li>Chúng ta sẽ không thể rollback engine hiện tại (v8.0) về engine phiên bản trước (v5.7). Mà sẽ buộc phải upgrade latest snapshot được tạo trước khi tiến hành nâng cấp:</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">new_db_instance_name</span><span class="o">=</span><span class="s2">"new-rds-upgrade-testing"</span>

 aws rds restore-db-instance-from-db-snapshot <span class="se">\</span>
  <span class="nt">--db-instance-identifier</span> <span class="nv">$new_db_instance_name</span> <span class="se">\</span>
  <span class="nt">--db-snapshot-identifier</span> <span class="nv">$db_snapshot_name</span>
</code></pre></div></div>

<ul>
  <li>Cập nhật lại endpoints / Route 53 trỏ về new RDS, và connection endpoint trong ứng dụng sang new MySQL RDS instance.</li>
</ul>

<h4 id="532-trường-hợp-upgrade-using-read-replica">5.3.2. Trường hợp Upgrade using read replica</h4>

<ul>
  <li>Chúng ta chỉ cần upgrade DNS endpoint sang DB endpoint (v5.7) cũ vì DB instance này chạy hoàn toàn độc lập với RDS master instance (v8.0).</li>
</ul>

<h2 id="6-kết-luận">6. Kết luận</h2>

<ul>
  <li>Phương án 2 được khuyến khích sử dụng vì những lợi ích sau
    <ul>
      <li>Short downtime duration</li>
      <li>Easy to rollback.</li>
    </ul>
  </li>
  <li>Dù dùng cách gì thì chúng ta cũng cần cẩn thận kiểm tra, diễn tập trước khi tiến hành, và đây là các đầu mục lớn cần làm:
    <ul>
      <li>Prechecks for upgrades.</li>
      <li>Backup data to snapshot.</li>
      <li>Testing an upgrade with test DB instance. ( Tạo một snapshot từ staging db instance =&gt; Tạo một DB instance mới từ snapshot này. Thử tiến hành upgrade trên DB instance này).</li>
      <li>Để hạn chế lỗi, hãy biến toàn bộ các thao tác về scripts: backup, upgrade and rollback vì làm như vậy sẽ giảm thiểu rủi ro, nhớ nhớ quên quên.</li>
    </ul>
  </li>
  <li>Tính toán sơ bộ thời gian dùng để upgrade: việc này có thể phụ thuộc vào số lượng instance, kích thước database, phương án upgrade, database schema, engine, …</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.MySQL.html#USER_UpgradeDBInstance.MySQL.Major">[1] AWS Official User Guide - Upgrading the MySQL DB engine</a></li>
  <li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Upgrading.html">[2] AWS Official User Guide - Upgrading a DB instance engine version</a></li>
  <li><a href="https://aws.amazon.com/premiumsupport/knowledge-center/rds-mysql-version-upgrade/">[3] AWS Premium Support - I want to upgrade my Amazon RDS MySQL version</a></li>
  <li><a href="https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-for-mysql-and-amazon-rds-for-mariadb/">[4] Best Practices for Upgrading Amazon RDS for MySQL and Amazon RDS for MariaDB</a></li>
</ul>]]></content><author><name>Cùi Bắp</name></author><category term="Migration" /><category term="Real Life" /><category term="AWS" /><category term="RDS" /><summary type="html"><![CDATA[Đã hơn 5 tháng trời trôi qua không viết một bài nào trên Blog, và mình đã quay trở lại rồi đây 😂]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2022-09-09-upgrade-aws-rds-mysql-from-57-to-80/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2022-09-09-upgrade-aws-rds-mysql-from-57-to-80/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Phát hiện tình cờ về timezone của Việt Nam</title><link href="https://euclid1990.github.io/til/vietnam-timezone-before-1975-year/" rel="alternate" type="text/html" title="Phát hiện tình cờ về timezone của Việt Nam" /><published>2022-03-06T00:49:04+00:00</published><updated>2022-03-06T00:49:04+00:00</updated><id>https://euclid1990.github.io/til/vietnam-timezone-before-1975-year</id><content type="html" xml:base="https://euclid1990.github.io/til/vietnam-timezone-before-1975-year/"><![CDATA[<p>Nửa đêm ngồi code function chuyển đổi hiển thị thời gian cho người dùng trên nhiều vị trí địa lý mới phát hiện 1 sự thú vị không hề nhẹ 😂.</p>

<p>Anh em coder động vào bài toán xử lý các trường datetime trong Database chắc hẳn ai cũng từng phải convert thời gian từ múi giờ này qua múi giờ khác. Trên <strong>PHP</strong> thì có <a href="https://carbon.nesbot.com">Carbon</a>, <strong>Javascript</strong> thì dùng <a href="https://momentjs.com/timezone">Moment</a>, <a href="https://date-fns.org/docs/Getting-Started">Date-fns</a>… đều là những thư viện khá mạnh phục vụ tận răng.</p>

<p>Để convert thời gian từ múi giờ <code class="language-plaintext highlighter-rouge">UTC</code> sang múi giờ của <code class="language-plaintext highlighter-rouge">Việt Nam (UTC+07)</code> thì có mấy timezone có thể dùng:</p>
<ul>
  <li><em>Asia/Bangkok</em></li>
  <li><em>Asia/Ho_Chi_Minh</em></li>
  <li><em>Asia/Saigon</em></li>
</ul>

<p>Mình thì trước giờ hay xài đại <em>Asia/Ho_Chi_Minh</em>, nay thử convert cái field time trong Database thì kết quả hơi choáng, code mình vẫn viết test đầy đủ mà nay dính case sau:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">moment</span><span class="p">.</span><span class="nx">tz</span><span class="p">(</span><span class="dl">'</span><span class="s1">1970-01-01T01:00:00.000Z</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">UTC</span><span class="dl">'</span><span class="p">).</span><span class="nx">tz</span><span class="p">(</span><span class="dl">'</span><span class="s1">Asia/Ho_Chi_Minh</span><span class="dl">'</span><span class="p">).</span><span class="nx">format</span><span class="p">()</span>
<span class="dl">'</span><span class="s1">1970-01-01T09:00:00+08:00</span><span class="dl">'</span>
</code></pre></div></div>

<p>Bạn thấy đấy, <strong>1970-01-01T01:00:00.000Z</strong> ở múi giờ <code class="language-plaintext highlighter-rouge">UTC</code> bị chuyển thành <strong>1970-01-01T09:00:00<em>+08:00</em></strong> ở múi giờ <code class="language-plaintext highlighter-rouge">UTC+07</code>. Kết quả chính xác phải là <strong>1970-01-01T08:00:00<em>+07:00</em></strong> chứ 😥 Tại sao timezone của Việt Nam ở thời điểm năm 1970 lại là <code class="language-plaintext highlighter-rouge">UTC+08</code> WTF !!!!</p>

<p>Vấn đề này làm mình mất 30p debug code, đâm vào cả trong thư viện mà chẳng hiểu sao nó xử lý vậy :joy:, cuối cùng phải search Wikipedia về lịch sử timezone của Việt Nam mới biết:</p>

<p><img src="/assets/img/posts/2022-03-06-vietnam-timezone-before-1975-year/vietnam_timezone.png" alt="" class="align-center" /></p>

<p>Hoá ra trước thời điểm <strong>13/06/1975</strong> các vùng miền của Việt Nam sử dụng múi giờ là khác nhau, chỗ <code class="language-plaintext highlighter-rouge">UTC+08</code> chỗ <code class="language-plaintext highlighter-rouge">UTC+07</code>.</p>

<p>Như vậy thì thư viện không hề sai :v Nhưng nếu dùng để convert field <code class="language-plaintext highlighter-rouge">TIME()</code> trong Database ra giờ thì sẽ cần lưu ý để phù hợp với logic.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">moment</span><span class="p">.</span><span class="nx">tz</span><span class="p">(</span><span class="dl">'</span><span class="s1">1975-06-13T01:00:00.000Z</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">UTC</span><span class="dl">'</span><span class="p">).</span><span class="nx">tz</span><span class="p">(</span><span class="dl">'</span><span class="s1">Asia/Ho_Chi_Minh</span><span class="dl">'</span><span class="p">).</span><span class="nx">format</span><span class="p">()</span>
<span class="dl">'</span><span class="s1">1975-06-13T08:00:00+07:00</span><span class="dl">'</span>
</code></pre></div></div>

<p>Test lại cho chắc 😎</p>

<p>Tóm lại là đừng có nghĩ <strong>timezone</strong> của một quốc gia luôn cố định ở mọi thời điểm, mà nó có thể thay đổi 🐖</p>]]></content><author><name>Cùi Bắp</name></author><category term="TIL" /><category term="Javascript" /><category term="Timezone" /><summary type="html"><![CDATA[Nửa đêm ngồi code function chuyển đổi hiển thị thời gian cho người dùng trên nhiều vị trí địa lý mới phát hiện 1 sự thú vị không hề nhẹ 😂.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2022-03-06-vietnam-timezone-before-1975-year/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2022-03-06-vietnam-timezone-before-1975-year/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tổng hợp các mẫu tin nhắn xin nghỉ thông dụng gửi cho khách hàng Nhật</title><link href="https://euclid1990.github.io/tips/collection-of-common-leave-message-templates-sent-to-japanese-clients/" rel="alternate" type="text/html" title="Tổng hợp các mẫu tin nhắn xin nghỉ thông dụng gửi cho khách hàng Nhật" /><published>2021-12-16T00:49:04+00:00</published><updated>2021-12-16T00:49:04+00:00</updated><id>https://euclid1990.github.io/tips/collection-of-common-leave-message-templates-sent-to-japanese-clients</id><content type="html" xml:base="https://euclid1990.github.io/tips/collection-of-common-leave-message-templates-sent-to-japanese-clients/"><![CDATA[<p>Trong khoảng thời gian làm việc với khác hàng là người Nhật Bản, việc đứng ở vị trí trung gian giữa đội phát triển và khách hàng nên mình phải sử dụng tiếng Nhật hằng ngày để truyền tải thông tin. Do có khác biệt về nền văn hoá và phong tục tập quán giữa hai bên nên ngày nghỉ phép, nghỉ lễ giữa phía đội Việt Nam và Nhật Bản cũng khác nhau.</p>

<p>Trước các kỳ nghỉ lễ, nghỉ phép của Việt Nam, mình sẽ cần phải thông báo cho khách những thông tin về ngày nghỉ trước khoảng 2 tuần để hai bên cũng nhau điều chỉnh &amp; sắp xếp lại công việc cho phù hợp, tránh gây ảnh hưởng tới tiến độ dự án. Và cứ mỗi lần như vậy, mình lại lọ mọ soạn các tin nhắn thông báo kiểu vậy, hoặc lục lại tin nhắn trong quá khứ để gửi cho khách hàng. Hoặc lên Google tìm kiếm. (Ví dụ với ngày Giỗ Tổ Hùng Vương bạn có thể dùng keyword: <a href="https://www.google.com/search?q=%E3%83%95%E3%83%B3%E7%8E%8B%E3%81%AE%E5%91%BD%E6%97%A5+%E7%A5%9D%E6%97%A5%E3%81%AE%E3%81%8A%E7%9F%A5%E3%82%89%E3%81%9B"><code class="language-plaintext highlighter-rouge">フン王の命日　祝日のお知らせ</code></a> sẽ ra rất nhiều kết quả). Tuy nhiên việc chọn lọc nội dung cũng khá mất thời gian.</p>

<p>Nhân dịp cuối năm 2021, cần gửi lịch nghỉ Tết dương lịch cho khách hàng, mình sẽ tổng hợp lại toàn bộ các mẫu thông báo xin nghỉ trong bài viết này. Hy vọng sẽ giúp ích ít nhiều cho mình và bạn đọc sau này. Cảm ơn anh <a href="https://www.facebook.com/ntnam1">Nguyễn Thành Nam</a> đã bỏ thời gian review và giúp mình bổ sung thêm các mẫu tin nhắn vào bài viết này 😎</p>

<p>Các mẫu tin nhắn bên dưới có các ký tự △, 〇, ◎, ◇, □, … Nếu sử dụng lại bạn cần thay thế lại bằng câu chữ, ngày giờ cho phù hợp.</p>

<p><img src="/assets/img/posts/2021-12-16-collection-of-common-leave-message-templates-sent-to-japanese-clients/announcement.jpeg" alt="" class="align-center" /></p>

<h2 id="1-thông-báo-nghỉ-lễ">1. Thông Báo Nghỉ Lễ</h2>

<h3 id="11-tết-dương-lịch">1.1. Tết Dương Lịch</h3>

<h4 id="thông-báo-trước-ngày-nghỉ-2-tuần">Thông báo trước ngày nghỉ 2 tuần</h4>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>△△株式会社　〇〇様
いつも大変お世話になっております。
弊社のベトナム年末年始休暇の件でご連絡させて頂きます。

弊社は下記の通り休業とさせていただきます。
営業休止中はご不便をお掛けいたしますが、何卒ご理解・ご協力の程、宜しくお願い申し上げます。
＜営業休止期間＞
(*)年末年始：12月◇◇日（木）～ 1月□□日（月）
※1月◉◉日より通常営業

当該期間中に頂きましたご連絡につきましては、なるべく早めに回答できるように致しますが、
対応は休業明け以降になりますので、予めご了承の程宜しくお願いいたします。
</code></pre></div></div>

<h5 id="trường-hợp-bổ-sung"><em>Trường hợp bổ sung</em></h5>

<ul>
  <li>
    <p>Nghỉ kéo dài hơn do ảnh hưởng COVID-19 khiến công ty không thể sắp xếp được cho nhân viên đi du lịch, nên cho nghỉ bù vào dịp cuối năm</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>※コロナウィルスの状況により、今年7月△△日及び〇〇日で行う予定がある会社旅行ができずに、
全員通常に通勤しましたので、年末の☆☆日（12月▲▲日~●●日）をその福祉として、お休みとなります。
また、1月◇◇日は土曜日に当たるために、1月□□日（月）が振替休日となります。
弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="thông-báo-nhắc-lại-trước-ngày-nghỉ-1-ngày">Thông báo nhắc lại trước ngày nghỉ 1 ngày</h4>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>△△株式会社　〇〇様
いつも大変お世話になっております。
◇◇です。
早いもので本年も残すところ、あとわずかとなりました。
〇〇様には1年間大変お世話になり、心より感謝しております。
なお、弊社の年内の営業は12月▲▲日までとなり、新年は1月●●日から営業開始となります。
来年も本年同様のご愛顧のほど、よろしくお願い申し上げます。
</code></pre></div></div>

<h3 id="12-tết-âm-lịch">1.2. Tết Âm Lịch</h3>

<ul>
  <li>
    <p>Mẫu 1</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>お世話になっております。
弊社の旧正月（テト）の休暇の件でご連絡させて頂きます。
弊社は下記の通り休業とさせていただきます。
営業休止中はご不便をお掛けいたしますが、何卒ご理解・ご協力の程、宜しくお願い申し上げます。

＜営業休止期間＞
旧正月（テト）: ◆◆月◇◇日 ~ ◼︎◼︎月□□日
※ ▲▲月△△日より通常営業
弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar

当該期間中に頂きましたご連絡につきましては、なるべく早めに回答できるように致します。
以上になりますが、ご了承の程宜しくお願いいたします。
</code></pre></div>    </div>
  </li>
  <li>
    <p>Mẫu 2 (Mẫu này mình cũng chưa thử gửi bao giờ 😢)</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>春節 (旧正月) 休みのお知らせ

拝啓 初冬の候、貴社ますますご繁栄のこととお慶び申し上げます。
平素は格別の御高配を賜り、厚く御礼申し上げます。
さて、誠に勝手ながら弊社では春節(旧正月)休みにつきまして、下記のとおり休業日とさせていただきます。
皆様には大変ご迷惑をおかけいたしますが、何卒ご容赦くださいませ。
これからも変わらぬお引き立てを賜りますよう宜しくお願い申し上げます。
敬具
記
1．休業実施期間：▲▲月△△日 ~ ◎◎月〇〇日
2．休業実施日数：□□日
※休業中もメールはお受けしておりますが、ご返答につきましては◆◆月◇◇日以降順次対応させて頂きます。
春節 (旧正月) 休業期間中、皆様方には大変ご迷惑をおかけいたしますが、ご理解とご協力を賜りますよう宜しくお願い致します。
以上
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="13-giỗ-tổ-hùng-vương">1.3. Giỗ Tổ Hùng Vương</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>いつもお世話になっております。
弊社では下記の通り、4月下旬の【ベトナムの雄王記念日】休暇を実施させていただきます。

＜営業休止期間＞
雄王記念日：4月〇〇（◎）

※ ▲▲月△△日より通常営業
弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar

当該期間中に頂きましたご連絡につきましては、なるべく早めに回答できるように致します。
ご不便をお掛けいたしますが、何卒ご理解頂きますようお願い申し上げます。
</code></pre></div></div>

<h3 id="14-giải-phóng-miền-nam-và-quốc-tế-lao-động-304---015">1.4. Giải phóng miền Nam và Quốc tế Lao động (30/4 - 01/5)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>いつもお世話になっております。
弊社では下記の通り、4月下旬の【南部解放記念日＆メーデー】休暇を実施させていただきます。

＜営業休止期間＞
南部解放記念日 &amp; メーデー：4月△△日(金) ~ 5月□□日（月）

※ 5月◇◇日が土曜日に当たるため、5月◆◆日（月）振替休日となります。
5月◆◆日（火）より平常通り営業いたします。
弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar

当該期間中に頂きましたご連絡につきましては、なるべく早めに回答できるように致します。
ご不便をお掛けいたしますが、何卒ご理解頂きますようお願い申し上げます。
</code></pre></div></div>

<h3 id="15-quốc-khánh-0209">1.5. Quốc Khánh (02/09)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>平素は格別のお引き立てを頂きまして、誠にありがとうございます。
誠に勝手ながら、ベトナムの建国記念日休みに伴い、
▲▲年9月02日（□）から▲▲年09月〇〇（◎）まで休業とさせて頂きます。
営業開始日は▲▲年9月□□日（◆）となります。
この間、弊社からのご返信やサービス提供ができなくなりますのでご了承願います。
皆様にはご不便をお掛けいたしますが、何卒ご寛容くださいますようお願い申し上げます。

※ 弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar
</code></pre></div></div>

<p><img src="/assets/img/posts/2021-12-16-collection-of-common-leave-message-templates-sent-to-japanese-clients/company_trip.jpeg" alt="" class="align-center" /></p>
<h2 id="2-thông-báo-nghỉ-khác">2. Thông Báo Nghỉ Khác</h2>

<h3 id="21-nghỉ-du-lịch-công-ty">2.1. Nghỉ Du Lịch Công Ty</h3>

<ul>
  <li>Thông báo nghỉ du lịch
    <ul>
      <li>
        <p>Mẫu 1</p>

        <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>いつもお世話になっております。
誠に勝手ながら、弊社では下記の期間を社員旅行のため、臨時休業とさせて頂きます。
皆様にはご不便をおかけしますが、何卒ご了承の程宜しくお願い申し上げます。

【期間：▲▲月△△日 ~ ◎◎月〇〇日】
※ ▲▲月△△日より通常勤務となります。

弊社の勤務カレンダーも共有させていただきます。
https://company.domain/calendar

当該期間中に頂きましたご連絡につきましては、なるべく早めに回答できるように致します。
予めご了承の程宜しくお願いいたします。
</code></pre></div>        </div>
      </li>
      <li>
        <p>Mẫu 2</p>

        <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>社員旅行に伴う臨時休業のお知らせ
拝啓　〇〇の候、ますますご清栄のこととお慶び申し上げます。
平素は格別のご高配を賜り、厚く御礼申し上げます。
さて、誠に勝手ながら弊社では、▲▲月△△日 ~ ◎◎月〇〇日までを社員旅行につき、臨時休業とさせていただきます。
休業中にいただきましたご連絡につきましては、◆◆月◇◇日以降に順次ご対応させていただきます。
皆様にはご不便をおかけしますが、何卒ご了承の程宜しくお願い申し上げます。
敬具
記
※ 社員旅行による休業日　▲▲月△△日 ~ ◎◎月〇〇日
※ ◆◆月◇◇日より通常通りに営業いたします。
お問い合わせ先：●●●●●●●●●●
住所：◎◎◎◎◎◎◎◎◎◎
TEL：★-★★★★-★★★★
FAX：☆-☆☆☆☆-☆☆☆☆
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>
    <p>Rủ khách đi du lịch cùng 😂</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>毎年恒例の社内旅行が下記のとおりに決定しましたのでお知らせいたします。
日時：▲▲月△△日 ~ ◎◎月〇〇日
行き先：◆◆省、□□市
宿泊先： ROYAL LOTUS RESORT AND VILLAS （5★リゾート）
参加対象：弊社の全社員および弊社のお客様
参加費：無料
*交通費（ハノイ ~ □□往復）および□□での他の費用は弊社の負担となります。
お忙しいことと存じますが、是非ご参加賜わりますようご招待申し上げます。
以上、ご返事をお待ちしております。
</code></pre></div>    </div>
  </li>
</ul>

<p>Do không phải là một Native Japanese nên có thể sẽ có sai sót về ngữ pháp, câu cú trong tiếng Nhật. Và mục đích chính là truyền tải thông tin về ngày nghỉ với đội dự án nên có thể với một số bạn nội dung này hơi đơn điệu. Nếu bạn đọc thấy có điều gì chưa hợp lý, hãy để lại comment để mình có thể cập nhật nội dung tốt hơn nhé. Cảm ơn rất nhiều 💯</p>]]></content><author><name>Cùi Bắp</name></author><category term="Tips" /><category term="Japanese" /><summary type="html"><![CDATA[Trong khoảng thời gian làm việc với khác hàng là người Nhật Bản, việc đứng ở vị trí trung gian giữa đội phát triển và khách hàng nên mình phải sử dụng tiếng Nhật hằng ngày để truyền tải thông tin. Do có khác biệt về nền văn hoá và phong tục tập quán giữa hai bên nên ngày nghỉ phép, nghỉ lễ giữa phía đội Việt Nam và Nhật Bản cũng khác nhau.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2021-12-16-collection-of-common-leave-message-templates-sent-to-japanese-clients/thumbnail.png" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2021-12-16-collection-of-common-leave-message-templates-sent-to-japanese-clients/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tuỳ biến phím tắt trên Slack và Mac với Karabiner</title><link href="https://euclid1990.github.io/tips/custom-slack-keyboard-shortcut-with-karabiner-on-mac/" rel="alternate" type="text/html" title="Tuỳ biến phím tắt trên Slack và Mac với Karabiner" /><published>2021-12-14T00:49:04+00:00</published><updated>2021-12-14T00:49:04+00:00</updated><id>https://euclid1990.github.io/tips/custom-slack-keyboard-shortcut-with-karabiner-on-mac</id><content type="html" xml:base="https://euclid1990.github.io/tips/custom-slack-keyboard-shortcut-with-karabiner-on-mac/"><![CDATA[<p>Mình hiện tại đang dùng song song 2 công cụ chat khi làm việc đó là <strong><a href="https://www.chatwork.com">Chatwork</a></strong> và <strong><a href="https://slack.com">Slack</a></strong>. Tuy nhiên khi sử dụng 2 công cụ này mình gặp phải một vấn đề đó là:</p>

<ul>
  <li>Phím tắt của 2 công cụ chat này không giống nhau
    <ul>
      <li>Ví dụ: Trên Chatwork để gửi tin nhắn bạn có thể dùng <code class="language-plaintext highlighter-rouge">shift + enter</code> trong khi trên Slack sử dụng <code class="language-plaintext highlighter-rouge">command + enter</code></li>
    </ul>
  </li>
</ul>

<p>Việc này có thể không vấn đề với nhiều người dùng nhưng với mình nó lại là cả một vấn đề, việc học và thay đổi một thói quen là một việc khá tốn thời gian và không cần thiết.</p>

<p>Ban đầu mình có hỏi thăm một người bạn, và nhận được lời khuyên sử dụng: <a href="https://www.keyboardmaestro.com">Keyboard Maestro</a>, tuy nhiên phần mềm này có giá ~36$, do đó mình quyết định thử tìm hiểu xem có cách nào mapping lại phím mà đỡ tốn kém hơn không. Kết quả là việc này có thể hoàn thành rất dễ dàng với công cụ có tên là: <strong><a href="https://karabiner-elements.pqrs.org">Karabiner Elements</a></strong>.</p>

<p>Để cài đặt <strong>Karabiner Elements</strong> các bạn truy cập trang web <a href="https://karabiner-elements.pqrs.org">https://karabiner-elements.pqrs.org</a> và tải về cũng như cài đặt một cách hoàn toàn miễn phí.</p>

<p><img src="/assets/img/posts/2021-12-14-custom-slack-keyboard-shortcut-with-karabiner-on-mac/karabiner-simple-modifications.png" alt="" class="align-center" /></p>

<p>Sau khi cài đặt xong, chúng ta sẽ thấy biểu tượng của phần mềm trên thanh trạng thái hệ thống, và có thể tuỳ biến phím đơn trên bàn phím với giao diện <code class="language-plaintext highlighter-rouge">Simple modifications</code> sẵn có. Ngoài ra bạn có thể map lại các phím chức năng tiêu chuẩn <code class="language-plaintext highlighter-rouge">F1</code>, <code class="language-plaintext highlighter-rouge">F2</code>, <code class="language-plaintext highlighter-rouge">F3</code>, <code class="language-plaintext highlighter-rouge">F4</code>, <code class="language-plaintext highlighter-rouge">F5</code>, <code class="language-plaintext highlighter-rouge">F6</code>, <code class="language-plaintext highlighter-rouge">F7</code>, <code class="language-plaintext highlighter-rouge">F8</code>, <code class="language-plaintext highlighter-rouge">F9</code>, <code class="language-plaintext highlighter-rouge">F10</code>, <code class="language-plaintext highlighter-rouge">F11</code>, <code class="language-plaintext highlighter-rouge">F12</code> trên bàn phím ngoài sang các phím chức năng của Apple MacOS như:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">F1</code> – Giảm độ sáng màn hình</li>
  <li><code class="language-plaintext highlighter-rouge">F2</code> – Tăng độ sáng màn hình</li>
  <li><code class="language-plaintext highlighter-rouge">F3</code> – Điều khiển nhiệm vụ mở</li>
  <li><code class="language-plaintext highlighter-rouge">F4</code> – Mở Launchpad</li>
  <li><code class="language-plaintext highlighter-rouge">F5</code> – Giảm độ sáng bàn phím (Chỉ trên máy tính xách tay tương thích)</li>
  <li><code class="language-plaintext highlighter-rouge">F6</code> – Tăng độ sáng bàn phím (Chỉ trên máy tính xách tay tương thích)</li>
  <li><code class="language-plaintext highlighter-rouge">F7</code> – Bỏ qua lại (Âm thanh)</li>
  <li><code class="language-plaintext highlighter-rouge">F8</code> – Tạm dừng / Phát (Âm thanh)</li>
  <li><code class="language-plaintext highlighter-rouge">F9</code> – Bỏ qua phía trước (Âm thanh)</li>
  <li><code class="language-plaintext highlighter-rouge">F10</code> – Tắt tiếng</li>
  <li><code class="language-plaintext highlighter-rouge">F11</code> – Giảm âm lượng</li>
  <li><code class="language-plaintext highlighter-rouge">F12</code> – Tăng âm lượng</li>
</ul>

<p><img src="/assets/img/posts/2021-12-14-custom-slack-keyboard-shortcut-with-karabiner-on-mac/karabiner-function-keys.png" alt="" class="align-center" /></p>

<p>bằng cách sử dụng <code class="language-plaintext highlighter-rouge">Function keys</code>.</p>

<h3 id="tuỳ-biến-tổ-hợp-phím">Tuỳ biến tổ hợp phím</h3>

<p>Để tuỳ biến lại tổ hợp phím tắt một cách phức tạp hơn bạn cần sửa config ở file sau</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>vim ~/.config/karabiner/karabiner.json
</code></pre></div></div>

<p>Bạn tìm tới mục <code class="language-plaintext highlighter-rouge">"rules":</code>, thêm object sau vào</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shift+enter to command+enter / custom Slack send messages"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"manipulators"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"return_or_enter"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"modifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"mandatory"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                        </span><span class="s2">"shift"</span><span class="w">
                    </span><span class="p">]</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"to"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"return_or_enter"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"modifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                        </span><span class="s2">"right_command"</span><span class="w">
                    </span><span class="p">]</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="p">{</span><span class="w">
                  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"frontmost_application_if"</span><span class="p">,</span><span class="w">
                  </span><span class="nl">"bundle_identifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                    </span><span class="s2">"^com</span><span class="se">\\</span><span class="s2">.tinyspeck</span><span class="se">\\</span><span class="s2">.slackmacgap$"</span><span class="w">
                  </span><span class="p">]</span><span class="w">
                </span><span class="p">}</span><span class="w">
              </span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Ở trên mình tiến hành tuỳ biến shortcut key cho việc gửi tin nhắn trên <strong>Slack</strong> sang giống như trên <strong>Chatwork</strong> (Cá nhân mình thích gõ <code class="language-plaintext highlighter-rouge">shift + enter</code> hơn), ngoài ra bạn có thể tham khảo thêm một số rules mẫu tại <a href="https://github.com/pqrs-org/KE-complex_modifications/tree/main/public/json">đây</a> hoặc một số do mình chia sẻ tại <a href="https://gist.github.com/euclid1990/ee6ed30164c8936f0cecba0194b1b8b4">đây</a></p>

<p>Chia sẻ thêm là trong đoạn tuỳ biến trên có phần <code class="language-plaintext highlighter-rouge">"bundle_identifiers"</code> để giới hạn phạm vi application sẽ kích hoạt việc tuỳ biến, và <code class="language-plaintext highlighter-rouge">bundle_identifiers</code> được mình lấy theo cách sau:</p>

<ul>
  <li>Tìm link app (Qua Google hoặc App store).
    <ul>
      <li>Ví dụ app <strong>Slack</strong>: <a href="https://apps.apple.com/app/slack/id803453959">https://apps.apple.com/app/slack/id803453959</a></li>
    </ul>
  </li>
  <li>Copy number ID từ URL
    <ul>
      <li><code class="language-plaintext highlighter-rouge">803453959</code></li>
    </ul>
  </li>
  <li>Mở đường dẫn <a href="https://itunes.apple.com/lookup?id=xxx">https://itunes.apple.com/lookup?id=xxx</a>, thay xxx thành number ID bạn có
    <ul>
      <li><a href="https://itunes.apple.com/lookup?id=803453959">https://itunes.apple.com/lookup?id=803453959</a>.</li>
    </ul>
  </li>
  <li>Truy cập đường dẫn trên trình duyệt sẽ tải về một file txt, Tìm kiếm từ khoá <code class="language-plaintext highlighter-rouge">"bundleId"</code> và bạn sẽ có <code class="language-plaintext highlighter-rouge">bundle_identifiers</code>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">com.tinyspeck.slackmacgap</code></li>
    </ul>
  </li>
</ul>

<p>Chúc các bạn thành công 🦞</p>]]></content><author><name>Cùi Bắp</name></author><category term="Tips" /><category term="MacOS" /><category term="Slack" /><category term="Chatwork" /><category term="Karabiner" /><summary type="html"><![CDATA[Mình hiện tại đang dùng song song 2 công cụ chat khi làm việc đó là Chatwork và Slack. Tuy nhiên khi sử dụng 2 công cụ này mình gặp phải một vấn đề đó là:]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://euclid1990.github.io/assets/img/posts/2021-12-14-custom-slack-keyboard-shortcut-with-karabiner-on-mac/thumbnail.jpeg" /><media:content medium="image" url="https://euclid1990.github.io/assets/img/posts/2021-12-14-custom-slack-keyboard-shortcut-with-karabiner-on-mac/thumbnail.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>