Using Pulumi and AWS Secrets Manager to Launch NGINX Plus Instances in AWS
Introduction
Some time ago we published an article on using CloudFormation Templates to create NGINX Plus instances. In this follow up article, we will be doing the same thing using Pulumi, an infrastructure as code platform that allows you to use familiar languages to write the code that builds your infrastructure. Within NGINX, we have used Pulumi to build a Modern Application Reference Architecture, which initially targets Kubernetes deployments. It might also be helpful to look at how you can use Pulumi to deploy NGINX Plus instances on AWS, into whatever VPC and subnet you specify.
A critical part of NGINX Plus install is to copy your NGINX Plus SSL Certificate and Key - required for access to the NGINX Plus private repository - into /etc/nginx/ssl/. There a few different ways to achieve this, but the AWS SecretsManager service provides a lot of audit, access control, and security capabilities.
In the previous CloudFormation example, the new EC2 Instance was configured to connect to AWS secrets manager and retrieve the secrets as part of the cloudinit post-install scripts. This is fine, but also requires creating an IAM role for the new server, which is an additional step, and increases the permissions of your instances. Because Pulumi integrates into common languages - Python in my example - we can be a little more flexible with how we do things, and we can retrieve the secrets and include them into the start up scrips (using the userdata field).
Prerequisites
You're going to need a functioning Pulumi installation that can communicate with AWS and a sample Python project. We recommend following the tutorial and checking you can perform the AWS equivalent of "Hello World' - creating an S3 Bucket. You can use this same project to create an NGINX install by editing the __main__.py file or create a new blank one.
You will also need your NGINX repo access certificate and key saved as secrets in AWS Secrets manager - in this case we are storing them with the names "nginxcert" and "nginxkey". You can get a free 30 day trial of NGINX Plus from NGINX.com
The Code Snippets
Let's take a look at the key parts of the Python code:
First import the pulumi modules
import pulumi import pulumi_aws as aws
Now define some variables - in a production setting these would obviously NOT be hard coded, but read as parameters.
# Variables VPC = "<your VPC>" AMI = "ami-0964546d3da97e3ab" # This is the ubuntu 20.04 LTS AMI for us-west-2 WebSecGroup = "<web server security group id>" AdminSecGroup = "<admin SSH security group>" subNet = "<subnet to deploy into>" size= "t2.micro" keyPair = "<SSH Key pair>"
Get the secret from AWS Secrets Manager:
# Get the secrets cert = aws.secretsmanager.get_secret_version(secret_id="nginxcert") key = aws.secretsmanager.get_secret_version(secret_id="nginxkey")
Next build the userdata for post install, this involves concatenating a number of strings together, along with the retrieved secrets, which need some minor text processing:
userdata = """#!/bin/bash -xe sudo apt-get update -y # good practice to update existing packages sudo mkdir /etc/ssl/nginx # install the key and secret echo \" """ # Add the certificate userdata = userdata + cert.secret_string userdata = userdata + """ \"| tr -d '"{}' | sudo tee /etc/ssl/nginx/nginx-repo.crt echo \" """ # Add the key userdata = userdata + key.secret_string # Do the NGINX install userdata = userdata + """ \"| tr -d '"{}' | sudo tee /etc/ssl/nginx/nginx-repo.key sudo wget https://cs.nginx.com/static/keys/nginx_signing.key && sudo apt-key add nginx_signing.key sudo wget https://cs.nginx.com/static/keys/app-protect-security-updates.key && sudo apt-key add app-protect-security-updates.key sudo apt-get install -y apt-transport-https lsb-release ca-certificates printf "deb https://pkgs.nginx.com/plus/ubuntu `lsb_release -cs` nginx-plus\n" | sudo tee /etc/apt/sources.list.d/nginx-plus.list sudo wget -P /etc/apt/apt.conf.d https://cs.nginx.com/static/files/90pkgs-nginx sudo apt-get update -y # good practice to update existing packages sudo apt-get install nginx-plus -y # install web server sudo systemctl start nginx.service # start webserver """
Finally, we are going to create the instance:
server = aws.ec2.Instance('webserver-www', instance_type=size, vpc_security_group_ids=[WebSecGroup, AdminSecGroup], # reference security group from above ami=AMI, subnet_id=subNet, key_name=keyPair, user_data=userdata,associate_public_ip_address = True )
And then print out the private DNS name - although there are plenty more fields you may wish to export:
pulumi.export('ip',server.private_dns)
To create the instance, we issue the "pulumi up" command:
% pulumi up Previewing update (test) View Live: https://app.pulumi.com/RuncibleSpoon/nginx/test/previews/2663305a-8544-4776-b2c3-4cd2abe0f723 Type Name Plan Info + pulumi:pulumi:Stack nginx-test create 6 warnings + └─ aws:ec2:Instance webserver-www create Diagnostics: pulumi:pulumi:Stack (nginx-test): warning: rotation_enabled is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_lambda_arn is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_rules is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_enabled is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_lambda_arn is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_rules is deprecated: Use the aws_secretsmanager_secret_rotation data source instead Do you want to perform this update? yes Updating (test) View Live: https://app.pulumi.com/RuncibleSpoon/nginx/test/updates/30 Type Name Status Info + pulumi:pulumi:Stack nginx-test created 6 warnings + └─ aws:ec2:Instance webserver-www created Diagnostics: pulumi:pulumi:Stack (nginx-test): warning: rotation_enabled is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_lambda_arn is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_rules is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_enabled is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_lambda_arn is deprecated: Use the aws_secretsmanager_secret_rotation data source instead warning: rotation_rules is deprecated: Use the aws_secretsmanager_secret_rotation data source instead Outputs: ip : "ip-10-0-1-90.us-west-2.compute.internal" Resources: + 2 created Duration: 50s
This will build an NGINX Plus instance in the subnet of your choice. While there is more to be done to fully configure an NGINX instance this code provides a base on which to build more production-ready deployments.