使用Encore.ts构建和部署TypeScript微服务到Kubernetes集群

2024/12/20 21:03:35

本文主要是介绍使用Encore.ts构建和部署TypeScript微服务到Kubernetes集群,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

构建和部署微服务应用可能会非常具有挑战性。 这通常是因为,使用传统框架时,需要花费大量精力来设置和管理必要的基础设施。设置好之后,还需要确保服务正常运作、安全和可扩展,并且能够彼此连接和沟通。

Encore.ts 是一个开源的 TypeScript 框架,解决了这些问题并简化了整个过程。 它让你能够构建强大的分布式系统,其中大部分工作会自动处理。

在这篇文章中,你将学习如何使用Encore.ts构建微服务,并将微服务部署到你的AWS账户中的Kubernetes集群。我们将展示Encore Cloud,一个Encore的托管DevOps自动化平台,如何自动化部署过程,从设置Kubernetes集群,配置所有必要的IAM策略及其他资源,直至你的微服务部署完成。

前提条件
  • 在你的电脑上本地安装 Encore(我们将在下一步进行安装)
  • Docker(你需要 Docker 来本地运行带有数据库的 Encore 应用)
  • 你选择的代码编辑工具

本教程的代码在这里可以找到这里,欢迎克隆并跟着一起做微服务,一起学习。

安装Encore软件

安装 Encore CLI 来启动你的本地开发环境:

  • macOS: brew install encoredev/tap/encore (Mac 操作系统)
  • Linux: curl -L https://encore.dev/install.sh | bash (Linux 系统)
  • Windows: iwr https://encore.dev/install.ps1 | iexiwr 表示使用 PowerShell 下载文件,iex 表示执行下载的脚本)(Windows 操作系统)
用Encore创建微服务架构

我们用Encore.ts先创建一个微服务。我们将构建一个简单的博客微服务来展示,展示如何用“Encore.ts”部署微服务。

创建一个新的应用

运行以下命令以创建一个新的Encore.ts应用程序:

执行 encore app 创建 blog-microservices

全屏模式,退出全屏

下面的命令将提示您为应用程序及其项目模板选择语言。应类似于下方的截图所示选择。

创建应用

现在进入到项目文件夹并使用以下命令运行你的应用程序:

    cd blog-microservices && encore run

进入全屏 退出全屏

下面的命令会在您的浏览器页面中打开API。

本地开发板

我们现在来看我们这个微服务应用的文件结构。在你的项目中创建如下文件结构。

    blog-microservices/
    ├── encore.app
    ├── posts-service/
    │   ├── encore.service.ts    // 服务定义
    │   ├── posts.ts            // API接口
    │   └── migrations/
    │       └── 1_create_posts.up.sql // 创建posts的迁移文件
    └── comments-service/
        ├── encore.service.ts    // 服务定义
        ├── comments.ts         // API接口
        └── migrations/
            └── 1_create_comments.up.sql // 创建comments的迁移文件

进入全屏模式,退出全屏模式

在这个项目结构中,我们有两个微服务,分别是posts-servicecomments-serviceposts-service处理与帖子相关的所有逻辑和功能,例如创建新帖子、获取帖子信息、更新和删除帖子。而comments-service则处理所有与评论相关的内容。这样,你的应用就被解耦了,你可以轻松独立地管理每个服务。

集成数据库

在创建了应用和项目文件之后,我们现在来创建数据库和模式。

在你的 posts-service/posts.ts 文件里添加如下代码以设置帖子数据库。

    import { SQLDatabase } from "encore.dev/storage/sqldb";

    // 数据库初始化
    const db = new SQLDatabase("comments", {
      migrations: "./迁移",
    });

全屏 退出全屏

然后将你的posts-service/migrations/1_create_posts.up.sql文件更新为,添加以下代码来定义posts表结构:

-- 下面的SQL语句是用来创建一个名为posts的表,这个表存储了文章的相关信息。
CREATE TABLE posts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid() -- 文章ID,主键,使用随机生成的UUID
    title TEXT NOT NULL, -- 文章标题,不能为空
    content TEXT NOT NULL, -- 文章内容,不能为空
    author_name TEXT NOT NULL, -- 作者姓名,不能为空
    created_at TIMESTAMP NOT NULL DEFAULT NOW() -- 创建时间,不能为空,使用当前时间
);

全屏,退出全屏

上述代码将创建一个包含如下字段的文章表。

  • id: 一个随机生成的唯一标识,用于区分每个帖子
  • title: 每个帖子的标题
  • content: 博客内容
  • author_name: 发帖者的用户名
  • created_at: 发帖时间

接下来,在你的 comments-service/posts.ts 文件中添加以下代码,以创建一个评论数据表:

    import { SQLDatabase } from "encore.dev/storage/sqldb";

    // 设置数据库
    const db = new SQLDatabase("posts", { // 表名:posts
      migrations: "./migrations",
    });

全屏显示 退出全屏

更新 comments-service/migrations/1_create_comments.up.sql 文件来创建 comments 表结构。

    CREATE TABLE comments (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        post_id UUID NOT NULL,
        文本 TEXT NOT NULL,
        作者名 TEXT NOT NULL,
        创建时间戳 TIMESTAMP NOT NULL DEFAULT NOW(),
        FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
    );

点击全屏模式, 点击退出全屏

上述代码将创建一个评论表格,包含以下字段:

  • id: 一个随机生成的 ID,用于唯一标识每个评论。
  • post_id: 关联的帖子 ID,指向帖子表。
  • content: 帖子的实际评论内容。
  • author_name: 评论者的名称。
  • created_at: 评论创建时间。

创建一个简单的REST API服务

现在更新你的 posts-service/posts 文件,为帖子服务微服务创建 REST API 服务。我们将添加一些端点,用于创建帖子、获取所有帖子以及根据 ID 获取帖子。

    //...
    import { api, Query } from "encore.dev/api";
    import { Topic } from "encore.dev/pubsub";

    //...

    // 接口定义
    interface PostEvent {
      id: string;
      title: string;
      authorName: string;
      action: "created" | "updated" | "deleted";
    }

    export const postCreatedTopic = new Topic<PostEvent>("post-created", {
      deliveryGuarantee: "at-least-once",
    });

    interface Post {
      id: string;
      title: string;
      content: string;
      authorName: string;
      createdAt: Date;
    }

    interface CreatePostRequest {
      title: string;
      content: string;
      authorName: string;
    }

    interface ListPostsRequest {
      limit?: Query<number>;
      offset?: Query<number>;
    }

    interface ListPostsResponse {
      posts: Post[];
      total: number;
    }

    export const createPost = api(
      {
        method: "POST",
        path: "/posts",
        expose: true,
      },
      async (req: CreatePostRequest): Promise<Post> => {
        const post = await db.queryRow<Post>`
                INSERT INTO posts (title, content, author_name)
                VALUES (${req.title}, ${req.content}, ${req.authorName})
                RETURNING 
                    id,
                    title,
                    content,
                    author_name as "authorName",
                    created_at as "createdAt"
            `;

        await postCreatedTopic.发布({
          id: post?.id as string,
          title: post?.title as string,
          authorName: post?.authorName as string,
          action: "created",
        });
        return post as Post;
      }
    );

    export const getPost = api(
      {
        method: "GET",
        path: "/posts/:id",
        expose: true,
      },
      async (params: { id: string }): Promise<Post> => {
        return (await db.queryRow<Post>`
                SELECT 
                    id,
                    title,
                    content,
                    author_name as "authorName",
                    created_at as "createdAt"
                FROM posts 
                WHERE id = ${params.id}
            `) as Post;
      }
    );

    export const listPosts = api(
      {
        method: "GET",
        path: "/posts",
        expose: true,
      },
      async (params: ListPostsRequest): Promise<ListPostsResponse> => {
        const limit = params.limit || 10;
        const offset = params.offset || 0;

        // 计算总数
        const totalResult = await db.queryRow<{ count: string }>`
                SELECT COUNT(*) as count FROM posts
            `;
        const total = parseInt(totalResult?.count || "0");

        // 获取分页的帖子
        const posts = await db.query<Post>`
                SELECT 
                    id,
                    title,
                    content,
                    author_name as "authorName",
                    created_at as "createdAt"
                FROM posts
                ORDER BY created_at DESC
                LIMIT ${limit} OFFSET ${offset}
            `;

        const result: Post[] = [];
        for await (const post of posts) {
          result.push(post);
        }

        return {
          posts: result,
          total,
        };
      }
    );

进入全屏 退出全屏

在上面的代码示例中,我们定义了一系列的 TypeScript 接口和 API 路由,用来管理帖子的服务。PostEvent 接口用于塑造发送到消息主题(postCreatedTopic)的消息,这个主题用于记录帖子的创建、更新和删除等操作。Post 接口主要定义了博客帖子的结构,而 CreatePostRequestListPostsRequestListPostsResponse 则提供了服务端点请求和响应处理的类型。我们创建了三条路由:createPost 用于将新帖子添加到数据库,并发布相关事件到主题;getPost 通过 ID 获取特定帖子;listPosts 提供分页帖子数据。

接下来,更新 comments-service/posts 以创建评论服务的 REST API。

    //...
    import { api, Query } from "encore.dev/api";

    //...

    // 类型
    interface Comment {
      id: string;
      postId: string;
      content: string;
      authorName: string;
      createdAt: Date;
    }

    interface CreateCommentRequest {
      postId: string;
      content: string;
      authorName: string;
    }

    interface ListCommentsRequest {
      limit?: Query<number>;
      offset?: Query<number>;
      postId: string;
    }

    interface ListCommentsResponse {
      comments: Comment[];
    }

    // 获取评论列表
    export const listComments = api(
      {
        method: "GET",
        path: "/comments/:postId",
        expose: true,
      },
      async (params: ListCommentsRequest): Promise<ListCommentsResponse> => {
        const limit = params.limit || 10;
        const offset = params.offset || 0;

        const comments = await db.query<Comment>`
                SELECT
                    id,
                    post_id as "postId",
                    content,
                    author_name as "authorName",
                    created_at as "createdAt"
                FROM comments
                WHERE post_id = ${params.postId}
                ORDER BY created_at DESC
                LIMIT ${limit} OFFSET ${offset}
            `;

        const result = (await comments).map(comment => comment);
        return { comments: result };
      }
    );

切换到全屏 退出全屏

在这个代码里,我们定义了接口和API路由来管理博客文章的评论。Comment接口定义了每个评论的数据模型,包括如postIdcreatedAt这样的字段。CreateCommentRequestListCommentsRequestListCommentsResponse接口则结构化了请求和响应,确保端点间交互的类型安全。我们实现了一个listComments功能,用来获取指定文章的分页评论。

实现服务间通信:

首先,我们需要在Encore中将每个微服务定义为一个服务。

posts-service/encore.service.ts 文件中添加以下代码:

    import { Service } from "encore.dev/service";

    // 导入Service类,并创建一个名为"posts"的服务实例
    export default new Service("posts");

全屏 退出全屏

请在 comments-service/encore.service.ts 文件中添加以下代码,以实现评论功能:

import { Service } from "encore.dev/service";

export default new Service("评论");

切换到全屏,退出全屏

接下来,要在 comments 服务中调用在 posts 服务中的这些端点,只需如下所示在 comments 服务中导入这些端点。

    import { posts } from "~encore/clients";

点击全屏/退出全屏

现在你可以像调用普通函数一样调用 comments 服务的各个端点,如下所示:

    // API
    export const createComment = api(
      {
        method: "POST",
        path: "/comments",
        expose: true,
      },
      async (req: CreateCommentRequest): Promise<Comment> => {
        // 检查帖子是否存在
        const post = await posts.getPost({ id: req.postId as string });
        if (!post) {
          throw new Error("未找到帖子");
        }

        return (await db.queryRow<Comment>`
                INSERT INTO comments (post_id, content, author_name)
                VALUES (${req.postId}, ${req.content}, ${req.authorName})
                RETURNING 
                    id,
                    post_id as "postId",
                    content,
                    author_name as "authorName",
                    created_at as "createdAt"
            `) as Comment;
      }
    );

切换到全屏模式 退出全屏模式

在这里,我们创建了一个名为 createComment 的功能,它允许用户给现有的帖子添加评论(在验证该帖子存在后),我们导入了帖子服务,并使用它来调用 getPost 方法以检查用户想评论的帖子是否存在。

本地功能测试

现在我们来测试一下微服务的API路由。回到你的API测试工具,测试一下你的端点。

本地测试

部署到 Kubernetes:

既然你已经成功运行了微服务应用,让我们使用Encore Cloud(Encore的DevOps自动化管理托管服务)自动将其部署到你的AWS(亚马逊网络服务)账户中的Kubernetes集群中。

我们将部署您的微服务应用到一个全新的Kubernetes集群中。您可以在那里找到将微服务部署到现有Kubernetes集群的指南文档:点击这里。

运行命令以部署应用程序:

    git add -A .
    git commit -m '第一次部署'
    git push encore # 再次或特定项目/环境名

全屏(点击进入/退出)

连接您的云账号:

将你的Encore.ts微服务部署到Kubernetes集群的第一步是将你的云账号(如AWS或GCP)连接到Encore Cloud中的应用程序。在这个教程中,我们将使用例如亚马逊网络服务(AWS)作为例子。按照以下步骤将你的AWS账号连接到Encore Cloud中的应用。

  1. 从您的 Encore Cloud 仪表板 中导航到:
  • 选择您的应用

  • 进入 应用设置 > 集成 > 连接云 选项
    1. 登录到您的 AWS 账户并按照以下步骤创建新的 IAM 角色:
  1. 在身份和访问管理(IAM)控制台中转到 创建角色 页面。

  2. 选择 另一个 AWS 账户 作为账户类型。

  3. 复制并粘贴您的 账户 ID 从 Encore Cloud。

  4. 选择 要求提供外部 ID 选项。

  5. 复制您的 外部 ID 从 Encore Cloud 并粘贴到 AWS 的 外部 ID 字段。AWS 配置

  6. 管理员访问 权限策略附加到该角色(Encore 代表您创建资源所需)。AWS 配置

  7. 输入角色名称和描述,然后点击 创建角色 按钮。AWS 配置

  8. 粘贴您的 AWS 角色 ARN,然后点击 继续 按钮,以将您的 Encore Cloud 连接到 AWS。云连接成功
搭建环境

现在已经成功连接了 AWS 控制台到你的 Encore.ts 云,让我们继续创建一个新的环境来部署微服务到 Kubernetes。接下来,请按照以下步骤操作:

  1. 在 Encore Cloud 控制台 打开你的应用,然后进入 环境,点击 创建环境创建环境
  2. 选择你的云服务提供商和计算平台,比如:环境配置
  • 选择 AWS 作为云服务提供商。

  • 指定 Kubernetes 作为计算平台(Encore 支持在 GCP 上使用 GKE 和在 AWS 上使用 EKS Fargate)。计算配置
    1. 决定是将所有服务放在一个进程中,还是为每个服务单独创建一个进程。进程配置

一旦您设置了这些配置,点击创建按钮以启动新的环境。Encore将根据您的环境配置在Kubernetes上部署基础设施。

创建了环境,如下图所示: 图片

在您的应用程序正在部署时,您可以在 Encore Cloud 仪表板中查看部署状态和环境详情。您也可以通过 kubectl 命令行工具查看您的 Kubernetes 集群。

收尾

这次教程里,你学会了如何将你的 Encore.ts 微服务应用程序构建和部署到 Kubernetes(通常简称 K8s)。

我们首先了解了Encore.ts是什么,以及它是如何帮助开发人员解决构建和部署微服务过程中遇到的挑战。

然后我们搭建了一个微服务应用,并将其部署到了AWS上的Kubernetes集群,使用了Encore Cloud平台。

既然你知道它是怎么运作的,也许你可以试着给这个应用添加一些新功能。

相关链接:

  • 更多关于使用 Encore 构建应用程序的信息,请参阅Encore 文档。
  • 您可以在 GitHub 上为 Encore 点 Star
  • 您还可以在 Encore 的 YouTube 频道 查看视频教程。


这篇关于使用Encore.ts构建和部署TypeScript微服务到Kubernetes集群的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程