Advent-of-spin Challenge 4

이 포스트는 advent-of-spin에 업로드 된 Challenge 4에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • /distance-latlong route에서 아래와 같은 데이터 형식을 포함한 요청을 POST로 받아야 합니다.
    • Payload body: { d1: { lat: 0f, long: 0f }, d2: { lat: 0f, long: 0f }}
  • Response Header에 Content-Type: application/json 를 포함하여 아래와 같은 데이터 형식을 반환합니다.
    • Payload response body: { distance: 0f }****

Work

  • spin new로 새로운 프로젝트를 만들고 spin.toml 에서 /disatance-latlong 에 대해서 요청을 받을 수 있게 route를 수정합니다.

    spin_version = "1"
    authors = ["Cute_Wisp <sweatpotato13@gmail.com>"]
    description = ""
    name = "challenge"
    trigger = { type = "http", base = "/" }
    version = "0.1.0"
    
    [[component]]
    id = "challenge"
    source = "target/wasm32-wasi/release/challenge.wasm"
    [component.trigger]
    route = "/distance-latlong"
    [component.build]
    command = "cargo build --target wasm32-wasi --release"
    
  • src/lib.rs 파일을 아래와 같이 수정하였습니다.

    extern crate haversine;
    use anyhow::Result;
    use http::*;
    use serde::{Deserialize, Serialize};
    use spin_sdk::{
        http::{Request, Response},
        http_component,
    };
    
    #[derive(Debug, Serialize, Deserialize)]
    pub struct Coordinate {
        lat: f64,
        long: f64,
    }
    
    #[derive(Debug, Serialize, Deserialize)]
    pub struct DistanceRequest {
        d1: Coordinate,
        d2: Coordinate,
    }
    
    #[derive(Debug, Serialize)]
    pub struct DistanceResponse {
        distance: f64,
    }
    
    /// A simple Spin HTTP component.
    #[http_component]
    fn distance_latlog(req: Request) -> Result<Response> {
        assert_eq!(*req.method(), Method::POST);
    
        let distance_request: DistanceRequest =
            serde_json::from_slice(req.body().clone().unwrap().to_vec().as_slice()).unwrap();
    
        let d1: Coordinate = distance_request.d1;
        let d2: Coordinate = distance_request.d2;
        let start1 = haversine::Location {
            latitude: d1.lat,
            longitude: d1.long,
        };
        let end1 = haversine::Location {
            latitude: d2.lat,
            longitude: d2.long,
        };
        let distance = haversine::distance(start1, end1, haversine::Units::Miles);
    
        let nautical_miles = unit_conversions::length::miles::to_nautical_miles(distance);
    
        let rounded_miles = (nautical_miles * 10.0).round() / 10.0;
    
        let distance_response = DistanceResponse {
            distance: rounded_miles,
        };
    
        let json_response = serde_json::to_string(&distance_response)?;
    
        Ok(http::Response::builder()
            .status(200)
            .header("Content-Type", "application/json")
            .body(Some(json_response.into()))?)
    }
    
  • make test 로 테스트 수행 Untitled
  • spin deploy로 클라우드에 업로드 Untitled
  • 아래 명령어로 Submit
    • serviceUrl 에는 위에서 얻은 클라우드 주소를 넣습니다.
      hurl --variable serviceUrl="https://challenge-rcunxh7u.fermyon.app" submit.hurl
      

      Untitled



Advent-of-spin Challenge 3

이 포스트는 advent-of-spin에 업로드 된 Challenge 3에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • / route에서 HTTP 200을 응답으로 리턴
    • response header에 Content-Type: text/html
    • <h1> 태그에 “Welcome to the Advent of Spin!” 메시지가 페이지에 포함되어야 함
    • Body에 “Santa’s on his way” 가 포함되어야 함

Work

  • https://github.com/fermyon/bartholomew-site-template.git 를 기반으로 프로젝트를 구성합니다.
  • spin up 으로 프로젝트 실행 후 curl -v [http://localhost:3000](http://localhost:3000) 으로 접속해보면 아래와 같은 로그를 통하여 헤더에 text/html 이 포함되어 있음을 확인할 수 있다. Untitled
  • content/index.md 를 아래와 같이 수정하였습니다. Untitled
  • spin deploy 로 클라우드에 업로드 Untitled
  • 위에서 획득한 URl을 통해 제출
    hurl --variable serviceUrl="https://bartholomew-template-oskncmgc.fermyon.app" submit.hurl
    

    Untitled



Advent-of-spin Challenge 2

이 포스트는 advent-of-spin에 업로드 된 Challenge 2에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • / 요청에 404 에러 띄우기
  • /lowercase 요청에 아래와 같은 동작을 수행합니다.
    • Response Header에 Content-Type: application/json
    • POST Method 로 요청을 받고 body에 value 값을 포함하여 받습니다.
    • {message: lowercase_string_of_value} 를 응답으로 보냅니다.
  • /hello/* 요청에 아래와 같은 동작을 수행합니다.
    • Response Header에 Content-Type: application/json
    • {message: "Hello, world!} 가 포함된 JSON Payload를 응답으로 보냅니다.
    • /hello/cutewisp 처럼 path가 뒤에 추가로 붙었을 경우 lower-case로 Hello, cutewisp!와 같이 응답을 보냅니다.
      • outbount_http 를 사용하여 위에서 만든 Endpoint로 요청을 보내서 구현하면 간단하게 구현할 수 있습니다.

Work

  • Spin 프레임워크가 하나의 파일에 2개 이상의 route를 포함하는걸 지원하지 않기 때문에 각각의 route에 대해서 별도의 파일로 구성해야 합니다.
  • 이 Challenge 에서는 총 3개의 Route가 필요합니다 (/ , /lowercase , /hello/* ) 그래서 아래와 같이 spin.toml을 설정해 3개의 디렉토리를 지정했습니다.

    • 각각의 디렉터리는 spin new 를 통해서 생성하였습니다.
    spin_version = "1"
    authors = ["Cute_Wisp <sweatpotato13@gmail.com>"]
    description = ""
    name = "challenge"
    trigger = { type = "http", base = "/" }
    version = "0.1.0"
    
    [[component]]
    id = "handle_404"
    source = "handle_404/target/wasm32-wasi/release/handle_404.wasm"
    allowed_http_hosts = ["insecure:allow-all"]
    [component.trigger]
    route = "/"
    [component.build]
    command = "cargo build --target wasm32-wasi --release"
    workdir = "handle_404"
    
    [[component]]
    id = "lowercase"
    source = "lowercase/target/wasm32-wasi/release/lowercase.wasm"
    allowed_http_hosts = ["insecure:allow-all"]
    [component.trigger]
    route = "/lowercase"
    [component.build]
    command = "cargo build --target wasm32-wasi --release"
    workdir = "lowercase"
    
    [[component]]
    id = "hello"
    source = "hello/target/wasm32-wasi/release/hello.wasm"
    allowed_http_hosts = ["insecure:allow-all"]
    [component.trigger]
    route = "/hello/..."
    [component.build]
    command = "cargo build --target wasm32-wasi --release"
    workdir = "hello"
    
  • handle_404 컴포넌트는 단순하게 / 라우터를 통해서 접속하는 요청에 대해 404를 응답하는 코드를 작성하였습니다.

    use anyhow::Result;
    use spin_sdk::{
        http::{not_found, Request, Response},
        http_component,
    };
    
    /// A simple Spin HTTP component.
    #[http_component]
    fn handle_404(req: Request) -> Result<Response> {
        return not_found();
    }
    
  • lowercase 컴포넌트는 아래와 같이 코드를 작성하였습니다.

    use anyhow::Result;
    use serde::{Deserialize, Serialize};
    use spin_sdk::{
        http::{not_found, Request, Response},
        http_component,
    };
    
    #[derive(Debug, Deserialize)]
    struct LowercaseRequest {
        value: String,
    }
    
    #[derive(Serialize)]
    struct LowercaseResponse {
        message: String,
    }
    
    /// A simple Spin HTTP component.
    #[http_component]
    fn lowercase(req: Request) -> Result<Response> {
        let method = req.method();
        if method == "POST" {
            let lowercase_request: LowercaseRequest =
                serde_json::from_slice(req.body().clone().unwrap().to_vec().as_slice()).unwrap();
    
            let lowercase_response = LowercaseResponse {
                message: lowercase_request.value.to_lowercase(),
            };
    
            let json_response = serde_json::to_string(&lowercase_response)?;
    
            return Ok(http::Response::builder()
                .status(200)
                .header("Content-Type", "application/json")
                .body(Some(json_response.into()))?);
        } else {
            return not_found();
        }
    }
    
    • req.method() 를 통해서 request 의 method를 가져올 수 있습니다.
    • req.body() 를 파싱하여 LowercaseRequest 형식의 JSON으로 파싱하여 lowercase_request 변수에 저장합니다. 이 과정에서 serde_json 패키지를 사용하였습니다.
    • lowercase_request.value 를 통해서 value 값을 가져와 이를 lowercase로 변환하고 response body에 담을 데이터를 정의합니다.
    • json_response 형식으로 만들어서 body에 담아 리턴합니다.
    • .header("Content-Type", "application/json") 도 추가 해 줍니다.
  • hello 컴포넌트는 아래와 같이 코드를 작성하였습니다.

    use anyhow::Result;
    use serde::{Deserialize, Serialize};
    use spin_sdk::{
        http::{Request, Response},
        http_component,
    };
    
    #[derive(Serialize)]
    struct LowercaseRequest {
        value: String,
    }
    
    #[derive(Debug, Deserialize)]
    struct LowercaseResponse {
        message: String,
    }
    
    #[derive(Serialize)]
    struct HelloResponse {
        message: String,
    }
    
    /// A simple Spin HTTP component.
    #[http_component]
    fn hello(req: Request) -> Result<Response> {
        let default_value: &str = "world";
    
        let name: &str = req
            .headers()
            .get("spin-path-info")
            .unwrap()
            .to_str()
            .unwrap();
    
        let mut hello_response = HelloResponse {
            message: format!("Hello, {}!", default_value),
        };
    
        if name != "" {
            let lowercase_request = LowercaseRequest {
                value: name.strip_prefix("/").unwrap().to_string(),
            };
            let host: &str = req.headers().get("host").unwrap().to_str().unwrap();
            let spin_full_url: &str = req
                .headers()
                .get("spin-full-url")
                .unwrap()
                .to_str()
                .unwrap();
    
            let protocol = spin_full_url.split_once("://").unwrap().0;
    
            let uri = format!("{}://{}/lowercase", protocol, host);
            println!("{:?}", uri);
            let res = spin_sdk::outbound_http::send_request(
                http::Request::builder().method("POST").uri(uri).body(Some(
                    serde_json::to_string(&lowercase_request).unwrap().into(),
                ))?,
            )?;
    
            let lowercase_response: LowercaseResponse =
                serde_json::from_slice(res.body().clone().unwrap().to_vec().as_slice()).unwrap();
    
            hello_response.message = format!("Hello, {}!", lowercase_response.message);
        }
    
        let json_response = serde_json::to_string(&hello_response)?;
    
        Ok(http::Response::builder()
            .status(200)
            .header("Content-Type", "application/json")
            .body(Some(json_response.into()))?)
    }
    
    • path 가 없을 경우 기본 값을 default_value 로 정의하여 선언하였습니다.
    • header에 담긴 spin-path-info 를 참고하여 path를 가져옵니다.
    • name 이 빈 스트림이 아니라면 path가 있는것이므로 분기 처리를 합니다.
    • path가 없을경우 기본값을 사용하여 json_response 를 만들어 바로 리턴합니다.
    • path가 있을경우 path의 값을 따와서 lowercase route로 보낼 request를 정의합니다
    • 테스트 및 제출은 동일한 host에서 이루어지므로 해당 요청으로 들어온 host 및 protocol을 header에서 가져와 lowercase route의 host를 정의합니다.
    • lowercase route로 요청을 보내고 받은 응답을 가지고 hello_response 응답 메시지를 정의한 뒤 리턴합니다.
  • make test 명령어로 테스트를 수행 해 보았습니다. Untitled
  • 아래 명령어로 과제 제출
    • serviceUrl 은 위에 deploy를 통해서 획득한 endpoint를 넣으면 됩니다.
      hurl --variable serviceUrl="https://challenge-rtbzeodc.fermyon.app" submit.hurl
      

      Untitled



Advent-of-spin Challenge 1

이 포스트는 advent-of-spin에 업로드 된 Challenge 1에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • Response의 Header에 Content-Type: application/json 명시하기
  • {message: "Hello, world!"} 형식의 Json Payload를 Response에 담기

Work

  • spin new 명령어로 새로운 프로젝트를 생성합니다. Untitled Untitled
  • spin build 로 빌드 할 수 있습니다 Untitled
  • spin up 으로 실행 가능 Untitled Untitled
  • lib.rs 파일을 아래와 같이 수정하여 response body의 데이터를 변경해 주었습니다.

    use anyhow::Result;
    use spin_sdk::{
        http::{Request, Response},
        http_component,
    };
    
    /// A simple Spin HTTP component.
    #[http_component]
    fn challenge(req: Request) -> Result<Response> {
        println!("{:?}", req.headers());
        let data = r#"{
            "message": "Hello, world!"
        }"#;
        Ok(http::Response::builder()
            .status(200)
            .header("Content-Type", "application/json")
            .body(Some(data.into()))?)
    }
    
  • 위에서 안내한 대로 빌드 후 서버를 실행해 Response를 확인 해 보았습니다.
    • 정상적으로 header에 application/json 이 명시되어 있고 response body도 정상적으로 출력 되고 있음을 확인할 수 있습니다. Untitled
  • make test 명령어로 테스트를 수행 해 보았습니다.
    • Hurl 을 설치해야 합니다. Untitled
  • spin deploy 로 cloud 에 deploy 합니다. Untitled
  • 아래 명령어로 과제 제출
    • serviceUrl 은 위에 deploy를 통해서 획득한 endpoint를 넣으면 됩니다.
      hurl --variable serviceUrl="https://challenge-rtbzeodc.fermyon.app" submit.hurl
      

      Untitled



WASM 기반의 Microservice Framework Spin

Spin


Untitled

Spin은 WebAssembly를 이용하여 Microservice를 구현할 수 있는 오픈소스 프레임워크입니다. Rust나 Go 로 구현할 수 있는 간단한 이벤트 중심의 프레임워크 입니다.

현재 Rust, Go, JavaScript 등 언어를 지원하고 있지만 spin은 WASM 기반으로 동작하기 때문에 WASM을 빌드할 수 있는 대부분에 언어로 사용할 수 있습니다.

Install


Spin은 아래 커맨드를 실행하여 설치할 수 있습니다.

curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
sudo mv ./spin /usr/local/bin/spin

혹은 깃허브에서 직접 바이너리를 받아서 설치할 수 있습니다.

  • 아래 예제는 Arm 맥북을 기준으로 설명한 예시 입니다. OS 및 Architecture에 따라 설치 파일이 다르기 때문에 다른 환경을 사용하시는 분들은 https://github.com/fermyon/spin/releases 에 가셔서 본인 환경에 맞는 파일을 설치하시면 됩니다.
% mkdir spin && cd spin
% curl -L https://github.com/fermyon/spin/releases/download/v0.2.0/spin-v0.2.0-macos-aarch64.tar.gz > spin-v0.2.0-macos-aarch64.tar.gz
% tar xfv spin-v0.2.0-macos-aarch64.tar.gz

Try


Spin을 사용하여 프로젝트를 만들고자 할 때 템플릿을 이용하여 만들 수 있습니다.

$ spin templates list
You have no templates installed. Run
spin templates install --git https://github.com/fermyon/spin
to install a starter set.

처음 실행한다면 아무런 템플릿을 가지고 있지 않아서 위와 같이 나타납니다. 아래 커맨드를 이용하여 기본적으로 제공하는 템플릿을 설치할 수 있습니다.

spin templates install --git https://github.com/fermyon/spin
Copying remote template source
Installing template redis-rust...
Installing template http-rust...
Installing template http-go...
+--------------------------------------------------+
| Name         Description                         |
+==================================================+
| http-go      HTTP request handler using (Tiny)Go |
| http-rust    HTTP request handler using Rust     |
| redis-rust   Redis message handler using Rust    |
| ...                                              |
+--------------------------------------------------+

spin new 명령어를 이용하여 템플릿을 사용해 새로운 프로젝트를 만들 수 있습니다.

$ spin new
Pick a template to start your project with:
  http-c (HTTP request handler using C and the Zig toolchain)
  http-csharp (HTTP request handler using C# (EXPERIMENTAL))
  http-go (HTTP request handler using (Tiny)Go)
  http-grain (HTTP request handler using Grain)
> http-rust (HTTP request handler using Rust)
  http-swift (HTTP request handler using SwiftWasm)
  http-zig (HTTP request handler using Zig)
  redis-go (Redis message handler using (Tiny)Go)
  redis-rust (Redis message handler using Rust)

Enter a name for your new project: hello_rust
Project description: My first Rust Spin application
HTTP base: /
HTTP path: /...
$ tree
├── .cargo
│   └── config.toml
├── .gitignore
├── Cargo.toml
├── spin.toml
└── src
    └── lib.rs

Spin 프로젝트는 spin.toml 과 다른 구성요소로 이루어 집니다. spin.toml 은 spin 애플리케이션의 매니페스트 파일입니다.

Line 5: 이 애플리케이션이 http 요청을 트리거 하는 앱임을 나타냅니다.

Line 10: challenge.wasm 모듈을 실행한다는 내용을 정의하고 있습니다.

Untitled

src/lib.rs 는 다음과 같습니다.

Untitled

기본으로 작성되어 있는 함수는 단순히 요청을 받고 응답을 주는 간단한 코드로 되어 있습니다.

프로젝트를 빌드하기 위해서 spin build 명령어를 사용합니다.

Untitled

spin up 명령어로 빌드한 Wasm 파일을 실행할 수 있습니다.

Untitled

CLI에 나온 http://127.0.0.1:3000 으로 접속하면 실제로 코드에 구현된 대로 동작이 이루어짐을 확인할 수 있습니다.

Untitled

Description

fermyon에서 제공하는 Spin 프레임워크에 대해서 알아보았습니다. WASM으로 마이크로서비스를 이런 방식으로 실행하여 제공할 수 있다는 방식이 너무 흥미로웠습니다. 베타버전이긴 하지만 Docker도 현재 WASM 방식의 컨테이너를 지원하고 있는것으로 보아 해당 방식이 주목받고 있다는 느낌이 듭니다.

이를 잘 다듬으면 AWS Lambda처럼 가볍게 함수 단위로 마이크로서비스를 띄워서 운영할 수도 있지 않을까 하는 예측을 조심스레 해봅니다.