Wing It: Cloud CRUD with Winglang

When it comes to infrastructure as code, tools like Terraform, AWS CDK, and Pulumi immediately come to mind. Although these tools include testing capabilities, they often lack a fully integrated local simulation environment that accurately mimics cloud behavior. This gap can lead to longer, more expensive iteration cycles during development. Winglang addresses this challenge by providing a local simulator that mimics cloud behavior, enabling developers to test their code locally before deploying it to the cloud. Additionally, its simple and intuitive syntax and concept of preflight(static infrastructure code) and inflight(the application logic) code makes it easy for developers to work with cloud resources, regardless of their level of experience. In this post, we’ll build a CRUD API using Winglang, deploy it to AWS, and build a GitOps workflow for the deployment and explore some of Winglang’s key capabilities.

Prerequisites: Docker, Node.js, Terraform, AWS account

This post is organized in the following sections: Outline:


Introduction to Winglang

Winglang is a high-level language that abstracts away the complexity of cloud infrastructure so you can focus on application logic. It unifies resource provisioning (preflight code) with dynamic operations (inflight code):

  • Preflight: Code that runs at compile time to set up static resources (e.g., VPCs, subnets).
  • Inflight: Code that runs at runtime to manage dynamic operations (e.g., ECS Containers, AWS Lambda).

Eventhough Winglang facilitates development in the cloud for non-experts and folks with little to no infra exposure, i think its main superpower is its local simulator. The local simulator gives you a simulation of all the resources in your code and you can interact with them as if they were already deployed. For example you can make api calls to validate if things get persisted in your database or if a notification gets triggered when certain event occurs in the system. And this could bring huge cost saving potentials in DevOps cycle as we are shifting a lot to the left.

Creating a CRUD API with Winglang

In this section, we will create a simple CRUD API using Winglang. Firstly please ensure you have installed Winglang on your machine. You can do this by running the following command:

npm install -g Winglang

Next, we can ‘import’ the modules we like to interact with or as Winglang really expressively introduces bring the modules we need as follows:

bring cloud; **// this will bring all the cloud modules**
bring dynamodb; **// for database**
bring http;  **// for testing our apis in local simulator**
bring expect; **// for testing**

I’d like to persist the data in a DynamoDB table, but you can of course opt for a bucket or any other db choice you are more comfortable with. Now I am going to create a table in DynamoDB and then create the CRUD operations for the table. Here is the code:

    let messagesTable = new dynamodb.Table(
      attributes: [
        {
          name: "id",
          type: "S"
        }
      ],
      hashKey: "id"
    );

Now if you notice, this code is considered preflight because it is static and it will run at compile time. The table will be created when the code is compiled and the table will be available when the code is deployed.

next I am going to ask wing to create an api for me that will interact with the table. Here is the code:

let api = new cloud.Api();

Then I’d like to also create a counter, which let’s me change id of the items that are being saved in DynamoDB. Here is the code: let messageCounter = new cloud.Counter();

This is again preflight code and static in nature. now let’s see how inflight code operates by creating a post method on our api which persists data to our database.

    api.post("/messages", inflight (request) => {
      let newMessage = Json{
        id: messageCounter.inc(),
        message: request.body
      };
      log("New message: {Json.stringify(newMessage)}");
    
      let recordId = messageCounter.inc();
      messagesTable.put(
        Item: {
          id: Json.stringify(recordId),
          message: newMessage
        }
      );
      return {
        status: 200,
        headers: {
          "Content-Type" => "text/html",
          "Access-Control-Allow-Origin" => "*",
        },
        body: Json.stringify(newMessage),
      };
    });

so as you can see, contrary to previous objects, we annotated this one with ‘inflight’ keyword. This means that this code will run at runtime and it will be executed.

Before deploying I’d like to add one final step, which is testing our api. To that end I added the following tests:

    let validateResponse = inflight(response: http.Response, expectedContent: str) => {
      log("Response status: {response.status}");
      expect.equal(response.status, 200);
      assert(response.body?.contains(expectedContent) == true);
    };
    
    test "messages API returns correct response" {
      let response = http.post("{api.url}/messages", {
        body: "xyz",
      });
      log("Response body: {response.body}");
      assert(response.body?.contains("xyz") == true);
    }

The first test is a helper function that validates the response of the api. The second test is the actual test that sends a post request to the api and validates the response.

Now run the following command to start the local simulator (docker must be running): bash wing it Now you should see the simulator like depicated in the image below: Winglang simulator

The simulator allows you to try many different aspects of your code to ensure that it works as expected. You can make api calls, check the logs, and even interact with the database. This is a very powerful tool that can help you to test your code before deploying it to the cloud.

Deploying the API to AWS

After successfully testing the code in the local simulator, we can now deploy the code to AWS. To do this, we need to compile the code the code to the platform of our choice. In this case I opted for Terraform , but AWS CDK also should be supported. Here is the command to compile the code:

wing compile -t tf-aws main.w

This code will generate the terraform code necessary to be deployed on AWS. You can navigate to target/main.tfaws and have a examine the generated files. Of course, at this point you can run the following terraform commands to deploy the changes to AWS:

terraform init
terraform apply

As this will create a local state file and does not enable a GitOps workflow, I’d like to show you how you can enable a GitOps workflow with Winglang.

Enabling a GitOps Workflow

To enable a GitOps workflow with Winglang, there is one addition that we need in our wing code which can be stored as a javascript file in the root of the project. Here is the code:

exports.Platform = class TFBackend {
    postSynth(config) {
        config.terraform.backend = {
        s3: {
            bucket: process.env.TF_BACKEND_BUCKET,          
            region: process.env.TF_BACKEND_REGION,          
            key: "state/terraform.tfstate",
            dynamodb_table: process.env.TF_BACKEND_TABLE    
        }
        };
        return config;
    }
}

This will ensure that the terraform state is stored in an s3 bucket and a dynamodb table is used to lock the state. This is necessary to enable a GitOps workflow with Winglang. the ‘script.sh’ file in the repository should help with the creation of the s3 and dynamodb resources. To improve things a bit further, we can also add a github action that will run the Winglang commands and deploy the code to AWS. Here is a link to the github action file. Now before everything falls in place, we need to create a github repository and push the code to the repository. After that, we need to create a secret in the repository with for aws credentials as well as Terraform state file. Of course the recommended approach is to use GitHub OIDC to authenticate with AWS, but for simplicity I created a temporary IAM user with minimal permissions and generated access key for it. Now you can add all these secrets and variables in github as variables in the repository settings. So the ones required by aws are:

AWS_ACCESS_KEY_ID
AWS_REGION
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
TF_BACKEND_BUCKET: [Terraform] the name of the s3 bucket
TF_BACKEND_TABLE: [Terraform] the name of the dynamodb table

Now you can push the code to the repository and the github action should run and deploy the code to AWS. You can check the status of the action in the actions tab in the repository.

Conclusion

In this post, we have seen how to create a simple CRUD API using Winglang and deploy it to AWS. We have also seen how to enable a GitOps workflow with Winglang. Winglang is a powerful tool that can help developers to build and deploy cloud applications with ease. It provides a simple and intuitive syntax that makes it easy to work with cloud resources. It also provides a local simulator that allows developers to test their code before deploying it to the cloud. I hope this post has been helpful and that you have learned something new. If you have any questions or comments, please feel free to leave them below. Thank you for reading!

This project is maintained by pedramha