Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jakejscott/Humidifier

AWS Cloudformation using C#
https://github.com/jakejscott/Humidifier

aws cloudformation dotnet

Last synced: 12 days ago
JSON representation

AWS Cloudformation using C#

Awesome Lists containing this project

README

        

# Humidifier [![Build status](https://ci.appveyor.com/api/projects/status/qidmpegskc7tp020/branch/master?svg=true)](https://ci.appveyor.com/project/superlogical/humidifier/branch/master) [![NuGet](https://img.shields.io/nuget/v/Humidifier.svg)](https://www.nuget.org/packages/Humidifier/)

Humidifier allows you to build AWS CloudFormation templates programmatically. Stacks and resources are represented as C# objects with accessors for all their supported properties.

The code is automatically generated by parsing the official Cloudformation [specification](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html).

## Similar Projects
- Humidifier (Ruby) https://github.com/localytics/humidifier
- GoFormation (Go) https://github.com/awslabs/goformation

## New feature: Project templates

To get up and running with Humidifier quickly, we've included a production ready template for deploying and managing your AWS Cloudformation/Serverless projects. The template is installed using the `dotnet new` cli command. Your can [view the code](/templates/Humidifier.Solution.Template) here.

Features:

- Cross platform deploy tool (Windows/Mac OSX/Linux) built using dotnet core.
- Cloudformation stacks are defined in strongly typed C# using Humidifer of course.
- A README that has a quickstart with commands generated for you to get up and running quickly.
- Includes a simple Lambda function that can be deployed in seconds, configured with Serilog and Cloudwatch metrics.
- Includes code for invoking the Lambda function, tailing the log and viewing the response.
- Config using .env files and environment variables for CI/CD.
- Secrets management using .aes files (encrypt/decrypt) and sync to parameter store.
- Unit test project

Step 1: Install the template

```ps
dotnet new -i Humidifier.Templates::*
```

This will download the template from https://www.nuget.org/packages/Humidifier.Templates and install it into the dotnet cli template cache.

Step 2: Check to see the template is installed

```ps
dotnet new --list
````

You should see output similar to this:

| Templates | Short Name | Language | Tags
| --------------------------------------------------| --------------------| ------------------| ---------------------
| Humidifier Solution Template | humidifier.sln | [C#] | AWS/Lambda/Serverless
| Console Application | console | [C#], F#, VB | Common/Console
| Class library | classlib | [C#], F#, VB | Common/Library
| Unit Test Project | mstest | [C#], F#, VB | Test/MSTest
| xUnit Test Project | xunit | [C#], F#, VB | Test/xUnit
| global.json file | globaljson | | Config
| NuGet Config | nugetconfig | | Config
| Web Config | webconfig | | Config
| Solution File | sln | | Solution

Step 3: Create your project

```ps
dotnet new humidifier.sln --name Example --output Example --env test --region us-west-2 --stack example --profile default
```

This will generate the solution in the `--output` folder. The `--profile` flag is your AWS credential profile. To setup a AWS profile, follow the docs on how to [configure the AWS Cli](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) here.

Step 3: Read the generated README.

The template generates a README that has a quickstart with all the commands for building, testing, deploying and destroying your stacks. If your reading this on Github, browse to the [template README](templates/Humidifier.Solution.Template/README.md) to see what's included.

-----------------

## Getting started

Nuget:

```powershell
dotnet add package Humidifier
dotnet add package Humidifier.Json
```

Stacks are represented by the Humidifier.Stack class. Resources are represented by an exact mapping from AWS resource names to Humidifier resources names (e.g. AWS::EC2::Instance becomes Humidifier.EC2.Instance). Resources have properties for each JSON attribute.

There's also a [demo application](https://github.com/jakejscott/Humidifier/blob/master/src/Humidifier.ConsoleTest/Program.cs) which creates a template and writes it [out to a file](https://github.com/jakejscott/Humidifier/blob/master/src/Humidifier.ConsoleTest/cloudformation.template) using JSON.

### Example usage
````csharp
using System.Collections.Generic;
using System.IO;
using Humidifier.Json;

namespace Humidifier.ConsoleTest
{
public static class Program
{
public static void Main(string[] args)
{
Stack stack = BuildStack();

var serializer = new JsonStackSerializer();
var template = serializer.Serialize(stack);

File.WriteAllText("cloudformation.template", template);
}

private static Stack BuildStack()
{
var stack = new Stack
{
AWSTemplateFormatVersion = "2010-09-09",
Description = "Description"
};

stack.Add("Environment", new Parameter
{
Type = "String",
Description = "Deployment environment",
MinLength = 3,
MaxLength = 4,
AllowedValues = new List { "test", "uat", "prod" },
ConstraintDescription = "Allowed values: [test, uat, prod]"
});

stack.Add("AutomationStack", new Parameter
{
Type = "String",
Description = "Automation stack name",
MinLength = 1,
MaxLength = 255,
AllowedPattern = "^[a-zA-Z][-a-zA-Z0-9]*$",
ConstraintDescription = "Must be a valid Cloudformation Stack name"
});

stack.Add("CodeS3Key", new Parameter
{
Type = "String",
MinLength = 3
});

stack.Add("VPC", new EC2.VPC
{
CidrBlock = "10.0.0.0/16",
EnableDnsSupport = false,
EnableDnsHostnames = false,
InstanceTenancy = "dedicated",
Tags = new List
{
new Tag { Key = "foo", Value = "bar" }
}
});

stack.Add("Subnet", new EC2.Subnet
{
VpcId = Fn.Ref("VPC"),
CidrBlock = "10.0.0.0/24",
AvailabilityZone = Fn.Select("0", Fn.GetAZs(Fn.Ref("AWS::Region")))
});

stack.Add("Ec2Instance", new EC2.Instance
{
ImageId = Fn.FindInMap("RegionMap", Fn.Ref("AWS::Region"), "64"),
InstanceType = "m1.small",
UserData = Fn.Base64(
@"#!/bin/bash -e
wget https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64/chef_11.6.2-1.ubuntu.12.04_amd64.deb
dpkg -i chef_11.6.2-1.ubuntu.12.04_amd64.deb"
)
});

stack.Add("AutomationServiceRole", new IAM.Role
{
AssumeRolePolicyDocument = new PolicyDocument
{
Statement = new List
{
new Statement
{
Effect = "Allow",
Principal = new { Service = "cloudformation.amazonaws.com" },
Action = "sts:AssumeRole"
}
}
}
});

stack.Add("DeploymentBucket", new S3.Bucket { BucketName = Fn.Ref("AWS::StackName") });
stack.Add("DeploymentBucketPolicy", new S3.BucketPolicy
{
Bucket = Fn.Ref("DeploymentBucket"),
PolicyDocument = new PolicyDocument
{
Version = "2012-10-17",
Statement = new List
{
new Statement
{
Effect = "Allow",
Principal = new
{
AWS = Fn.GetAtt("AutomationServiceRole", IAM.Role.Attributes.Arn)
},
Action = "s3:*",
Resource = new[]
{
Fn.Join("", "arn:aws:s3:::", Fn.Ref("DeploymentBucket")),
Fn.Join("", "arn:aws:s3:::", Fn.Ref("DeploymentBucket"), "/*")
}
}
}
}
});

stack.Add("LambdaFunction", new Lambda.Function
{
Timeout = 30,
FunctionName = new { Ref = "AWS::StackName" },
Runtime = "dotnetcore1.0",
Description = "",
Handler = "SomeProject::SomeProject.SomeFunction::FunctionHandler",
MemorySize = 256,
Code = new Code
{
S3Bucket = Fn.ImportValue(Fn.Sub("${AutomationStack}-DeploymentBucket")),
S3Key = new { Ref = "CodeS3Key" },
},
Environment = new Environment
{
Variables = new Dictionary
{
["EnvironmentName"] = Fn.Ref("Environment")
}
},
});

stack.Add("MonitoringSnsTopic", new SNS.Topic
{
DisplayName = Fn.Ref("AWS::StackName"),
Subscription = new List
{
new SNS.Subscription { Endpoint = "[email protected]", Protocol = "email" }
}
});

stack.Add("KmsKey", new KMS.Key
{
Description = "A Key",
KeyPolicy = new PolicyDocument
{
Id = "key-default-1",
Version = "2012-10-17",
Statement = new List
{
new Statement
{
Sid = "Allow the administration of the key",
Effect = "Allows",
Principal = new {AWS = "arn:aws:iam::123456789012:user/Alice"},
Action = new[]
{
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
},
Resource = "*"
}
}
}
});

var regionMap = new Mapping
{
["us-east-1"] = new Dictionary { ["32"] = "ami-6411e20d", ["64"] = "ami-7a11e213" },
["us-west-1"] = new Dictionary { ["32"] = "ami-c9c7978c", ["64"] = "ami-cfc7978a" },
["ue-west-1"] = new Dictionary { ["32"] = "ami-37c2f643", ["64"] = "ami-31c2f645" },
["ap-southeast-1"] = new Dictionary { ["32"] = "ami-66f28c34", ["64"] = "ami-60f28c32" },
["ap-northeast-1"] = new Dictionary { ["32"] = "ami-9c03a89d", ["64"] = "ami-a003a8a1" }
};

stack.Mappings.Add("RegionMap", regionMap);

return stack;
}
}
}

````

### Functions

You can use CFN intrinsic functions and references using Fn.[name]. Those will build appropriate structures that know how to be dumped to CFN syntax appropriately.

```csharp
Fn.FindInMap("RegionMap", Fn.Ref("AWS::Region"), "64");
```

```csharp
Fn.GetAtt("MyElb", ElasticLoadBalancing.LoadBalancer.Attributes.DNSName);
```

```csharp
Fn.GetAZs("us-east-2");
```

```csharp
Fn.ImportValue(Fn.Sub("${NetworkStackNameParameter}-SubnetID"));
```

```csharp
Fn.Join("", "arn:aws:s3:::", Fn.Ref("DeployBucket"), "/*");
```

```csharp
Fn.Ref("BucketName");
```

```csharp
Fn.Select("1", new[] { "a", "b", "c" });
```

```csharp
Fn.Split("|", "a|b|c");
```

```csharp
Fn.Sub("${AWS::StackName}-${AWS::Region}-bucket");
```

```csharp
Fn.Select("1", Fn.Split("|", "a|b|c"));
```

NOTE: Because JSON doesn't allow newlines, there's a known hack where you can join multiple lines together using Fn::Join

```csharp
Fn.Base64(Fn.Join("",
"#!/bin/bash -e\n",
"wget https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64/chef_11.6.2-1.ubuntu.12.04_amd64.deb\n",
"dpkg -i chef_11.6.2-1.ubuntu.12.04_amd64.deb\n"
));
```

But that's gross and unreadable when outputted as JSON. Instead use a multiline C# string, and let the library take care
of encoding it for you:

- Whitespace on the start of the line is trimmed, which means you can indent your code nicely (Like you can in YAML).
- Newlines are encoded as `\r\n` automatically by NewtonSoft.Json.

```csharp
Fn.Base64(
@"#!/bin/bash -e
wget https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64/chef_11.6.2-1.ubuntu.12.04_amd64.deb
dpkg -i chef_11.6.2-1.ubuntu.12.04_amd64.deb"
);
````

### Conditions

````csharp
stack.Add("CreateProdResources", new Condition(Fn.Equals(Fn.Ref("Environment"), "prod")));
````

````csharp
stack.Add("CreateDevResources", new Condition(Fn.Equals(Fn.Ref("Environment"), "dev")));
````

````csharp
stack.Add("NotCondition", new Condition(Fn.Not(Fn.Equals(Fn.Ref("Environment"), "prod"))));
````

````csharp
stack.Add("AndCondition",
new Condition(
Fn.And(
Fn.Equals("sg-mysqgroup", Fn.Ref("SecurityGroup")),
new { Condition = "NotCondition" }
)
)
);
````

````csharp
stack.Add("OrCondition",
new Condition(
Fn.Or(
Fn.Equals("sg-mysqgroup", Fn.Ref("SecurityGroup")),
new { Condition = "NotCondition" }
)
)
);
````

To specify a condition on a resource use the overload to `stack.Add` and pass in the `condition` parameter.

````csharp
stack.Add("Volume", new EC2.Volume
{
Size = 100,
AvailabilityZone = Fn.GetAtt("Ec2Instance", EC2.Instance.Attributes.AvailabilityZone)
},
condition: "CreateProdResources");
````