<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yitaek Hwang</title>
    <description>The latest articles on DEV Community by Yitaek Hwang (@yitaek).</description>
    <link>https://dev.to/yitaek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F576618%2F5a20f7e5-706a-4ecb-8f3e-aa4d000038fc.jpeg</url>
      <title>DEV Community: Yitaek Hwang</title>
      <link>https://dev.to/yitaek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yitaek"/>
    <language>en</language>
    <item>
      <title>Creating a AI-enabled Slackbot with AWS Bedrock Knowledge Base</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Mon, 19 Jan 2026 22:35:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/creating-a-ai-enabled-slackbot-with-aws-bedrock-knowledge-base-4pdm</link>
      <guid>https://dev.to/aws-builders/creating-a-ai-enabled-slackbot-with-aws-bedrock-knowledge-base-4pdm</guid>
      <description>&lt;p&gt;One of the lowest-friction, highest-ROI applications of large language models (LLMs) so far has been the internal AI assistant. Yes, AI doesn't have to be all about customer-facing chatbots or fully autonomous agents. Just a simple interface for users to ask questions like the following can be a powerful tool: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do I deploy this service?"&lt;/li&gt;
&lt;li&gt;"What's the on-call runbook for this alert?"&lt;/li&gt;
&lt;li&gt;"Where is the latest diagram for the design doc?" &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions already have answers — scattered across Confluence pages, Google Docs, GitHub READMEs, and Slack threads. The problem isn’t generation. It’s retrieval.&lt;/p&gt;

&lt;p&gt;Out of the box, LLMs are great at reasoning and summarization, but they’re completely disconnected from your organization’s institutional knowledge. Prompt stuffing helps a bit. Fine-tuning helps in very narrow cases. But neither scales when your knowledge base changes weekly, or when correctness actually matters.&lt;/p&gt;

&lt;p&gt;This is the void that retrieval-augmented generation (RAG) fills.&lt;/p&gt;

&lt;p&gt;RAG bridges the gap between probabilistic language models and deterministic internal knowledge. Instead of asking an LLM to guess, you retrieve relevant documents first, then ask the model to synthesize an answer grounded in that context. The result is an assistant that feels intelligent without being reckless — and, crucially, one that stays up to date without constant retraining.&lt;/p&gt;

&lt;p&gt;If you're already on AWS, Amazon Bedrock Knowledge Bases provides an easy way to create, deploy, and integrate a RAG into your existing infrastructure. In this post, we'll walk through how to use AWS Bedrock Knowledge Base and connected to a Slackbot for a realistic internal, AI-enabled assistant use case. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ca44vu37os1l2oonwyx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ca44vu37os1l2oonwyx.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up AWS Bedrock Knowledge Base
&lt;/h2&gt;

&lt;p&gt;From AWS console, navigate to Amazon Bedrock. Under Build, choose Knowledge Bases. As of time of writing, AWS currently supports indexing unstructured data via creating a custom vector store, utilizing Kendra GenAI service, or enabling semantic search with structured data (e.g., databases, tables). &lt;/p&gt;

&lt;p&gt;Since most internal data is likely to be unstructured (e.g., Confluence documentation, markdown files, etc), we'll choose "Create knowledge base with vector store" option. As of time of writing, AWS supports Confluence, Salesforce, Sharepoint, and Web Crawlers on top of S3 (note: there is a limit of 5 data sources at the moment). For the purpose of this demo, let's choose Confluence. To connect, we'll need to store credentials in AWS Secret Manager as described in the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/confluence-data-source-connector.html" rel="noopener noreferrer"&gt;detailed guide&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Next, we need to configure our data source parsing strategy (either AWS default parser or utilizing a foundation model like Claude as a parser) as well as chunking strategy for our vector database. Bedrock will automatically chunk documents, generate embeddings, and store vectors in OpenSearch Serverless service based on our configurations here. The performance of the RAG will depend on these parameters, but for a quick demo, we can use default chunking and use Amazon Titan embeddings to start out with. &lt;/p&gt;

&lt;p&gt;Once the vector store is set up, we just have to manually sync our data store by syncing the data source. You can imagine adding Sharepoint for internal PDFs, crawling open source library documentation websites, as well as some internally hosted S3 files. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a Slack bot
&lt;/h2&gt;

&lt;p&gt;With the "hard" part out of the way, we need to set up a Slack App via the Slack Admin Console. The key things we need are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enabling Socket Mode&lt;/li&gt;
&lt;li&gt;Minimally, chat:write, app_mentions:read, and channels:history under OAuth Scopes&lt;/li&gt;
&lt;li&gt;Then grab the bot tokens under "Basic Information" page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final part is to actually code up a Slack bot. We can use the Slack Bolt SDK to quickly spin up a bot using Python. We want the bot to do three things at a high-level:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse Slack events (or respond to mentions, slash commands, etc)&lt;/li&gt;
&lt;li&gt;Query the Knowledge Base&lt;/li&gt;
&lt;li&gt;Generate a response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faphvux2wpb1a54nx4fna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faphvux2wpb1a54nx4fna.png" alt=" " width="593" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick pseudocode could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def handler(event, context):
    text = extract_slack_message(event)

    retrieval = bedrock.retrieve(
        knowledgeBaseId=KB_ID,
        query=text,
        retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 5}}
    )

    prompt = build_prompt(text, retrieval["results"])

    response = bedrock_runtime.invoke_model(
        modelId="arn:aws:bedrock:us-east-1:...:inference-profile/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
        body=prompt
    )

    post_to_slack(response)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tuning for performance
&lt;/h2&gt;

&lt;p&gt;Now time for the real magic. Because LLMs are non-deterministic, we need to guide it with some context for better performance. While RAG provides most of our "internal" knowledge, we can still use prompt engineering to guide the generation side. &lt;/p&gt;

&lt;p&gt;You can include a prompt like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an internal engineering assistant.

Answer the question using ONLY the provided context.
If the answer is not in the context, say you do not know.

&amp;lt;context&amp;gt;
{{retrieved_chunks}}
&amp;lt;/context&amp;gt;

Question: {{user_question}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and pass it with the user's questions to dictate what the LLM will do. &lt;/p&gt;

&lt;p&gt;The other dial we can turn is how we embed and store our internal knowledge. AWS has a &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking.html" rel="noopener noreferrer"&gt;great guide on how content chunking works&lt;/a&gt; for knowledge bases. The key takeaway is that depending on how the data is structured, different chunking schemes will perform better. For example, lots of Confluence documentation has a natural hierarchical pattern with headings and body, so using hierarchical chunking can link information better and lead to better retrieval performance. &lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;AI-enabled Slackbots are quickly becoming the front door to internal knowledge. With Amazon bedrock Knowledge Bases, AWS has made it easy to build a RAG without knowing how to operate and maintain a vector database for the most part. &lt;/p&gt;

&lt;p&gt;With powerful LLMs like ChatGPT and Claude, creating a Slack bot is easier than ever. But if you would like to compare your solution with a working model, there is a slightly outdated yet functional example from AWS team on &lt;a href="https://github.com/aws-samples/amazon-bedrock-knowledgebase-slackbot/blob/main/lambda/BedrockKbSlackbotFunction/index.py" rel="noopener noreferrer"&gt;Github&lt;/a&gt; that you can follow. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Setting up AWS Bedrock with Claude</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Mon, 22 Dec 2025 19:43:05 +0000</pubDate>
      <link>https://dev.to/aws-builders/setting-up-aws-bedrock-with-claude-5f67</link>
      <guid>https://dev.to/aws-builders/setting-up-aws-bedrock-with-claude-5f67</guid>
      <description>&lt;p&gt;As 2025 draws to a close, the AI race is still not showing any signs of slowing down. We have new foundational models coming out between OpenAI, Anthropic, and Google to name a few, and every day, there's yet another strategic partnership being announced by major players up and down the stack. &lt;/p&gt;

&lt;p&gt;It's interesting to take a look at what the major hyperscalers have done with AI. Google of course has their own model (i.e., Gemini) as well as significant investments in infrastructure with TPU chips. Microsoft still has deep ties to OpenAI, while making new agreements to expand their offerings. Until recently, Amazon was lagging in the AI race besides their initial deal with Anthropic in 2023 and investments in Trainium chips. &lt;/p&gt;

&lt;p&gt;But with Anthropic forming a &lt;a href="https://blogs.microsoft.com/blog/2025/11/18/microsoft-nvidia-and-anthropic-announce-strategic-partnerships" rel="noopener noreferrer"&gt;new partnership with Microsoft&lt;/a&gt; and &lt;a href="https://www.cnbc.com/2025/12/16/openai-in-talks-with-amazon-about-investment-could-top-10-billion.html" rel="noopener noreferrer"&gt;OpenAI in talks with Amazon about investment&lt;/a&gt;, exclusive access is looking like a thing of the past. This puts AWS in an interesting position as it can leverage its lead in the cloud space to offer the latest AI models and features to its customers. &lt;/p&gt;

&lt;p&gt;In this blog post, we'll quickly go over how to setup Anthropic models with AWS Bedrock and configure Claude VSCode extension to go through AWS Bedrock. &lt;/p&gt;

&lt;h2&gt;
  
  
  But why?
&lt;/h2&gt;

&lt;p&gt;You might be wondering why someone would go through AWS Bedrock instead of going directly to Anthropic. For personal use, getting a Pro or Max plan is likely the cheaper and easier route. However, for enterprise use cases, leveraging existing AWS infrastructure is often easier in terms of compliance, security, and billing. Also, unless you are a large enterprise customer, establishing an enterprise relationship with Anthropic is a slow process at the moment as they scale up. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up AWS Bedrock
&lt;/h2&gt;

&lt;p&gt;Setting up AWS Bedrock to use Anthropic's models is straight forward. Navigate to AWS Bedrock via the Console. Previously, you needed to enable specific models under &lt;code&gt;Configure and learn&lt;/code&gt; &amp;gt; &lt;code&gt;Model access&lt;/code&gt;, but now foundational models are automatically enabled when first invoked. Instead, jump to &lt;code&gt;Chat / Text playground&lt;/code&gt; and select any Anthropic models like Sonnet 4.5. &lt;/p&gt;

&lt;p&gt;Anthropic requires first-time users to submit a use case form. Fill out the form, and you should promptly get an email from AWS confirming AWS Marketplace subscription for the model you chose. Subsequent models that are selected will be automatically enabled without having to submit another form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuum4ro52k4jjhs6yduvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuum4ro52k4jjhs6yduvb.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring AWS credentials for Claude Code
&lt;/h2&gt;

&lt;p&gt;Now that Anthropic models are enabled, we just need to configure AWS credentials for Claude Code. Since Claude Code uses the default AWS SDK credentials, we can leverage existing methods to authenticate with AWS (as long as we have IAM permissions to access Bedrock). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: AWS CLI configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B: Environment variables (access key)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export AWS_ACCESS_KEY_ID=your-access-key-id
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
export AWS_SESSION_TOKEN=your-session-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option C: Environment variables (SSO profile)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws sso login --profile=&amp;lt;your-profile-name&amp;gt;

export AWS_PROFILE=your-profile-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other option is to leverage &lt;a href="https://aws.amazon.com/blogs/machine-learning/accelerate-ai-development-with-amazon-bedrock-api-keys/" rel="noopener noreferrer"&gt;Bedrock API keys&lt;/a&gt; to authenticate directly with Bedrock. To generate an API key, navigate back to AWS Bedrock console and select &lt;code&gt;API keys&lt;/code&gt;. Then you can generate a short-term or long-term API keys with desired expiration time. &lt;/p&gt;

&lt;p&gt;Finally, you need to set two environment variables to let Claude Code know you want to authenticate with Bedrock instead of going directly to Anthropic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=us-east-1 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can set other environment variables like models or disable caching like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Using inference profile ID
export ANTHROPIC_MODEL='global.anthropic.claude-sonnet-4-5-20250929-v1:0'
export ANTHROPIC_SMALL_FAST_MODEL='us.anthropic.claude-haiku-4-5-20251001-v1:0'

# Using application inference profile ARN
export ANTHROPIC_MODEL='arn:aws:bedrock:us-east-2:your-account-id:application-inference-profile/your-model-id'

# Optional: Disable prompt caching if needed
export DISABLE_PROMPT_CACHING=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting up VS Code Plugin
&lt;/h2&gt;

&lt;p&gt;At this point, you can use Claude Code with Bedrock in any terminal shell. However, since VS Code plugin launches its own shell, we need to configure VS Code to let the Claude Code extension know where to pull the right credentials. &lt;/p&gt;

&lt;p&gt;To enable this, open VS Code, and type &lt;code&gt;Preferences: Open User Settings (JSON)&lt;/code&gt; via the command palette (either &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; or &lt;code&gt;Cmd+Shift+P&lt;/code&gt; on Windows or Mac). &lt;/p&gt;

&lt;p&gt;Depending on your previous interactions with the Claude Code extension, there may already be some &lt;code&gt;claudeCode&lt;/code&gt; related settings (e.g., selected model). The crucial part here is to add &lt;code&gt;claudeCode.environmentVariables&lt;/code&gt; in an array like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    "claudeCode.environmentVariables": [
        {
            "name": "AWS_PROFILE",
            "value": "AWS_PROFILE_NAME"
        },
        {
            "name": "AWS_REGION",
            "value": "us-east-1"
        },
        {
            "name": "CLAUDE_CODE_USE_BEDROCK",
            "value": "1"
        }
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add other environment variables or use your preferred authentication method (including the API keys) there. Once configured, reload VS Code and relaunch the Claude Code extension. &lt;/p&gt;

&lt;p&gt;Now you're ready to use Claude Code without being prompted a login:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8d9r9ko0x4c5ruglzkjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8d9r9ko0x4c5ruglzkjm.png" alt=" " width="800" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>llm</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Understanding the True Cost of Ownership: ECS vs. EKS</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Sun, 02 Mar 2025 19:50:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/understanding-the-true-cost-of-ownership-ecs-vs-eks-2l0</link>
      <guid>https://dev.to/aws-builders/understanding-the-true-cost-of-ownership-ecs-vs-eks-2l0</guid>
      <description>&lt;p&gt;While there are plenty of articles already on the &lt;strong&gt;Total Cost of Ownership (TCO)&lt;/strong&gt; between a fully-managed service like ECS vs. one that shares the responsibility more with its users like EKS, the discussion is almost always very high-level, geared towards C-level executives. There's certainly value in having those discussions, but problem I see over and over again, is more at the ground-level between developers and DevOps teams struggling to internalize what it really means for them on a day-to-day basis. &lt;/p&gt;

&lt;p&gt;I recently went through this exercise that highlights some of these key points so wanted to walk through how TCO actually plays out in practice in terms of concrete workstreams for both dev and infra teams. &lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;To lay out some context, there is a homegrown, legacy ETL system that has been running on ECS for years. This system was developed when there were no embedded DevOps engineers on the team, meaning that some developers on the team wrote some bespoke Terraform code and decided to use ECS as it required lower DevOps overhead upfront. &lt;/p&gt;

&lt;p&gt;While the system is fairly simple (e.g., moves files from S3 to a data lake, does some simple transformations), it become a critical component of the entire data pipeline that it became one of those "don't break what works" systems that was always on the backlog for migrations but never had enough momentum to carry it through. &lt;/p&gt;

&lt;p&gt;During this time, the DevOps team grew in size and EKS became the norm at the company for container orchestration. All of the new workloads were deployed onto EKS, and all the internal tooling to help manage not just the cluster itself but adding some controls onto the applications as well were geared towards supporting Kubernetes workloads (e.g., network policies, security, etc). &lt;/p&gt;

&lt;p&gt;At every quarterly planning event, the question of "why aren't we using a single container orchestration system?" would be brought up. Every now and then, the DevOps team would do an initial analysis on how ECS is actually costing more in terms of operational and management costs as backporting new EKS features to ECS was expensive in terms of time and internal resources. This would in turn trigger the dev teams to do their due diligence in estimating how much effort it would take to migrate, but because things are "still working", it would always fall behind in priority and the issue would become stale and forgotten until the next time TCO discussion would bubble up again. &lt;/p&gt;

&lt;h2&gt;
  
  
  Problems Bubbling Up
&lt;/h2&gt;

&lt;p&gt;Cracks started showing when there were finally new feature requests to add to the legacy ETL system. From the dev side, this was a well-scoped problem. For example, instead of storing data in CSV, this system would now convert the format into Parquet for other systems to efficiently ingest. After the feature was developed, the dev team worked with infra teams to run some preliminary scaling analysis and pushed to prod with no problem.&lt;/p&gt;

&lt;p&gt;Or so they thought. &lt;/p&gt;

&lt;p&gt;After a few weeks, the team was getting paged for two reasons. First, sometimes the pods would eat up too many resources on the node and not let other pods including observability agents from being scheduled. Secondly, the finance team was noticing a huge uptick in network costs as soon as this feature was released. &lt;/p&gt;

&lt;p&gt;Both the dev team and the infra teams were confused. Afterall, they had done some scalability testing and nothing they were doing was ground-breaking (meaning these exact problems were already solved on the EKS side). But what they found was that even though best-practices like anti-affinity rules, container limits, and using S3 Private Endpoints were thought to be in place, due to bespoke Terraform code and subtle differences in ECS and EKS, it was in fact not working as intended (e.g., S3 Private Endpoint was only on for VPCs hosting EKS and not ECS). &lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;This "incident" finally illustrated to the dev teams what the hidden operational and maintenances costs are and how it can manifest in practice. Even though ECS is easier to manage and requires very little input from developers, there is a hidden cost to maintaining two difference infrastructure systems across teams. So the argument of "ECS is so easy to use and it's working" is true, it does not diminish the fact that it is masking a TCO problem that can bubble up in the future. &lt;/p&gt;

&lt;p&gt;Most of the TCO discussion is often focused on how running EKS adds on more operational burden, but this can be a nuanced discussion as this case study shows. If the rest of the team is running on EKS and has more expertise, maintaining a more "fully-managed" solution can bring on more challenges as well. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>ecs</category>
      <category>eks</category>
    </item>
    <item>
      <title>Running Jobs in a Container via GitHub Actions Securely</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Sun, 04 Aug 2024 00:38:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/running-jobs-in-a-container-via-github-actions-securely-p0c</link>
      <guid>https://dev.to/aws-builders/running-jobs-in-a-container-via-github-actions-securely-p0c</guid>
      <description>&lt;p&gt;Like any modern CI/CD platform, GitHub allows users to run CI jobs in a container. This is great for running consistent and reproducible CI jobs as well as reducing the amount of setup steps that is required for the job to run (e.g., running &lt;code&gt;actions/setup-python&lt;/code&gt; to install Python environment and installing necessary packages via &lt;code&gt;pip&lt;/code&gt;) as those environments and dependencies can be baked into the container.&lt;/p&gt;

&lt;p&gt;In order to make use of this feature, in the GitHub yaml file, specify the container to run any steps in a job via &lt;code&gt;jobs.&amp;lt;job_id&amp;gt;.container&lt;/code&gt;. This will tell GitHub to spin up a container and run any steps in that job to run inside. If you have both scripts and container actions, GitHub will run the container actions as sibling containers on the same network with the same volume mounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;container-test-job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:18&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check for dockerenv file&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;(ls /.dockerenv &amp;amp;&amp;amp; echo Found dockerenv) || (echo No dockerenv)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While using public images are great, for most non-open-source use cases, you'll need to pull from private registries. To do so, you can pass in a &lt;code&gt;map&lt;/code&gt; of &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-registry/my-image&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
     &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.github_token }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy, right? But let's take a look at when the above approach can become problematic. &lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;GitHub's current approach works great if you already have a static password that you can pass in securely via GitHub's secret mechanism. However, if you are dealing with temporarily credentials, then there's no way to pass them in securely currently. &lt;/p&gt;

&lt;p&gt;To illustrate, let's take AWS ECR as an example. To grab a private image from ECR, you might have two steps like &lt;code&gt;login-to-amazon-ecr&lt;/code&gt; and &lt;code&gt;run-tests&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;In the first step, you can use the &lt;code&gt;aws-actions&lt;/code&gt; to configure credentials and login to ECR. Finally, you will have to set the username and password in the output to send to the next job &lt;code&gt;run-tests&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;login-to-amazon-ecr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/my-github-actions-role&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Amazon ECR&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login-ecr&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/amazon-ecr-login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;mask-password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.registry }}&lt;/span&gt;
      &lt;span class="na"&gt;docker_username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.docker_username_123456789012_dkr_ecr_us_east_1_amazonaws_com }}&lt;/span&gt; 
      &lt;span class="na"&gt;docker_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.docker_password_123456789012_dkr_ecr_us_east_1_amazonaws_com }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the flag &lt;code&gt;mask-password: 'false'&lt;/code&gt;. This is because in order for GitHub to make use of this output, it needs to be unmasked. As of the time of writing, masked outputs cannot be passed to separate jobs (see &lt;a href="https://github.com/actions/runner/issues/1498#issuecomment-1066836352" rel="noopener noreferrer"&gt;this issue&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This means that while this will technically work, it is insecure as now the &lt;code&gt;docker_password&lt;/code&gt; output will be logged unmasked if debug logging is enabled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;run-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login-to-amazon-ecr&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.login-to-amazon-ecr.outputs.registry }}/my-ecr-repo:latest&lt;/span&gt;
      &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.login-to-amazon-ecr.outputs.docker_username }}&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.login-to-amazon-ecr.outputs.docker_password }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run steps in container&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "run steps in container"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solutions
&lt;/h2&gt;

&lt;p&gt;Until GitHub supports a way to either pass masked values as outputs or support a different way to authenticate and pull private images, we have a few options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disable debug logging
&lt;/h3&gt;

&lt;p&gt;Currently, anyone who has access to run a workflow can enable step debug logging for a workflow re-run. You could either opt to remove human access to trigger workflows or disable re-runs. While this technically solves the issue, it will severely impact developer productivity and experience in a negative way. &lt;/p&gt;

&lt;h3&gt;
  
  
  Limit private repos runners can access
&lt;/h3&gt;

&lt;p&gt;We could instead elect to accept the risk of having temporary docker credentials printed out to debug logs for a short duration. As a compromise, you can limit which ECR repositories that GitHub actions can pull from. The rationale here is that if your private container does not contain any confidential IP (e.g., mostly just running tests and setup scripts) then temporarily giving attackers to download or list containers images may be acceptable. To take this to the extreme, you could also consider just using a public repo as well. &lt;/p&gt;

&lt;h3&gt;
  
  
  Run custom runner images
&lt;/h3&gt;

&lt;p&gt;If neither of those quick-fix solutions are acceptable, then you will need to create customer runner images and bake in the docker login step yourself. &lt;/p&gt;

&lt;p&gt;Continuing with our AWS example, we could use the &lt;a href="https://github.com/awslabs/amazon-ecr-credential-helper" rel="noopener noreferrer"&gt;amazon-ecr-credential-helper&lt;/a&gt; to automatically set credentials. Just download the binary to the runner image and mount a &lt;code&gt;~/.docker/config.json&lt;/code&gt; file with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "credsStore": "ecr-login"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can specify that image to run for repos needing to pull private container images, then once it hits the login step, the above binary will take care of it behind the scenes. &lt;/p&gt;

&lt;p&gt;The only potential downside of this approach is that now the login step is abstracted away from developers and other docker-login GitHub actions might conflict. &lt;/p&gt;

</description>
      <category>github</category>
      <category>aws</category>
      <category>cicd</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cyclops: Simple Kubernetes Deployment Manager</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Tue, 05 Dec 2023 01:39:19 +0000</pubDate>
      <link>https://dev.to/yitaek/cyclops-simple-kubernetes-deployment-manager-9gc</link>
      <guid>https://dev.to/yitaek/cyclops-simple-kubernetes-deployment-manager-9gc</guid>
      <description>&lt;p&gt;A user-friendly dashboard for developers to leverage existing Kubernetes frameworks to deploy and manage applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3200%2F0%2AzXOx-wle6vFleZxw" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3200%2F0%2AzXOx-wle6vFleZxw"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes to deploying applications to Kubernetes, the division of responsibilities between developers and infrastructure teams at various organizations is always an interesting (or contentious) topic. Some teams prefer their developers to fully buy into the DevOps ethos and own the entire stack. In this model, developers are expected to not only write their application code, but also the necessary Kubernetes components to run their applications. On the other end of the spectrum, I’ve also seen teams with embedded DevOps or SREs who take on this burden of writing Helm charts and owning everything after code has been merged into their main branch.&lt;/p&gt;

&lt;p&gt;Obviously, the “best” option in dividing these responsibilities will depend on the makeup of your team, especially along their familiarity with Kubernetes concepts and/or their willingness to learn. The latter point is especially important as I’ve seen some developers simply refuse to touch anything YAML. The learning curve going from Docker to Kubernetes has always been a huge challenge.&lt;/p&gt;

&lt;p&gt;The community has not been ignoring this problem either. In fact, there’s been many attempts to address this tension. Some tools have really focused on the templating and generation of Kubernetes manifests that developers can leverage. Others have focused more on developer environments to promote remote development and execution, and thereby having the developers interact with Kubernetes earlier in the development lifecycle. I’m also sure there are tons of internal tools that address some aspect of this problem in very opinionated means (as I can attest with lots of tools I’ve written in the past).&lt;/p&gt;

&lt;p&gt;This brings me to an interesting new open-source project called &lt;a href="https://github.com/cyclops-ui/cyclops" rel="noopener noreferrer"&gt;Cyclops&lt;/a&gt; that takes yet another crack at this problem. I was able to speak with Petar Cvitanovic, who is one of the main contributors, and decided to give it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Cyclops
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cyclops-ui/cyclops" rel="noopener noreferrer"&gt;Cyclops&lt;/a&gt; is a web-based tool that focuses on making Kubernetes deployment and configuration into a more developer-friendly experience. Instead of reinventing the wheel, Cyclops decided to adopt Helm charts that have been the de facto standard for packaging and supporting templatization. Cyclops takes in pre-built Helm charts and translates all those fields into a form that developers can easily adjust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ApfULRFCA_AnBVn8s" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ApfULRFCA_AnBVn8s"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of having developers override variables in YAML or via command line arguments, Cyclops exposes all those fields into a form that is easier to grok for developers. For most applications, developers simply have to change a few things: container image, service ports, configuration files, and secrets. Cyclops is targeting those use cases to streamline that process, although it can handle more complex applications as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Cyclops
&lt;/h2&gt;

&lt;p&gt;To give Cyclops a try, I spun up a minikube cluster and installed Cyclops using the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minikube start

kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.0.1-alpha.5/install/cyclops-install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Cyclops components are installed into cyclops namespace: &lt;code&gt;cyclops-ctrl&lt;/code&gt; and &lt;code&gt;cyclops-ui&lt;/code&gt;. The backend pod &lt;code&gt;cyclops-ctrl&lt;/code&gt; takes a bit to come up so wait for the pod to be healthy. Then expose both the backend and the frontend locally (the frontend expects the backend to be available on localhost:8080).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward svc/cyclops-ui 3000:3000 -n cyclops
kubectl port-forward svc/cyclops-ctrl 8080:8080 -n cyclops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding a Module
&lt;/h2&gt;

&lt;p&gt;Cyclops groups Kubernetes deployments into what it calls as &lt;code&gt;modules&lt;/code&gt;. Click the &lt;code&gt;Add module&lt;/code&gt; button on localhost:3000 to create one.&lt;/p&gt;

&lt;p&gt;Cyclops team has some templates that we can leverage on &lt;a href="https://github.com/cyclops-ui/templates" rel="noopener noreferrer"&gt;https://github.com/cyclops-ui/templates&lt;/a&gt; . We’ll load the &lt;code&gt;demo&lt;/code&gt; from those templates and fill out the module details with a simple nginx container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AF9cmeHr4jH7a_7oQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AF9cmeHr4jH7a_7oQ"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking save, Cyclops will interpret those details and deploy those pods accordingly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AO05o-5BGkcKHT_eY" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AO05o-5BGkcKHT_eY"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that any Helm chart is supported, but in order for Cyclops to work, you’ll need to provide a &lt;code&gt;values.schema.json&lt;/code&gt; file in the following format: &lt;a href="https://cyclops-ui.com/docs/templates/" rel="noopener noreferrer"&gt;https://cyclops-ui.com/docs/templates/&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts
&lt;/h2&gt;

&lt;p&gt;Cyclops is an early stage Kubernetes tooling project with a limited set of features currently. However, that limited focus may actually be a benefit to some teams. Some of the more mature tools, by necessity, grow into a bloated tool to satisfy security, compliance, and various feature requests. But if you are looking for a lightweight tool for developers to quickly override templates in a familiar form format, Cyclops could be a better alternative.&lt;/p&gt;

&lt;p&gt;It is important to note that Cyclops does make some assumptions about how you think of division of responsibilities. In order to use Cyclops effectiveness, some teams must first create the Helm charts and the schema file. If your team is more in the camp of “you write it then you own it”, then having developers buy into GitOps tooling might be sufficient. On the other hand, Cyclops caters to DevOps and infrastructure teams who want to expose a more curated experience for developers to interact with. You can control what fields the devs can override then they can use the Cyclops UI to view the basic status and logs.&lt;/p&gt;

&lt;p&gt;I can see Cyclops being part of a good building block for teams looking to create an internal development platform. There are lots of commercial or large OSS projects dealing with cluster management, CI/CD, etc, but those may be overkill for local development setup or smaller developer platforms. Once Cyclops supports more templates (e.g., sample schemas that work with popular Helm charts like various databases or monitoring tools), it’ll be even more useful.&lt;/p&gt;

&lt;p&gt;If you’re interested, check out Cyclops on Github: &lt;a href="https://github.com/cyclops-ui/cyclops" rel="noopener noreferrer"&gt;https://github.com/cyclops-ui/cyclops&lt;/a&gt;. The team is also very responsive to feedback, so create issues/comments for the team if you are looking to adopt Cyclops!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Ingesting Financial Tick Data Using a Time-Series Database</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Tue, 25 Apr 2023 00:48:16 +0000</pubDate>
      <link>https://dev.to/yitaek/ingesting-financial-tick-data-using-a-time-series-database-59l3</link>
      <guid>https://dev.to/yitaek/ingesting-financial-tick-data-using-a-time-series-database-59l3</guid>
      <description>&lt;p&gt;Compared to traditional financial markets, crypto markets experience more volatility with price swinging in either direction at a quicker pace. Price of each cryptocurrency also tends to vary across exchanges. Given such a dynamic&lt;br&gt;
nature, investors and traders looking to navigate the market need fast andreliable data from various crypto exchanges. In this tutorial, we’ll take a look at three different ways to ingest crypto market data into QuestDB for further analysis:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using the &lt;a href="https://github.com/bmoscon/cryptofeed"&gt;Cryptofeed&lt;/a&gt; library&lt;/li&gt;
&lt;li&gt;Writing a custom data pipeline&lt;/li&gt;
&lt;li&gt;Via Change Data Capture (CDC)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NtNCGhRi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/juz9gv31puuwz1hsmtrb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NtNCGhRi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/juz9gv31puuwz1hsmtrb.png" alt="Image description" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;We will be using QuestDB to ingest and store crypto market data. Create a new directory and from the directory, run the following to start a local instance of QuestDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;cryptofeed-questdb
&lt;span class="nb"&gt;cd &lt;/span&gt;cryptofeed-questdb
docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="nt"&gt;-p&lt;/span&gt; 9009:9009 &lt;span class="nt"&gt;-p&lt;/span&gt; 8812:8812 &lt;span class="nt"&gt;-p&lt;/span&gt; 9003:9003 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:/var/lib/questdb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  questdb/questdb:7.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Method 1: ingesting data using the Cryptofeed library
&lt;/h2&gt;

&lt;p&gt;One of the easiest ways to ingest market data is to use an open-source tool called Cryptofeed. The Python library establishes websocket connections to various exchanges including Binance, Coinbase, Gemini, and Kraken and returns trade, market, and book update data in a standardized format. Cryptofeed also has native integration with QuestDB, making it a great choice to ingest data rapidly.&lt;/p&gt;

&lt;p&gt;To get started, create a virtual environment with Python 3.8+. We will use &lt;a href="https://docs.python.org/3/library/venv.html"&gt;venv&lt;/a&gt; but you can use conda, poetry, or virtualenv as well. We will create a venv for cryptofeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv cryptofeed
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;cryptofeed/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install cryptofeed: &lt;code&gt;pip install cryptofeed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Navigate into the &lt;code&gt;cryptofeed&lt;/code&gt; directory and create a new file &lt;code&gt;questdb.py&lt;/code&gt;. We will then paste the following to ingest trade data for BTC-USD pair from Coinbase and Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeedHandler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.backends.quest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TradeQuest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.defines&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TRADES&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.exchanges&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Coinbase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Gemini&lt;/span&gt;


&lt;span class="n"&gt;QUEST_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'127.0.0.1'&lt;/span&gt;
&lt;span class="n"&gt;QUEST_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9009&lt;/span&gt;




&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeedHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Coinbase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'BTC-USD'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TradeQuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_PORT&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;
   &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'BTC-USD'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TradeQuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_PORT&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;
   &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;




&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'__main__'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this code, it will automatically create a socket connection with Coinbase and Gemini API and push data to QuestDB. Note that it may take a while to see data populated (especially from Gemini).&lt;/p&gt;

&lt;p&gt;Navigate to localhost:9000 to access the web console. We can query data from Coinbase via &lt;code&gt;SELECT * FROM trades-COINBASE&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IMwHpSLy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/legmuulr4h59v0jkgd12.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IMwHpSLy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/legmuulr4h59v0jkgd12.png" alt="Image description" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see all the supported exchanges and supported channels (e.g., L1/L2/L3 books, trades, ticket, candles, open interest, etc) on the &lt;a href="https://github.com/bmoscon/cryptofeed"&gt;Cryptofeed GitHub&lt;/a&gt; page.&lt;/p&gt;

&lt;p&gt;If you want to modify the structure of the data ingested into QuestDB, you can override the callback handler. For example, if you want to change the name of the table it writes to or the columns, you can specify the &lt;code&gt;write&lt;/code&gt; function. In&lt;br&gt;
fact, the &lt;a href="https://github.com/questdb/demo-data"&gt;QuestDB demo site&lt;/a&gt; implements cryptofeed to ingest data to the &lt;code&gt;trades&lt;/code&gt; table with the following custom callback function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeedHandler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.backends.backend&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BackendCallback&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.backends.socket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SocketCallback&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.defines&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TRADES&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;cryptofeed.exchanges&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Coinbase&lt;/span&gt;


&lt;span class="n"&gt;QUEST_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'127.0.0.1'&lt;/span&gt;
&lt;span class="n"&gt;QUEST_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9009&lt;/span&gt;




&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuestCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SocketCallback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9009&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"tcp://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
       &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;none_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


   &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
               &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
               &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeQuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackendCallback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;default_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'trades'&lt;/span&gt;


   &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,symbol=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,side=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"side"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; price=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,amount=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
       &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;




&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeedHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Coinbase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'BTC-USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ETH-USD'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                             &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TRADES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TradeQuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUEST_PORT&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;
   &lt;span class="n"&gt;hanlder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;




&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'__main__'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath the hood, cryptofeed library utilizes plain socket connections via &lt;a href="https://dev.to/docs/reference/api/ilp/overview/"&gt;Influx Line Protocol&lt;/a&gt; (ILP) to push data to&lt;br&gt;
QuestDB. As such, it is important to provide the raw ILP string in the write callback function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v6oqYyDz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uv43bvzwwoqsljyx5s8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v6oqYyDz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uv43bvzwwoqsljyx5s8v.png" alt="Image description" width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The biggest advantage of using Cryptofeed is the large number of preconfigured integrations with various exchanges. The library does the heavy lifting of normalizing the data so ingesting it into QuestDB is very simple. However, if you need more control over the type or format of the data, you may need to call the exchange API directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Method 2: Build a custom market data pipeline with Cryptofeed data fetcher
&lt;/h2&gt;

&lt;p&gt;If Cryptofeed does not support the exchange you are interested in or if you need more control over the type or format of the data, you can opt to write your own data ingestion function. With QuestDB, you have the option to use PostgreSQL wire or ILP. Since the ILP is faster and supports schemaless ingestion, we will show an example of using the InfluxDB Line Protocol via QuestDB Node.js SDK to ingest price data from Binance and Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const axios = require("axios")
const { Sender } = require("@questdb/nodejs-client");


async function main() {
 // create a sender with a 4k buffer
 const sender = new Sender({ bufferSize: 4096 });


 // connect to QuestDB
 // host and port are required in connect options
 await sender.connect({ port: 9009, host: "localhost" });


 async function getBinanceData() {
   const { data } = await axios.get(
     "https://api.binance.us/api/v3/avgPrice?symbol=BTCUSD",
   )


   // add rows to the buffer of the sender
   sender
     .table("prices")
     .symbol("pair", "BTCUSD")
     .stringColumn("exchange", "Binance")
     .floatColumn("bid", parseFloat(data.price))
     .atNow();


   await sender.flush();


   setTimeout(getBinanceData, 1000)
 }


 async function getGeminiData() {
   const { data } = await axios.get("https://api.gemini.com/v1/pricefeed")
   const { price } = data.find((i) =&amp;gt; i.pair === "BTCUSD")


   // add rows to the buffer of the sender
   sender
     .table("prices")
     .symbol("pair", "BTCUSD")
     .stringColumn("exchange", "Gemini")
     .floatColumn("bid", parseFloat(price))
     .atNow();


   await sender.flush();
   setTimeout(getGeminiData, 1000)
 }

 getBinanceData()
 getGeminiData()
}


main()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above polls the REST endpoints of Binance and Gemini API and writes the data to a table called &lt;code&gt;prices&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZARKBOu8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mmqiohbxyzfif97bvcmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZARKBOu8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mmqiohbxyzfif97bvcmq.png" alt="Image description" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While writing a custom data ingestion function is more work than simply using Cryptofeed, it can be a great option if you need to customize the fields or run some preprocessing logic prior to sending it to QuestDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Ingest market data using Cange Data Capture (CDC)
&lt;/h2&gt;

&lt;p&gt;Finally, you can ingest data via Change Data Capture (CDC) if you have an external data stream or database that you can listen on. For example, an external data market team might publish price data on Kafka or push updates to a relational database. Instead of polling this data directly, you could opt to leverage CDC patterns to stream changes to QuestDB instead.&lt;/p&gt;

&lt;p&gt;An example of this architecture is detailed in&lt;br&gt;
&lt;a href="https://dev.to/blog/realtime-crypto-tracker-with-questdb-kafka-connector/"&gt;Realtime crypto tracker with QuestDB Kafka Connector&lt;/a&gt;.&lt;br&gt;
This reference architecture has a function that polls Coinbase API for latest price data and publishes it to Kafka topics. QuestDB Kafka Connector in turn publishes that data to QuestDB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HxZ-SFM_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/di2gdacqxmzwtmme3i6b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HxZ-SFM_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/di2gdacqxmzwtmme3i6b.png" alt="Image description" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Wrapping up QuestDB offers various ways to ingest crypto market data quickly. For a starting point, utilize the Cryptofeed library to connect to various exchanges that are already supported, and optionally modify the ingestion by implementing your own callback. If you need to integrate with a data feed not supported by Cryptofeed, you can write a custom data ingestor and publish data over InfluxDB line protocol to QuestDB. Finally, if there’s an existing data&lt;br&gt;
feed that Debezium supports (e.g., Kafka, PostgreSQL) then using CDC can be a great choice to minimize the infrastructure burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/blog/time-series-monitoring-dashboard-grafana-questdb/"&gt;How to start a time-series dashboard with Grafana&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/blog/processing-time-series-with-questdb-apache-kafka/"&gt;Processing time-series with QuestDB and Apache Kafka&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/blog/2022/04/12/demo-live-crypto-data-streamed-with-questdb-and-grafana/"&gt;Demo of live crypto data streamed with QuestDB and Grafana&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/blog/2022/03/15/cryptocurrency-grafana-questdb/"&gt;Visualizing cryptocurrency data with Python, Grafana, and QuestDB&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tutorial</category>
      <category>database</category>
      <category>python</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Data Lifecycle with QuestDB</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Wed, 02 Nov 2022 23:13:54 +0000</pubDate>
      <link>https://dev.to/yitaek/data-lifecycle-with-questdb-8mk</link>
      <guid>https://dev.to/yitaek/data-lifecycle-with-questdb-8mk</guid>
      <description>&lt;p&gt;For most applications dealing with time series data, the value of each data point diminishes over time as the granularity of the dataset loses relevance as it gets stale. For example, when applying a real-time anomaly detection model, more granular data (e.g., data collected at second resolution), would yield better results. However, to train forecasting models afterwards, recording data at such high frequency may not be needed and would be costly in terms of storage and compute.&lt;/p&gt;

&lt;p&gt;When I was working for an IoT company, to combat this issue, we stored data in three separate databases. To show the most up to date value, latest updates were pushed to a NoSQL realtime database. Simultaneously, all the data was appended to both a time series database storing up to 3 months of data for quick analysis and to an OLAP database for long-term storage. To stop the time series database from exploding in size, we also ran a nightly job to delete old data. As the size of the data grew exponentially with IoT devices, this design caused operational issues with maintaining three different databases.&lt;/p&gt;

&lt;p&gt;QuestDB solves this by providing easy ways to downsample the data and also detach or drop partitions when old data is no longer necessary. This helps to keep all the data in a single database for most operations and move stale data to cheaper storage in line with a mature data retention policy.&lt;/p&gt;

&lt;p&gt;To illustrate, let’s revisit the &lt;a href="https://questdb.io/blog/2021/02/05/streaming-heart-rate-data-with-iot-core-and-questdb/" rel="noopener noreferrer"&gt;IoT application involving heart rate data&lt;/a&gt;. Unfortunately, Google decided to &lt;a href="https://techcrunch.com/2022/08/17/google-cloud-will-shutter-its-iot-core-service-next-year/" rel="noopener noreferrer"&gt;shut down its Cloud IoT Core service&lt;/a&gt;, so we’ll use randomized data for this demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Populating heart rate data
&lt;/h2&gt;

&lt;p&gt;Let’s begin by running &lt;a href="https://questdb.io/docs/get-started/docker/" rel="noopener noreferrer"&gt;QuestDB via Docker&lt;/a&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 9000:9000 \
-p 9009:9009 \
-p 8812:8812 \
-p 9003:9003 \
-v “$(pwd):/var/lib/questdb” \
questdb/questdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We’ll create the a simple heart-rate data table with a timestamp, heart rate, and sensor ID partitioned by month via the console at localhost:9000:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE heart_rate AS(
  SELECT
    x ID,
    timestamp_sequence(
      to_timestamp('2022–10–10T00:00:00', 'yyyy-MM-ddTHH:mm:ss'),
      rnd_long(1, 10, 0) * 100000L
    ) ts,
    rnd_double(0) * 100 + 60 heartrate,
    rnd_long(0, 10000, 0) sensorId
  FROM
    long_sequence(10000000) x
) TIMESTAMP(ts) PARTITION BY MONTH;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We now have randomized data from 10,000 sensors over ~2 months time frame (10M data points). Suppose we are continuously appending to this dataset from a data stream, then having such frequent updates will be useful to detect anomalies in heart rate. This could be useful to detect and alert on health issues that could arise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsampling the data
&lt;/h2&gt;

&lt;p&gt;However, if no anomalies are detected, having a dataset with heart rate collected every second is not useful if we simply want to note general trends over time. Instead we can record the average heart rate in one hour intervals to compact data. For example, if we’re interested in the min, max, and avg heart rate of a specific sensor, sampled every hour, we can invoke:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT
  min(heartrate),
  max(heartrate),
  avg(heartrate),
  ts
FROM
  heart_rate
WHERE
  sensorId = 1000 SAMPLE BY 1h FILL(NULL, NULL, PREV);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Once you are happy with the downsampled results, we can store those results into a separate sampled_data table for other data science time to create forecasting models or do further analysis:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE sampled_data (ts *timestamp*, min_heartrate *double*, max_heartrate *double*, avg_heartrate *double*, sensorId *long*) *timestamp*(ts);

INSERT INTO sampled_data (ts, min_heartrate, max_heartrate, avg_heartrate, sensorId);

SELECT ts, min(heartrate), max(heartrate), avg(heartrate), sensorId FROM heart_rate SAMPLE BY 1h FILL(NULL, NULL, PREV);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This downsampling operation can be done periodically (e.g., daily, monthly) to populate the new table. This way the data science team does not have to import the massive raw dataset and can simply work with sampled data with appropriate resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data retention strategy
&lt;/h2&gt;

&lt;p&gt;Downsampling alone, however, does not solve the growing data size. The raw sensor heart_rate table will continue to grow in size. In this case, we have some options in QuestDB to detach or even drop partitions.&lt;/p&gt;

&lt;p&gt;Since we partitioned the original dataset by month, we have 3 partitions: 2022–10, 2022–11, and 2022–12. This can be seen under /db/heart_rate/ directories, along with other files holding metadata.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/db/heart_rate
├── 2022–10
├── 2022–11
├── 2022–12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After we have downsampled the data, we probably no longer need data from older months. In this case, we can &lt;a href="https://questdb.io/docs/reference/sql/alter-table-detach-partition/" rel="noopener noreferrer"&gt;DETACH&lt;/a&gt; this partition to make it unavailable for reads.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALTER TABLE ‘heart_rate’ DETACH PARTITION LIST ‘2022–10’;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now the 2022–10 partition is renamed to 2022–10.detached and running queries in the heart_rate table returns data from 2022–11 onwards:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM ‘heart_rate’ LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3200%2F0%2AXdjQoRUjy_6czOvG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3200%2F0%2AXdjQoRUjy_6czOvG"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can then compress this data and move it to a cheaper block storage option like S3 or GCS:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tar cfz — ‘/db/heart_rate/2022–10.detached’ | aws s3 cp — s3://my-data-backups/2022–10.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If we need to restore this partition for further analysis, we can re-download the tar file to a new directory named .attachable under /db/ (or where the rest of the QuestDB data lives) and uncompress the tar file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir 2022–02.attachable | aws s3 cp s3:/my-data-backups/2022–10.tar.gz — | tar xvfz — -C 2022–10.attachable — strip-components 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With the data in place, simply use the &lt;a href="https://questdb.io/docs/reference/sql/alter-table-attach-partition/" rel="noopener noreferrer"&gt;ATTACH&lt;/a&gt; command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALTER TABLE heart_rate ATTACH PARTITION LIST ‘2022–10’;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We can verify the partition has been attached back by running the count query and seeing 10M records:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT count() FROM heart_rate;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Alternatively, if we want to simply delete partitions, we can use the &lt;a href="https://questdb.io/docs/reference/sql/alter-table-drop-partition/" rel="noopener noreferrer"&gt;DROP&lt;/a&gt; command to do so. Unlike the DETACH command, this operation is irreversible:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALTER TABLE heart_rate DROP PARTITION LIST ‘2022–10’;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Simplifying operations&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;As the volume of data continues to explode, it’s important to consider a data retention strategy that is both cost-effective and useful to teams depending on that data. Time series data, by its nature, lend itself well to aggregation and partitioning by time. We can leverage this quality to serve raw data for teams requiring real-time decisions and then move to a downsampled dataset for other analytic needs. Finally, to control the cost and performance of the time series database, we can detach and store partitions in cheaper, long-term storage options.&lt;/p&gt;

&lt;p&gt;QuestDB makes these operations simple with built-in support so that teams don’t have to build custom data pipelines to manually delete and replicate data into different databases. The use case shown here with heart rate data can easily be applied to other industries with high-frequency, time series data (e.g., financial markets, infrastructure monitoring).&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Simplifying Kubernetes CI/CD With Devtron</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Thu, 23 Jun 2022 21:57:33 +0000</pubDate>
      <link>https://dev.to/yitaek/simplifying-kubernetes-cicd-with-devtron-34gm</link>
      <guid>https://dev.to/yitaek/simplifying-kubernetes-cicd-with-devtron-34gm</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1j1ynv2rlcgxrzywr6j8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1j1ynv2rlcgxrzywr6j8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When organizations think of fostering a DevOps culture, building out an effective continuous integration (CI) and continuous deployment (CD) pipeline is usually brought up as the first step to success. Nowadays, infrastructure teams have a plethora of both open-source and licensed tools such as Jenkins, CircleCI, Github Actions, and ArgoCD to implement various CI/CD pipelines and deployment strategies. However, most of these tools rely on complex YAML templating to trigger the pipelines, which may discourage developers who simply want an easy way to build and deploy their application to cloud-native environments.&lt;/p&gt;

&lt;p&gt;Devtron is an open-source software delivery workflow orchestrator for Kubernetes with a built-in CI/CD builder to address this issue. In this article, we’ll review how to configure some common CI/CD steps via Devtron.&lt;/p&gt;

&lt;h1&gt;
  
  
  CI Pipeline
&lt;/h1&gt;

&lt;p&gt;In Devtron, CI pipelines can be created via a CI Workflow Editor (trigger from a code repository), linked to an existing pipeline (e.g. templates), or be integrated with an external provider via an incoming webhook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lv91fqo1hhicrbmidex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lv91fqo1hhicrbmidex.png" alt="Image description"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;To create a new CI pipeline, choose the “Continuous integrations” option to open up the Workflow Editor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn62aa05tu2vabdf82wp7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn62aa05tu2vabdf82wp7.png" alt="Image description"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Instead of specifying various branch types and triggers via a YAML file, developers can simply choose the source type (e.g. branch, PR, tag) or branch name to trigger the pipeline. Devtron provides three simple stages in the CI steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-build stage: tasks to run before building the container image (e.g. linting, unit tests)&lt;/li&gt;
&lt;li&gt;Build stage: creating the container image&lt;/li&gt;
&lt;li&gt;Post-build stage: tasks to run after image creation (e.g. scanning for vulnerabilities)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zlzf7df8uhkufgi0oqf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zlzf7df8uhkufgi0oqf.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build wizard guides through setting up each of these configuration parameters. If the team already has an existing CI template, developers can opt to link that pipeline or integrate with external tools if the team is migrating from legacy providers (e.g. Jenkins).&lt;/p&gt;

&lt;p&gt;If the pipeline is set to trigger automatically, either commit to the branch or submit a PR to trigger the action. Alternatively, users can click on “Select Material” to trigger the builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghoszdiyods8cj1t5uud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghoszdiyods8cj1t5uud.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the Build History tab, developers can also see vulnerabilities if that feature was enabled under the advanced options. This built-in integration is a nice way to avoid having to add in open-source scanners (e.g. Anchore, Clair, Trivy) or paid-tools (e.g. Jfrog Xray) manually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55grfkmam3yu9yljxpil.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55grfkmam3yu9yljxpil.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  CD Pipeline
&lt;/h1&gt;

&lt;p&gt;Once the CI pipeline is set, we can extend the pipeline to include the CD portion. Simply click on the (+) sign of the pipeline via the Workflow Editor and select the deployment environment (i.e. target namespace/cluster) and deployment strategy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k61qp85ywy455kpx4a9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k61qp85ywy455kpx4a9.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As with the CI portion, CD comes with three different stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-deployment stage: useful to carry out DB/schema migrations or config setup before the application deployment&lt;/li&gt;
&lt;li&gt;Deployment stage: step to deploy utilizing one of four strategies (recreate, canary, blue-green, and rolling upgrades) that can be configured per use case&lt;/li&gt;
&lt;li&gt;Post-deployment stage: runs after the deployment to either update Jira ticket, send notifications, or run clean up tasks&lt;/li&gt;
&lt;li&gt;All of these stages can be configured using the Workflow Editor. Since CD step is more open-ended, more complex workflows will require writing up some YAML but the config for each stage is relatively minimal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Configure the pre-deployment stage to run automatically or manually with a config like the following. The following example shows using Flyway to manage database migrations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo78qt82bnfa9uib3py44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo78qt82bnfa9uib3py44.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Post-deployment stage works similarly as the pre-deployment stage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3d26kf1u335jgjf7jugc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3d26kf1u335jgjf7jugc.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can run smoke tests or end-to-end tests on lower environments after deploying. As shown above, you can use a node or cypress docker image to run tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -it -v $PWD:/e2e -w /e2e --entrypoint=cypress cypress/base
Finally, these pipelines can be linked to create sequential pipelines if multiple deployments or special jobs must trigger in order. To create a sequential pipeline, click on the + sign on the right side of the existing pipeline components to add new jobs:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These pipelines can be linked to sequentially deploy to multiple environments (e.g. dev → QA → UAT → prod).&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;With so many choices in the market today, most teams struggle to create a cohesive CI/CD experience without cobbling together a multitude of tools. While the flexibility of each of these tools provides tremendous value, for some teams, just setting up a simple pipeline is all that is needed. This is where Devtron can provide value in guiding developers through an intuitive widget to set up a pipeline that is ready for cloud-native applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dzone.com/articles/zero-to-hero-on-kubernetes-with-devtron" rel="noopener noreferrer"&gt;Full end-to-end demo of deploying a sample application using the CI/CD pipeline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.devgenius.io/devtron-open-source-software-delivery-workflow-for-k8s-23bd136efe06" rel="noopener noreferrer"&gt;Installing Devtron and deploying a sample Helm chart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to Provision and Manage Amazon EKS with Ease</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Thu, 05 May 2022 15:32:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-provision-and-manage-amazon-eks-with-ease-3eda</link>
      <guid>https://dev.to/aws-builders/how-to-provision-and-manage-amazon-eks-with-ease-3eda</guid>
      <description>&lt;p&gt;AWS is the unquestioned leader of the $180-billion cloud market today, with a 33% overall market share according to &lt;a href="https://www.statista.com/chart/18819/worldwide-market-share-of-leading-cloud-infrastructure-service-providers/"&gt;Synergy Research Group&lt;/a&gt;. Their dominance also extends to the managed Kubernetes space. Both the &lt;a href="https://www.cncf.io/wp-content/uploads/2020/08/CNCF_Survey_Report.pdf"&gt;Cloud Native Computing Foundation 2019 survey&lt;/a&gt; and a more recent &lt;a href="https://www.logicata.com/blog/amazon-eks-elastic-kubernetes-service-is-the-favourite-paas-kubernetes-orchestration-tool/"&gt;Logicata Kubernetes poll results&lt;/a&gt; show EKS with the lead in terms of popularity amongst its competition (e.g. GKE, AKS, etc).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JkcnKGLD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/2KUeNBw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JkcnKGLD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/2KUeNBw.png" alt="" width="880" height="446"&gt;&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://www.cncf.io/wp-content/uploads/2020/08/CNCF_Survey_Report.pdf"&gt;CNCF Survey 2019&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, as more applications are onboarded onto EKS, managing multiple clusters and workloads remain a challenge. In this post, we'll discuss a few ways to provision an EKS cluster and using KubeSphere as the platform layer to securely deploy and maintain containerized applications on Kubernetes. &lt;/p&gt;
&lt;h2&gt;
  
  
  eksctl
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://eksctl.io/"&gt;eksctl&lt;/a&gt; is an open-source tool jointly developed by the AWS and &lt;a href="https://www.weave.works/"&gt;Weaveworks&lt;/a&gt; to create and manage EKS clusters. Behind the scenes, eksctl creates a CloudFormation stack to provision and update AWS artifacts. &lt;/p&gt;

&lt;p&gt;After &lt;a href="https://eksctl.io/introduction/#installation"&gt;installing eksctl&lt;/a&gt;, a cluster can be bootstrapped imperatively with command line flags or via a config file declaratively:&lt;/p&gt;

&lt;p&gt;via command-line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;eksctl create cluster --name=cluster-1 --nodes=4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or via config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eksctl.io/v1alpha5&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterConfig&lt;/span&gt;

&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;basic-cluster&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-2&lt;/span&gt;

&lt;span class="na"&gt;nodeGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ng-1&lt;/span&gt;
    &lt;span class="na"&gt;instanceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;m5.large&lt;/span&gt;
    &lt;span class="na"&gt;desiredCapacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;volumeSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
    &lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# will use ~/.ssh/id_rsa.pub as the default ssh key&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ng-2&lt;/span&gt;
    &lt;span class="na"&gt;instanceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;m5.xlarge&lt;/span&gt;
    &lt;span class="na"&gt;desiredCapacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;volumeSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;publicKeyPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.ssh/ec2_id_rsa.pub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similar to &lt;code&gt;kubectl&lt;/code&gt; commands, config file changes can be applied via the &lt;code&gt;-f&lt;/code&gt; flag: &lt;code&gt;eksctl create cluster -f &amp;lt;file-name.yaml&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;eksctl maintains a comprehensive documentation website with various configuration parameters like enabling CloudWatch, Fargate nodegroups, EKS addons (e.g. CNI, EBS driver, core-dns), as well as nice integrations for &lt;a href="https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html"&gt;IRSA support&lt;/a&gt;. &lt;a href="https://www.eksworkshop.com/030_eksctl/"&gt;EKS workshop&lt;/a&gt; also hosts a step-by-step guide with a video to bootstrap an EKS cluster. &lt;/p&gt;

&lt;p&gt;If you are familiar with CloudFormation or would like to use a nice wrapper that is jointly maintained by the AWS team, eksctl is a great choice to use to maintain EKS clusters. &lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform EKS Module
&lt;/h2&gt;

&lt;p&gt;Another popular option is to use Terraform to provision EKS cluster either with the &lt;a href="https://github.com/terraform-aws-modules/terraform-aws-eks"&gt;official EKS module&lt;/a&gt; or use submodules for VPC, EKS, and/or nodegroups separately. The &lt;a href="https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/complete"&gt;complete example&lt;/a&gt; for the EKS module will bootstrap an EKS cluster with self-managed and AWS managed nodes with KMS encryption enabled by default. &lt;/p&gt;

&lt;p&gt;Since Terraform is one of the more popular IaC providers, the advantage of using Terraform to manage EKS is the ability to keep everything with the same tech stack. If you are also using multi-cloud or have plans to branch out into AKS or GKE in the future, using Terraform would be more desirable than eksctl as well. &lt;/p&gt;

&lt;p&gt;Finally, for a deep-dive into designing and provisioning a production ready EKS cluster, you can check out &lt;a href="https://itnext.io/how-to-design-and-provision-a-production-ready-eks-cluster-f24156ac29b2"&gt;some tips in this article&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Installing KubeSphere on Amazon EKS
&lt;/h2&gt;

&lt;p&gt;Now that we have a functional EKS cluster, we can install KubeSphere using kubectl. &lt;/p&gt;

&lt;p&gt;First, we need to update the kubeconfig to match our newly created cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;aws eks --region &amp;lt;my-region&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;update-kubeconfig &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;my-cluster-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can apply the kubectl manifests to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.2.1/kubesphere-installer.yaml

kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.2.1/cluster-configuration.yaml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the installation is complete, you should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#####################################################
###              Welcome to KubeSphere!           ###
#####################################################
Account: admin
Password: P@88w0rd
NOTES：
1. After logging into the console, please check the
   monitoring status of service components in
   the "Cluster Management". If any service is not
   ready, please wait patiently until all components
   are ready.
2. Please modify the default password after login.
#####################################################
https://kubesphere.io             2020-xx-xx xx:xx:xx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, KubeSphere has partnered with AWS to easily install KubeSphere as an &lt;a href="https://aws.amazon.com/quickstart/architecture/qingcloud-kubesphere/"&gt;AWS Quick Start&lt;/a&gt;. This will use a CloudFormation template to deploy an EKS cluster and install KubeSphere automatically. You can edit the CloudFormation template to remove VPC and EKS creation and only trigger KubeSphere installation with an existing cluster. &lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Amazon EKS with KubeSphere
&lt;/h2&gt;

&lt;p&gt;Although Amazon provides some add-on services such as VPC CNI, CoreDNS, EBS CSI, and kube-proxy to the core EKS offering, it is pretty barebones in terms of the extra tooling needed for a production-ready Kubernetes platform. It does not come with any ingress controllers (e.g. nginx, traefik, etc), autoscalers (e.g. karpenter, cluster autoscaler), logging and monitoring agents, or common tools like external dns or cert-manager. If you also plan to support multitenancy on EKS, then you are also responsible for configuring namespaces and necessary RBAC components yourself. &lt;/p&gt;

&lt;p&gt;This is where KubeSphere can help ease the burden by providing a platform layer on top. KubeSphere comes prepackaged with integrations with Jenkins, logging/monitoring, service mesh, ingress controllers, and more deliver a complete application management experience. User management is also built in via workspaces and projects, which will assign users and RBAC roles to namespaces corresponding to each project. The main dashboard can also be used to deploy new microservices with Jenkins pipelines or utilize the App Store to deploy popular Helm charts like etcd, redis, tomcat, postgresql, etc. &lt;/p&gt;

&lt;p&gt;KubeSphere also shines when there are multiple clusters (e.g. multi-region or multi-environment) involved. KubeSphere follows the federation model where the KubeSphere running in the host cluster can control downstream member clusters. Using KubeSphere, platform teams can consistently install, upgrade, and manage not only the infrastructure components but also their application across multiple clusters. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vlTrh1op--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ItoeOCS.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vlTrh1op--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ItoeOCS.png" alt="" width="880" height="503"&gt;&lt;/a&gt;&lt;br&gt;
Image Credit: &lt;a href="https://thenewstack.io/tutorial-use-kubesphere-to-manage-digitalocean-kubernetes-and-amazon-eks/"&gt;New Stack&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The learning curve for mastering Kubernetes is high. But with Amazon EKS, users can offload the management of the master plane and core addon components to Amazon. With eksctl and Terraform, teams can easily provision many Kubernetes clusters at scale. To go a step further, utilize the rich ecosystem of integrations that KubeSphere provides to further ease the burden of having to manage Kubernetes clusters and the applications in a cloud-native way. &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running QuestDB on GKE Autopilot</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Fri, 26 Mar 2021 13:37:41 +0000</pubDate>
      <link>https://dev.to/yitaek/running-questdb-on-gke-autopilot-3cfg</link>
      <guid>https://dev.to/yitaek/running-questdb-on-gke-autopilot-3cfg</guid>
      <description>&lt;p&gt;Extending the QuestDB Helm chart with monitoring and automated backups for a production-ready setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ADpRugft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3368/1%2Ay-VWDVTYPDQmW0-Mp_S0UA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ADpRugft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3368/1%2Ay-VWDVTYPDQmW0-Mp_S0UA.png" alt="" width="880" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently, I’ve been experimenting with QuestDB as the primary time-series database to stream and analyze IoT/financial data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://levelup.gitconnected.com/streaming-heart-rate-data-with-iot-core-and-questdb-84304069592e"&gt;Streaming Heart Rate Data with IoT Core and QuestDB&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/swlh/realtime-crypto-tracker-with-kafka-and-questdb-b33b19048fc2"&gt;Realtime Crypto Tracker with Kafka and QuestDB&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While I was able to validate the power of QuestDB in storing massive amounts of data and querying them quickly in those two projects, I was mostly running them on my laptop via Docker. In order to scale my experiments, I wanted to create a more production-ready setup, including monitoring and disaster recovery on Kubernetes. So in this guide, we’ll walk through setting up QuestDB on GKE with Prometheus and Velero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cloud.google.com/"&gt;GCP account&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cloud.google.com/sdk/docs/install"&gt;gcloud CLI&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://v3.helm.sh/docs/intro/install/"&gt;Helm 3&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up GKE Autopilot
&lt;/h2&gt;

&lt;p&gt;As a DevOps engineer/SRE, I’m a huge fan of GKE since it provides a lot of features out of the box such as cluster autoscaling, network policy plugins, and managed Istio compared to other managed Kubernetes options available. Recently Google Cloud announced &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/introducing-gke-autopilot"&gt;GKE Autopilot&lt;/a&gt;, a new mode that further automates Kubernetes operations, including node management, security/hardening, and resource optimization. It brings together the serverless experience of Cloud Run with the flexibility and features of GKE. In practice, this means that you are now charged for pod usage rather than paying for the compute and storage of the underlying Kubernetes nodes, making it a great choice for projects with unknown resource utilization.&lt;/p&gt;


&lt;center&gt;&lt;/center&gt;

&lt;p&gt;Creating a new GKE Autopilot cluster is also extremely simple. There’s no need to set up Terraform or VPCs/autoscalers/node groups. Simply create a Google Cloud account, navigate to &lt;a href="https://cloud.google.com/"&gt;Google Kubernetes Engine&lt;/a&gt;, enable the Kubernetes Engine API, and click on “Create Cluster”:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gLj2-NWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2804/1%2A369tWdShI_HC4jy0WMkJcA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gLj2-NWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2804/1%2A369tWdShI_HC4jy0WMkJcA.png" alt="" width="880" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the “Configure” button under “Autopilot mode”, give the cluster a name, and wait for a few minutes for the cluster to be ready. I went with the default name autopilot-cluster-1 in us-east1 region (feel free to skip the other sections for now).&lt;/p&gt;

&lt;p&gt;To configure your Kubernetes context to interact with the cluster via Helm, fetch the credentials using the following gcloud command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud container clusters get-credentials autopilot-cluster-1 --region us-east1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Deploying QuestDB
&lt;/h2&gt;

&lt;p&gt;QuestDB provides an official Helm chart that deploys a single StatefulSet pod with 50Gi PVC by default. To install the chart, add the QuestDB Helm repo and deploy it by running the following:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ helm repo add questdb [https://helm.questdb.io/](https://helm.questdb.io/)
$ helm upgrade --install questdb questdb/questdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Since GKE Autopilot does not pre-provision node capacity, the pod will be initially marked as unschedulable. Give it a few seconds until cluster autoscaler triggers a scaling up event and schedules the questdb pod.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Of4X_cI0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2832/1%2AR1k-AVPDzVRfES84Fm5NMw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Of4X_cI0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2832/1%2AR1k-AVPDzVRfES84Fm5NMw.png" alt="" width="880" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a quick smoke test, port-forward the HTTP endpoint and interact with the web console UI:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl port-forward questdb-0 9000:9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hsgHOD_S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/5088/1%2A4y3ykUUAyIIw-mXmVHW7CA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hsgHOD_S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/5088/1%2A4y3ykUUAyIIw-mXmVHW7CA.png" alt="" width="880" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Postgres Metrics
&lt;/h2&gt;

&lt;p&gt;At this point, we have a healthy, running instance of QuestDB. However, without inspecting the application logs, there is no easy way to determine the overall system health and grab metrics from QuestDB. For performance reasons, QuestDB team decided to decouple the REST endpoint from its min &lt;a href="https://questdb.io/docs/operations/health-monitoring/"&gt;HTTP server that holds a simple health status on port 9003&lt;/a&gt;. This port is currently not exposed via the Helm chart, so the StatefulSet and Service sections need to be changed to allow probes to periodically check this endpoint.&lt;/p&gt;

&lt;p&gt;Unfortunately, QuestDB currently does not provide a comprehensive metrics endpoint that natively integrates with Prometheus or Postgres. Open Github issues for this support is listed below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/questdb/questdb/issues/532"&gt;[#532] Add Prometheus metrics endpoint&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/questdb/questdb/issues/837"&gt;[#837] Add support for exposing database state similar to pg_stat_database and pg_stat_activity&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still, I wanted to deploy Prometheus Postgres Exporter to at least track QuestDB up/down status and set up the framework to monitor Postgres metrics once these features became available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expose Postgres Port
&lt;/h3&gt;

&lt;p&gt;First, we need to modify the default QuestDB chart to expose port 8812 (Postgres endpoint). Create a new YAML file called questdb-values.yaml :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service:
  expose:
    postgresql:
      enabled: true
      port: 8812
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Update the existing deployment:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ helm upgrade --install questdb questdb/questdb -f questdb-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Install Prometheus Postgres Exporter
&lt;/h3&gt;

&lt;p&gt;Prometheus Helm charts are managed by the prometheus-community :&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, we need to modify the default values to scrape QuestDB and also disable default metrics since QuestDB doesn’t currently store pg_stat_database or pg_stat_activity . We also want Prometheus to scrape our metrics, so we need to add annotations specify our scrape port.&lt;/p&gt;

&lt;p&gt;Create another YAML file called exporter-values.yaml :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config:
  datasource:
    host: questdb-0.questdb-headless
    user: admin
    password: quest
    port: '8812'
    database: qdb
    sslmode: disable
  disableDefaultMetrics: true

annotations: 
  prometheus.io/scrape: 'true'
  prometheus.io/port: '9187'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we can install Postgres Exporter:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ helm upgrade -i postgres-exporter prometheus-community/prometheus-postgres-exporter -f exporter-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we can install Prometheus to collect these metrics. Create a new YAML file to hold our Prometheus configurations, prometheus-values.yaml :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alertmanager:
  enabled: false
nodeExporter:
  enabled: false
pushgateway:
  enabled: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Since we don’t have real metrics from QuestDB yet, we will only deploy the Prometheus server and accept the default configurations.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ helm upgrade -i prometheus prometheus-community/prometheus -f prometheus-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Access the dashboard to run our queries by port-forwarding:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ export POD_NAME=$(kubectl get pods --namespace default -l \ "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")
  kubectl --namespace default port-forward $POD_NAME 9090
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Open up localhost:9090 and check that we’re able to scrape pg_up :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LlW50J2h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4248/1%2ARYC-gxAIDQmPir1s7mZEvQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LlW50J2h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4248/1%2ARYC-gxAIDQmPir1s7mZEvQ.png" alt="" width="880" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: If you are looking for a production-ready monitoring setup, please refer to the four-part series “&lt;a href="https://yitaek.medium.com/practical-monitoring-with-prometheus-grafana-part-i-22d0f172f993"&gt;Practical Monitoring with Prometheus and Grafana&lt;/a&gt;”.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Backups with Velero
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://velero.io/"&gt;Velero&lt;/a&gt; is an open-source tool to back up and restore Kubernetes resources and persistent volumes. This is useful for disaster recovery (taking snapshots) or for data migration. Velero runs inside the Kubernetes cluster and integrates with various storage providers (e.g. AWS S3, GCP Storage, Minio) as well as &lt;a href="https://velero.io/docs/v1.5/restic/"&gt;restic&lt;/a&gt; to take snapshots either on-demand or on a schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Velero can be installed via &lt;a href="https://vmware-tanzu.github.io/helm-charts/"&gt;Helm&lt;/a&gt; or via the &lt;a href="https://velero.io/docs/v1.5/basic-install/"&gt;CLI&lt;/a&gt; tool. In general, it seems like the CLI gets the latest updates, and the Helm chart lags behind slightly with compatible Docker images.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# for MacOS
$ brew install velero

# for Windows
$ choco install velero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Configuring Server Components
&lt;/h3&gt;

&lt;p&gt;To set up Velero on GCP, we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/vmware-tanzu/velero-plugin-for-gcp#Create-an-GCS-bucket"&gt;Create an GCS bucket&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/vmware-tanzu/velero-plugin-for-gcp#Set-permissions-for-Velero"&gt;Set permissions for Velero&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/vmware-tanzu/velero-plugin-for-gcp#Install-and-start-Velero"&gt;Install and start Velero&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Create an GCS Bucket
&lt;/h3&gt;

&lt;p&gt;Give a unique bucket name and use the gsutil tool to create the bucket (replace  with the name of your bucket):&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROJECT_ID=$(gcloud config get-value project)
BUCKET=&amp;lt;YOUR_BUCKET&amp;gt;

gsutil mb gs://$BUCKET/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Set GCP Permissions
&lt;/h3&gt;

&lt;p&gt;Create a service account:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcloud iam service-accounts create velero \
 — display-name “Velero service account”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Attach the compute permissions to the service account:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SERVICE_ACCOUNT_EMAIL=$(gcloud iam service-accounts list \
  --filter="displayName:Velero service account" \
  --format 'value(email)')

ROLE_PERMISSIONS=(
    compute.disks.get
    compute.disks.create
    compute.disks.createSnapshot
    compute.snapshots.get
    compute.snapshots.create
    compute.snapshots.useReadOnly
    compute.snapshots.delete
    compute.zones.get
)

gcloud iam roles create velero.server \
    --project $PROJECT_ID \
    --title "Velero Server" \
    --permissions "$(IFS=","; echo "${ROLE_PERMISSIONS[*]}")"

gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member serviceAccount:$SERVICE_ACCOUNT_EMAIL \
    --role projects/$PROJECT_ID/roles/velero.server

gsutil iam ch serviceAccount:$SERVICE_ACCOUNT_EMAIL:objectAdmin gs://${BUCKET}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Download the service account key and save it as credential-velero :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcloud iam service-accounts keys create credentials-velero \
    --iam-account $SERVICE_ACCOUNT_EMAIL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we can install Velero with the GCS plugin enabled:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;velero install \
    --provider gcp \
    --plugins velero/velero-plugin-for-gcp:v1.1.0 \
    --bucket $BUCKET \
    --secret-file ./credentials-velero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we can create a schedule using cron string:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;velero schedule create questdb --schedule "0 7 * * *" -l "app.kubernetes.io/instance=questdb" --include-namespaces default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We can verify backups being made in our bucket:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wF2-9D04--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4572/1%2Ahqon6Z2WCYpVlnzJ5QD7ag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wF2-9D04--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4572/1%2Ahqon6Z2WCYpVlnzJ5QD7ag.png" alt="" width="880" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;Note: For a deep-dive on Velero, refer to “&lt;a href="https://medium.com/dev-genius/disaster-recovery-on-kubernetes-98c5c78382bb"&gt;Disaster Recovery on Kubernetes&lt;/a&gt;.”&lt;/em&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;To optimize for performance, QuestDB opted to build a time-series database from scratch with ANSI SQL compatibility, as opposed to building on top of Postgres like TimescaleDB. Also, as a newer product, QuestDB is missing some enterprise features (e.g. replication, high-availability, recovery from snapshot) and integrations to other popular projects. Still, with Prometheus Postgres exporter and Velero, we can configure a production-ready QuestDB on Kubernetes. I look forward to enhancing this setup in future releases when monitoring and ops features will be supported.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>tutorial</category>
      <category>googlecloud</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Realtime Crypto Tracker with Kafka and QuestDB</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Thu, 18 Feb 2021 17:58:14 +0000</pubDate>
      <link>https://dev.to/yitaek/realtime-crypto-tracker-with-kafka-and-questdb-11f3</link>
      <guid>https://dev.to/yitaek/realtime-crypto-tracker-with-kafka-and-questdb-11f3</guid>
      <description>&lt;p&gt;Analyze cryptocurrency price trends in realtime via Kafka and store for further investigation in a timeseries database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QNzpTQM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/12000/0%2AlvU5t2nwTjIS5TOA" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QNzpTQM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/12000/0%2AlvU5t2nwTjIS5TOA" alt="Photo by [M. B. M.](https://unsplash.com/@m_b_m?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="880" height="587"&gt;&lt;/a&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@m_b_m?utm_source=medium&amp;amp;utm_medium=referral"&gt;M. B. M.&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;h1&gt;
  
  
  “Bitcoin soars past $50,000 for the first time” — &lt;a href="https://www.cnn.com/2021/02/16/investing/bitcoin-50000-price-record/index.html"&gt;CNN&lt;/a&gt;
&lt;/h1&gt;
&lt;h1&gt;
  
  
  “Tesla invests $1.5 billion in bitcoin, will start accepting it as payment” — &lt;a href="https://www.washingtonpost.com/business/2021/02/08/tesla-bitcoin-musk-dogecoin/"&gt;Washington Post&lt;/a&gt;
&lt;/h1&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not a day goes by without some crypto news stealing the headlines these days. From institutional support of Bitcoin to central banks around the world exploring some form of digital currency, interest in cryptocurrency has never been higher. This is also reflected in the daily exchange volume:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7JhSbnP---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2532/1%2AngFuLwi6QU4ibmzSR3EQVw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7JhSbnP---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2532/1%2AngFuLwi6QU4ibmzSR3EQVw.png" alt="" width="880" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As someone interested in the future of DeFi (&lt;a href="https://yitaek.medium.com/intro-to-defi-b4ab2ec0f156"&gt;decentralized finance&lt;/a&gt;), I wanted to better track the price of different cryptocurrencies and store them into a timeseries database for further analysis. I found an interesting talk by Ludvig Sandman and Bruce Zulu at Kafka Summit London 2019, “&lt;a href="https://www.confluent.io/kafka-summit-lon19/using-kafka-streams-analyze-trading-crypto-exchanges/"&gt;Using Kafka Streams to Analyze Live Trading Activity for Crypto Exchanges&lt;/a&gt;”, so I decided to leverage Kafka and modify it for my own use. In this tutorial, we will use Python to send real-time cryptocurrency metrics into Kafka topics, store these records in QuestDB, and perform moving average calculations on this time series data with numpy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;p&gt;At a high level, this project polls the public Coinbase API for the price of Bitcoin, Ethereum, and Chainlink. This information is then published onto individual topics on Kafka (e.g. topic_BTC). The raw price information is sent to a QuestDB via Kafka Connect to populate the timeseries database. At the same time, a separate consumer also pulls that data and calculates a moving average for a quick trend analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lzeVgetw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4384/1%2ArmhaHVmus3WFNZxa1kqUtg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lzeVgetw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4384/1%2ArmhaHVmus3WFNZxa1kqUtg.png" alt="" width="880" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The codebase is organized into three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;docker-compose&lt;/strong&gt;: holds docker-compose file to start Kafka (zookeeper, broker, kafka connect), QuestDB, and JSON file to initialize Kafka Connect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;docker&lt;/strong&gt;: Dockerfile to build Kafka Connect image (pre-built image is available via docker-compose)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python files&lt;/strong&gt;: grabs latest pricing information from Coinbase, pubishes information to Kafka, and calculates a moving average&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you would like to analyze different cryptocurrencies or extend the simple moving average example with a more complicated algorithm like relative strength index analysis, feel free to fork the repo on &lt;a href="https://github.com/Yitaek/kafka-crypto-questdb"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Docker (with at least 4GB memory): if using Docker Desktop, go to Settings -&amp;gt; Resources -&amp;gt; Memory and increase he default limit from 2GB to 4GB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python 3.7+&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up Kafka &amp;amp; QuestDB
&lt;/h2&gt;

&lt;p&gt;Before pulling data from Coinbase, we need a running instance of a Kafka cluster and QuestDB. In the repo, I have a working docker-compose file with Confluent Kafka components (i.e. zookeeper, broker, Kafka Connect) and QuestDB. If you would like to run this on the cloud or run it locally, follow the instructions on the &lt;a href="https://docs.confluent.io/platform/current/quickstart/index.html"&gt;Confluent website&lt;/a&gt;. Otherwise simply apply the docker-compose file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd docker-compose
docker-compose up -d 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The docker-compose file runs the following services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Zookeeper&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kafka Broker&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kafka Connect with JDBC driver&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;QuestDB&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Kafka Connect image is based on confluentinc/cp-kafka-connect-base:6.1.0 . If you wish to modify this image (e.g. add a new connector to MongoDB or modify the bootup process), you can override the &lt;a href="https://github.com/Yitaek/kafka-crypto/blob/main/docker/Dockerfile"&gt;Dockerfile&lt;/a&gt; and build it locally.&lt;/p&gt;

&lt;p&gt;Wait for the Kafka cluster to come up. Watch the logs in the connect container until you see the following messages:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2021-02-17 01:55:54,456] INFO [Worker clientId=connect-1, groupId=compose-connect-group] Starting connectors and tasks using config offset -1 (org.apache.kafka.connect.runtime.distributed.DistributedHerder)

[2021-02-17 01:55:54,456] INFO [Worker clientId=connect-1, groupId=compose-connect-group] Finished starting connectors and tasks (org.apache.kafka.connect.runtime.distributed.DistributedHerder)

[2021-02-17 01:55:54,572] INFO [Worker clientId=connect-1, groupId=compose-connect-group] Session key updated (org.apache.kafka.connect.runtime.distributed.DistributedHerder)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Configuring Postgres Sink
&lt;/h2&gt;

&lt;p&gt;At this point, we have a health Kafka cluster and a running instance of QuestDB, but they are not connected. Since QuestDB supports Kafka Connect JDBC driver, we can leverage the PostgreSQL sink to populate our database automatically. Post this connector definition to our Kafka Connect container:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Make sure you're inside the docker-compose directory

$ curl -X POST -H "Accept:application/json" -H "Content-Type:application/json" --data @postgres-sink-btc.json [http://localhost:8083/connectors](http://localhost:8083/connectors)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;postgres-sink-btc.json holds the following configuration details:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "postgres-sink-btc",
  "config": {
    "connector.class":"io.confluent.connect.jdbc.JdbcSinkConnector",
    "tasks.max":"1",
    "topics": "topic_BTC",
    "key.converter": "org.apache.kafka.connect.storage.StringConverter",
    "value.converter": "org.apache.kafka.connect.json.JsonConverter",
    "connection.url": "jdbc:postgresql://questdb:8812/qdb?useSSL=false",
    "connection.user": "admin",
    "connection.password": "quest",
    "key.converter.schemas.enable": "false",
    "value.converter.schemas.enable": "true",
    "auto.create": "true",
    "insert.mode": "insert",
    "pk.mode": "none"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Some important fields to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;topics&lt;/strong&gt;: Kafka topic to consume and convert into Postgres format&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;connection&lt;/strong&gt;: Using default credentials for QuestDB (admin/quest) on port 8812&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;value.converter:&lt;/strong&gt; This example uses JSON with schema, but you can also use Avro or raw JSON. If you would like to override the default configuration, you can refer to &lt;a href="https://docs.mongodb.com/kafka-connector/v1.3/kafka-sink-data-formats/"&gt;Kafka Sink Connector Guide&lt;/a&gt; from MongoDB.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Poll Coinbase for Latest Crypto Prices
&lt;/h2&gt;

&lt;p&gt;Now our that our Kafka-QuestDB connection is made, we can start pulling data from Coinbase. The Python code requires numpy , kafka-python , and pandasto run. Using pip , install those packages and run the getData.py script:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pip install -r requirements.txt
$ python getData.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It will now print out debug message with pricing information as well as the schema we’re using to populate QuestDB:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initializing Kafka producer at 2021-02-17 14:38:18.655069
Initialized Kafka producer at 2021-02-17 14:38:18.812354
API request at time 2021-02-17 14:38:19.170623

Record: {'schema': {'type': 'struct', 'fields': [{'type': 'string', 'optional': False, 'field': 'currency'}, {'type': 'float', 'optional': False, 'field': 'amount'}, {'type': 'string', 'optional': False, 'field': 'timestamp'}], 'optional': False, 'name': 'coinbase'}, 'payload': {'timestamp': datetime.datetime(2021, 2, 17, 14, 38, 19, 170617), 'currency': 'BTC', 'amount': 50884.75}}

API request at time 2021-02-17 14:38:19.313046
Record: {'schema': {'type': 'struct', 'fields': [{'type': 'string', 'optional': False, 'field': 'currency'}, {'type': 'float', 'optional': False, 'field': 'amount'}, {'type': 'string', 'optional': False, 'field': 'timestamp'}], 'optional': False, 'name': 'coinbase'}, 'payload': {'timestamp': datetime.datetime(2021, 2, 17, 14, 38, 19, 313041), 'currency': 'ETH', 'amount': 1809.76}}

API request at time 2021-02-17 14:38:19.471573
Record: {'schema': {'type': 'struct', 'fields': [{'type': 'string', 'optional': False, 'field': 'currency'}, {'type': 'float', 'optional': False, 'field': 'amount'}, {'type': 'string', 'optional': False, 'field': 'timestamp'}], 'optional': False, 'name': 'coinbase'}, 'payload': {'timestamp': datetime.datetime(2021, 2, 17, 14, 38, 19, 471566), 'currency': 'LINK', 'amount': 31.68216}}

API request at time 2021-02-17 14:38:23.978928
Record: {'schema': {'type': 'struct', 'fields': [{'type': 'string', 'optional': False, 'field': 'currency'}, {'type': 'float', 'optional': False, 'field': 'amount'}, {'type': 'string', 'optional': False, 'field': 'timestamp'}], 'optional': False, 'name': 'coinbase'}, 'payload': {'timestamp': datetime.datetime(2021, 2, 17, 14, 38, 23, 978918), 'currency': 'BTC', 'amount': 50884.75}}

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Query Data on QuestDB
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://questdb.io/"&gt;QuestDB&lt;/a&gt; is a fast, open-source, timeseries database with SQL support. This makes it a great candidate to store financial market data for further historical trend analysis and generating trade signals. By default, QuestDB ships with a console UI exposed on port 9000. Navigate to localhost:9000 and query Bitcoin tracking topic topic_BTC to see price data stream in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--h_G1yqIs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2008/1%2AefUTBhIkifU5Vgg4lX3ecA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--h_G1yqIs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2008/1%2AefUTBhIkifU5Vgg4lX3ecA.png" alt="" width="880" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can repeat this process for the other topics as well. If you prefer to run without a UI, you can also use the REST API to check:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -G \
--data-urlencode "query=select * from topic_BTC" \
http://localhost:9000/exp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;QuestDB console UI also provides the ability to generate basic graphs. Click on the Chart tab underneath the Tables. Select line as the chart type, timestamp as the label, and click Draw :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OprEy_yd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2548/1%2Acn0zn94ZieOLuYbdnpjkYQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OprEy_yd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2548/1%2Acn0zn94ZieOLuYbdnpjkYQ.png" alt="" width="880" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, the QuestDB native charting capabilities are currently limited. For more advanced visualization, check out my previous guide on &lt;a href="https://yitaek.medium.com/streaming-heart-rate-data-with-iot-core-and-questdb-84304069592e"&gt;streaming heart rate data to QuestDB&lt;/a&gt; under the “Visualizing Data with Grafana” section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate Moving Average
&lt;/h2&gt;

&lt;p&gt;While we store the raw data on QuestDB for more sophisticated analysis, we can also consume from the same topics to calculate a quick moving average. This may be useful if you want to also post these records to another Kafka topic that you may use on a dashboard or to set alerts on pricing trends.&lt;/p&gt;

&lt;p&gt;On a separate terminal, run the moving average script:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python movingAverage.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It will print out the moving average of 25 data points and post it to topic__ma_25 :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting Apache Kafka consumers and producer
Initializing Kafka producer at 2021-02-17 16:28:33.584649
Initialized Kafka producer at 2021-02-17 16:28:33.699208

Consume record from topic 'topic_BTC' at time 2021-02-17 16:28:34.933318
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.072581
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.075352
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.077106
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.088821
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.091865
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.094458
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.096814
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.098512
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.100150
Produce record to topic 'topic_BTC_ma_25' at time 2021-02-17 16:28:35.103512
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you wish to also populate these data points into QuestDB, supplement the JSON data with schema information in movingAverage.py similar to the way it is defined in the new_data JSON block in getData.py . Then create another Postgres sink via curl with topic set as topic__ma_25 .&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;To stop streaming data, simply stop the Python scripts. To destroy the Kafka cluster and QuestDB, run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker-compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;While this is a simple example, you can extend this to optimize the data format with Avro, connect it with your Coinbase account to execute trades based on trading signals, or test out different statistical methods on the raw data. Feel free to submit a PR to make this repo more useful.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>kafka</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Stream heart rate data into QuestDB via Google IoT Core</title>
      <dc:creator>Yitaek Hwang</dc:creator>
      <pubDate>Thu, 11 Feb 2021 18:40:11 +0000</pubDate>
      <link>https://dev.to/yitaek/stream-heart-rate-data-into-questdb-via-google-iot-core-lc5</link>
      <guid>https://dev.to/yitaek/stream-heart-rate-data-into-questdb-via-google-iot-core-lc5</guid>
      <description>&lt;p&gt;An end-to-end demo of a simple IoT system to stream and visualize heart rate data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1Yug8xAX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/10368/0%2Am_6w4yHF3UKsXlLA" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1Yug8xAX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/10368/0%2Am_6w4yHF3UKsXlLA" alt="Photo by [Louis Reed](https://unsplash.com/@_louisreed?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="880" height="587"&gt;&lt;/a&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@_louisreed?utm_source=medium&amp;amp;utm_medium=referral"&gt;Louis Reed&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks to the growing popularity of fitness trackers and smartwatches, more people are tracking their biometrics data closely and integrating IoT into their everyday lives. In my search for a DIY heart rate tracker, I found an excellent walkthrough from Brandon Freitag and &lt;a href="https://dev.toundefined"&gt;Gabe Weiss&lt;/a&gt;, using Google Cloud services to stream data from a Raspberry Pi with a heart rate sensor to BigQuery via IoT Core and Cloud Dataflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T-TO1Y8w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/0%2APOR1pmV-c868f6KM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T-TO1Y8w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/0%2APOR1pmV-c868f6KM.png" alt="Image Credit: [Google Codelab](https://codelabs.developers.google.com/codelabs/iotcore-heartrate#0)" width="668" height="224"&gt;&lt;/a&gt;&lt;em&gt;Image Credit: &lt;a href="https://codelabs.developers.google.com/codelabs/iotcore-heartrate#0"&gt;Google Codelab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Although Cloud Dataflow supports streaming inserts to BigQuery, I wanted to take this opportunity to try out a new time-series database I came across called &lt;a href="https://questdb.io/"&gt;QuestDB&lt;/a&gt;. QuestDB is a fast open-source time-series database with Postgres compatibility. The &lt;a href="http://try.questdb.io:9000/"&gt;live demo&lt;/a&gt; on the website queried the NYC taxi rides dataset with over 1.6 billion rows in milliseconds, so I was excited to give this database a try. To round out the end-to-end demo, I used Grafana to pull and visualize data from QuestDB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jQjNcGZr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2APcFIaFkLgtTSkqMduXZmSg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jQjNcGZr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2APcFIaFkLgtTSkqMduXZmSg.png" alt="Data Pipeline" width="795" height="170"&gt;&lt;/a&gt;&lt;em&gt;Data Pipeline&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;NodeJS v14+&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://console.developers.google.com/billing/freetrial?hl=en&amp;amp;pli=1"&gt;Google Cloud Account&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cloud.google.com/sdk/docs/install"&gt;gcloud sdk&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Optional&lt;/em&gt;: &lt;a href="https://www.arrow.com/en/research-and-events/articles/codelabs-using-iot-core-to-stream-heart-rate-data"&gt;*Raspberry Pi kit&lt;/a&gt;*&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this tutorial, we will use a Debian image to run simulated heart rate data through IoT Core. If you wish to send real sensor data from Raspberry Pi, purchase the optional kit listed above and follow the &lt;a href="https://codelabs.developers.google.com/codelabs/iotcore-heartrate#6"&gt;install instructions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Cloud Setup
&lt;/h2&gt;

&lt;p&gt;In order to use Cloud IoT Core and Cloud Pub/Sub, you need to first create a Google Cloud Platform account and a new project (mine is called questdb-iot-demo ). Then, enable IoT Core, Compute Engine, and Pub/Sub APIs under APIs &amp;amp; Services -&amp;gt; Enable APIs and Services -&amp;gt; Search for APIs &amp;amp; Services:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TksbinGe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AJASmiIHrAIw-ZNWktXmE0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TksbinGe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AJASmiIHrAIw-ZNWktXmE0g.png" alt="" width="670" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  IoT Core Setup
&lt;/h3&gt;

&lt;p&gt;IoT Core is Google’s fully-managed IoT service to help securely connect and manage IoT devices. In this demo, we will create a registry called heartrate and send MQTT data. Click on “Create Registry” and set the Registry ID and Region based on the geographic region closest to you (for me it was us-central1):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9ASDNep1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ApI-2zJUOaNOi6NPeVVJ1mw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9ASDNep1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ApI-2zJUOaNOi6NPeVVJ1mw.png" alt="" width="548" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we need to configure a Pub/Sub topic to publish device data to. Under “Select a Cloud Pub/Sub topic”, click on “Create a Topic” and give it the Topic ID heartratedata :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K2ETwaYg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2082/1%2Az18ffFaCdBURZO60qX1Y7A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K2ETwaYg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2082/1%2Az18ffFaCdBURZO60qX1Y7A.png" alt="" width="880" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the “Registry properties” and “Cloud Pub/Sub topics” are configured, click on “Create”.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute Engine Setup
&lt;/h3&gt;

&lt;p&gt;Now it’s time to add our simulated device. In order for our device to communicate with IoT Core, we need to add a public key. Head over to Compute Engine -&amp;gt; Create.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Pd_TmHAD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AE37dquvwXKb0xf28dNFVyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Pd_TmHAD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AE37dquvwXKb0xf28dNFVyg.png" alt="" width="826" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default options (e2-medium, Debian 10 image, us-central1) will work for our simulator (*Note: make sure to match the region with the IoT Core registry region if you chose something other than us-central1 *). Once the VM is ready, click on the SSH button under “Connect” and install the project code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install git
sudo apt-get update
sudo apt-get install git

# Clone project code
git clone [https://github.com/googlecodelabs/iotcore-heartrate](https://github.com/googlecodelabs/iotcore-heartrate)
cd iotcore-heartrate

# Install all the core packages
chmod +x initialsoftware.sh
./initialsoftware.sh

# Generate the keys
chmod +x generate_keys.sh
./generate_keys.sh

# View the keys (highlighting will copy the contents)
cat ../.ssh/ec_public.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  IoT Device Setup
&lt;/h3&gt;

&lt;p&gt;Once you have the ec_public.pem key, head back to the IoT Core Registry. Under “Devices”, click on “Create a Device”. For Device ID, enter raspberryHeartRate and expand the “Communication, Cloud Logging, Authentication” pull down:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4NSQdBbA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AZeqjhCAVqyJ3VhKEbdNc1Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4NSQdBbA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AZeqjhCAVqyJ3VhKEbdNc1Q.png" alt="" width="543" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under Authentication, change the “Public key format” to ES256 and paste in the key from our VM or Raspberry Pi and click “Create”:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S1dCVSxo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AvXv2Qb2xSqKZnvcKjD_3BQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S1dCVSxo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AvXv2Qb2xSqKZnvcKjD_3BQ.png" alt="" width="531" height="385"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Pub/Sub Setup
&lt;/h3&gt;

&lt;p&gt;Finally, we need to create a subscription to our Pub/Sub topic to pull messages and insert into QuestDB. Head over to “Pub/Sub” and click on our heartratedata topic. Name the subscription questdb and click create.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_idYP6ns--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4428/1%2AE8RlFLMkdfJVDvtVk4bE1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_idYP6ns--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4428/1%2AE8RlFLMkdfJVDvtVk4bE1w.png" alt="" width="880" height="112"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  QuestDB Setup
&lt;/h2&gt;

&lt;p&gt;At this point, we have everything on Google Cloud to send data to our Pub/Sub topic. Now we need to write some code to take those messages and insert them into QuestDB. Let’s start by starting up QuestDB via Docker.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 9000:9000 -p 8812:8812 questdb/questdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The above command pulls the latest QuestDB image (v5.0.6) and maps port 9000 for the console UI and port 8812 for Postgres operations. Open up the QuestDB console at (&lt;a href="http://127.0.0.1:9000/"&gt;http://127.0.0.1:9000/&lt;/a&gt;) and create our heart_rate table:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE heart_rate(sensorID STRING, uniqueID STRING, timecollected TIMESTAMP, heartrate DOUBLE);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1RcAXV_v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/5056/1%2A4zKbzzD3sM8Cf8Zn426SiA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1RcAXV_v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/5056/1%2A4zKbzzD3sM8Cf8Zn426SiA.png" alt="" width="880" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;Note: the UI doesn’t automatically refresh so if you don’t see the heart_rate table populated on the tables panel, click on the refresh icon above the tables.&lt;/em&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  Pub/Sub to QuestDB
&lt;/h3&gt;

&lt;p&gt;Since there’s no native integration for Pub/Sub, we will need to write a simple program to listen to new Pub/Sub messages and insert the data into QuestDB. I’m using NodeJS v14.15.4, but you can use similar client libraries for Pub/Sub and Postgres to achieve the same.&lt;/p&gt;

&lt;p&gt;First, configure the gcloud sdk to authenticate with your GCP project without having to download a service account (see &lt;a href="https://medium.com/dev-genius/simple-gcp-authentication-with-service-accounts-6b877c2e2649"&gt;Simple GCP Authentication with Service Accounts&lt;/a&gt; for more details).&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Set default profile
$ gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, create a new NodeJS workspace and install @google-cloud/pubsub and pg libraries. You can use the code below to listen to Pub/Sub and stream to QuestDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending Data
&lt;/h3&gt;

&lt;p&gt;Finally, we are ready to send the simulated data. Switch back to the Compute Engine and ssh into the VM again. Issue the command below to send the data to our IoT Core device:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python heartrateSimulator.py --project_id=questdb-iot-demo --registry_id=heartrate --device_id=raspberryHeartRate --private_key_file=../.ssh/ec_private.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If successful, you should see some logs like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
Publishing message #544: '{"sensorID": "heartrate.raspZero", "heartrate": 72.56881801680139, "uniqueID": "c1ca9656-671f-4fa7-8c03-12fdfb4f422f-heartrate.raspZero", "timecollected": "2018-07-07 20:54:50"}'Publishing message #545: '{"sensorID": "heartrate.raspZero", "heartrate": 72.8324264524384, "uniqueID": "8d6337b7-204f-4209-88c0-46a79d1911bb-heartrate.raspZero", "timecollected": "2018-07-07 20:54:59"}'
Finished.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now run our NodeJS code and we should see data populated in QuestDB:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--epGa0a8c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4204/1%2AWtWKc4mVd8S96CTyj8obeg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--epGa0a8c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4204/1%2AWtWKc4mVd8S96CTyj8obeg.png" alt="" width="880" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualizing Data with Grafana
&lt;/h2&gt;

&lt;p&gt;Although QuestDB console provides some default visualizations out of the box, to simulate a more realistic scenario of combining all the metrics to Grafana, we’ll set up a Postgres data source and visualize our heart rate data.&lt;/p&gt;

&lt;p&gt;Download Grafana and login at &lt;a href="http://localhost:3000/login"&gt;http://localhost:3000/login&lt;/a&gt; with admin/admin&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -p 3000:3000 grafana/grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Under “Configuration” -&amp;gt; “Data Sources”, search for PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ErcW7PP8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3812/1%2AaOiJOAGcOGa-ebyaCb3P3A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ErcW7PP8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3812/1%2AaOiJOAGcOGa-ebyaCb3P3A.png" alt="" width="880" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For PostgreSQL connection, enter the following (password: quest) and save&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iV-mYNEQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2080/1%2ALd-vyY53sO0IeFzVkL8vaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iV-mYNEQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2080/1%2ALd-vyY53sO0IeFzVkL8vaw.png" alt="" width="880" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let’s create a dashboard. Create a dashboard and add the following SQL query:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT
  timecollected AS "time",
  heartrate
FROM heart_rate 
ORDER BY time;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we see the sample heart rate data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uR3nH9S6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2800/1%2AyD7pPkHLvsPUjRc_5HAVrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uR3nH9S6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2800/1%2AyD7pPkHLvsPUjRc_5HAVrg.png" alt="" width="880" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a side note, the same chart can be drawn on QuestDB console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kdJgnQ5f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4012/1%2ASlCNCxOXEWbJW31WUtmf6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kdJgnQ5f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4012/1%2ASlCNCxOXEWbJW31WUtmf6w.png" alt="" width="880" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, we have an end-to-end system of a device securely sending data via IoT Core and streaming data into QuestDB. We can extend this example to multiple devices by adding them under IoT Core and scaling our server to using pooled connections to more efficiently add data to QuestDB. At scale, we can also look at average heart rates instead of raw data points (e.g. avg(heartrate) as average_heartrate from heart_rate ).&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>node</category>
      <category>postgres</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
