diff --git a/aws-js-webserver/README.md b/aws-js-webserver/README.md index 0bbb17198..f3589febb 100755 --- a/aws-js-webserver/README.md +++ b/aws-js-webserver/README.md @@ -1,4 +1,3 @@ # AWS Web Server example in JavaScript Deploy an EC2 instance using `@pulumi/aws`. This example shows how to use multiple infrastructure resources in one program. For a detailed walkthrough, see the tutorial [Infrastructure on AWS](https://pulumi.io/quickstart/aws-ec2.html). - diff --git a/aws-ts-ruby-on-rails/README.md b/aws-ts-ruby-on-rails/README.md index f00625e66..a186a7d2d 100644 --- a/aws-ts-ruby-on-rails/README.md +++ b/aws-ts-ruby-on-rails/README.md @@ -1,5 +1,67 @@ # AWS EC2 Ruby on Rails This is a conversion of the AWS CloudFormation Application Framework template for a basic Ruby on Rails server. -It creates a single EC2 instance and uses a local MySQL database for storage. -See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-us-west-2.html. +It creates a single EC2 virtual machine instance and uses a local MySQL database for storage. Sourced from +https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-us-west-2.html. + +## Deploying the App + +To deploy your Ruby on Rails application, follow the below steps. + +### Prerequisites + +1. [Install Pulumi](https://pulumi.io/install) +2. [Configure AWS Credentials](https://pulumi.io/install/aws.html) + +### Steps + +After cloning this repo, from this working directory, run these commands: + +1. Create a new stack, which is an isolated deployment target for this example: + + ```bash + $ pulumi stack init + ``` + +2. Set the required configuration variables for this program: + + ```bash + $ pulumi config set aws:region us-east-1 + $ pulumi config set dbUser [your-mysql-user-here] + $ pulumi config set dbPassword [your-mysql-password-here] --secret + $ pulumi config set dbRootPassword [your-mysql-root-password-here] --secret + + # Optionally, if you have an AWS KMS key to use for SSH access: + $ pulumi config set keyName [your-aws-kms-key-name-here] + ``` + +3. Stand up the VM, which will also install and configure Ruby on Rails and MySQL: + + ```bash + $ pulumi up + ``` + +4. After a couple minutes, your VM will be ready, and two stack outputs are printed: + + ```bash + $ pulumi stack output + Current stack outputs (2): + OUTPUT VALUE + vmIP 53.40.227.82 + websiteURL http://ec2-53-40-227-82.us-west-2.compute.amazonaws.com/notes + ``` + +5. Visit your new website by entering the websiteURL into your browser, or running: + + ```bash + $ curl curl $(pulumi stack output websiteURL) + ``` + +6. From there, feel free to experiment. Simply making edits and running `pulumi up` will incrementally update your VM. + +7. Afterwards, destroy your stack and remove it: + + ```bash + $ pulumi destroy --yes + $ pulumi stack rm --yes + ``` diff --git a/aws-ts-ruby-on-rails/files/install_application b/aws-ts-ruby-on-rails/files/install_application index cd88546e0..a466d5ae7 100644 --- a/aws-ts-ruby-on-rails/files/install_application +++ b/aws-ts-ruby-on-rails/files/install_application @@ -1,5 +1,5 @@ #!/bin/bash -e -source /etc/profile.d/rvm.sh +source /etc/profile.d/rvm.sh || true rvm use 2.3.1 export HOME=/home/ec2-user export PATH=$PATH:/usr/local/bin @@ -15,7 +15,8 @@ if pgrep ruby &> /dev/null ; then pkill -TERM ruby ; fi # Create a new application, with therubyracer javascript library rails new sample -d mysql --skip-spring --skip-bundle --force cd /home/ec2-user/sample -sed -i 's/^# \\(.*therubyracer.*$\\)/\\1/' Gemfile +sed -i 's/^# \(.*mini_racer.*$\)/\1/' Gemfile +sed -i 's/^# \(.*therubyracer.*$\)/\1/' Gemfile bundle install # Create a sample scoffold diff --git a/aws-ts-ruby-on-rails/files/install_ruby b/aws-ts-ruby-on-rails/files/install_ruby index ec9b5896d..ccfe20c4f 100644 --- a/aws-ts-ruby-on-rails/files/install_ruby +++ b/aws-ts-ruby-on-rails/files/install_ruby @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash -e curl -sSL https://get.rvm.io | bash -source /etc/profile.d/rvm.sh +source /etc/profile.d/rvm.sh || true rvm install 2.3.1 rvm --default use 2.3.1 gem install rails diff --git a/aws-ts-ruby-on-rails/files/start-application b/aws-ts-ruby-on-rails/files/start_application similarity index 83% rename from aws-ts-ruby-on-rails/files/start-application rename to aws-ts-ruby-on-rails/files/start_application index 588d82a44..23ee6fe62 100644 --- a/aws-ts-ruby-on-rails/files/start-application +++ b/aws-ts-ruby-on-rails/files/start_application @@ -1,5 +1,5 @@ #!/bin/bash -e -source /etc/profile.d/rvm.sh +source /etc/profile.d/rvm.sh || true rvm use 2.3.1 export HOME=/home/ec2-user export PATH=$PATH:/usr/local/bin diff --git a/aws-ts-ruby-on-rails/index.ts b/aws-ts-ruby-on-rails/index.ts index 07cb580a5..a9c175f2e 100644 --- a/aws-ts-ruby-on-rails/index.ts +++ b/aws-ts-ruby-on-rails/index.ts @@ -84,8 +84,8 @@ const webServer = new aws.ec2.Instance("webServer", { owner: "root", group: "root", }, - "/home/ec2-user/start-application": { - content: renderConfigFile("./files/start-application", config), + "/home/ec2-user/start_application": { + content: renderConfigFile("./files/start_application", config), mode: "000500", owner: "root", group: "root", @@ -96,19 +96,24 @@ const webServer = new aws.ec2.Instance("webServer", { command: "/tmp/install_application > /var/log/install_application.log", }, "02_configure_reboot": { - command: "echo /home/ec2-user/start-application >> /etc/rc.local", + command: "echo /home/ec2-user/start_application >> /etc/rc.local", }, "03_start_application": { - command: "/home/ec2-user/start-application", + command: "/home/ec2-user/start_application > var/log/start_application.log", }, + /* "04_cleanup": { command: "rm /tmp/install_application", }, + */ }, }, }, ), }); -// Expor the URL for our newly created Rails application. +// Export the VM IP in case we want to SSH. +export let vmIP = webServer.publicIp; + +// Export the URL for our newly created Rails application. export let websiteURL = webServer.publicDns.apply(url => `http://${url}/notes`); diff --git a/aws-ts-ruby-on-rails/init.ts b/aws-ts-ruby-on-rails/init.ts index a81187fcf..08d8148ff 100644 --- a/aws-ts-ruby-on-rails/init.ts +++ b/aws-ts-ruby-on-rails/init.ts @@ -1,6 +1,8 @@ import * as fs from "async-file"; import * as mustache from "mustache"; +// Init supports CloudFormation cfn-init-like structures for expressing post-provisioning virtual machine +// initialization steps, including installing packages and files, running commands, and managing services. type Init = {[config: string]: InitConfig}; interface InitConfig { @@ -32,35 +34,47 @@ interface InitService { ensureRunning?: boolean; } -// createUserData produces a single user data script out of the given init structure. +// createUserData produces a cloud-init payload, suitable for user data, out of the given init structure. export async function createUserData(configs: string[], init: Init): Promise { - let script = `#!/bin/bash -xe\n\n`; + // We need to encode the user data into multiple MIME parts. This ensures that all of the pieces are processed + // correctly by the cloud-init system, without needing to do map merging, etc. + const boundary = "cd4621d39f783ba4"; + let result = `Content-Type: multipart/mixed; ` + + `Merge-Type: list(append)+dict(recurse_array)+str(); ` + + `boundary="${boundary}"\n` + + `MIME-Version: 1.0\r\n\r\n`; - // For each config entry, generate bash script that carries out its wishes. + // For each config entry, generate the cloud-init statements to carry out its wishes. for (let name of configs) { let config = init[name]; if (!config) { throw new Error(`Missing config entry for ${name}`); } - script += `# ${name}:\n`; - if (config.files) { - // Emit files with the appropriate content and permissions: - for (let path of Object.keys(config.files)) { - let file = config.files[path]; - script += `echo "${await file.content}" > ${path}\n`; - script += `chown ${file.owner}:${file.group} ${path}\n`; - script += `chmod ${file.mode} ${path}\n`; - } - } - if (config.packages) { + result += `--${boundary}\nContent-Type: text/cloud-config\r\n\r\n`; + result += `merge_how: list(append)+dict(recurse_array)+str()\n`; + + // Process the sections in the same order as cfn-init; from: + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-init.html: + // + // "The cfn-init helper script processes these configuration sections in the following order: packages, + // groups, users, sources, files, commands, and then services. If you require a different order, separate + // your sections into different config keys, and then use a configset that specifies the order in which the + // config keys should be processed." + // + // TODO: support for groups, users, and sources. + + if (config.packages && Object.keys(config.packages).length) { // Install package manager packages: + result += `update_packages: true\n`; + result += `packages:\n`; for (let manager of Object.keys(config.packages)) { let packages = config.packages[manager]; switch (manager) { case "yum": - script += `yum update -y\n`; // TODO: do this earlier. - script += `yum install -y ${packages.join(" ")}\n`; + for (let pkg of packages) { + result += `- ${pkg}\n`; + } break; default: // TODO: support more package managers. @@ -68,29 +82,39 @@ export async function createUserData(configs: string[], init: Init): Promise {