Skip to content

Create EC2 Module

This quickstart creates a Nullstone Application module that provisions an AWS EC2 instance. This walks you through:

  • Registering a new module in Nullstone (and defining the contract)
  • Developing the Terraform code
  • Testing the module in a sandbox environment
  • Publishing the final version to Nullstone

The following category, provider, and platform are used when registering the module:

  • Category: app
  • Subcategory: server
  • Provider: aws
  • Platform: ec2

A working copy of this code can be found at https://github.com/nullstone-modules/create-ec2-guide.

Prerequisites

Before following this quickstart, make sure the following are installed and configured.

  1. An AWS Account
  2. Install/Configure Nullstone CLI
  3. Install Terraform CLI

TIP

If you are unfamiliar with Terraform, it may be helpful to follow their basic guide.

Create EC2 module

Create a new repository/directory to store the module. We recommend having a repository for each module; however, you may have multiple modules in a single repository as long as they are in different directories.

The following will create a .nullstone/module.yaml that contains metadata about the Nullstone module.

shell
mkdir test-ec2 && cd test-ec2
nullstone modules generate
# This command will ask you a set of questions to configure the module
# Specify the following:
#   Module Name: test-ec2
#   Friendly Name: Test EC2 Instance
#   Description: Creates an EC2 instance
#   Make this module available to everybody? No
#   Category: app
#   Subcategory: server
#   ProviderTypes: aws
#   Platform: ec2
mkdir test-ec2 && cd test-ec2
nullstone modules generate
# This command will ask you a set of questions to configure the module
# Specify the following:
#   Module Name: test-ec2
#   Friendly Name: Test EC2 Instance
#   Description: Creates an EC2 instance
#   Make this module available to everybody? No
#   Category: app
#   Subcategory: server
#   ProviderTypes: aws
#   Platform: ec2

Register module

Run modules new to register the module. This uses .nullstone/module.yaml to register in the Nullstone catalog.

shell
nullstone modules new
nullstone modules new

Define network connection

Add a network to our module by creating a network.tf with the following.

hcl
data "ns_connection" "network" {
  name     = "network"
  contract = "aws/network/vpc"
}

locals {
  vpc_id             = data.ns_connection.network.outputs.vpc_id
  private_subnet_ids = data.ns_connection.network.outputs.private_subnet_ids
}
data "ns_connection" "network" {
  name     = "network"
  contract = "aws/network/vpc"
}

locals {
  vpc_id             = data.ns_connection.network.outputs.vpc_id
  private_subnet_ids = data.ns_connection.network.outputs.private_subnet_ids
}

Define EC2 instance

Create a file named instance.tf with the following contents.

hcl
data "aws_ami" "this" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

locals {
  ami = data.aws_ami.this.id
}

resource "aws_instance" "this" {
  ami                         = local.ami
  instance_type               = "t3.nano"
  subnet_id                   = local.private_subnet_ids[0]
  vpc_security_group_ids      = [aws_security_group.this.id]
  associate_public_ip_address = false
  tags                        = merge(local.tags, { Name = local.resource_name })
}

resource "aws_security_group" "this" {
  name   = local.resource_name
  vpc_id = local.vpc_id
  tags   = merge(local.tags, { Name = local.resource_name })
}

resource "aws_security_group_rule" "this-https-to-world" {
  security_group_id = aws_security_group.this.id
  type              = "egress"
  protocol          = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_blocks       = ["0.0.0.0/0"]
}
data "aws_ami" "this" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

locals {
  ami = data.aws_ami.this.id
}

resource "aws_instance" "this" {
  ami                         = local.ami
  instance_type               = "t3.nano"
  subnet_id                   = local.private_subnet_ids[0]
  vpc_security_group_ids      = [aws_security_group.this.id]
  associate_public_ip_address = false
  tags                        = merge(local.tags, { Name = local.resource_name })
}

resource "aws_security_group" "this" {
  name   = local.resource_name
  vpc_id = local.vpc_id
  tags   = merge(local.tags, { Name = local.resource_name })
}

resource "aws_security_group_rule" "this-https-to-world" {
  security_group_id = aws_security_group.this.id
  type              = "egress"
  protocol          = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_blocks       = ["0.0.0.0/0"]
}

Enable nullstone ssh

To enable SSH to the server using Nullstone, add an adminer user to adminer.tf.

hcl
resource "aws_iam_user" "adminer" {
  name = "adminer-${local.resource_name}"
  tags = local.tags
}

resource "aws_iam_access_key" "adminer" {
  user = aws_iam_user.adminer.name
}

resource "aws_iam_user_policy" "adminer" {
  user   = aws_iam_user.adminer.name
  policy = data.aws_iam_policy_document.adminer.json
}

data "aws_iam_policy_document" "adminer" {
  statement {
    sid     = "AllowSSMSession"
    effect  = "Allow"
    actions = ["ssm:StartSession"]

    resources = [
      aws_instance.this.arn,
      "arn:aws:ssm:us-east-1::document/AWS-StartSSHSession",
    ]
  }
}
resource "aws_iam_user" "adminer" {
  name = "adminer-${local.resource_name}"
  tags = local.tags
}

resource "aws_iam_access_key" "adminer" {
  user = aws_iam_user.adminer.name
}

resource "aws_iam_user_policy" "adminer" {
  user   = aws_iam_user.adminer.name
  policy = data.aws_iam_policy_document.adminer.json
}

data "aws_iam_policy_document" "adminer" {
  statement {
    sid     = "AllowSSMSession"
    effect  = "Allow"
    actions = ["ssm:StartSession"]

    resources = [
      aws_instance.this.arn,
      "arn:aws:ssm:us-east-1::document/AWS-StartSSHSession",
    ]
  }
}

Adhere to server/aws-ec2

This quickstart launches an EC2 instance using Nullstone. To get the full benefits of Nullstone, amend outputs.tf to adhere to the app/aws/ec2 contract.

TIP

Terraform does not support output types in the definition. Nullstone encodes these into the description field as seen below.

hcl
data "aws_region" "this" {}
output "region" {
  value       = data.aws_region.this.name
  description = "string ||| The AWS region where the EC2 instance resides."
}

output "instance_id" {
  value       = aws_instance.this.id
  description = "string ||| The Instance ID of the EC2 instance."
}

output "adminer" {
  value = {
    name       = aws_iam_user.adminer.name
    access_key = aws_iam_access_key.adminer.id
    secret_key = aws_iam_access_key.adminer.secret
  }

  description = "object({ name: string, access_key: string, secret_key: string }) ||| An AWS User with explicit privilege to admin the EC2 instance."
  sensitive   = true
}
data "aws_region" "this" {}
output "region" {
  value       = data.aws_region.this.name
  description = "string ||| The AWS region where the EC2 instance resides."
}

output "instance_id" {
  value       = aws_instance.this.id
  description = "string ||| The Instance ID of the EC2 instance."
}

output "adminer" {
  value = {
    name       = aws_iam_user.adminer.name
    access_key = aws_iam_access_key.adminer.id
    secret_key = aws_iam_access_key.adminer.secret
  }

  description = "object({ name: string, access_key: string, secret_key: string }) ||| An AWS User with explicit privilege to admin the EC2 instance."
  sensitive   = true
}

Test in Sandbox

To test our new module, create a sandbox environment and necessary dependencies. In this scenario, we need a network and an app for our new EC2 module.

Create sandbox provider

Follow the guide to Connect to AWS to create a provider named aws-sandbox.

Create sandbox environment

Create a sandbox stack and a sandbox environment.

shell
nullstone stacks new --name=sandbox --description="Sandbox for module testing"
nullstone envs new --name=sandbox --stack=sandbox --provider=aws-sandbox --region=us-east-1
nullstone stacks new --name=sandbox --description="Sandbox for module testing"
nullstone envs new --name=sandbox --stack=sandbox --provider=aws-sandbox --region=us-east-1

Create sandbox network

In our sandbox stack, create a network block using the nullstone/aws-network module. In our example, let's name the network network1 and launch the network.

shell
nullstone blocks new --name=network1 --stack=sandbox --module=nullstone/aws-network
nullstone up --block=network1 --env=sandbox --watch
nullstone blocks new --name=network1 --stack=sandbox --module=nullstone/aws-network
nullstone up --block=network1 --env=sandbox --watch

Create sandbox EC2

In our sandbox stack, create a block using our new module named test-ec2.

shell
nullstone blocks new --name=test-ec2 --stack=sandbox --module=test-ec2
nullstone blocks new --name=test-ec2 --stack=sandbox --module=test-ec2

Initialize Terraform using the following command.

shell
nullstone workspaces select --block=test-ec2 --env=sandbox
nullstone workspaces select --block=test-ec2 --env=sandbox

TIP

This command will generate __backend__.tf and add to .gitignore. This contains state backend information that is automatically changed when workspaces select is run.

Configure AWS credentials

Previously, you configured a provider in Nullstone, but you need to authenticate with AWS locally test the module locally. Authenticate with AWS and export AWS_REGION, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY. These access keys should have access to the aws-sandbox AWS account.

shell
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=<access key id>
export AWS_SECRET_ACCESS_KEY=<secret access key>
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=<access key id>
export AWS_SECRET_ACCESS_KEY=<secret access key>

Apply infrastructure changes

Run terraform to test the module.

shell
terraform apply
terraform apply

SSH into server

Once the infrastructure is provisioned, SSH into the box using the following command.

TIP

It can take several minutes for an EC2 instance to boot and become available depending on the image.

shell
nullstone ssh --app=test-ec2 --env=sandbox
nullstone ssh --app=test-ec2 --env=sandbox

Publish module

Once your module is complete, publish the module to the Nullstone registry. A user can launch this module directly from the Nullstone UI without any coding or Terraform setup.

sh
nullstone modules publish --version=v0.0.1
nullstone modules publish --version=v0.0.1