├── .ruby-version ├── lib ├── default │ ├── Moonfile.rb │ ├── moonshot │ │ └── template.yml │ ├── appspec.yml │ └── bin │ │ └── build.sh ├── moonshot │ ├── config.rb │ ├── task.rb │ ├── ssh_config.rb │ ├── stack_config.rb │ ├── commands │ │ ├── status.rb │ │ ├── version.rb │ │ ├── push.rb │ │ ├── interactive_command.rb │ │ ├── doctor.rb │ │ ├── ssh.rb │ │ ├── show_all_events_option.rb │ │ ├── list.rb │ │ ├── deploy.rb │ │ ├── build.rb │ │ ├── tag_arguments.rb │ │ ├── delete.rb │ │ ├── console.rb │ │ ├── parent_stack_option.rb │ │ ├── parameter_arguments.rb │ │ ├── update.rb │ │ ├── generate_template.rb │ │ ├── create.rb │ │ └── new.rb │ ├── yaml_stack_template.rb │ ├── resources.rb │ ├── json_stack_template.rb │ ├── account_context.rb │ ├── tools │ │ ├── asg_rollout │ │ │ ├── asg_instance.rb │ │ │ ├── instance_health.rb │ │ │ └── hook_exec_environment.rb │ │ └── asg_rollout_config.rb │ ├── stack_output_printer.rb │ ├── stack_list_printer.rb │ ├── ssh_fork_executor.rb │ ├── resources_helper.rb │ ├── creds_helper.rb │ ├── stack_template.rb │ ├── stack_parameter_printer.rb │ ├── always_use_default_source.rb │ ├── ask_user_source.rb │ ├── ssh_command_builder.rb │ ├── interactive_logger_proxy.rb │ ├── ssh_command.rb │ ├── ssh_target_selector.rb │ ├── doctor_helper.rb │ ├── stack_lister.rb │ ├── stack_parameter.rb │ ├── parameter_collection.rb │ ├── stack_events_poller.rb │ ├── command.rb │ ├── unicode_table.rb │ ├── build_mechanism │ │ ├── version_proxy.rb │ │ └── script.rb │ ├── parent_stack_parameter_loader.rb │ ├── dynamic_template.rb │ ├── controller_config.rb │ ├── command_line_dispatcher.rb │ ├── changelog_parser.rb │ ├── shell.rb │ ├── artifact_repository │ │ └── s3_bucket.rb │ └── change_set.rb ├── moonshot.rb └── plugins │ ├── encrypted_parameters │ ├── parameter_encrypter.rb │ └── kms_key.rb │ ├── dynamic_template.rb │ └── code_deploy_setup.rb ├── sample ├── build │ └── .placeholder ├── docroot │ └── index.php ├── bin │ ├── start.sh │ ├── stop.sh │ ├── clean.sh │ ├── aws-codedeploy-samples │ │ ├── conf-mgmt │ │ │ ├── puppet │ │ │ │ ├── aws-codedeploy │ │ │ │ │ ├── .rspec │ │ │ │ │ ├── spec │ │ │ │ │ │ ├── classes │ │ │ │ │ │ │ ├── coverage_spec.rb │ │ │ │ │ │ │ └── example_spec.rb │ │ │ │ │ │ └── spec_helper.rb │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── Guardfile │ │ │ │ │ ├── .fixtures.yml │ │ │ │ │ ├── manifests │ │ │ │ │ │ ├── install.pp │ │ │ │ │ │ ├── service.pp │ │ │ │ │ │ ├── params.pp │ │ │ │ │ │ └── init.pp │ │ │ │ │ ├── Modulefile │ │ │ │ │ ├── metadata.json │ │ │ │ │ ├── tests │ │ │ │ │ │ └── init.pp │ │ │ │ │ ├── Rakefile │ │ │ │ │ └── README.md │ │ │ │ ├── masterless │ │ │ │ │ ├── deploy_hooks │ │ │ │ │ │ ├── puppet-apply.sh │ │ │ │ │ │ ├── verify_service.sh │ │ │ │ │ │ └── install-puppet.sh │ │ │ │ │ ├── application_vars.sh │ │ │ │ │ ├── puppet │ │ │ │ │ │ └── manifests │ │ │ │ │ │ │ └── hello_world.pp │ │ │ │ │ ├── appspec.yml │ │ │ │ │ ├── src │ │ │ │ │ │ └── main │ │ │ │ │ │ │ ├── java │ │ │ │ │ │ │ └── com │ │ │ │ │ │ │ │ └── amazonaws │ │ │ │ │ │ │ │ └── sample │ │ │ │ │ │ │ │ └── HelloServlet.java │ │ │ │ │ │ │ └── webapp │ │ │ │ │ │ │ └── WEB-INF │ │ │ │ │ │ │ └── web.xml │ │ │ │ │ └── pom.xml │ │ │ │ └── README.md │ │ │ ├── salt │ │ │ │ ├── local-only │ │ │ │ │ ├── salt │ │ │ │ │ │ ├── top.sls │ │ │ │ │ │ └── webserver.sls │ │ │ │ │ ├── TODO.txt │ │ │ │ │ ├── index.html │ │ │ │ │ ├── deploy_hooks │ │ │ │ │ │ ├── before_install.sh │ │ │ │ │ │ ├── application_start.sh │ │ │ │ │ │ └── verify_service.sh │ │ │ │ │ ├── application_vars.sh │ │ │ │ │ └── appspec.yml │ │ │ │ └── README.md │ │ │ ├── chef │ │ │ │ ├── aws-codedeploy-agent │ │ │ │ │ ├── node.json │ │ │ │ │ ├── solo.rb │ │ │ │ │ ├── cookbooks │ │ │ │ │ │ └── codedeploy-agent │ │ │ │ │ │ │ ├── metadata.rb │ │ │ │ │ │ │ └── recipes │ │ │ │ │ │ │ └── default.rb │ │ │ │ │ └── README.md │ │ │ │ ├── solo │ │ │ │ │ ├── chef │ │ │ │ │ │ ├── node.json │ │ │ │ │ │ ├── Cheffile │ │ │ │ │ │ ├── site-cookbooks │ │ │ │ │ │ │ └── homesite │ │ │ │ │ │ │ │ └── recipes │ │ │ │ │ │ │ │ └── default.rb │ │ │ │ │ │ └── solo.rb │ │ │ │ │ ├── deploy_hooks │ │ │ │ │ │ ├── verify_service.sh │ │ │ │ │ │ ├── chef-solo.sh │ │ │ │ │ │ └── install-chef.sh │ │ │ │ │ ├── appspec.yml │ │ │ │ │ ├── src │ │ │ │ │ │ └── main │ │ │ │ │ │ │ ├── java │ │ │ │ │ │ │ └── com │ │ │ │ │ │ │ │ └── amazonaws │ │ │ │ │ │ │ │ └── sample │ │ │ │ │ │ │ │ └── HelloServlet.java │ │ │ │ │ │ │ └── webapp │ │ │ │ │ │ │ └── WEB-INF │ │ │ │ │ │ │ └── web.xml │ │ │ │ │ └── pom.xml │ │ │ │ └── README.md │ │ │ └── ansible │ │ │ │ ├── local-only │ │ │ │ ├── lamp_simple │ │ │ │ │ ├── hosts │ │ │ │ │ ├── roles │ │ │ │ │ │ ├── web │ │ │ │ │ │ │ ├── tasks │ │ │ │ │ │ │ │ ├── main.yml │ │ │ │ │ │ │ │ ├── copy_code.yml │ │ │ │ │ │ │ │ └── install_httpd.yml │ │ │ │ │ │ │ ├── handlers │ │ │ │ │ │ │ │ └── main.yml │ │ │ │ │ │ │ └── templates │ │ │ │ │ │ │ │ └── index.php.j2 │ │ │ │ │ │ ├── db │ │ │ │ │ │ │ ├── handlers │ │ │ │ │ │ │ │ └── main.yml │ │ │ │ │ │ │ ├── templates │ │ │ │ │ │ │ │ └── my.cnf.j2 │ │ │ │ │ │ │ └── tasks │ │ │ │ │ │ │ │ └── main.yml │ │ │ │ │ │ └── common │ │ │ │ │ │ │ ├── templates │ │ │ │ │ │ │ ├── ntp.conf.j2 │ │ │ │ │ │ │ └── iptables.j2 │ │ │ │ │ │ │ ├── handlers │ │ │ │ │ │ │ └── main.yml │ │ │ │ │ │ │ └── tasks │ │ │ │ │ │ │ └── main.yml │ │ │ │ │ ├── group_vars │ │ │ │ │ │ ├── all │ │ │ │ │ │ └── dbservers │ │ │ │ │ ├── LICENSE.md │ │ │ │ │ ├── site.yml │ │ │ │ │ └── README.md │ │ │ │ ├── verify_service.sh │ │ │ │ ├── before_install.sh │ │ │ │ ├── application_start.sh │ │ │ │ ├── common_variables.sh │ │ │ │ └── appspec.yml │ │ │ │ ├── module │ │ │ │ └── site.yml │ │ │ │ └── README.md │ │ ├── applications │ │ │ ├── SampleApp_Linux │ │ │ │ ├── scripts │ │ │ │ │ ├── start_server │ │ │ │ │ ├── install_dependencies │ │ │ │ │ └── stop_server │ │ │ │ ├── appspec.yml │ │ │ │ └── index.html │ │ │ └── SampleApp_Windows │ │ │ │ ├── appspec.yml │ │ │ │ ├── before-install.bat │ │ │ │ └── index.html │ │ ├── load-balancing │ │ │ └── elb │ │ │ │ ├── stop_httpd.sh │ │ │ │ ├── start_httpd.sh │ │ │ │ ├── appspec.yml │ │ │ │ ├── README.md │ │ │ │ ├── register_with_elb.sh │ │ │ │ └── deregister_from_elb.sh │ │ ├── README.md │ │ └── version-control │ │ │ └── git │ │ │ ├── README.md │ │ │ └── pre-push │ └── build.sh ├── .bundle │ └── config ├── Gemfile ├── cloud_formation │ └── parameters │ │ └── moonshot-sample-app.yml ├── Moonfile ├── appspec.yml └── Gemfile.lock ├── spec ├── .rspec ├── fixtures │ ├── empty1.yml │ ├── answer1.yml │ ├── answer2.yml │ ├── update1.yml │ ├── create1.yml │ └── update2.yml ├── fs_fixtures │ ├── moonshot │ │ ├── template.yml │ │ ├── custom.json │ │ └── template.json │ └── cloud_formation │ │ ├── rspec-app.yml │ │ └── rspec-app.json ├── moonshot │ ├── command_spec.rb │ ├── commands │ │ ├── console_spec.rb │ │ ├── build_spec.rb │ │ ├── update_spec.rb │ │ └── create_spec.rb │ ├── command_optional_argument_spec.rb │ ├── interactive_logger_proxy_spec.rb │ ├── plugins_spec.rb │ ├── ssh_spec.rb │ ├── stub_rollout_asg.rb │ ├── plugins │ │ └── code_deploy_setup_spec.rb │ ├── shell_spec.rb │ ├── artifact_repository │ │ └── s3_bucket_via_github_releases_spec.rb │ └── command_restrictions.rb └── spec_helper.rb ├── .gitignore ├── .rspec ├── docs ├── logo.png ├── moonshot.png ├── requirements.txt ├── about │ ├── license.md │ └── contribute.md ├── mechanisms │ ├── artifact-repository.md │ ├── build.md │ └── deployment.md ├── user-guide │ ├── parent_stacks.md │ ├── custom-commands.md │ └── include_in_your_project.md ├── plugins │ ├── dynamic_template.md │ ├── code_deploy_setup.md │ └── backup.md ├── index.md └── plugins.md ├── Gemfile ├── bin └── moonshot ├── Rakefile ├── mkdocs.yml ├── .rubocop.yml ├── moonshot.gemspec └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 -------------------------------------------------------------------------------- /lib/default/Moonfile.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/moonshot/config.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/build/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper.rb --color -------------------------------------------------------------------------------- /sample/docroot/index.php: -------------------------------------------------------------------------------- 1 | 0.7.0' 6 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/salt/webserver.sls: -------------------------------------------------------------------------------- 1 | httpd: 2 | pkg: 3 | - installed 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Linux/scripts/start_server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | service httpd start 3 | 4 | -------------------------------------------------------------------------------- /lib/default/moonshot/template.yml: -------------------------------------------------------------------------------- 1 | # Place your CloudFormation template here! 2 | --- 3 | Resources: 4 | Parameters: 5 | Outputs: 6 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Linux/scripts/install_dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | yum install -y httpd 3 | 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/aws-codedeploy-agent/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_list": [ "recipe[codedeploy-agent]" ] 3 | } 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/chef/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_list": [ "recipe[homesite]", "recipe[tomcat]" ] 3 | } 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/spec/classes/coverage_spec.rb: -------------------------------------------------------------------------------- 1 | at_exit { RSpec::Puppet::Coverage.report! } 2 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/module_spec_helper' 2 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/hosts: -------------------------------------------------------------------------------- 1 | [webservers] 2 | localhost 3 | 4 | [dbservers] 5 | localhost 6 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/chef/Cheffile: -------------------------------------------------------------------------------- 1 | site "https://supermarket.getchef.com/api/v1" 2 | 3 | cookbook "tomcat" 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/TODO.txt: -------------------------------------------------------------------------------- 1 | * write scripts for running salt modules in masterless mode via CodeDeploy 2 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | pkg 3 | spec/fixtures 4 | .rspec_system 5 | .vagrant 6 | .bundle 7 | vendor 8 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/web/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: install_httpd.yml 3 | - include: copy_code.yml 4 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/chef/site-cookbooks/homesite/recipes/default.rb: -------------------------------------------------------------------------------- 1 | node.default["tomcat"]["user"] = "root" 2 | node.default["tomcat"]["port"] = 8888 3 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/Guardfile: -------------------------------------------------------------------------------- 1 | notification :off 2 | 3 | guard 'rake', :task => 'test' do 4 | watch(%r{^manifests\/(.+)\.pp$}) 5 | end 6 | -------------------------------------------------------------------------------- /spec/fs_fixtures/moonshot/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - Resources: 3 | - Parameters: 4 | - Parent1: 5 | - Type: String 6 | - Description: This is imported from the parent stack on create. -------------------------------------------------------------------------------- /spec/fs_fixtures/cloud_formation/rspec-app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - Resources: 3 | - Parameters: 4 | - Parent1: 5 | - Type: String 6 | - Description: This is imported from the parent stack on create. -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/module/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: install codedeploy agent 5 | sudo: yes 6 | action: aws_codedeploy enabled=true 7 | -------------------------------------------------------------------------------- /sample/cloud_formation/parameters/moonshot-sample-app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ArtifactBucket: "moonshot-sample-bucket2" 3 | AvailabilityZone1: "us-east-1a" 4 | AvailabilityZone2: "us-east-1b" 5 | DesiredCapacity: "2" 6 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Linux/scripts/stop_server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | isExistApp=`pgrep httpd` 3 | if [[ -n $isExistApp ]]; then 4 | service httpd stop 5 | fi 6 | 7 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/aws-codedeploy-agent/solo.rb: -------------------------------------------------------------------------------- 1 | # These are here just for testing only. 2 | file_cache_path "/etc/chef" 3 | cookbook_path "/etc/chef/cookbooks" 4 | json_attribs "/etc/chef/node.json" 5 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/.fixtures.yml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | repositories: 3 | stdlib: "git://github.com/puppetlabs/puppetlabs-stdlib.git" 4 | symlinks: 5 | codedeploy: "#{source_dir}" 6 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/aws-codedeploy-agent/cookbooks/codedeploy-agent/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'codedeploy-agent' 2 | recipe 'codedeploy-agent::default', 'Fetches, installs, and starts the AWS CodeDeploy host agent' 3 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | AWS CodeDeploy with Salt Masterless 4 | 5 | 6 |

Hello World

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'rake', require: false 8 | 9 | group :test do 10 | gem 'codeclimate-test-reporter' 11 | gem 'pry' 12 | gem 'rubocop' 13 | end 14 | -------------------------------------------------------------------------------- /lib/default/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 2 | os: 3 | files: 4 | - source: 5 | destination: 6 | hooks: 7 | ApplicationStop: 8 | - location: 9 | BeforeInstall: 10 | - location: 11 | ApplicationStart: 12 | - location: 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/deploy_hooks/puppet-apply.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR="/etc/puppet/" 4 | 5 | /usr/bin/puppet apply --modulepath=${BASE_DIR}/modules ${BASE_DIR}/codedeploy/manifests/hello_world.pp 6 | -------------------------------------------------------------------------------- /docs/about/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The legal stuff. 4 | 5 | --- 6 | 7 | ## Moonshot License (Apache License) 8 | 9 | Please [read the full license](https://github.com/acquia/moonshot/blob/master/LICENSE.txt) and make sure you adhere. 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/deploy_hooks/verify_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(curl -s http://localhost:8888/hello/) 4 | 5 | if [[ "$result" =~ "Hello World" ]]; then 6 | exit 0 7 | else 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/group_vars/all: -------------------------------------------------------------------------------- 1 | --- 2 | # Variables listed here are applicable to all host groups 3 | 4 | httpd_port: 80 5 | ntpserver: localhost 6 | repository: https://github.com/bennojoy/mywebapp.git 7 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/deploy_hooks/verify_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(curl -s http://localhost:8080/hello/) 4 | 5 | if [[ "$result" =~ "Hello World" ]]; then 6 | exit 0 7 | else 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/chef/solo.rb: -------------------------------------------------------------------------------- 1 | file_cache_path "/etc/chef/codedeploy/" 2 | cookbook_path [ 3 | "/etc/chef/codedeploy/cookbooks", 4 | "/etc/chef/codedeploy/site-cookbooks" 5 | ] 6 | json_attribs "/etc/chef/codedeploy/node.json" 7 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/application_vars.sh: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME="PuppetMasterless" 2 | DEPLOY_GROUP="PuppetMasterless" 3 | BUCKET="" 4 | BUNDLE_PATH="puppet-masterless.tar" 5 | EC2_TAG_KEY='Puppet' 6 | EC2_TAG_VALUE='Masterless' 7 | -------------------------------------------------------------------------------- /sample/Moonfile: -------------------------------------------------------------------------------- 1 | Moonshot.config do |m| 2 | m.app_name = 'moonshot-sample-app' 3 | m.artifact_repository = S3Bucket.new('moonshot-sample-bucket2') 4 | m.build_mechanism = Script.new('bin/build.sh') 5 | m.deployment_mechanism = CodeDeploy.new(asg: 'AutoScalingGroup') 6 | end 7 | 8 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/deploy_hooks/chef-solo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # First, make sure the tomcat cookbook is installed 4 | cd /etc/chef/codedeploy/ 5 | /usr/local/bin/librarian-chef install 6 | 7 | /usr/local/bin/chef-solo -c /etc/chef/codedeploy/solo.rb 8 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/stop_httpd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Here is where you'd want to stop your http daemon. For example: 4 | #service httpd stop 5 | #exit $? 6 | 7 | # In this case, since it's just a placeholder, we don't need to do anything. 8 | exit 0 9 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/start_httpd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Here is where you'd want to start your http daemon. For example: 4 | #service httpd start 5 | #exit $? 6 | 7 | # In this case, since it's just a placeholder, we don't need to do anything. 8 | exit 0 9 | -------------------------------------------------------------------------------- /spec/fs_fixtures/moonshot/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | 4 | }, 5 | 6 | "Parameters": { 7 | "Parent1": { 8 | "Type": "String", 9 | "Description": "This is imported from the parent stack on create." 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/fs_fixtures/moonshot/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | 4 | }, 5 | 6 | "Parameters": { 7 | "Parent1": { 8 | "Type": "String", 9 | "Description": "This is imported from the parent stack on create." 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Windows/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: windows 3 | files: 4 | - source: \index.html 5 | destination: C:\inetpub\wwwroot 6 | hooks: 7 | BeforeInstall: 8 | - location: \before-install.bat 9 | timeout: 900 10 | -------------------------------------------------------------------------------- /lib/moonshot/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class Task 5 | attr_reader :name, :desc, :block 6 | 7 | def initialize(name, desc, &block) 8 | @name = name.to_sym 9 | @desc = desc 10 | @block = block 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/db/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Handler to handle DB tier notifications 3 | 4 | - name: restart mysql 5 | service: name=mysqld state=restarted 6 | 7 | - name: restart iptables 8 | service: name=iptables state=restarted 9 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/deploy_hooks/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check for salt, and attempt to install if not 4 | 5 | yum list installed | grep salt-minion &> /dev/null 6 | if [ $? != 0 ]; then 7 | yum install -y --enablerepo=epel salt-minion 8 | fi 9 | -------------------------------------------------------------------------------- /spec/fs_fixtures/cloud_formation/rspec-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | 4 | }, 5 | 6 | "Parameters": { 7 | "Parent1": { 8 | "Type": "String", 9 | "Description": "This is imported from the parent stack on create." 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/application_vars.sh: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME="SaltMasterless" 2 | DEPLOY_GROUP="Salt" 3 | BUCKET="" 4 | BUNDLE_PATH="salt-masterless.tar" 5 | EC2_TAG_KEY='Salt' 6 | EC2_TAG_VALUE='Masterless' 7 | 8 | DESTINATION_PATH="/etc/salt/codedeploy" 9 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 AnsibleWorks, Inc. 2 | 3 | This work is licensed under the Creative Commons Attribution 3.0 Unported License. 4 | To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/deed.en_US. 5 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/common/templates/ntp.conf.j2: -------------------------------------------------------------------------------- 1 | 2 | driftfile /var/lib/ntp/drift 3 | 4 | restrict 127.0.0.1 5 | restrict -6 ::1 6 | 7 | server {{ ntpserver }} 8 | 9 | includefile /etc/ntp/crypto/pw 10 | 11 | keys /etc/ntp/keys 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/update1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | Parameters: 4 | InputParameter1: 5 | Type: String 6 | Default: 'Default1' 7 | InputParameter2: 8 | Type: String 9 | Default: 'Default2' 10 | InputParameter3: 11 | Type: String 12 | Outputs: 13 | Output1: !Ref "ResourceName" 14 | -------------------------------------------------------------------------------- /spec/moonshot/command_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Command do 2 | describe '#parser' do 3 | subject { described_class.new.parser } 4 | 5 | it 'should parse the verbose option' do 6 | allow(described_class).to receive(:usage) 7 | subject.parse('-v') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/manifests/install.pp: -------------------------------------------------------------------------------- 1 | # == Class codedeploy::install 2 | # 3 | class codedeploy::install { 4 | 5 | package { 'codedeploy-agent': 6 | ensure => present, 7 | source => $codedeploy::package_source, 8 | provider => rpm, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/moonshot/ssh_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class SSHConfig 5 | attr_accessor :ssh_identity_file, :ssh_user 6 | 7 | def initialize 8 | @ssh_identity_file = ENV['MOONSHOT_SSH_KEY_FILE'] 9 | @ssh_user = ENV['MOONSHOT_SSH_USER'] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/verify_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy hooks are run via absolute path, so taking dirname of this script will give us the path to 4 | # our deploy_hooks directory. 5 | . $(dirname $0)/common_variables.sh 6 | 7 | curl -s http://localhost:80/ansible/index.php 8 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/web/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Handler for the webtier: handlers are called by other plays. 3 | # See http://ansible.cc/docs/playbooks.html for more information about handlers. 4 | 5 | - name: restart iptables 6 | service: name=iptables state=restarted 7 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/Modulefile: -------------------------------------------------------------------------------- 1 | name 'aws-codedeploy' 2 | version '0.1.0' 3 | source '' 4 | author 'Amazon Web Services' 5 | license 'Apache 2.0' 6 | summary '' 7 | description '' 8 | project_page '' 9 | 10 | dependency 'puppetlabs/stdlib' 11 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Windows/before-install.bat: -------------------------------------------------------------------------------- 1 | REM Install Internet Information Server (IIS). 2 | c:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe -Command Import-Module -Name ServerManager 3 | c:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe -Command Install-WindowsFeature Web-Server -------------------------------------------------------------------------------- /lib/moonshot/stack_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Configuration for the Moonshot::Stack class. 5 | class StackConfig 6 | attr_accessor :parent_stacks, :show_all_events 7 | 8 | def initialize 9 | @parent_stacks = [] 10 | @show_all_events = false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/create1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | Parameters: 4 | InputParameter1: 5 | Type: String 6 | Default: 'Default1' 7 | InputParameter2: 8 | Type: String 9 | Default: 'Default2' 10 | InputParameter3: 11 | Type: String 12 | InputParameter4: 13 | Type: String 14 | Outputs: 15 | Output1: !Ref "ResourceName" 16 | -------------------------------------------------------------------------------- /spec/fixtures/update2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | Parameters: 4 | InputParameter1: 5 | Type: String 6 | Default: 'Default1' 7 | InputParameter2: 8 | Type: String 9 | Default: 'Default2' 10 | InputParameter3: 11 | Type: String 12 | InputParameter4: 13 | Type: String 14 | Outputs: 15 | Output1: !Ref "ResourceName" 16 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/deploy_hooks/application_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy hooks are run via absolute path, so taking dirname of this script will give us the path to 4 | # our deploy_hooks directory. 5 | . $(dirname $0)/../application_vars.sh 6 | 7 | salt-call --local --file-root=$DESTINATION_PATH state.highstate 8 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/group_vars/dbservers: -------------------------------------------------------------------------------- 1 | --- 2 | # The variables file used by the playbooks in the dbservers group. 3 | # These don't have to be explicitly imported by vars_files: they are autopopulated. 4 | 5 | mysqlservice: mysqld 6 | mysql_port: 3306 7 | dbuser: root 8 | dbname: foodb 9 | upassword: abc 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/manifests/service.pp: -------------------------------------------------------------------------------- 1 | # == Class codedeploy::service 2 | # 3 | # This class is meant to be called from codedeploy 4 | # It ensure the service is running 5 | # 6 | class codedeploy::service { 7 | 8 | service { 'codedeploy-agent': 9 | ensure => running, 10 | enable => true, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/moonshot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'moonshot' 5 | 6 | # This is the main entry point for the `moonshot` command-line tool. 7 | begin 8 | Moonshot::CommandLine.new.run! 9 | rescue StandardError => e 10 | warn "#{e} (at #{e.backtrace.first})" 11 | raise e if ENV['MOONSHOT_BACKTRACE'] 12 | 13 | exit 1 14 | end 15 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: / 5 | destination: /tmp/elb-test 6 | hooks: 7 | ApplicationStop: 8 | - location: deregister_from_elb.sh 9 | - location: stop_httpd.sh 10 | ApplicationStart: 11 | - location: start_httpd.sh 12 | - location: register_with_elb.sh 13 | -------------------------------------------------------------------------------- /lib/moonshot/commands/status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Status < Moonshot::Command 6 | self.usage = 'status [options]' 7 | self.description = 'Show the status of an existing environment' 8 | 9 | def execute 10 | controller.status 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/moonshot/commands/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Version < Moonshot::Command 6 | self.usage = 'version' 7 | self.description = 'Display the version of Moonshot' 8 | 9 | def execute 10 | puts Gem.loaded_specs['moonshot'].version 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check for ansible tools, and attempt to install if not 4 | 5 | yum list installed python-pip &> /dev/null 6 | if [ $? != 0 ]; then 7 | yum install -y python-pip 8 | fi 9 | 10 | pip list | grep -q ansible 11 | if [ $? != 0 ]; then 12 | pip install ansible 13 | fi 14 | -------------------------------------------------------------------------------- /lib/moonshot/commands/push.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Push < Moonshot::Command 6 | self.usage = 'push [options]' 7 | self.description = 'Build and deploy a development artifact from the working directory' 8 | 9 | def execute 10 | controller.push 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/application_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy hooks are run via absolute path, so taking dirname of this script will give us the path to 4 | # our deploy_hooks directory. 5 | . $(dirname $0)/common_variables.sh 6 | 7 | ansible-playbook $DESTINATION_PATH/lamp_simple/site.yml -i $DESTINATION_PATH/lamp_simple/hosts --connection=local 8 | -------------------------------------------------------------------------------- /lib/moonshot/commands/interactive_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | module InteractiveCommand 6 | def parser 7 | parser = super 8 | 9 | parser.on('--[no-]interactive', TrueClass, 'Use interactive prompts.') do |v| 10 | Moonshot.config.interactive = v 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/moonshot/yaml_stack_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module Moonshot 6 | # Handles YAML formatted AWS template files. 7 | class YamlStackTemplate < StackTemplate 8 | def body 9 | template_body.to_yaml 10 | end 11 | 12 | private 13 | 14 | def template_body 15 | @template_body ||= YAML.load_file(@filename) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/db/templates/my.cnf.j2: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | datadir=/var/lib/mysql 3 | socket=/var/lib/mysql/mysql.sock 4 | user=mysql 5 | # Disabling symbolic-links is recommended to prevent assorted security risks 6 | symbolic-links=0 7 | port={{ mysql_port }} 8 | 9 | [mysqld_safe] 10 | log-error=/var/log/mysqld.log 11 | pid-file=/var/run/mysqld/mysqld.pid 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | desc 'Run RuboCop against the source code.' 8 | RuboCop::RakeTask.new(:rubocop) do |task| 9 | task.options << '--display-cop-names' 10 | task.options << '--display-style-guide' 11 | end 12 | 13 | RSpec::Core::RakeTask.new(:spec) 14 | 15 | task default: %i[spec rubocop] 16 | -------------------------------------------------------------------------------- /lib/moonshot/commands/doctor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Doctor < Moonshot::Command 6 | self.usage = 'doctor [options]' 7 | self.description = 'Run configuration checks against the local environment' 8 | 9 | def execute 10 | controller.doctor || raise('One or more checks failed.') 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/moonshot/commands/ssh.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../ssh_command' 4 | 5 | module Moonshot 6 | module Commands 7 | class Ssh < Moonshot::SSHCommand 8 | self.usage = 'ssh [options]' 9 | self.description = 'SSH into the first (or specified) instance on the stack' 10 | 11 | def execute 12 | controller.ssh 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/common/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Handler to handle common notifications. Handlers are called by other plays. 3 | # See http://ansible.cc/docs/playbooks.html for more information about handlers. 4 | 5 | - name: restart ntp 6 | service: name=ntpd state=restarted 7 | 8 | - name: restart iptables 9 | service: name=iptables state=restarted 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/puppet/manifests/hello_world.pp: -------------------------------------------------------------------------------- 1 | class { 'tomcat': } 2 | 3 | class { 'epel': }-> 4 | tomcat::instance{ 'default': 5 | install_from_source => false, 6 | package_name => 'tomcat6', 7 | package_ensure => 'present', 8 | }-> 9 | tomcat::service { 'default': 10 | use_jsvc => false, 11 | use_init => true, 12 | service_name => 'tomcat6', 13 | } 14 | -------------------------------------------------------------------------------- /lib/moonshot/commands/show_all_events_option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | module ShowAllEventsOption 6 | def parser 7 | parser = super 8 | 9 | parser.on('--[no-]show-all-events', TrueClass, 'Show all stack events during update') do |v| 10 | Moonshot.config.show_all_stack_events = v 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/moonshot/commands/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class List < Moonshot::Command 6 | self.usage = 'list [options]' 7 | self.description = 'List stacks for this application' 8 | 9 | def execute 10 | stacks = StackLister.new(controller.config.app_name).list 11 | StackListPrinter.new(stacks).print 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/web/tasks/copy_code.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # These tasks are responsible for copying the latest dev/production code from 3 | # the version control system. 4 | 5 | - name: Copy the code from repository 6 | git: repo={{ repository }} dest=/var/www/html/ansible 7 | 8 | - name: Creates the index.php file 9 | template: src=index.php.j2 dest=/var/www/html/ansible/index.php 10 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-codedeploy", 3 | "version": "0.1.0", 4 | "author": "Amazon Web Services", 5 | "summary": "", 6 | "license": "Apache 2.0", 7 | "source": "", 8 | "project_page": "", 9 | "issues_url": "", 10 | "dependencies": [ 11 | { 12 | "name": "puppetlabs-stdlib", 13 | "version_requirement": ">= 1.0.0" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/deploy_hooks/verify_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy hooks are run via absolute path, so taking dirname of this script will give us the path to 4 | # our deploy_hooks directory. 5 | . $(dirname $0)/../application_vars.sh 6 | 7 | result=$(curl -s http://localhost/salt/index.html) 8 | 9 | if [[ "$result" =~ "Hello World" ]]; then 10 | exit 0 11 | else 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /lib/moonshot/commands/deploy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Deploy < Moonshot::Command 6 | include InteractiveCommand 7 | 8 | self.usage = 'deploy VERSION' 9 | self.description = 'Deploy a versioned release to the environment' 10 | 11 | def execute(version_name) 12 | controller.deploy_version(version_name) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/moonshot/resources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Resources is a dependency container that holds references to instances 5 | # provided to a Mechanism (build, deploy, etc.). 6 | class Resources 7 | attr_reader :stack, :ilog, :controller 8 | 9 | def initialize(stack:, ilog:, controller:) 10 | @stack = stack 11 | @ilog = ilog 12 | @controller = controller 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/moonshot/json_stack_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require_relative 'stack_template' 5 | 6 | module Moonshot 7 | # Handles JSON formatted AWS template files. 8 | class JsonStackTemplate < StackTemplate 9 | def body 10 | template_body.to_json 11 | end 12 | 13 | private 14 | 15 | def template_body 16 | @template_body ||= JSON.parse(File.read(@filename)) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /sample/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: docroot/ 5 | destination: /var/www/html 6 | hooks: 7 | ApplicationStop: 8 | - location: bin/aws-codedeploy-samples/load-balancing/elb/deregister_from_elb.sh 9 | - location: bin/stop.sh 10 | BeforeInstall: 11 | - location: bin/clean.sh 12 | ApplicationStart: 13 | - location: bin/start.sh 14 | - location: bin/aws-codedeploy-samples/load-balancing/elb/register_with_elb.sh 15 | -------------------------------------------------------------------------------- /sample/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CWD=$(pwd) 4 | PROJECT_DIR=$(dirname $0)/.. 5 | BUILD_DIR=$PROJECT_DIR/build/$(date +"%s") 6 | 7 | mkdir $BUILD_DIR 8 | cp $PROJECT_DIR/appspec.yml $BUILD_DIR 9 | cp -r $PROJECT_DIR/bin $BUILD_DIR 10 | cp -r $PROJECT_DIR/docroot $BUILD_DIR 11 | rm -f $BUILD_DIR/docroot/index.php.dist 12 | 13 | cd $BUILD_DIR 14 | tar -zcvf output.tar.gz ./ 15 | cd $CWD 16 | 17 | mv $BUILD_DIR/output.tar.gz $PROJECT_DIR 18 | rm -rf $BUILD_DIR 19 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/common_variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | # 3 | # This is not an executable script, just a set of names and variable declarations. 4 | # 5 | # Use it with: 6 | # source common_variables.sh 7 | # Or: 8 | # . common_variables.sh 9 | 10 | APPLICATION_NAME="AnsibleApp" 11 | DEPLOY_GROUP="ansible" 12 | BUCKET="" 13 | BUNDLE_PATH="ansible-playbooks.zip" 14 | 15 | DESTINATION_PATH="/etc/ansible/codedeploy" 16 | -------------------------------------------------------------------------------- /lib/moonshot/commands/build.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'interactive_command' 4 | 5 | module Moonshot 6 | module Commands 7 | class Build < Moonshot::Command 8 | include InteractiveCommand 9 | 10 | self.usage = 'build VERSION' 11 | self.description = 'Build a release artifact, ready for deployment' 12 | 13 | def execute(version_name) 14 | controller.build_version(version_name) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/moonshot/account_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module AccountContext 5 | def self.get 6 | @account ||= determine_account_name 7 | end 8 | 9 | def self.set(account_name) 10 | @account = account_name 11 | end 12 | 13 | def self.reset 14 | @account = nil 15 | end 16 | 17 | def self.determine_account_name 18 | Aws::IAM::Client.new.list_account_aliases.account_aliases.first 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/default/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building version '$VERSION_NAME', using target file '$OUTPUT_FILE'." 4 | 5 | # Add code here to assemble your project into a compressed tarball (.tar.gz) 6 | # located at $OUTPUT_FILE. Be sure to include your appspec.yml in the tarball's 7 | # root! 8 | # 9 | # For example: 10 | # 11 | # mkdir build_dir 12 | # rsync -avP path-to-source build_dir/src 13 | # cp moonshot/appspec.yml build_dir/ 14 | # tar czf $OUTPUT_FILE -C build_dir . 15 | # rm -rf build_dir 16 | -------------------------------------------------------------------------------- /lib/moonshot/tools/asg_rollout/asg_instance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Tools 5 | class ASGRollout 6 | # Provides an abstraction around an Auto Scaling Group's 7 | # relationship with an instance. 8 | class ASGInstance 9 | attr_reader :asg_name, :id 10 | 11 | def initialize(asg_name, id, _config) 12 | @asg_name = asg_name 13 | @instance_id = id 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Linux/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: /index.html 5 | destination: /var/www/html/ 6 | hooks: 7 | BeforeInstall: 8 | - location: scripts/install_dependencies 9 | timeout: 300 10 | runas: root 11 | - location: scripts/start_server 12 | timeout: 300 13 | runas: root 14 | ApplicationStop: 15 | - location: scripts/stop_server 16 | timeout: 300 17 | runas: root 18 | 19 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: / 5 | destination: /etc/ansible/codedeploy 6 | hooks: 7 | BeforeInstall: 8 | - location: before_install.sh 9 | timeout: 300 10 | runas: root 11 | ApplicationStart: 12 | - location: application_start.sh 13 | timeout: 300 14 | runas: root 15 | ValidateService: 16 | - location: verify_service.sh 17 | timeout: 300 18 | runas: root 19 | -------------------------------------------------------------------------------- /lib/moonshot/stack_output_printer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Display the stack outputs to the user. 5 | class StackOutputPrinter 6 | def initialize(stack, table) 7 | @stack = stack 8 | @table = table 9 | end 10 | 11 | def print 12 | o_table = @table.add_leaf('Stack Outputs') 13 | rows = @stack.outputs.sort.map do |key, value| 14 | ["#{key}:", value] 15 | end 16 | o_table.add_table(rows) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/aws-codedeploy-agent/cookbooks/codedeploy-agent/recipes/default.rb: -------------------------------------------------------------------------------- 1 | remote_file "#{Chef::Config[:file_cache_path]}/codedeploy-agent-install" do 2 | source "https://s3.amazonaws.com/aws-codedeploy-us-east-1/latest/install" 3 | mode 0755 4 | end 5 | 6 | bash "install-codedeploy-agent" do 7 | code <<-EOH 8 | #{Chef::Config[:file_cache_path]}/codedeploy-agent-install auto 9 | EOH 10 | end 11 | 12 | service "codedeploy-agent" do 13 | action [:enable, :start] 14 | end 15 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/manifests/params.pp: -------------------------------------------------------------------------------- 1 | # == Class codedeploy::params 2 | # 3 | # This class is meant to be called from codedeploy 4 | # It sets variables according to platform 5 | # 6 | class codedeploy::params { 7 | case $::osfamily { 8 | 'RedHat', 'Linux': { 9 | $package_source = 'https://s3.amazonaws.com/aws-codedeploy-us-east-1/latest/codedeploy-agent.noarch.rpm' 10 | } 11 | default: { 12 | fail("${::operatingsystem} not supported") 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/local-only/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: salt/ 5 | destination: /etc/salt/codedeploy/ 6 | - source: index.html 7 | destination: /var/www/html/salt/ 8 | hooks: 9 | BeforeInstall: 10 | - location: deploy_hooks/before_install.sh 11 | runas: root 12 | ApplicationStart: 13 | - location: deploy_hooks/application_start.sh 14 | runas: root 15 | ValidateService: 16 | - location: deploy_hooks/verify_service.sh 17 | runas: root 18 | -------------------------------------------------------------------------------- /lib/moonshot/stack_list_printer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class StackListPrinter 5 | attr_accessor :stacks 6 | 7 | def initialize(stacks) 8 | @stacks = stacks 9 | @table = UnicodeTable.new('Environment List') 10 | end 11 | 12 | def print 13 | rows = @stacks.map do |stack| 14 | [stack.name, stack.creation_time, stack.status] 15 | end 16 | 17 | @table.add_table(rows) 18 | 19 | @table.draw 20 | @table.draw_children 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: puppet/ 5 | destination: /etc/puppet/codedeploy 6 | - source: target/hello.war 7 | destination: /var/lib/tomcat6/webapps 8 | hooks: 9 | BeforeInstall: 10 | - location: deploy_hooks/install-puppet.sh 11 | runas: root 12 | ApplicationStart: 13 | - location: deploy_hooks/puppet-apply.sh 14 | runas: root 15 | ValidateService: 16 | - location: deploy_hooks/verify_service.sh 17 | runas: root 18 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: chef/ 5 | destination: /etc/chef/codedeploy 6 | - source: target/hello.war 7 | destination: /var/lib/tomcat6/webapps 8 | hooks: 9 | BeforeInstall: 10 | - location: deploy_hooks/install-chef.sh 11 | timeout: 1800 12 | runas: root 13 | ApplicationStart: 14 | - location: deploy_hooks/chef-solo.sh 15 | runas: root 16 | ValidateService: 17 | - location: deploy_hooks/verify_service.sh 18 | runas: root 19 | -------------------------------------------------------------------------------- /spec/moonshot/commands/console_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Commands::Console do 2 | describe '#execute' do 3 | it 'should start a pry session' do 4 | expect_any_instance_of(Moonshot::ControllerConfig).to receive(:update_for_account!) 5 | expect(Aws::EC2::Client).to receive(:new) 6 | expect(Aws::IAM::Client).to receive(:new) 7 | expect(Aws::AutoScaling::Client).to receive(:new) 8 | expect(Aws::CloudFormation::Client).to receive(:new) 9 | expect(Pry).to receive(:start) 10 | subject.execute 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/moonshot/ssh_fork_executor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | 5 | module Moonshot 6 | # Run an SSH command via fork/exec. 7 | class SSHForkExecutor 8 | Result = Struct.new(:output, :exitstatus) 9 | 10 | def run(cmd) 11 | output = StringIO.new 12 | 13 | exit_status = nil 14 | Open3.popen3(cmd) do |_, stdout, _, wt| 15 | output << stdout.read until stdout.eof? 16 | exit_status = wt.value.exitstatus 17 | end 18 | 19 | Result.new(output.string.chomp, exit_status) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/README.md: -------------------------------------------------------------------------------- 1 | AWS CodeDeploy Samples 2 | ====================== 3 | 4 | The samples in the repository each demonstrate one of a few different scenarios. 5 | These fall into one of a few different categories, including: 6 | 7 | - Sample Applications 8 | - Integrations and templates for configuration management systems 9 | - Integrations with load-balancers like Elastic Load Balancing 10 | - Sample hooks for version control systems like Git. 11 | 12 | License 13 | ======= 14 | 15 | These samples and templates are all licensed under Apache 2.0. 16 | 17 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/manifests/init.pp: -------------------------------------------------------------------------------- 1 | # == Class: codedeploy 2 | # 3 | # Install and manage the AWS CodeDeploy agent 4 | # 5 | # === Parameters 6 | # 7 | # [*package_source*] 8 | # URL or filepath passed to package provider to install agent 9 | # 10 | class codedeploy ( 11 | $package_source = $codedeploy::params::package_source, 12 | ) inherits codedeploy::params { 13 | 14 | validate_string($package_source) 15 | 16 | class { 'codedeploy::install': } ~> 17 | class { 'codedeploy::service': } -> 18 | Class['codedeploy'] 19 | } 20 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook deploys the whole application stack in this site. 3 | 4 | - name: apply common configuration to all nodes 5 | hosts: all 6 | remote_user: root 7 | 8 | roles: 9 | - common 10 | 11 | - name: configure and deploy the webservers and application code 12 | hosts: webservers 13 | remote_user: root 14 | 15 | roles: 16 | - web 17 | 18 | - name: deploy MySQL and configure the databases 19 | hosts: dbservers 20 | remote_user: root 21 | 22 | roles: 23 | - db 24 | -------------------------------------------------------------------------------- /lib/moonshot/commands/tag_arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | module TagArguments 6 | def parser 7 | parser = super 8 | 9 | parser.on('--tag KEY=VALUE', '-TKEY=VALUE', 'Specify Stack Tag on the command line') do |v| 10 | data = v.split('=', 2) 11 | unless data.size == 2 12 | raise "Invalid tag format '#{v}', expected KEY=VALUE (e.g. MyStackTag=12)" 13 | end 14 | 15 | Moonshot.config.extra_tags << { key: data[0], value: data[1] } 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/tests/init.pp: -------------------------------------------------------------------------------- 1 | # The baseline for module testing used by Puppet Labs is that each manifest 2 | # should have a corresponding test manifest that declares that class or defined 3 | # type. 4 | # 5 | # Tests are then run by using puppet apply --noop (to check for compilation 6 | # errors and view a log of events) or by fully applying the test in a virtual 7 | # environment (to compare the resulting system state to the desired state). 8 | # 9 | # Learn more about module testing here: 10 | # http://docs.puppetlabs.com/guides/tests_smoke.html 11 | # 12 | include codedeploy 13 | -------------------------------------------------------------------------------- /lib/moonshot/commands/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Delete < Moonshot::Command 6 | include ShowAllEventsOption 7 | 8 | self.usage = 'delete [options]' 9 | self.description = 'Delete an existing environment' 10 | 11 | def parser 12 | parser = super 13 | parser.on('--template-file=FILE', 'Override the path to the CloudFormation template.') do |v| 14 | Moonshot.config.template_file = v 15 | end 16 | end 17 | 18 | def execute 19 | controller.delete 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/moonshot/commands/console.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pry' 4 | 5 | module Moonshot 6 | module Commands 7 | class Console < Moonshot::Command 8 | self.usage = 'console [options]' 9 | self.description = 'Launch a interactive Ruby console with configured access to AWS' 10 | 11 | def execute 12 | controller 13 | 14 | ec2 = Aws::EC2::Client.new 15 | iam = Aws::IAM::Client.new 16 | autoscaling = Aws::AutoScaling::Client.new 17 | cf = Aws::CloudFormation::Client.new 18 | 19 | Pry.start binding, backtrace: nil 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/moonshot/commands/parent_stack_option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | module ParentStackOption 6 | def parser 7 | parser = super 8 | 9 | parser.on('-pPARENT_STACK', '--parent=PARENT_STACK', 10 | 'Parent stack to import parameters from') do |v| 11 | Moonshot.config.parent_stacks = [v] 12 | end 13 | 14 | parser.on('--parents a,b,c', Array, 15 | 'List of parent stacks to import parameters from') do |v| 16 | Moonshot.config.parent_stacks = v 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/moonshot/resources_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Provides shorthand methods for accessing resources provided by the Resources 5 | # container. 6 | module ResourcesHelper 7 | attr_writer :resources 8 | 9 | private 10 | 11 | # TODO: Deprecate this interface. 12 | def log 13 | @log ||= Logger.new($stdout) 14 | end 15 | 16 | def stack 17 | raise 'Resources not provided to Mechanism!' unless @resources 18 | 19 | @resources.stack 20 | end 21 | 22 | def ilog 23 | raise 'Resources not provided to Mechanism!' unless @resources 24 | 25 | @resources.ilog 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/web/tasks/install_httpd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # These tasks install http and the php modules. 3 | 4 | - name: Install http and php etc 5 | yum: name={{ item }} state=present 6 | with_items: 7 | - httpd 8 | - php 9 | - php-mysql 10 | - git 11 | 12 | - name: insert iptables rule for httpd 13 | lineinfile: dest=/etc/sysconfig/iptables create=yes state=present regexp="{{ httpd_port }}" insertafter="^:OUTPUT " 14 | line="-A INPUT -p tcp --dport {{ httpd_port }} -j ACCEPT" 15 | notify: restart iptables 16 | 17 | - name: http service state 18 | service: name=httpd state=started enabled=yes 19 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Moonshot 2 | theme: 'mkdocs' 3 | pages: 4 | - Home: 'index.md' 5 | - Example: example.md 6 | - User Guide: 7 | - 'CLI Commands': 'user-guide/cli.md' 8 | - 'Including Moonshot in your project': 'user-guide/include_in_your_project.md' 9 | - 'Working with parent stacks': 'user-guide/parent_stacks.md' 10 | - Mechanisms: 11 | - 'Artifact Repository': 'mechanisms/artifact-repository.md' 12 | - 'Build': 'mechanisms/build.md' 13 | - 'Deployment': 'mechanisms/deployment.md' 14 | - Plugins: plugins.md 15 | - Bundled plugins: 16 | - 'Backup plugin': 'plugins/backup.md' 17 | - About: 18 | - 'How to Contribute': 'about/contribute.md' 19 | - 'License': 'about/license.md' 20 | -------------------------------------------------------------------------------- /lib/moonshot/creds_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Create convenience methods for various AWS client creation. 5 | module CredsHelper 6 | def cf_client(**args) 7 | Aws::CloudFormation::Client.new(args) 8 | end 9 | 10 | def cd_client(**args) 11 | Aws::CodeDeploy::Client.new(args) 12 | end 13 | 14 | def ec2_client(**args) 15 | Aws::EC2::Client.new(args) 16 | end 17 | 18 | def iam_client(**args) 19 | Aws::IAM::Client.new(args) 20 | end 21 | 22 | def as_client(**args) 23 | Aws::AutoScaling::Client.new(args) 24 | end 25 | 26 | def s3_client(**args) 27 | Aws::S3::Client.new(args) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/deploy_hooks/install-puppet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check to see that puppet itself is installed 4 | yum list installed puppet &> /dev/null 5 | if [ $? != 0 ]; then 6 | yum -y install puppet 7 | fi 8 | 9 | # Create the base directory for the system-wide puppet modules 10 | mkdir -p /etc/puppet/modules 11 | 12 | puppet="/usr/bin/puppet" 13 | 14 | # Check for each of the modules we need. If they're not installed, install them. 15 | for module in puppetlabs/stdlib puppetlabs/java puppetlabs/tomcat stahnma/epel; do 16 | $puppet module list | grep -q $(basename $module) 17 | if [ $? != 0 ]; then 18 | $puppet module install $module 19 | fi 20 | done 21 | 22 | exit 0 23 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/src/main/java/com/amazonaws/sample/HelloServlet.java: -------------------------------------------------------------------------------- 1 | package com.amazonaws.sample; 2 | 3 | import javax.servlet.http.HttpServlet; 4 | import javax.servlet.ServletException; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | 11 | public class HelloServlet extends HttpServlet { 12 | 13 | protected void doGet(HttpServletRequest request, 14 | HttpServletResponse response) 15 | throws ServletException, IOException 16 | { 17 | PrintWriter writer = response.getWriter(); 18 | writer.print("

Hello World

"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/src/main/java/com/amazonaws/sample/HelloServlet.java: -------------------------------------------------------------------------------- 1 | package com.amazonaws.sample; 2 | 3 | import javax.servlet.http.HttpServlet; 4 | import javax.servlet.ServletException; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | 11 | public class HelloServlet extends HttpServlet { 12 | 13 | protected void doGet(HttpServletRequest request, 14 | HttpServletResponse response) 15 | throws ServletException, IOException 16 | { 17 | PrintWriter writer = response.getWriter(); 18 | writer.print("

Hello World

"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/moonshot/stack_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # A StackTemplate loads the template from disk and stores information 5 | # about it. 6 | class StackTemplate 7 | attr_reader :filename 8 | 9 | def initialize(filename) 10 | @filename = filename 11 | end 12 | 13 | def parameters 14 | template_body.fetch('Parameters', {}).map do |k, v| 15 | StackParameter.new(k, 16 | default: v['Default'], 17 | description: v.fetch('Description', '')) 18 | end 19 | end 20 | 21 | def resource_names 22 | template_body.fetch('Resources', {}).keys 23 | end 24 | 25 | def exist? 26 | File.exist?(@filename) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Hello World Web Application 8 | 9 | 10 | HelloServlet 11 | com.amazonaws.sample.HelloServlet 12 | 13 | 14 | 15 | HelloServlet 16 | / 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/moonshot/stack_parameter_printer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Displays information about existing stack parameters to the user, with 5 | # information on what a stack update would do. 6 | class StackParameterPrinter 7 | def initialize(stack, table) 8 | @stack = stack 9 | @table = table 10 | end 11 | 12 | def print 13 | p_table = @table.add_leaf('Stack Parameters') 14 | rows = @stack.parameters.sort.map do |key, value| 15 | ["#{key}:", format_value(value)] 16 | end 17 | 18 | p_table.add_table(rows) 19 | end 20 | 21 | def format_value(value) 22 | if value.size > 60 23 | "#{value[0..60]}..." 24 | else 25 | value 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Hello World Web Application 8 | 9 | 10 | HelloServlet 11 | com.amazonaws.sample.HelloServlet 12 | 13 | 14 | 15 | HelloServlet 16 | / 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/moonshot/always_use_default_source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # The AlwaysUseDefaultSource will always use the previous value in 5 | # the stack, or use the default value during stack creation. This is 6 | # useful if plugins provide the value for a parameter, and we don't 7 | # want to prompt the user for an override. Of course, overrides from 8 | # answer files or command-line arguments will always apply. 9 | class AlwaysUseDefaultSource 10 | def get(sp) 11 | # Don't do anything, the default will apply on create, and the 12 | # previous value will be used on update. 13 | return if sp.default? 14 | 15 | raise "Parameter #{sp.name} does not have a default, cannot use AlwaysUseDefaultSource!" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook contains common plays that will be run on all nodes. 3 | 4 | - name: Install ntp 5 | yum: name=ntp state=present 6 | tags: ntp 7 | 8 | - name: Configure ntp file 9 | template: src=ntp.conf.j2 dest=/etc/ntp.conf 10 | tags: ntp 11 | notify: restart ntp 12 | 13 | - name: Start the ntp service 14 | service: name=ntpd state=started enabled=true 15 | tags: ntp 16 | 17 | - name: Check if /etc/sysconfig/iptables exists 18 | stat: path=/etc/sysconfig/iptables 19 | register: ipt 20 | 21 | - name: Place basic iptables rules 22 | template: src=iptables.j2 dest=/etc/sysconfig/iptables 23 | notify: restart iptables 24 | when: ipt is defined and ipt.stat.exists == false 25 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/README.md: -------------------------------------------------------------------------------- 1 | Chef and AWS CodeDeploy 2 | ----------------------- 3 | 4 | [Chef](https://www.getchef.com) is a configuration management tool that enables users to write 5 | recipes that describe how the instances in their fleet are configured. AWS provides two template 6 | samples for integrating Chef and AWS CodeDeploy. The first is a Chef cookbook that will install and 7 | start the AWS CodeDeploy host agent, giving you the ability to continue managing your host 8 | infrastructure via Chef while also being able to take advantage of the power of AWS CodeDeploy. The 9 | second sample template demonstrates how to use CodeDeploy to orchestrate running cookbooks and 10 | recipes via chef-solo on each node. 11 | 12 | [Learn more >](https://github.com/awslabs/aws-codedeploy-samples) 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/README.md: -------------------------------------------------------------------------------- 1 | Ansible and AWS CodeDeploy 2 | -------------------------- 3 | 4 | [Ansible](http://www.ansible.com) is a radically simple IT automation platform that makes your 5 | applications and systems easier to deploy. If you already have a set of Ansible playbooks, but just 6 | need somewhere to run them, our first template for Ansible and AWS CodeDeploy will demonstrate how a 7 | couple of simple deployment hooks will ensure Ansible is available on the local deployment instance 8 | and run the given playbooks. Alternatively, if you already have a process for building and 9 | maintaining your inventory, we've also built an Ansible module that you can use to install and run 10 | the AWS CodeDeploy host agent. 11 | 12 | [Learn more >](https://github.com/awslabs/aws-codedeploy-samples) 13 | -------------------------------------------------------------------------------- /lib/moonshot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'English' 4 | 5 | require 'aws-sdk-cloudformation' 6 | require 'aws-sdk-codedeploy' 7 | require 'aws-sdk-ec2' 8 | require 'aws-sdk-iam' 9 | require 'aws-sdk-autoscaling' 10 | require 'aws-sdk-s3' 11 | 12 | require 'logger' 13 | require 'thor' 14 | require 'interactive-logger' 15 | 16 | module Moonshot 17 | class << self 18 | attr_writer :config 19 | end 20 | 21 | def self.config 22 | @config ||= Moonshot::ControllerConfig.new 23 | block_given? ? yield(@config) : @config 24 | end 25 | 26 | module ArtifactRepository 27 | end 28 | 29 | module BuildMechanism 30 | end 31 | 32 | module DeploymentMechanism 33 | end 34 | 35 | module Plugins 36 | end 37 | end 38 | 39 | require 'require_all' 40 | require_rel 'moonshot' 41 | require_rel 'plugins' 42 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/README.md: -------------------------------------------------------------------------------- 1 | Puppet and AWS CodeDeploy 2 | ----------------------- 3 | 4 | [Puppet](http://puppetlabs.com/) is a configuration management system that allows you to define the 5 | state of your IT infrastructure, then automatically enforces the correct state. AWS provides a 6 | couple of sample templates to get you working with Puppet and AWS CodeDeploy faster. The first is a 7 | Puppet module that will install and start the AWS CodeDeploy host agent, giving you the ability to 8 | continue managing your host infrastructure via Puppet while also being able to take advantage of the 9 | power of AWS CodeDeploy. The second sample template demonstrates how to use CodeDeploy to 10 | orchestrate running modules and manifests via masterless puppet on each node. 11 | 12 | [Learn more >](https://github.com/awslabs/aws-codedeploy-samples) 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/salt/README.md: -------------------------------------------------------------------------------- 1 | SaltStack and AWS CodeDeploy 2 | ---------------------------- 3 | 4 | [Salt](http://www.saltstack.com) is a fast, scalable and flexible systems management software for 5 | data center automation, cloud orchestration, server provisioning, configuration management and more. 6 | The two sample templates AWS provides offer examples for how you can integrate existing Salt 7 | infrastructure with AWS CodeDeploy. On the one hand, you can use the AWS CodeDeploy module to 8 | install and run the CodeDeploy host agent on your minions. On the other hand, you can use AWS 9 | CodeDeploy via a couple of simple deployment hooks to orchestrate running your Salt States. Either 10 | way, you get to take advantage of the power of both Salt and AWS CodeDeploy. 11 | 12 | [Learn more >](https://github.com/awslabs/aws-codedeploy-samples) 13 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/web/templates/index.php.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ansible Application 4 | 5 | 6 |
7 | Homepage 8 |
9 | "; 13 | echo "List of Databases:
"; 14 | {% for host in groups['dbservers'] %} 15 | $link = mysql_connect('{{ hostvars[host].ansible_eth0.ipv4.address }}', '{{ hostvars[host].dbuser }}', '{{ hostvars[host].upassword }}') or die(mysql_error()); 16 | {% endfor %} 17 | $res = mysql_query("SHOW DATABASES"); 18 | while ($row = mysql_fetch_assoc($res)) { 19 | echo $row['Database'] . "\n"; 20 | } 21 | ?> 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /spec/moonshot/command_optional_argument_spec.rb: -------------------------------------------------------------------------------- 1 | class OptionalArgumentCommand < Moonshot::Command 2 | def execute(version = 'development') 3 | puts "selected version: #{version}" 4 | end 5 | end 6 | 7 | describe 'optional argument for command' do 8 | def try(klass, args = []) 9 | Moonshot::CommandLineDispatcher.new('stuff', klass, args) 10 | .dispatch! 11 | end 12 | 13 | Moonshot::AccountContext.set('optional-account') 14 | ARGV.clear 15 | 16 | it 'should let us run OptionalArgumentCommand without extra arguments' do 17 | expect { try(OptionalArgumentCommand) } 18 | .to output(/selected version: development/).to_stdout 19 | end 20 | 21 | it 'should let us run OptionalArgumentCommand with extra argument' do 22 | expect { try(OptionalArgumentCommand, ['production']) } 23 | .to output(/selected version: production/).to_stdout 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /docs/mechanisms/artifact-repository.md: -------------------------------------------------------------------------------- 1 | # ArtifactRepository 2 | 3 | ## S3Bucket 4 | 5 | The store action will upload the file using the S3 PutObject API call. 6 | The local environment must be configured with appropriate 7 | credentials. Running `doctor` will verify those credentials for you. 8 | 9 | To create a new S3Bucket ArtifactRepository: 10 | ```ruby 11 | Moonshot.config do |c| 12 | c.artifact_repository = S3Bucket.new('my-bucket-name') 13 | end 14 | ``` 15 | 16 | ## S3BucketViaGithubReleases 17 | 18 | S3 Bucket repository backed by GitHub releases. If a SemVer package 19 | isn't found in S3, it is downloaded from GitHub releases to avoid not 20 | being able to release in case there is trouble with AWS S3. 21 | 22 | To create a new S3BucketViaGithubReleases ArtifactRepository: 23 | ```ruby 24 | Moonshot.config do |c| 25 | c.artifact_repository = S3BucketViaGithubReleases.new('my-bucket-name') 26 | end 27 | ``` 28 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Linux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Deployment 6 | 26 | 27 | 28 |
29 |

Congratulations

30 |

This application was deployed using AWS CodeDeploy.

31 |

For next steps, read the AWS CodeDeploy Documentation.

32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /spec/moonshot/commands/build_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Commands::Build do 2 | 3 | it 'should raise RuntimeError when send --skip-ci-status param without GithubRelease plugin' do 4 | cli_dispatcher = Moonshot::CommandLineDispatcher.new('build', subject, {}) 5 | parser = cli_dispatcher.send(:build_parser, subject) 6 | expect { parser.parse(%w(--skip-ci-status)) }.to raise_error(RuntimeError) 7 | end 8 | 9 | it 'should not raise RuntimeError when send --skip-ci-status param with GithubRelease plugin' do 10 | Moonshot.config = Moonshot::ControllerConfig.new 11 | Moonshot.config do |c| 12 | c.build_mechanism = Moonshot::BuildMechanism::GithubRelease.new('') 13 | end 14 | cli_dispatcher = Moonshot::CommandLineDispatcher.new('build', subject, {}) 15 | parser = cli_dispatcher.send(:build_parser, subject) 16 | expect { parser.parse(%w(--skip-ci-status)) }.to_not raise_error 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/applications/SampleApp_Windows/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Deployment 6 | 29 | 30 | 31 |
32 |

Congratulations

33 |

This application was deployed using AWS CodeDeploy.

34 |

For next steps, read the AWS CodeDeploy Documentation.

35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/plugins/encrypted_parameters/parameter_encrypter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | module Moonshot 5 | module Plugins 6 | class EncryptedParameters 7 | # Class that can encrypt and decrypt parameters using KMS. 8 | class ParameterEncrypter 9 | # @param [String] key_arn The ARN for the KMS key. 10 | def initialize(key_arn) 11 | @kms_client = Aws::KMS::Client.new 12 | @key_arn = key_arn 13 | end 14 | 15 | # Encrypt and base64 encode the parameter value. 16 | # 17 | # @param [String] param_value The parameter to encrypt. 18 | # @return [String] base64 encoded encrypted ciphertext. 19 | def encrypt(param_value) 20 | resp = @kms_client.encrypt(key_id: @key_arn, plaintext: param_value) 21 | 22 | # Use strict here to avoid newlines which cause issues with parameters. 23 | Base64.strict_encode64(resp.ciphertext_blob) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/moonshot/ask_user_source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'colorize' 4 | 5 | module Moonshot 6 | class AskUserSource 7 | def get(sp) 8 | return unless Moonshot.config.interactive 9 | 10 | @sp = sp 11 | 12 | prompt 13 | loop do 14 | input = gets.chomp 15 | 16 | if String(input).empty? && @sp.default? 17 | # We will use the default value, print it here so the output is clear. 18 | puts 'Using default value.' 19 | return 20 | elsif String(input).empty? 21 | puts "Cannot proceed without value for #{@sp.name}!" 22 | else 23 | @sp.set(String(input)) 24 | return 25 | end 26 | 27 | prompt 28 | end 29 | end 30 | 31 | private 32 | 33 | def prompt 34 | print "(#{@sp.name})".light_black 35 | print " #{@sp.description}" unless @sp.description.empty? 36 | print " [#{@sp.default}]".light_black if @sp.default? 37 | print ': ' 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/db/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This playbook will install mysql and create db user and give permissions. 3 | 4 | - name: Install Mysql package 5 | yum: name={{ item }} state=installed 6 | with_items: 7 | - mysql-server 8 | - MySQL-python 9 | 10 | - name: Create Mysql configuration file 11 | template: src=my.cnf.j2 dest=/etc/my.cnf 12 | notify: 13 | - restart mysql 14 | 15 | - name: Start Mysql Service 16 | service: name=mysqld state=started enabled=true 17 | 18 | - name: insert iptables rule 19 | lineinfile: dest=/etc/sysconfig/iptables state=present regexp="{{ mysql_port }}" 20 | insertafter="^:OUTPUT " line="-A INPUT -p tcp --dport {{ mysql_port }} -j ACCEPT" 21 | notify: restart iptables 22 | 23 | - name: Create Application Database 24 | mysql_db: name={{ dbname }} state=present 25 | 26 | - name: Create Application DB User 27 | mysql_user: name={{ dbuser }} password={{ upassword }} priv=*.*:ALL host='%' state=present 28 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/roles/common/templates/iptables.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | *filter 3 | :INPUT ACCEPT [0:0] 4 | :FORWARD ACCEPT [0:0] 5 | :OUTPUT ACCEPT [0:0] 6 | 7 | #Syn-flood protection 8 | -A INPUT -p tcp ! --syn -m state --state NEW -j DROP 9 | 10 | #Force SYN packets check 11 | -A INPUT -f -j DROP 12 | 13 | #Force Fragments packets check 14 | -A INPUT -p tcp --tcp-flags ALL ALL -j DROP 15 | 16 | #XMAS packets 17 | -A INPUT -p tcp --tcp-flags ALL NONE -j DROP 18 | 19 | # Accept packets belonging to established and related connections 20 | -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 21 | 22 | # Allow pings 23 | -A INPUT -p icmp --icmp-type any -j ACCEPT 24 | 25 | # Set access for localhost 26 | -A INPUT -i lo -j ACCEPT 27 | 28 | # Allow SSH connections on tcp port 22 29 | -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT 30 | 31 | # Set default policies for INPUT, FORWARD and OUTPUT chains 32 | -P INPUT DROP 33 | -P FORWARD DROP 34 | -P OUTPUT ACCEPT 35 | 36 | COMMIT 37 | -------------------------------------------------------------------------------- /lib/moonshot/commands/parameter_arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Layout/LineLength 4 | module Moonshot 5 | module Commands 6 | module ParameterArguments 7 | def parser 8 | parser = super 9 | 10 | parser.on('--[no-]interactive', TrueClass, 'Use interactive prompts for gathering missing configuration.') do |v| 11 | Moonshot.config.interactive = v 12 | end 13 | 14 | parser.on('--answer-file FILE', '-aFILE', 'Load Stack Parameters from a YAML file') do |v| 15 | Moonshot.config.answer_file = File.expand_path(v) 16 | end 17 | 18 | parser.on('--parameter KEY=VALUE', '-PKEY=VALUE', 'Specify Stack Parameter on the command line') do |v| 19 | data = v.split('=', 2) 20 | raise "Invalid parameter format '#{v}', expected KEY=VALUE (e.g. MyStackParameter=12)" unless data.size == 2 21 | 22 | Moonshot.config.parameter_overrides[data[0]] = data[1] 23 | end 24 | end 25 | end 26 | end 27 | end 28 | # rubocop:enable Layout/LineLength 29 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.amazonaws.sample 5 | hello 6 | war 7 | 1.0-SNAPSHOT 8 | hello Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | 13 | junit 14 | junit 15 | 3.8.1 16 | test 17 | 18 | 19 | javax.servlet 20 | javax.servlet-api 21 | 3.0.1 22 | provided 23 | 24 | 25 | 26 | 27 | hello 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/masterless/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.amazonaws.sample 5 | hello 6 | war 7 | 1.0-SNAPSHOT 8 | hello Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | 13 | junit 14 | junit 15 | 3.8.1 16 | test 17 | 18 | 19 | javax.servlet 20 | javax.servlet-api 21 | 3.0.1 22 | provided 23 | 24 | 25 | 26 | 27 | hello 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/plugins/dynamic_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Plugins 5 | class DynamicTemplate 6 | def initialize(source:, parameters:, destination:) 7 | @dynamic_template = ::Moonshot::DynamicTemplate.new( 8 | source:, 9 | parameters:, 10 | destination: 11 | ) 12 | end 13 | 14 | def run_hook 15 | @dynamic_template.process 16 | end 17 | 18 | def cli_hook(parser) 19 | parser.on('--template-file=FILE', 'Override the path to the CloudFormation template.') do |v| 20 | @dynamic_template.destination = v 21 | Moonshot.config.template_file = v 22 | end 23 | end 24 | 25 | # Moonshot hooks to trigger this plugin. 26 | alias setup_create run_hook 27 | alias setup_update run_hook 28 | alias setup_delete run_hook 29 | 30 | # Moonshot hooks to add CLI options. 31 | alias create_cli_hook cli_hook 32 | alias delete_cli_hook cli_hook 33 | alias update_cli_hook cli_hook 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /docs/user-guide/parent_stacks.md: -------------------------------------------------------------------------------- 1 | # Parent Stacks 2 | 3 | Moonshot supports referencing another CloudFormation stack as a 4 | "Parent" during creation time. This relationship is used only for creation, 5 | where any outputs of that stack that match names of the parameters for the local 6 | stack will be used as parameters, and saved into a local .yml file for future 7 | use. 8 | 9 | The order of precedence for parameters is: 10 | 11 | - Existing parameter overrides in the .yml file. 12 | - The value from the parent stack's output. 13 | - Any default value in the CloudFormation template. 14 | 15 | ## A word of caution 16 | 17 | It's not advisable to use default values in the CloudFormation template. 18 | Consider the following example: 19 | 20 | - Developer A launches a stack, referencing a specific parent stack. 21 | - Values are copied into a local .yml file, and used during stack creation. 22 | - Developer B assists Developer A with a stack update issue and runs 'update' 23 | without the local overrides .yml file. In doing so, the default values in the 24 | template are used. 25 | - Developer A is sad. 26 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | TargetRubyVersion: 3.3.4 4 | Exclude: 5 | - '.gemspec' 6 | - 'vendor/**/*' 7 | - 'sample/bin/aws-codedeploy-samples/**/*' 8 | - 'sample/gems/**/*' 9 | - 'spec/**/*' 10 | - 'lib/default/**/*' 11 | Metrics/AbcSize: 12 | Max: 35 13 | Metrics/BlockLength: 14 | Max: 35 15 | Metrics/MethodLength: 16 | Max: 30 17 | Layout/LineLength: 18 | Max: 120 19 | Style/ClassAndModuleChildren: 20 | Enabled: false 21 | Metrics/ClassLength: 22 | Max: 130 23 | Style/Documentation: 24 | Enabled: false 25 | Lint/MissingSuper: 26 | Enabled: false 27 | Style/MissingRespondToMissing: 28 | Enabled: false 29 | Layout/HeredocIndentation: 30 | Enabled: false 31 | Naming/HeredocDelimiterNaming: 32 | Enabled: false 33 | Naming/MemoizedInstanceVariableName: 34 | Enabled: false 35 | Naming/MethodParameterName: 36 | Enabled: false 37 | Metrics/CyclomaticComplexity: 38 | Enabled: false 39 | Metrics/PerceivedComplexity: 40 | Enabled: false 41 | Style/OptionalBooleanParameter: 42 | Enabled: false 43 | Style/IfUnlessModifier: 44 | Enabled: false 45 | Lint/EmptyFile: 46 | Enabled: false -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/ansible/local-only/lamp_simple/README.md: -------------------------------------------------------------------------------- 1 | Building a simple LAMP stack and deploying Application using Ansible Playbooks. 2 | ------------------------------------------- 3 | 4 | These playbooks require Ansible 1.2. 5 | 6 | These playbooks are meant to be a reference and starter's guide to building 7 | Ansible Playbooks. These playbooks were tested on CentOS 6.x so we recommend 8 | that you use CentOS or RHEL to test these modules. 9 | 10 | This LAMP stack can be on a single node or multiple nodes. The inventory file 11 | 'hosts' defines the nodes in which the stacks should be configured. 12 | 13 | [webservers] 14 | localhost 15 | 16 | [dbservers] 17 | bensible 18 | 19 | Here the webserver would be configured on the local host and the dbserver on a 20 | server called "bensible". The stack can be deployed using the following 21 | command: 22 | 23 | ansible-playbook -i hosts site.yml 24 | 25 | Once done, you can check the results by browsing to http://localhost/index.php. 26 | You should see a simple test page and a list of databases retrieved from the 27 | database server. 28 | -------------------------------------------------------------------------------- /lib/moonshot/ssh_command_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shellwords' 4 | 5 | module Moonshot 6 | # Create an ssh command from configuration. 7 | class SSHCommandBuilder 8 | Result = Struct.new(:cmd, :ip) 9 | 10 | def initialize(ssh_config, instance_id) 11 | @config = ssh_config 12 | @instance_id = instance_id 13 | end 14 | 15 | def build(command = nil) 16 | cmd = ['ssh', '-t'] 17 | cmd << "-i #{@config.ssh_identity_file}" if @config.ssh_identity_file 18 | cmd << "-l #{@config.ssh_user}" if @config.ssh_user 19 | cmd << instance_ip 20 | cmd << Shellwords.escape(command) if command 21 | Result.new(cmd.join(' '), instance_ip) 22 | end 23 | 24 | private 25 | 26 | def instance_ip 27 | @instance_ip ||= Aws::EC2::Client.new 28 | .describe_instances(instance_ids: [@instance_id]) 29 | .reservations.first.instances.first.public_ip_address 30 | rescue StandardError 31 | raise "Failed to determine public IP address for instance #{@instance_id}!" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/moonshot/interactive_logger_proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | 5 | module Moonshot 6 | # This class pretends to be an InteractiveLogger for systems that are 7 | # non-interactive. 8 | class InteractiveLoggerProxy 9 | # Non-interactive version of InteractiveLogger::Step. 10 | class Step 11 | def initialize(logger) 12 | @logger = logger 13 | end 14 | 15 | def blank; end 16 | 17 | def continue(str = nil) 18 | @logger.info(str) if str 19 | end 20 | 21 | def failure(str = 'Failure') 22 | @logger.error(str) 23 | end 24 | 25 | def repaint; end 26 | 27 | def success(str = 'Success') 28 | @logger.info(str) 29 | end 30 | end 31 | 32 | extend Forwardable 33 | 34 | def_delegator :@debug, :itself, :debug? 35 | def_delegators :@logger, :debug, :error, :info 36 | alias msg info 37 | 38 | def initialize(logger, debug: false) 39 | @debug = debug 40 | @logger = logger 41 | end 42 | 43 | def start(str) 44 | @logger.info(str) 45 | yield Step.new(@logger) 46 | end 47 | alias start_threaded start 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/solo/deploy_hooks/install-chef.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -z "`which yum`" ]; then 4 | 5 | # On rpm-based system, use yum to install rubygems and related packages for building native 6 | # extensions 7 | 8 | for pkg in rubygems20 gcc-c++ ruby20-devel make autoconf automake; do 9 | yum list installed $pkg &> /dev/null 10 | if [ $? != 0 ]; then 11 | yum -y install $pkg 12 | fi 13 | done 14 | 15 | elif [ ! -z "`which apt-get`" ]; then 16 | # On debian-like system, use apt-get to install 17 | 18 | for pkg in ruby2.0-dev ruby2.0 make autoconf g++; do 19 | aptitude show $pkg | grep -q 'State: installed' 20 | if [ $? != 0 ]; then 21 | apt-get -y install $pkg 22 | fi 23 | done 24 | fi 25 | 26 | # Now, we can install the required gems 27 | for gem in chef ohai librarian-chef io-console; do 28 | gem2.0 list | grep -q $gem 29 | if [ $? != 0 ]; then 30 | gem2.0 install $gem 31 | 32 | if [ $? != 0 ]; then 33 | echo "Failed to install required gems. Cannot continue with deployment" 34 | exit 1 35 | fi 36 | fi 37 | done 38 | -------------------------------------------------------------------------------- /lib/moonshot/ssh_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | 5 | module Moonshot 6 | # A SSHCommand that is automatically registered with the 7 | # Moonshot::CommandLine, including options for SSH access. 8 | class SSHCommand < Moonshot::Command 9 | def parser 10 | parser = super 11 | parser.on('-l', '--user USER', 'User to log into remote machine as') do |v| 12 | Moonshot.config.ssh_config.ssh_user = v 13 | end 14 | 15 | parser.on('-i', '--identity-file FILE', 'SSH Private Key to authenticate with') do |v| 16 | Moonshot.config.ssh_config.ssh_identity_file = v 17 | end 18 | 19 | parser.on('-s', '--instance INSTANCE_ID', 'Specific AWS EC2 ID to connect to') do |v| 20 | Moonshot.config.ssh_instance = v 21 | end 22 | 23 | parser.on('-c', '--command COMMAND', 'Command to execute on the remote host') do |v| 24 | Moonshot.config.ssh_command = v 25 | end 26 | 27 | parser.on('-g', '--auto-scaling-group ASG_NAME', 28 | 'The logical ID of the ASG to SSH into, required for multiple stacks') do |v| 29 | Moonshot.config.ssh_auto_scaling_group_name = v 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/moonshot/interactive_logger_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'compatible class' do |expected_class| 2 | expected_class.public_instance_methods(false).each do |name| 3 | describe "##{name}" do 4 | it 'should be defined.' do 5 | described_class.public_instance_method(name) 6 | end 7 | 8 | it "should have method arguments compatible with #{expected_class.name}##{name}" do 9 | expected_method = expected_class.instance_method(name) 10 | actual_method = described_class.instance_method(name) 11 | expect(actual_method.arity).to eq(-1).or be >= expected_method.arity 12 | actual_method.parameters.each_with_index do |(type, arg), index| 13 | # If it's a required argument, expect it to be identical. 14 | if type == :req 15 | expect(name => expected_method.parameters[index]) 16 | .to eq(name => [type, arg]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | 24 | describe Moonshot::InteractiveLoggerProxy do 25 | include_examples 'compatible class', InteractiveLogger 26 | end 27 | 28 | describe Moonshot::InteractiveLoggerProxy::Step do 29 | include_examples 'compatible class', InteractiveLogger::Step 30 | end 31 | -------------------------------------------------------------------------------- /spec/moonshot/commands/update_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Commands::Update do 2 | before(:each) do 3 | Moonshot.config = Moonshot::ControllerConfig.new 4 | end 5 | 6 | tests = { 7 | %w(-P Key=Value -P OtherKey=OtherValue) => { 'Key' => 'Value', 'OtherKey' => 'OtherValue' }, 8 | %w(-PKey=ValueWith=Equals) => { 'Key' => 'ValueWith=Equals' } 9 | } 10 | 11 | tests.each do |input, expected| 12 | it "Should process #{input} correctly" do 13 | op = subject.parser 14 | op.parse(input) 15 | expect(Moonshot.config.parameter_overrides).to match(expected) 16 | end 17 | end 18 | 19 | extra_tags = { 20 | %w(-T Key=Value -T OtherKey=OtherValue) => 21 | [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }], 22 | %w(-TKey=ValueWith=Equals) => [{ key: 'Key', value: 'ValueWith=Equals' }], 23 | %w(--tag Key=Value --tag OtherKey=OtherValue) => 24 | [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }] 25 | } 26 | 27 | extra_tags.each do |input, expected| 28 | it "Should process #{input} correctly" do 29 | op = subject.parser 30 | op.parse(input) 31 | expect(Moonshot.config.extra_tags).to match(expected) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/moonshot/tools/asg_rollout_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Tools 5 | class ASGRolloutConfig 6 | attr_reader :pre_detach, :terminate_when, :terminate_when_timeout, :terminate 7 | attr_accessor :terminate_when_delay, :instance_health_delay 8 | 9 | def initialize 10 | @instance_health_delay = 2 11 | @terminate_when_delay = 1 12 | @terminate_when_timeout = 300 13 | @terminate = proc do |h| 14 | h.ec2_instance.terminate 15 | end 16 | end 17 | 18 | def pre_detach=(value) 19 | raise ArgumentError, 'pre_detach must be callable' unless value.respond_to?(:call) 20 | 21 | @pre_detach = value 22 | end 23 | 24 | def terminate_when=(value) 25 | raise ArgumentError, 'terminate_when must be callable' unless value.respond_to?(:call) 26 | 27 | @terminate_when = value 28 | end 29 | 30 | def terminate_when_timeout=(value) 31 | @terminate_when_timeout = Float(value) 32 | end 33 | 34 | def terminate=(value) 35 | raise ArgumentError, 'terminate must be callable' unless value.respond_to?(:call) 36 | 37 | @terminate = value 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/moonshot/tools/asg_rollout/instance_health.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Tools 5 | class ASGRollout 6 | class InstanceHealth 7 | attr_reader :asg_status, :elb_status 8 | 9 | VALID_ASG_IN_SERVICE_STATES = ['InService'].freeze 10 | VALID_ELB_IN_SERVICE_STATES = [nil, 'InService'].freeze 11 | 12 | VALID_ASG_OUT_OF_SERVICE_STATES = [nil, 'Missing', 'Detached'].freeze 13 | VALID_ELB_OUT_OF_SERVICE_STATES = [nil, 'Missing', 'OutOfService'].freeze 14 | 15 | def initialize(asg_status, elb_status) 16 | @asg_status = asg_status 17 | @elb_status = elb_status 18 | end 19 | 20 | def to_s 21 | result = "ASG:#{@asg_status}" 22 | result << "/ELB:#{@elb_status}" if @elb_status 23 | result 24 | end 25 | 26 | def in_service? 27 | VALID_ASG_IN_SERVICE_STATES.include?(@asg_status) && 28 | VALID_ELB_IN_SERVICE_STATES.include?(@elb_status) 29 | end 30 | 31 | def out_of_service? 32 | VALID_ASG_OUT_OF_SERVICE_STATES.include?(@asg_status) && 33 | VALID_ELB_OUT_OF_SERVICE_STATES.include?(@elb_status) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/moonshot/tools/asg_rollout/hook_exec_environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'moonshot/ssh_fork_executor' 4 | 5 | module Moonshot 6 | module Tools 7 | class ASGRollout 8 | # This object is passed into hooks defined in the ASGRollout 9 | # process, to give them access to instances and logging 10 | # facilities. 11 | class HookExecEnvironment 12 | attr_reader :instance_id 13 | 14 | def initialize(config, instance_id) 15 | @ilog = config.interactive_logger 16 | @command_builder = Moonshot::SSHCommandBuilder.new(config.ssh_config, instance_id) 17 | @instance_id = instance_id 18 | end 19 | 20 | def exec(cmd) 21 | cb = @command_builder.build(cmd) 22 | fe = SSHForkExecutor.new 23 | fe.run(cb.cmd) 24 | end 25 | 26 | def ec2 27 | Aws::EC2::Client.new 28 | end 29 | 30 | def ec2_instance 31 | res = Aws::EC2::Resource.new(client: ec2) 32 | res.instance(@instance_id) 33 | end 34 | 35 | def debug(msg) 36 | @ilog.debug(msg) 37 | end 38 | 39 | def info(msg) 40 | @ilog.info(msg) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/Rakefile: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/rake_tasks' 2 | require 'puppet-lint/tasks/puppet-lint' 3 | require 'puppet-syntax/tasks/puppet-syntax' 4 | 5 | # These gems aren't always present, for instance 6 | # if run with --without development 7 | begin 8 | require 'puppet_blacksmith/rake_tasks' 9 | rescue LoadError 10 | end 11 | 12 | PuppetLint.configuration.relative = true 13 | PuppetLint.configuration.send("disable_80chars") 14 | PuppetLint.configuration.log_format = "%{path}:%{linenumber}:%{check}:%{KIND}:%{message}" 15 | PuppetLint.configuration.fail_on_warnings = true 16 | 17 | # Forsake support for Puppet 2.6.2 for the benefit of cleaner code. 18 | # http://puppet-lint.com/checks/class_parameter_defaults/ 19 | PuppetLint.configuration.send('disable_class_parameter_defaults') 20 | # http://puppet-lint.com/checks/class_inherits_from_params_class/ 21 | PuppetLint.configuration.send('disable_class_inherits_from_params_class') 22 | 23 | exclude_paths = [ 24 | "pkg/**/*", 25 | "vendor/**/*", 26 | "spec/**/*", 27 | ] 28 | PuppetLint.configuration.ignore_paths = exclude_paths 29 | PuppetSyntax.exclude_paths = exclude_paths 30 | 31 | desc "Run syntax, lint, and spec tests." 32 | task :test => [ 33 | :syntax, 34 | :lint, 35 | :spec, 36 | ] 37 | -------------------------------------------------------------------------------- /lib/moonshot/ssh_target_selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Choose a publically accessible instance to run commands on, given a Moonshot::Stack. 5 | class SSHTargetSelector 6 | def initialize(stack, asg_name: nil) 7 | @asg_name = asg_name 8 | @stack = stack 9 | end 10 | 11 | def choose! 12 | groups = @stack.resources_of_type('AWS::AutoScaling::AutoScalingGroup') 13 | 14 | asg = if groups.count == 1 15 | groups.first 16 | elsif groups.count > 1 17 | unless @asg_name 18 | raise 'Multiple Auto Scaling Groups found in the stack. Please specify which '\ 19 | 'one to SSH into using the --auto-scaling-group (-g) option.' 20 | end 21 | groups.detect { |x| x.logical_resource_id == @asg_name } 22 | end 23 | raise 'Failed to find the Auto Scaling Group.' unless asg 24 | 25 | Aws::AutoScaling::Client.new.describe_auto_scaling_groups( 26 | auto_scaling_group_names: [asg.physical_resource_id] 27 | ).auto_scaling_groups.first.instances.map(&:instance_id).first 28 | rescue StandardError => e 29 | raise "Failed to select an instance from the Auto Scaling Group: #{e.message}" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/moonshot/commands/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class Update < Moonshot::Command 6 | include ParameterArguments 7 | include TagArguments 8 | include ShowAllEventsOption 9 | include ParentStackOption 10 | 11 | self.usage = 'update [options]' 12 | self.description = 'Update the CloudFormation stack within an environment.' 13 | 14 | def parser 15 | parser = super 16 | 17 | parser.on('--dry-run', TrueClass, 'Show the changes that would be applied, but do not execute them') do |v| 18 | @dry_run = v 19 | end 20 | 21 | parser.on('--force', '-f', TrueClass, 'Apply ChangeSet without confirmation') do |v| 22 | @force = v 23 | end 24 | 25 | parser.on('--refresh-parameters', TrueClass, 'Update parameters from parent stacks') do |v| 26 | @refresh_parameters = v 27 | end 28 | 29 | parser.on('--template-file=FILE', 'Override the path to the CloudFormation template.') do |v| 30 | Moonshot.config.template_file = v 31 | end 32 | end 33 | 34 | def execute 35 | @force = true unless Moonshot.config.interactive 36 | controller.update(dry_run: @dry_run, force: @force, refresh_parameters: @refresh_parameters) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/version-control/git/README.md: -------------------------------------------------------------------------------- 1 | Git Hooks 2 | ========= 3 | 4 | The scripts here provide basic functionality to hook AWS CodeDeploy into git 5 | events. The primary script is provided as a `pre-push` hook, which executes the 6 | `aws deploy push` command for the local repository before git finishes pushing 7 | to the remote. It then starts a new deployment of the revision. Both the AWS 8 | CodeDeploy Application and Deployment Group must already exist. 9 | 10 | !!! CAUTION !!! Because this does an S3 upload on every push, you may incur S3 transfer charges. 11 | 12 | --Note, you need to make this script executable ( chmod +x pre-push ) after installing it in ./.git/hooks/pre-push 13 | 14 | No changes to the script itself should be required. Instead, it pulls the 15 | necessary information from git config. The required keys are 16 | `aws-codedeploy.application-name`, `aws-codedeploy.s3bucket`, and 17 | `aws-codedeploy.deployment-group`. They can be set with the following commands 18 | (replace values with your own): 19 | 20 | git config aws-codedeploy.application-name MyApplication 21 | git config aws-codedeploy.s3bucket MyS3Bucket 22 | git config aws-codedeploy.deployment-group MyDeploymentGroup 23 | 24 | The deployment created with this script will use the default deployment 25 | configuration for the configured deployment group. 26 | -------------------------------------------------------------------------------- /lib/moonshot/commands/generate_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class GenerateTemplate < Moonshot::Command 6 | self.usage = 'generate-template [options]' 7 | self.description = 'Processes an ERB formatted CloudFormation template.' 8 | 9 | def initialize(*) 10 | super 11 | @parameters = {} 12 | end 13 | 14 | def parser 15 | parser = super 16 | 17 | parser.on('--source SOURCE_FILE', 'The ERB template file.') do |v| 18 | @source = v 19 | end 20 | 21 | parser.on('--parameter KEY=VALUE', '-PKEY=VALUE', 22 | 'Specify Stack Parameter on the command line') do |v| 23 | data = v.split('=', 2) 24 | unless data.size == 2 25 | raise "Invalid parameter format '#{v}',"\ 26 | 'expected KEY=VALUE (e.g. MyTemplateParameter=12)' 27 | end 28 | 29 | @parameters[data[0]] = data[1] 30 | end 31 | 32 | parser.on('--destination DESTINATION_FILE', 'Destionation file.') do |v| 33 | @destination = v 34 | end 35 | end 36 | 37 | def execute 38 | ::Moonshot::DynamicTemplate.new( 39 | source: @source, 40 | parameters: @parameters, 41 | destination: @destination 42 | ).process 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/moonshot/doctor_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'colorize' 4 | 5 | module Moonshot 6 | DoctorCritical = Class.new(RuntimeError) 7 | 8 | # 9 | # A series of methods for adding "doctor" checks to a mechanism. 10 | # 11 | module DoctorHelper 12 | def doctor_hook 13 | run_all_checks 14 | end 15 | 16 | private 17 | 18 | def run_all_checks 19 | success = true 20 | puts 21 | puts self.class.name.split('::').last 22 | private_methods.each do |meth| 23 | send(meth) if meth =~ /^doctor_check_/ 24 | rescue DoctorCritical 25 | # Stop running checks in this Mechanism. 26 | success = false 27 | break 28 | rescue StandardError => e 29 | success = false 30 | print ' ✗ '.red 31 | puts "Exception while running check: #{e.class}: #{e.message.lines.first}" 32 | break 33 | end 34 | 35 | success 36 | end 37 | 38 | def success(str) 39 | print ' ✓ '.green 40 | puts str 41 | end 42 | 43 | def warning(str, additional_info = nil) 44 | print ' ? '.yellow 45 | puts str 46 | additional_info&.lines&.each { |l| puts " #{l}" } 47 | end 48 | 49 | def critical(str, additional_info = nil) 50 | print ' ✗ '.red 51 | puts str 52 | additional_info&.lines&.each { |l| puts " #{l}" } 53 | raise DoctorCritical 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/moonshot/stack_lister.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # The StackLister is world renoun for it's ability to list stacks. 5 | class StackLister 6 | EnvironmentDescription = Struct.new(:name, :creation_time, :status) 7 | include CredsHelper 8 | 9 | def initialize(app_name) 10 | @app_name = app_name 11 | end 12 | 13 | # rubocop:disable Metrics/AbcSize 14 | def list 15 | result = [] 16 | next_token = nil 17 | loop do 18 | resp = cf_client.describe_stacks(next_token:) 19 | resp.stacks.each do |stack| 20 | app_tag = stack.tags.find { |t| t.key == 'moonshot_application' } 21 | env_tag = stack.tags.find { |t| t.key == 'moonshot_environment' } 22 | legacy_tag = stack.tags.find { |t| t.key == 'ah_stage' } 23 | 24 | if app_tag && app_tag.value == Moonshot.config.app_name 25 | result << 26 | EnvironmentDescription.new(env_tag.value, stack.creation_time, stack.stack_status) 27 | elsif legacy_tag&.value&.start_with?(Moonshot.config.app_name) 28 | result << 29 | EnvironmentDescription.new(legacy_tag.value, stack.creation_time, stack.stack_status) 30 | end 31 | end 32 | break unless resp.next_token 33 | 34 | next_token = resp.next_token 35 | end 36 | result.sort_by(&:name) 37 | end 38 | # rubocop:enable Metrics/AbcSize 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/spec/classes/example_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'codedeploy' do 4 | let(:facts) {{ 5 | :osfamily => 'RedHat', 6 | }} 7 | describe "codedeploy class without any parameters" do 8 | 9 | it { should compile.with_all_deps } 10 | 11 | it { should contain_class('codedeploy::params') } 12 | it { should contain_class('codedeploy::install') } 13 | it { should contain_class('codedeploy::service').that_subscribes_to('codedeploy::install') } 14 | 15 | it { should contain_service('codedeploy-agent') } 16 | it { should contain_package('codedeploy-agent') 17 | .with_ensure('present') 18 | .with_source('https://s3.amazonaws.com/aws-codedeploy-us-east-1/latest/codedeploy-agent.noarch.rpm') 19 | } 20 | end 21 | 22 | describe "codedeploy class with custom package source" do 23 | let(:params) {{ :package_source => 'https://example.com/package.rpm' }} 24 | it { should contain_package('codedeploy-agent').with_source('https://example.com/package.rpm') } 25 | end 26 | 27 | context 'unsupported operating system' do 28 | describe 'codedeploy class without any parameters on Solaris/Nexenta' do 29 | let(:facts) {{ 30 | :osfamily => 'Solaris', 31 | :operatingsystem => 'Nexenta', 32 | }} 33 | 34 | it { expect { should contain_package('codedeploy') }.to raise_error(Puppet::Error, /Nexenta not supported/) } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /docs/plugins/dynamic_template.md: -------------------------------------------------------------------------------- 1 | # Dynamic template plugin 2 | 3 | ## Overview 4 | The dynamic template plugin can be used to generate CloudFormation plugins 5 | on-the-fly. This is useful for properties that can't be set via parameters, 6 | like "DeletionPolicy". The plugin is using ERB templating and it's also 7 | available in the form of a CLI. 8 | 9 | ## Usage and configuration 10 | 11 | The plugin accepts three configuration parameters: 12 | - `source`: path to the source template file 13 | - `parameters`: parameters of your choice (see below - [parameters](#parameters)) 14 | - `destination`: the target file path 15 | 16 | Your template must be conforming the ERB standards. You can use both 17 | `<%= deletion_policy %>` and `<%= @deletion_policy %>` formats in your template. 18 | 19 | ## Example 20 | ```ruby 21 | Moonshot::Plugins::DynamicTemplate.new( 22 | source: 'cloud_formation/cdb-api.erb', 23 | parameters: { deletion_policy: 'Retain' }, 24 | destination: 'cloud_formation/cdb-api.json' 25 | ) 26 | ``` 27 | 28 | ## Parameters 29 | 30 | The 'hack' with lambdas created for the cases when you need to read 31 | CLI parameters before running the plugin. 32 | 33 | Parameters could be described in two ways: 34 | * key-value pairs 35 | ```ruby 36 | parameters: { deletion_policy: 'Retain' } 37 | ``` 38 | 39 | * lambdas 40 | ```ruby 41 | parameters = -> environment_name { 42 | environment = environment_name =~ prod_regexp ? 'prod' : 'dev' 43 | parameters = YAML.load_file("cloud_formation/parameters/#{environment}.yml") 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /lib/plugins/encrypted_parameters/kms_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../moonshot/stack' 4 | 5 | module Moonshot 6 | module Plugins 7 | class EncryptedParameters 8 | # Class that manages KMS keys in AWS. 9 | class KmsKey 10 | attr_reader :arn 11 | 12 | class << self 13 | def create 14 | standard_tags = stack_tags 15 | resp = Aws::KMS::Client.new.create_key({ 16 | tags: standard_tags # An array of tags. 17 | }) 18 | arn = resp.key_metadata.arn 19 | new(arn) 20 | end 21 | 22 | def stack_tags 23 | tags = Moonshot::Stack.make_tags(Moonshot.config) 24 | tags.map { |tag| { tag_key: tag[:key], tag_value: tag[:value] } } 25 | end 26 | end 27 | 28 | def initialize(arn) 29 | @arn = arn 30 | @kms_client = Aws::KMS::Client.new 31 | end 32 | 33 | def update 34 | standard_tags = self.class.stack_tags 35 | @kms_client.tag_resource({ 36 | key_id: @arn, # arn of the CMK being tagged 37 | tags: standard_tags # An array of tags. 38 | }) 39 | end 40 | 41 | def delete 42 | @kms_client.schedule_key_deletion(key_id: @arn, pending_window_in_days: 7) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /sample/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.6) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | aws-sdk (2.2.36) 11 | aws-sdk-resources (= 2.2.36) 12 | aws-sdk-core (2.2.36) 13 | jmespath (~> 1.0) 14 | aws-sdk-resources (2.2.36) 15 | aws-sdk-core (= 2.2.36) 16 | colorize (0.7.7) 17 | github-markup (1.4.0) 18 | highline (1.7.8) 19 | i18n (0.7.0) 20 | interactive-logger (0.1.1) 21 | colorize (~> 0.7.7) 22 | ruby-duration (~> 3.2, >= 3.2.1) 23 | iso8601 (0.9.0) 24 | jmespath (1.2.4) 25 | json_pure (>= 1.8.1) 26 | json (1.8.3) 27 | json_pure (1.8.3) 28 | minitest (5.8.4) 29 | moonshot (0.7.0) 30 | aws-sdk (~> 2.2.0) 31 | colorize 32 | highline (~> 1.7.2) 33 | interactive-logger (~> 0.1.1) 34 | rotp (~> 2.1.1) 35 | ruby-duration (~> 3.2.3) 36 | semantic 37 | thor (~> 0.19.1) 38 | vandamme 39 | redcarpet (3.3.4) 40 | rotp (2.1.2) 41 | ruby-duration (3.2.3) 42 | activesupport (>= 3.0.0) 43 | i18n 44 | iso8601 45 | semantic (1.4.1) 46 | thor (0.19.1) 47 | thread_safe (0.3.5) 48 | tzinfo (1.2.2) 49 | thread_safe (~> 0.1) 50 | vandamme (0.0.11) 51 | github-markup (~> 1.3) 52 | redcarpet (~> 3.3.2) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | moonshot (~> 0.7.0) 59 | 60 | BUNDLED WITH 61 | 1.11.2 -------------------------------------------------------------------------------- /lib/moonshot/commands/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'parameter_arguments' 4 | require_relative 'tag_arguments' 5 | require_relative 'show_all_events_option' 6 | require_relative 'parent_stack_option' 7 | 8 | module Moonshot 9 | module Commands 10 | class Create < Moonshot::Command 11 | include ParameterArguments 12 | include TagArguments 13 | include ShowAllEventsOption 14 | include ParentStackOption 15 | 16 | self.usage = 'create [options]' 17 | self.description = 'Create a new environment' 18 | 19 | attr_reader :version, :deploy 20 | 21 | def parser 22 | @deploy = true 23 | 24 | parser = super 25 | desc = 'Choose if code should be deployed immediately after the stack is created' 26 | parser.on('-d', '--[no-]deploy', TrueClass, desc) do |v| 27 | @deploy = v 28 | end 29 | 30 | desc = 'Version for initial deployment. If unset, a new development build is created from the local directory' 31 | parser.on('--version VERSION_NAME', desc) do |v| 32 | @version = v 33 | end 34 | 35 | parser.on('--template-file=FILE', 'Override the path to the CloudFormation template.') do |v| 36 | Moonshot.config.template_file = v 37 | end 38 | end 39 | 40 | def execute 41 | controller.create 42 | 43 | if @deploy && @version.nil? 44 | controller.push 45 | elsif @deploy 46 | controller.deploy_version(@version) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/moonshot/stack_parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class StackParameter 5 | attr_reader :name, :default, :description 6 | 7 | def initialize(name, default: nil, use_previous: false, description: '') 8 | @default = default 9 | @description = description 10 | @name = name 11 | @use_previous = use_previous 12 | @value = nil 13 | end 14 | 15 | # Does this Stack Parameter have a default value that will be used? 16 | def default? 17 | !@default.nil? 18 | end 19 | 20 | def use_previous? 21 | @use_previous ? true : false 22 | end 23 | 24 | # Has the user provided a value for this parameter? 25 | def set? 26 | !@value.nil? 27 | end 28 | 29 | def set(value) 30 | @value = value 31 | @use_previous = false 32 | end 33 | 34 | def use_previous!(value) 35 | raise "Value already set for StackParameter #{@name}, cannot use previous value!" if @value 36 | 37 | # Make the current value available to plugins. 38 | @value = value 39 | @use_previous = true 40 | end 41 | 42 | def value 43 | raise "No value set and no default for StackParameter #{@name}!" unless @value || default? 44 | 45 | @value || default 46 | end 47 | 48 | def to_cf 49 | result = { parameter_key: @name } 50 | 51 | if use_previous? 52 | result[:use_previous_value] = true 53 | else 54 | result[:parameter_value] = value 55 | end 56 | 57 | result 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/moonshot/parameter_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # A Rigid Hash-like structure that only accepts manipulation of 5 | # parameters defined in the Stack template. Anything else results in 6 | # an exception. 7 | class ParameterCollection 8 | extend Forwardable 9 | 10 | def_delegators :@hash, :key?, :fetch, :[], :keys, :values 11 | attr_reader :hash 12 | 13 | def self.from_template(template) 14 | obj = new 15 | 16 | template.parameters.each do |stack_parameter| 17 | obj.add(stack_parameter) 18 | end 19 | 20 | obj 21 | end 22 | 23 | def initialize 24 | @hash = {} 25 | end 26 | 27 | def []=(key, value) 28 | raise "Invalid StackParameter #{key}!" unless @hash.key?(key) 29 | 30 | @hash[key].set(value) 31 | end 32 | 33 | def add(parameter) 34 | raise ArgumentError, 'Can only add StackParameters!' unless parameter.is_a?(StackParameter) 35 | 36 | @hash[parameter.name] = parameter 37 | end 38 | 39 | # What parameters are missing for a CreateStack call, where 40 | # UsePreviousValue has no meaning. 41 | def missing_for_create 42 | # If we haven't set a value, and there is no default, we can't 43 | # create the stack. 44 | @hash.values.select { |v| !v.set? && !v.default? } 45 | end 46 | 47 | def missing_for_update 48 | # If we don't have a previous value to use, we haven't set a 49 | # value, and there is no default, we can't update a stack. 50 | @hash.values.select { |v| !v.set? && !v.default? && !v.use_previous? } 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /docs/user-guide/custom-commands.md: -------------------------------------------------------------------------------- 1 | # Custom Commands 2 | 3 | One powerful feature of Moonshot is the ability to add custom 4 | project-specific commands. You can hook into many of the features 5 | provided by the `Moonshot::Controller` and `Moonshot::Stack` 6 | interface, and make native calls to AWS using the AWS SDK for Ruby 7 | v2. 8 | 9 | To do this, place a file in `moonshot/cli_extensions/my_command.rb` 10 | that extends `Moonshot::Command` (or `Moonshot::SSHCommand`, if you 11 | want to use the Moonshot SSH interface). 12 | 13 | ## Example 14 | 15 | ```ruby 16 | # moonshot/cli_extensions/get_elb_address.rb 17 | class GetElbAddress < Moonshot::Command 18 | self.description = 'Display the ELBs external hostname.' 19 | self.usage = 'get-elb-address' 20 | 21 | def execute 22 | puts controller.stack.outputs['APIElasticLoadBalancerDNS'] 23 | end 24 | end 25 | ``` 26 | 27 | ## Account Restrictions 28 | 29 | If you have a development- or production-only command you want 30 | restricted to running in appropriately labelled accounts, you can use 31 | the `only_in_account` option. This can be either a single string, or 32 | an array of strings. For example: 33 | 34 | ```ruby 35 | # moonshot/cli_extensions/drop_database.rb 36 | class DropDatabase < Moonshot::Command 37 | self.description = 'Delete all the data (development only!)' 38 | self.usage = 'drop-database' 39 | self.only_in_account = 'company-dev' 40 | 41 | def execute 42 | # ... 43 | end 44 | end 45 | ``` 46 | 47 | If the IAM account alias for the current AWS account does not exactly 48 | match, the user will receive an error: 49 | 50 | ``` 51 | 'drop-database' can only be run in the following accounts: 52 | - company-dev 53 | ``` 54 | -------------------------------------------------------------------------------- /lib/moonshot/stack_events_poller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # The StackEventsPoller queries DescribeStackEvents every time #latest_events 5 | # is invoked, filtering out events that have already been returned. It can 6 | # also, optionally, filter all non-error events (@see #show_errors_only). 7 | class StackEventsPoller 8 | def initialize(cf_client, stack_name) 9 | @cf_client = cf_client 10 | @stack_name = stack_name 11 | 12 | # Start showing events from now. 13 | @last_time = Time.now 14 | end 15 | 16 | def show_only_errors 17 | @errors_only = true 18 | end 19 | 20 | # Build a list of events that have occurred since the last call to this 21 | # method. 22 | # 23 | # @return [Array] 24 | def latest_events 25 | events = get_stack_events.select do |event| 26 | event.timestamp > @last_time 27 | end 28 | 29 | @last_time = Time.now 30 | 31 | filter_events(events.sort_by(&:timestamp)) 32 | end 33 | 34 | def filter_events(events) 35 | if @errors_only 36 | events.select do |event| 37 | %w[CREATE_FAILED UPDATE_FAILED DELETE_FAILED].include?(event.resource_status) 38 | end 39 | else 40 | events 41 | end 42 | end 43 | 44 | def get_stack_events(token = nil) 45 | opts = { 46 | stack_name: @stack_name 47 | } 48 | 49 | opts[:next_token] = token if token 50 | 51 | result = @cf_client.describe_stack_events(**opts) 52 | events = result.stack_events 53 | events += get_stack_events(result.next_token) if result.next_token 54 | 55 | events 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/puppet/aws-codedeploy/README.md: -------------------------------------------------------------------------------- 1 | AWS CodeDeploy Puppet Module 2 | ============================ 3 | 4 | #### Table of Contents 5 | 6 | 1. [Overview](#overview) 7 | 2. [Module Description - What the module does and why it is useful](#module-description) 8 | 3. [Usage - Configuration options and additional functionality](#usage) 9 | 4. [Reference - An under-the-hood peek at what the module is doing and how](#reference) 10 | 5. [Limitations - OS compatibility, etc.](#limitations) 11 | 6. [Development - Guide for contributing to the module](#development) 12 | 13 | ## Overview 14 | 15 | This module installs the AWS CodeDeploy Agent. 16 | 17 | ## Module Description 18 | 19 | The AWS CodeDeploy agent will copy packages to an EC2 instance from a configured S3 bucket. This 20 | module should be used on an EC2 instance that already has Puppet installed. At this time, the only 21 | supported operating systems are RedHat variants and Amazon Linux, through the installation of an RPM 22 | package. 23 | 24 | ## Usage 25 | 26 | Put the classes, types, and resources for customizing, configuring, and doing the fancy stuff with 27 | your module here. 28 | 29 | 30 | ```puppet 31 | codedeploy {} 32 | ``` 33 | 34 | ##Reference 35 | 36 | ###Class: codedeploy 37 | 38 | ####Parameters 39 | 40 | * **package_source** - The url to install the CodeDeploy package from. Defaults to 41 | `https://s3.amazonaws.com/aws-codedeploy-us-east-1/latest/codedeploy-agent.noarch.rpm` 42 | 43 | ##Limitations 44 | 45 | * This module requires a RedHat or Amazon Linux based operating system that is capable of 46 | installing RPM packages. 47 | 48 | ##Development 49 | 50 | Please see our [GitHub repositories](https://github.com/awslabs) 51 | -------------------------------------------------------------------------------- /docs/mechanisms/build.md: -------------------------------------------------------------------------------- 1 | # BuildMechanism 2 | 3 | ## Script 4 | 5 | The Script BuildMechanism will execute a local shell script, with certain 6 | expectations. The script will run with some environment variables: 7 | 8 | - `VERSION`: The named version string passed to `build-version`. 9 | - `OUTPUT_FILE`: The file that the script is expected to produce. 10 | 11 | If the file is not created by the build script, deployment will 12 | fail. Otherwise, the output file will be uploaded using the 13 | ArtifactRepository. 14 | 15 | Sample Usage 16 | ```ruby 17 | Moonshot.config do |c| 18 | c.build_mechanism = Script.new('bin/build.sh') 19 | ... 20 | ``` 21 | 22 | ## GithubRelease 23 | 24 | A build mechanism that creates a tag and GitHub release. Could be used 25 | to delegate other building steps after GitHub release is created. 26 | 27 | Sample Usage 28 | 29 | ```ruby 30 | Moonshot.config do |c| 31 | c.build_mechanism = GithubRelease.new 32 | ... 33 | ``` 34 | 35 | **skip_ci_status** is an optional flag. It would allow us to skip checks 36 | on the commit's CI job status. Without this option, the GithubRelease mechanism will wait until the build is finished. 37 | 38 | Sample Usage 39 | 40 | ```ruby 41 | Moonshot.config do |c| 42 | c.build_mechanism = GithubRelease.new 43 | ... 44 | ``` 45 | Also a command-line option is available to override this value. 46 | 47 | Usage: moonshot build VERSION 48 | -v, --[no-]verbose Show debug logging 49 | -s, --skip-ci-status Skip checks on CI jobs 50 | -n, --environment=NAME Which environment to operate on. 51 | --[no-]interactive-logger Enable or disable fancy logging 52 | -F, --foo 53 | 54 | ## Version Proxy 55 | 56 | @Todo Document and clarify the use-case of the Version Proxy. 57 | -------------------------------------------------------------------------------- /moonshot.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'moonshot' 5 | s.version = '3.0.5' 6 | s.licenses = ['Apache-2.0'] 7 | s.summary = 'A library and CLI tool for launching services into AWS' 8 | s.description = 'A library and CLI tool for launching services into AWS.' 9 | s.authors = [ 10 | 'Cloud Engineering ' 11 | ] 12 | s.email = 'engineering@acquia.com' 13 | s.files = Dir['lib/**/*.rb'] + Dir['lib/default/**/*'] + Dir['bin/*'] 14 | s.bindir = 'bin' 15 | s.executables = ['moonshot'] 16 | s.homepage = 'https://github.com/acquia/moonshot' 17 | 18 | s.required_ruby_version = '>= 3.3.4' 19 | 20 | s.add_dependency('aws-sdk-autoscaling', '~> 1.5') 21 | s.add_dependency('aws-sdk-cloudformation', '~> 1.4') 22 | s.add_dependency('aws-sdk-codedeploy', '~> 1.5') 23 | s.add_dependency('aws-sdk-ec2', '~> 1.34') 24 | s.add_dependency('aws-sdk-elasticloadbalancing', '~> 1.3') 25 | s.add_dependency('aws-sdk-iam', '~> 1.4') 26 | s.add_dependency('aws-sdk-s3', '~> 1.12') 27 | 28 | s.add_dependency('activesupport') 29 | s.add_dependency('colorize') 30 | s.add_dependency('faraday', '~> 1.0') 31 | s.add_dependency('faraday-rack', '~> 1.0.0') 32 | s.add_dependency('faraday-retry', '~> 1.0') 33 | s.add_dependency('highline') 34 | s.add_dependency('interactive-logger') 35 | s.add_dependency('pry') 36 | s.add_dependency('require_all') 37 | s.add_dependency('retriable') 38 | s.add_dependency('rexml') 39 | s.add_dependency('rotp') 40 | s.add_dependency('ruby-duration') 41 | s.add_dependency('semantic') 42 | s.add_dependency('thor') 43 | 44 | s.add_development_dependency('fakefs') 45 | s.add_development_dependency('rspec') 46 | s.add_development_dependency('simplecov') 47 | end 48 | -------------------------------------------------------------------------------- /spec/moonshot/plugins_spec.rb: -------------------------------------------------------------------------------- 1 | class MockPlugin 2 | def pre_create 3 | end 4 | 5 | def post_create 6 | end 7 | end 8 | 9 | describe 'Plugins support' do 10 | let(:plugin1) { MockPlugin.new } 11 | let(:plugin2) { MockPlugin.new } 12 | 13 | let(:stack) { instance_double('Moonshot::Stack') } 14 | 15 | subject do 16 | config = Moonshot::ControllerConfig.new 17 | config.app_name = 'my-app' 18 | config.plugins = [plugin1, plugin2] 19 | 20 | Moonshot::Controller.new(config) 21 | end 22 | 23 | before(:each) do 24 | expect(Moonshot::Stack).to receive(:new).and_return(stack) 25 | template = Moonshot::YamlStackTemplate.new(fixture_path('empty1.yml')) 26 | allow(stack).to receive(:template).and_return(template) 27 | allow(stack).to receive(:parameters).and_return({}) 28 | end 29 | 30 | it 'calls defined methods on plugins in order, providing them with a Moonshot::Resources' do 31 | expect(plugin1).to receive(:pre_create).with(an_instance_of(Moonshot::Resources)).ordered 32 | expect(plugin2).to receive(:pre_create).with(an_instance_of(Moonshot::Resources)).ordered 33 | expect(stack).to receive(:create).ordered.and_return(true) 34 | expect(plugin1).to receive(:post_create).with(an_instance_of(Moonshot::Resources)).ordered 35 | expect(plugin2).to receive(:post_create).with(an_instance_of(Moonshot::Resources)).ordered 36 | 37 | subject.create 38 | end 39 | 40 | it "doesn't call an undefined method" do 41 | expect(stack).to receive(:delete).and_return(true) 42 | 43 | # The assertion here is that calling MockPlugin#pre_delete would cause an 44 | # exception. Using an expect().not_to receive() changes the behavior of 45 | # #respond_to?, so we can't write that expectation. 46 | expect { subject.delete } 47 | .not_to raise_error 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/moonshot/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'optparse' 4 | 5 | module Moonshot 6 | # A Command that is automatically registered with the Moonshot::CommandLine 7 | class Command 8 | module ClassMethods 9 | # TODO: Can we auto-generate usage for commands with no positional arguments, at least? 10 | attr_accessor :usage, :description, :only_in_account 11 | end 12 | 13 | def self.inherited(base) 14 | Moonshot::CommandLine.register(base) 15 | base.extend(ClassMethods) 16 | end 17 | 18 | def parser 19 | @use_interactive_logger = true 20 | 21 | OptionParser.new do |o| 22 | o.banner = "Usage: moonshot #{self.class.usage}" 23 | 24 | o.on('-v', '--[no-]verbose', 'Show debug logging') do |v| 25 | Moonshot.config.interactive_logger = InteractiveLogger.new(debug: true) if v 26 | end 27 | 28 | o.on('-nNAME', '--environment=NAME', 'Which environment to operate on.') do |v| 29 | Moonshot.config.environment_name = v 30 | end 31 | 32 | o.on('--[no-]interactive-logger', TrueClass, 'Enable or disable fancy logging') do |v| 33 | @use_interactive_logger = v 34 | end 35 | end 36 | end 37 | 38 | private 39 | 40 | # Build a Moonshot::Controller from the CLI options. 41 | def controller 42 | config = Moonshot.config 43 | config.update_for_account! 44 | controller = Moonshot::Controller.new(config) 45 | 46 | # Degrade to a more compatible logger if the terminal seems outdated, 47 | # or at the users request. 48 | if !$stdout.isatty || !@use_interactive_logger 49 | log = Logger.new($stdout) 50 | controller.config.interactive_logger = InteractiveLoggerProxy.new(log) 51 | end 52 | 53 | controller 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/moonshot/commands/create_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Commands::Create do 2 | before(:each) do 3 | Moonshot.config = Moonshot::ControllerConfig.new 4 | end 5 | 6 | tests = { 7 | %w(-P Key=Value -P OtherKey=OtherValue) => { 'Key' => 'Value', 'OtherKey' => 'OtherValue' }, 8 | %w(-PKey=ValueWith=Equals) => { 'Key' => 'ValueWith=Equals' } 9 | } 10 | 11 | tests.each do |input, expected| 12 | it "Should process #{input} correctly" do 13 | op = subject.parser 14 | op.parse(input) 15 | expect(Moonshot.config.parameter_overrides).to match(expected) 16 | end 17 | end 18 | 19 | extra_tags = { 20 | %w(-T Key=Value -T OtherKey=OtherValue) => 21 | [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }], 22 | %w(-TKey=ValueWith=Equals) => [{ key: 'Key', value: 'ValueWith=Equals' }], 23 | %w(--tag Key=Value --tag OtherKey=OtherValue) => 24 | [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }] 25 | } 26 | 27 | extra_tags.each do |input, expected| 28 | it "Should process #{input} correctly" do 29 | op = subject.parser 30 | op.parse(input) 31 | expect(Moonshot.config.extra_tags).to match(expected) 32 | end 33 | end 34 | 35 | it 'should handle version and deploy correctly' do 36 | op = subject.parser 37 | op.parse(%w(--version 1.2.3 --no-deploy -P Key=Value)) 38 | expect(subject.version).to eq('1.2.3') 39 | expect(subject.deploy).to eq(false) 40 | expect(Moonshot.config.parameter_overrides).to match('Key' => 'Value') 41 | end 42 | 43 | it 'should process multiple parent stacks' do 44 | op = subject.parser 45 | op.parse(%w(--parents parent1,parent2,parent3 --no-deploy)) 46 | expect(subject.deploy).to eq(false) 47 | expect(Moonshot.config.parent_stacks.count).to be(3) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/moonshot/unicode_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'colorize' 4 | 5 | module Moonshot 6 | # A class for drawing hierarchical information using unicode lines. 7 | class UnicodeTable 8 | def initialize(name) 9 | @name = name 10 | @lines = [] 11 | @children = [] 12 | end 13 | 14 | def add_leaf(name) 15 | new_leaf = UnicodeTable.new(name) 16 | @children << new_leaf 17 | new_leaf 18 | end 19 | 20 | def add_line(line) 21 | @lines << line 22 | self 23 | end 24 | 25 | def add_table(table) 26 | # Calculate widths 27 | widths = [] 28 | table.each do |line| 29 | line.each_with_index do |col, i| 30 | col = '?' unless col.respond_to?(:length) 31 | widths[i] = [widths[i] || 0, col.length].max 32 | end 33 | end 34 | 35 | format = widths.collect { |n| "%-#{n}s" }.join(' ') << "\n" 36 | table.each { |line| add_line(format(format, *line)) } 37 | end 38 | 39 | def draw(depth = 1, first = true) 40 | space = ' ' 41 | pipe = '|' 42 | print first ? '┌' : '├' 43 | print '─' * depth 44 | puts "#{space}" << @name.light_black # rubocop:disable Style/RedundantInterpolation 45 | @lines = [''] + @lines + [''] 46 | @lines.each do |line| 47 | puts "#{pipe}" << (' ' * depth) << line # rubocop:disable Style/RedundantInterpolation 48 | end 49 | @children.each do |child| 50 | child.draw(depth + 1, false) 51 | end 52 | end 53 | 54 | # Draw all children at the same level, for having multiple top-level 55 | # peer leaves. 56 | def draw_children 57 | first = true 58 | @children.each do |child| 59 | child.draw(1, first) 60 | first = false 61 | end 62 | puts '└──' 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/plugins/code_deploy_setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Plugins 5 | # Plugin to ensure CodeDeploy has all necessary S3 buckets created. 6 | # Defaults to using ENV['AWS_REGION'] as default region if not configured. 7 | class CodeDeploySetup 8 | include Moonshot::CredsHelper 9 | 10 | attr_reader :name, :prefix, :regions 11 | 12 | def initialize(name, prefix: '', regions: [ENV['AWS_REGION']]) 13 | @regions = regions.reject(&:nil?) 14 | if @regions.empty? 15 | raise ArgumentError, 'CodeDeploySetup requires at least one region.' \ 16 | ' Set regions argument or ENV[\'AWS_REGION\'].' 17 | end 18 | 19 | @name = name 20 | @prefix = prefix 21 | end 22 | 23 | def bucket_name(region = ENV['AWS_REGION']) 24 | if ENV.key?('S3_BUCKET') 25 | ENV['S3_BUCKET'] 26 | else 27 | "#{@name}-#{region}" 28 | end 29 | end 30 | 31 | def bucket_prefix 32 | @prefix.empty? ? '' : "#{@prefix}/" 33 | end 34 | 35 | # Create an S3 bucket in each supported region for CodeDeploy 36 | def setup_code_deploy_s3_buckets 37 | @regions.uniq.each do |region| 38 | client = s3_client(region:) 39 | name = bucket_name(region) 40 | bucket = Aws::S3::Bucket.new( 41 | name, 42 | client: 43 | ) 44 | bucket.create unless bucket.exists? 45 | end 46 | end 47 | 48 | # Hook entry points to ensure S3 Buckets are available for CodeDeploy 49 | def run_hook(_resources) 50 | setup_code_deploy_s3_buckets 51 | end 52 | 53 | # Moonshot hooks to trigger this plugin 54 | alias pre_create run_hook 55 | alias pre_deploy run_hook 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/conf-mgmt/chef/aws-codedeploy-agent/README.md: -------------------------------------------------------------------------------- 1 | Installing the AWS CodeDeploy Agent with Chef 2 | ============================================= 3 | 4 | In the previous post, we learned how to use the power of AWS CodeDeploy to orchestrate chef-solo. It took 5 | the perspective of having half of your dependencies already installed – namely, the CodeDeploy agent. For 6 | this post, we'll look at it from a different angle: the CodeDeploy host agent isn't installed yet, but you 7 | have a pre-existing Chef environment running on Amazon EC2 instances. 8 | 9 | Setup and Preconditions 10 | ----------------------- 11 | 12 | The post below makes a few assumptions about your environment that may or may not be true. First and 13 | foremost is that you have a working Chef environment. We'll assume that you've worked through your 14 | own workflow for managing that environment and your chef-repo. If you are still new to Chef, their 15 | documentation has a lot of very helpful information: [http://docs.getchef.com](http://docs.getchef.com) 16 | 17 | AWS CodeDeploy Host Agent Cookbook 18 | ---------------------------------- 19 | 20 | We've built a custom Chef cookbook to help ease the process of installing the CodeDeploy agent. You 21 | can download that cookbook 22 | [here](https://github.com/awslabs/aws-codedeploy-samples/tree/master/conf-mgmt/chef/aws-codedeploy-agent/cookbooks/codedeploy-agent/recipes). 23 | To install the CodeDeploy agent, simply download the linked archive, copy the codedeploy-agent 24 | directory into your chef-repo, and add `recipe[codedeploy-agent]` to your run list. If you just want 25 | to test this out with a chef-solo instance, we've included a sample configuration for you. 26 | 27 | The cookbook has three simple steps: 28 | 29 | 1. Download the package for the CodeDeploy host agent. 30 | 1. Install the agent. 31 | 1. Start the agent. 32 | -------------------------------------------------------------------------------- /lib/moonshot/build_mechanism/version_proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'semantic' 5 | 6 | # This proxies build request do different mechanisms. One for semver compliant 7 | # releases and another for everything else. 8 | class Moonshot::BuildMechanism::VersionProxy 9 | extend Forwardable 10 | include Moonshot::ResourcesHelper 11 | 12 | def_delegator :@active, :output_file 13 | 14 | def initialize(release:, dev:) 15 | @release = release 16 | @dev = dev 17 | end 18 | 19 | def doctor_hook 20 | @release.doctor_hook 21 | @dev.doctor_hook 22 | end 23 | 24 | def resources=(r) 25 | super 26 | @release.resources = r 27 | @dev.resources = r 28 | end 29 | 30 | def pre_build_hook(version) 31 | active(version).pre_build_hook(version) 32 | end 33 | 34 | def build_hook(version) 35 | active(version).build_hook(version) 36 | end 37 | 38 | def post_build_hook(version) 39 | active(version).post_build_hook(version) 40 | end 41 | 42 | def build_cli_hook(parser) 43 | # Expose any command line arguments provided by the build mechanisms. We 44 | # don't know the version at this point, so we can't call the hook on only 45 | # the one we're going to use, which may result in options being exposed that 46 | # are only applicable for one of the build mechanisms. 47 | parser = @release.build_cli_hook(parser) if @release.respond_to?(:build_cli_hook) 48 | parser = @dev.build_cli_hook(parser) if @dev.respond_to?(:build_cli_hook) 49 | 50 | parser 51 | end 52 | 53 | private 54 | 55 | def active(version) 56 | @active = if release?(version) 57 | @release 58 | else 59 | @dev 60 | end 61 | end 62 | 63 | def release?(version) 64 | ::Semantic::Version.new(version) 65 | rescue ArgumentError 66 | false 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /docs/user-guide/include_in_your_project.md: -------------------------------------------------------------------------------- 1 | # Including Moonshot-based tooling in your project 2 | 3 | ## Create a Moonfile.rb 4 | 5 | (TODO: Implement `moonshot init`) 6 | 7 | In the root of your project, create a file called `Moonfile.rb` with 8 | the following contents: 9 | 10 | ```ruby 11 | Moonshot.config do |c| 12 | c.application_name = 'my-service' 13 | c.artifact_repository = S3Bucket.new('my-service-builds') 14 | c.build_mechanism = Script.new('build/script.sh') 15 | c.deployment_mechanism = CodeDeploy.new(asg: 'AutoScalingGroup') 16 | end 17 | ``` 18 | 19 | This example assumes: 20 | - You have a CloudFormation JSON template in folder called "moonshot/template.json". 21 | - You have an S3 bucket called "my-service-builds". 22 | - You have a script in "script/build.sh" that will build a tarball output.tar.gz. 23 | - You have a working CodeDeploy setup, including the CodeDeployRole. 24 | 25 | If all that is true, you can now deploy your software to a new stack with: 26 | ``` 27 | $ moonshot create 28 | ``` 29 | 30 | By default, you'll get a development environment named `my-service-dev-giraffe`, 31 | where `giraffe` is your username. If you want to provision test or production 32 | named environment, use: 33 | ``` 34 | $ moonshot create -n my-service-staging 35 | $ moonshot create -n my-service-production 36 | ``` 37 | 38 | By default, create launches the stack and deploys code. If you want to only 39 | create the stack and not deploy code, use: 40 | ``` 41 | $ moonshot create --no-deploy 42 | ``` 43 | 44 | If you make changes to your application and want to release a development build 45 | to your stack, run: 46 | ``` 47 | $ moonshot push 48 | ``` 49 | 50 | To build a "named build" for releasing through test and production environments, 51 | use: 52 | ``` 53 | $ moonshot build v0.1.0 54 | $ moonshot deploy v0.1.0 -n 55 | ``` 56 | 57 | We recommend using a CI system like Jenkins to perform those activities, for 58 | consistency. 59 | -------------------------------------------------------------------------------- /lib/moonshot/parent_stack_parameter_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class ParentStackParameterLoader 5 | def initialize(config) 6 | @config = config 7 | end 8 | 9 | def load! 10 | @config.parent_stacks.each do |stack_name| 11 | count = 0 12 | 13 | resp = cf_client.describe_stacks(stack_name:) 14 | raise "Parent Stack #{stack_name} not found!" unless resp.stacks.size == 1 15 | 16 | # If there is an input parameters matching a stack output, pass it. 17 | resp.stacks[0].outputs.each do |output| 18 | next unless @config.parameters.key?(output.output_key) 19 | 20 | # Our Stack has a Parameter matching this output. Set it's 21 | # value to the Output's value. 22 | count += 1 23 | @config.parameters.fetch(output.output_key).set(output.output_value) 24 | end 25 | 26 | puts "Imported #{count} parameters from parent stack #{stack_name.blue}!" if count.positive? 27 | end 28 | end 29 | 30 | def load_missing_only! 31 | @config.parent_stacks.each do |stack_name| 32 | resp = cf_client.describe_stacks(stack_name:) 33 | raise "Parent Stack #{stack_name} not found!" unless resp.stacks.size == 1 34 | 35 | # If there is an input parameters matching a stack output, pass it. 36 | resp.stacks[0].outputs.each do |output| 37 | next unless @config.parameters.key?(output.output_key) 38 | 39 | # Our Stack has a Parameter matching this output. Set it's 40 | # value to the Output's value, but only if we don't already 41 | # have a previous value we're using. 42 | unless @config.parameters.fetch(output.output_key).use_previous? 43 | @config.parameters.fetch(output.output_key).set(output.output_value) 44 | end 45 | end 46 | end 47 | end 48 | 49 | private 50 | 51 | def cf_client 52 | Aws::CloudFormation::Client.new 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/moonshot/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Moonshot SSH features' do 2 | subject do 3 | c = Moonshot::ControllerConfig.new 4 | c.app_name = 'MyApp' 5 | c.environment_name = 'prod' 6 | c.ssh_config.ssh_user = 'joeuser' 7 | c.ssh_config.ssh_identity_file = '/Users/joeuser/.ssh/thegoods.key' 8 | c.ssh_command = 'cat /etc/passwd' 9 | 10 | Moonshot::Controller.new(c) 11 | end 12 | 13 | describe 'Moonshot::Controller#ssh' do 14 | context 'normally' do 15 | it 'should execute an ssh command with proper parameters' do 16 | ts = instance_double(Moonshot::SSHTargetSelector) 17 | expect(Moonshot::SSHTargetSelector).to receive(:new).and_return(ts) 18 | expect(ts).to receive(:choose!).and_return('i-04683a82f2dddcc04') 19 | 20 | expect_any_instance_of(Moonshot::SSHCommandBuilder).to receive(:instance_ip).exactly(2) 21 | .and_return('123.123.123.123') 22 | expect(subject).to receive(:exec) 23 | .with('ssh -t -i /Users/joeuser/.ssh/thegoods.key -l joeuser 123.123.123.123 cat\ /etc/passwd') # rubocop:disable LineLength 24 | expect { subject.ssh } 25 | .to output("Opening SSH connection to i-04683a82f2dddcc04 (123.123.123.123)...\n") 26 | .to_stderr 27 | end 28 | end 29 | 30 | context 'when an instance id is given' do 31 | subject do 32 | c = super() 33 | c.config.ssh_instance = 'i-012012012012012' 34 | c 35 | end 36 | 37 | it 'should execute an ssh command with proper parameters' do 38 | expect_any_instance_of(Moonshot::SSHCommandBuilder).to receive(:instance_ip).exactly(2) 39 | .and_return('123.123.123.123') 40 | expect(subject).to receive(:exec) 41 | .with('ssh -t -i /Users/joeuser/.ssh/thegoods.key -l joeuser 123.123.123.123 cat\ /etc/passwd') # rubocop:disable LineLength 42 | expect { subject.ssh } 43 | .to output("Opening SSH connection to i-012012012012012 (123.123.123.123)...\n").to_stderr 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/moonshot/stub_rollout_asg.rb: -------------------------------------------------------------------------------- 1 | class StubRolloutASG 2 | InstanceSpec = Struct.new(:id, :conforming) 3 | 4 | attr_accessor :max, :desired 5 | attr_reader :instances 6 | 7 | def initialize 8 | @max = 3 9 | @desired = 2 10 | @instances = [] 11 | @replacement_instances = [] 12 | @instance_healths = {} 13 | end 14 | 15 | # Test config methods 16 | def add_instances(*instances) 17 | instances.each { |i| @instances << InstanceSpec.new(*i) } 18 | end 19 | 20 | def add_replacement_instances(*instances) 21 | instances.each { |i| @replacement_instances << InstanceSpec.new(*i) } 22 | end 23 | 24 | def add_health_response(id, *healths) 25 | @instance_healths[id] ||= [] 26 | 27 | healths.each do |h| 28 | @instance_healths[id] << Moonshot::Tools::ASGRollout::InstanceHealth.new(*h) 29 | end 30 | end 31 | 32 | def everything_used? 33 | # All replacement instances were returned. 34 | @replacement_instances.empty? && 35 | # And all health queries were returned. 36 | @instance_healths.all? { |_, v| v.empty? } 37 | end 38 | 39 | # Implementation methods 40 | def current_max_and_desired 41 | [@max, @desired] 42 | end 43 | 44 | def set_max_and_desired(max, desired) 45 | @max = max 46 | @desired = desired 47 | end 48 | 49 | def wait_for_new_instance 50 | new_instance = @replacement_instances.shift 51 | 52 | unless new_instance 53 | raise 'No more instances with call to #wait_for_new_instance!' 54 | end 55 | 56 | @instances << new_instance 57 | new_instance.id 58 | end 59 | 60 | def instance_health(id) 61 | next_health = @instance_healths.fetch(id, []).shift 62 | 63 | raise "No InstanceHealth responses set for #{id}!" unless next_health 64 | 65 | next_health 66 | end 67 | 68 | def non_conforming_instances 69 | @instances.select { |i| !i.conforming }.map(&:id) 70 | end 71 | 72 | def detach_instance(id, decrement:) 73 | @desired -= 1 if decrement 74 | @instances.delete_if { |i| i.id == id } 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /docs/about/contribute.md: -------------------------------------------------------------------------------- 1 | # Contributing to Moonshot 2 | An introduction to contributing to the Moonshot project. 3 | 4 | The Moonshot project welcomes, and depends, on contributions from 5 | developers and users in the open source community. Contributions can 6 | be made in a number of ways, a few examples are: 7 | 8 | - Code patches via pull requests 9 | - Documentation improvements 10 | - Bug reports and patch reviews 11 | - Reporting an Issue 12 | 13 | ## How to Contribute 14 | 15 | So, you want to help? Awesome! In order to guide this the best way we 16 | can and to make sure we can help you in either getting a bug fixed or 17 | improve our documentation. we are asking you to please include as much 18 | detail as you can. Let us know your platform and Moonshot version. If 19 | the problem is visual please add a screenshot and if you get an error 20 | please include the full error and traceback. 21 | 22 | ## Submitting Pull Requests 23 | 24 | Once you are happy with your changes or you are ready for some 25 | feedback, push it to your fork and send a pull request. For a change 26 | to be accepted it will most likely need to have tests and 27 | documentation if it is a new feature. 28 | 29 | ## Expectations 30 | Even though this is an Open project, it does not mean that we have 31 | 24/7 support for it. The best way to make sure that we accept your 32 | bugfix or add in a feature you want is still to make a pull request 33 | and link it in the issue. Include some reasons how to reproduce or 34 | even better, make sure the bug is tested and passes highy increases 35 | your chances for the change to get committed. 36 | 37 | So, you've done all that? It very likely that it could get committed 38 | really quickly but since some of the contributers to this project 39 | could be on a holiday or we are in no way responsible in actually 40 | merging it in soon. But have no fear! Since you filed a pull request 41 | from your fork, you are able to use a version of moonshot 42 | yourselves. And we promise you, we will do our utter best. 43 | 44 | And again, thanks! We're looking forward working with you. 45 | -------------------------------------------------------------------------------- /spec/moonshot/plugins/code_deploy_setup_spec.rb: -------------------------------------------------------------------------------- 1 | describe Moonshot::Plugins::CodeDeploySetup do 2 | subject { Moonshot::Plugins::CodeDeploySetup } 3 | 4 | describe '#new' do 5 | it 'should raise ArgumentError if no region arg is set' do 6 | allow(ENV).to receive(:[]).with("AWS_REGION").and_return(nil) 7 | expect { subject.new('example') }.to raise_error(ArgumentError) 8 | end 9 | 10 | it 'should use the ENV[AWS_REGION] if no region arg is set' do 11 | allow(ENV).to receive(:[]).with("AWS_REGION").and_return('us-west-2') 12 | expect(subject.new('example').regions).to include('us-west-2') 13 | end 14 | 15 | it 'should use return the correct bucket name for the current active region' do 16 | allow(ENV).to receive(:[]).with("AWS_REGION").and_return('ap-southeast-1') 17 | expect(subject.new('example-builds').bucket_name).to eq('example-builds-ap-southeast-1') 18 | end 19 | 20 | it 'should use return the correct bucket prefix' do 21 | allow(ENV).to receive(:[]).with("AWS_REGION").and_return('us-east-1') 22 | expect(subject.new('example-builds').bucket_prefix).to eq('') 23 | expect(subject.new('example-builds', prefix: 'api').bucket_prefix).to eq('api/') 24 | end 25 | end 26 | 27 | describe '#run_hook' do 28 | let(:s3_client) { instance_double(Aws::S3::Client) } 29 | let(:s3_bucket) { instance_double(Aws::S3::Bucket) } 30 | 31 | before(:each) do 32 | allow(Aws::S3::Client).to receive(:new).and_return(s3_client) 33 | allow(Aws::S3::Bucket).to receive(:new).and_return(s3_bucket) 34 | allow(s3_bucket).to receive(:exists?).and_return(false) 35 | allow(s3_bucket).to receive(:create).and_return(true) 36 | end 37 | 38 | it 'creates a bucket for all set regions' do 39 | allow(ENV).to receive(:[]).with("AWS_REGION").and_return('us-east-1') 40 | plugin = subject.new('example') 41 | expect(plugin.regions).to include('us-east-1') 42 | expect(s3_bucket).to receive(:exists?) 43 | expect(s3_bucket).to receive(:create) 44 | 45 | plugin.pre_create(nil) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /docs/plugins/code_deploy_setup.md: -------------------------------------------------------------------------------- 1 | # Moonshot CodeDeploy Setup plugin 2 | Moonshot plugin to ensure CodeDeploy has all the necessary S3 buckets created 3 | to support deploying to multiple configured regions. 4 | 5 | ## Moonshot configuration 6 | Update your Moonfile.rb with the following configuration 7 | 8 | ```ruby 9 | Moonshot.config do |c| 10 | # ... 11 | c.app_name = 'example-project' 12 | 13 | bucket_name = "#{c.app_name}-builds" 14 | 15 | # bucket_name: Base name of the S3 bucket to create, will have region appended to it. 16 | # prefix: Optional. If using the same bucket for multiple apps then recommend adding a prefix. 17 | # regions: list of all supported regions you would like to use. Default: ENV['AWS_REGION']. 18 | code_deploy_setup_plugin = Moonshot::Plugins::CodeDeploySetup.new( 19 | bucket_name, 20 | prefix: c.app_name, 21 | regions: [ 'us-east-1', 'us-west-2' ] 22 | c.plugins << code_deploy_setup_plugin 23 | 24 | c.deployment_mechanism = CodeDeploy.new 25 | c.artifact_repository = S3BucketViaGithubReleases.new( 26 | code_deploy_setup_plugin.bucket_name, 27 | prefix: code_deploy_setup_plugin.bucket_prefix 28 | ) 29 | ``` 30 | 31 | ## CloudFormation Template additions 32 | To use the auto-created S3 Buckets in your Cloudformation add the following Parameter and 33 | IAM Role Policy to your template to allow CodeDeploy running on instances access to your 34 | newly created S3 buckets. 35 | 36 | Parameters 37 | ``` 38 | "CodeDeployBucketName": { 39 | "Type": "String", 40 | "Description": "Base name of the S3 Bucket used for CodeDeploy", 41 | "Default": "example-project-builds" 42 | } 43 | ``` 44 | 45 | Instance Role Policy: 46 | ``` 47 | { 48 | "PolicyName": "CodeDeployBuildAccess", 49 | "PolicyDocument": { 50 | "Version": "2012-10-17", 51 | "Statement": [{ 52 | "Effect": "Allow", 53 | "Action": [ 54 | "s3:GetObject" 55 | ], 56 | "Resource": {"Fn::Join": ["", ["arn:aws:s3:::" {"Ref": "CodeDeployBucketName"}, "-", { "Ref": "AWS::Region" } , "/*"]]} 57 | }] 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/version-control/git/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). 6 | # You may not use this file except in compliance with the License. 7 | # A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed 12 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | # express or implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | 16 | AWS="/usr/local/bin/aws" 17 | 18 | APPLICATION_NAME=$(git config --get aws-codedeploy.application-name) 19 | DEPLOYMENT_GROUP=$(git config --get aws-codedeploy.deployment-group) 20 | BUCKET_NAME=$(git config --get aws-codedeploy.s3bucket) 21 | BUNDLE_NAME=$(echo $(basename `pwd`).zip) 22 | 23 | # `aws deploy push` will overwrite the bundle in S3. If you'd rather ensure that you have a unique 24 | # object for each push, you could, for example, append the commit hash: 25 | #BUNDLE_NAME=$(echo $(basename `pwd`)-$(git log -n 1 --format=%H).zip) 26 | 27 | # Call `aws deploy push` to create a new revision of the current repo 28 | echo "Pushing $BUNDLE_NAME to s3://${BUCKET_NAME} and registering with application '${APPLICATION_NAME}'" 1>&2 29 | $AWS deploy push \ 30 | --application-name ${APPLICATION_NAME} \ 31 | --s3-location s3://${BUCKET_NAME}/${BUNDLE_NAME} \ 32 | --ignore-hidden-files \ 33 | --source . 34 | 35 | revision_json="{\"revisionType\":\"S3\",\"s3Location\":{\"bucket\":\"${BUCKET_NAME}\",\"bundleType\":\"zip\",\"key\":\"${BUNDLE_NAME}\"}}" 36 | 37 | if [ $? != 0 ]; then 38 | echo "Push to codedeploy failed; skipping create-deployment" 1>&2 39 | else 40 | echo "Deploying s3://${BUCKET_NAME}/${BUNDLE_NAME} to application ${APPLICATION_NAME} and deployment group ${DEPLOYMENT_GROUP}" 1>&2 41 | $AWS deploy create-deployment \ 42 | --application-name ${APPLICATION_NAME} \ 43 | --deployment-group-name ${DEPLOYMENT_GROUP} \ 44 | --revision $revision_json 45 | fi 46 | -------------------------------------------------------------------------------- /docs/mechanisms/deployment.md: -------------------------------------------------------------------------------- 1 | # DeploymentMechanism 2 | 3 | ## CodeDeploy 4 | 5 | The CodeDeploy DeploymentMechanism will create a CodeDeploy 6 | Application and Deployment Group matching the application name. The 7 | created Deployment Group will point at the logical resource id 8 | provided to the constructor (e.g. `CodeDeploy.new(asg: 9 | 'MyAutoScalingGroup')`). During the `deploy-code` action, the 10 | ArtifactRepository is checked for compatibility with 11 | CodeDeploy. Currently only the S3Bucket is supported, though 12 | CodeDeploy itself supports deploying from a git source. 13 | 14 | Assumptions made by the CodeDeploy mechanism: 15 | 16 | - You are using an S3Bucket ArtifactRepository. 17 | - Your build artifact contains an appspec.yml file. 18 | 19 | Sample Usage 20 | ```ruby 21 | Moonshot.config do |c| 22 | c.deployment_mechanism = CodeDeploy.new( 23 | asg: 'AutoScalingGroup', 24 | role: 'CodeDeployRole', 25 | app_name: 'my_app_name', 26 | config_name: 'CodeDeployDefault.OneAtATime') 27 | ... 28 | ``` 29 | Parameters 30 | 31 | ### asg | string,array 32 | 33 | The logical name of one or more Auto Scaling Groups to create and 34 | manage a Deployment Group for in CodeDeploy. 35 | 36 | ### optional_asg | string,array 37 | 38 | The logical name of one or more Auto Scaling Groups to add to the 39 | Deployment Group in CodeDeploy. These ASGs don't have to exist. 40 | If they do, they will be added to the Deployment Group. 41 | 42 | ### role | string 43 | 44 | IAM role with AWSCodeDeployRole policy. CodeDeployRole is considered 45 | as default role if its not specified. 46 | 47 | ### app_name | string,nil 48 | 49 | The name of the CodeDeploy Application and Deployment Group. By 50 | default, this is the same as the stack name, and probably what you 51 | want. If you have multiple deployments in a single Stack, they must 52 | have unique names. 53 | 54 | ### config_name | string 55 | 56 | The name of the Deplloyment 57 | Configuration. CodeDeployDefault.OneAtATime is the default if its not 58 | specified. 59 | 60 | For more information about CodeDeploy, see the [AWS Documentation][1]. 61 | 62 | [1]: http://docs.aws.amazon.com/codedeploy/latest/userguide/welcome.html 63 | -------------------------------------------------------------------------------- /spec/moonshot/shell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module Moonshot 4 | describe Shell do 5 | include ResourcesHelper 6 | include described_class 7 | 8 | let(:resources) do 9 | Resources.new( 10 | ilog: InteractiveLoggerProxy.new(log), 11 | stack: double(Stack).as_null_object, 12 | controller: instance_double(Moonshot::Controller).as_null_object 13 | ) 14 | end 15 | 16 | before { self.resources = resources } 17 | 18 | describe '#shell' do 19 | it 'should return a shell compatible with Thor::Shell::Basic.' do 20 | expect(shell).to be_a(Thor::Shell::Basic) 21 | end 22 | end 23 | 24 | describe '#sh_out' do 25 | it 'should raise on non-zero exit.' do 26 | expect { sh_out('false') }.to raise_error(/`false` exited 1/) 27 | end 28 | 29 | it 'should not raise if fail is disabled.' do 30 | sh_out('false', false) 31 | end 32 | end 33 | 34 | describe '#sh_step' do 35 | before do 36 | expect(InteractiveLoggerProxy::Step).to receive(:new).and_return(step) 37 | end 38 | 39 | let(:step) { instance_double(InteractiveLoggerProxy::Step) } 40 | 41 | it 'should raise an error if the step fails.' do 42 | expect { sh_step('false') }.to raise_error(/`false` exited 1/) 43 | end 44 | 45 | it 'should provide the step and sh output to a block.' do 46 | output = nil 47 | expect(step).to receive(:continue).with('reticulating splines') 48 | expect(step).to receive(:success) 49 | sh_step('echo yo') do |step, out| 50 | step.continue('reticulating splines') 51 | output = out 52 | end 53 | expect(output).to match('yo') 54 | end 55 | 56 | it 'should truncate a long messages.' do 57 | long_s = SecureRandom.urlsafe_base64(terminal_width) 58 | cmd = "echo #{long_s}" 59 | truncated_s = "#{cmd[0..(terminal_width - 22)]}..." 60 | expect(resources.ilog).to receive(:start_threaded).with(truncated_s) 61 | .and_call_original 62 | allow(step).to receive(:success) 63 | sh_step(cmd) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/moonshot/dynamic_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | 5 | module Moonshot 6 | class TemplateExists < StandardError; end 7 | class InvalidTemplate < StandardError; end 8 | 9 | class DynamicTemplate 10 | # A class to encapsulate template parameters, passing a hash to `process` is 11 | # only available from Ruby 2.5. 12 | class Parameters 13 | def initialize(parameters) 14 | parameters.each do |k, v| 15 | instance_variable_set("@#{k}".to_sym, v) 16 | # Adding an attribute reader for flexibility, this way you can add 17 | # either `@parameter` or just `parameter` to your template. 18 | self.class.send(:attr_reader, k.to_sym) 19 | end 20 | end 21 | 22 | def expose_binding 23 | binding 24 | end 25 | end 26 | 27 | attr_writer :destination 28 | 29 | def initialize(source:, parameters:, destination:) 30 | @source = File.read(source) 31 | @parameters = parameters 32 | @destination = destination 33 | end 34 | 35 | def parameters_obj 36 | @parameters_obj ||= Parameters.new(parameters_file) 37 | end 38 | 39 | def parameters_file 40 | env_name = Moonshot.config.environment_name 41 | @parameters.respond_to?(:call) ? @parameters.call(env_name) : @parameters 42 | end 43 | 44 | def process 45 | validate_destination_exists 46 | new_template = generate_template 47 | 48 | validate_template(new_template) 49 | write_output(new_template) 50 | end 51 | 52 | private 53 | 54 | def validate_destination_exists 55 | return unless File.file?(@destination) 56 | 57 | raise TemplateExists, "Output file '#{@destination}' already exists." 58 | end 59 | 60 | def validate_template(template) 61 | Aws::CloudFormation::Client.new.validate_template( 62 | template_body: template 63 | ) 64 | rescue Aws::CloudFormation::Errors::ValidationError => e 65 | raise InvalidTemplate, "Invalid template:\n#{e}" 66 | end 67 | 68 | def generate_template 69 | ERB.new(@source).result(parameters_obj.expose_binding) 70 | end 71 | 72 | def write_output(content) 73 | File.write(@destination, content) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/moonshot/controller_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | # Holds configuration for Moonshot::Controller 5 | class ControllerConfig 6 | attr_reader :account_alias 7 | 8 | attr_accessor :additional_tag, :answer_file, :app_name, :artifact_repository, :build_mechanism, 9 | :changeset_wait_time, :deployment_mechanism, :dev_build_name_proc, :environment_name, 10 | :interactive, :interactive_logger, :parameter_overrides, :parameters, :parent_stacks, 11 | :default_parameter_source, :parameter_sources, :plugins, :project_root, 12 | :show_all_stack_events, :ssh_auto_scaling_group_name, :ssh_command, :ssh_config, 13 | :ssh_instance, :template_file, :template_s3_bucket, :extra_tags 14 | 15 | def initialize 16 | @default_parameter_source = AskUserSource.new 17 | @interactive = true 18 | @interactive_logger = InteractiveLogger.new 19 | @parameter_overrides = {} 20 | @parameter_sources = {} 21 | @parameters = ParameterCollection.new 22 | @parent_stacks = [] 23 | @account_alias = nil 24 | @per_account_config = {} 25 | @plugins = [] 26 | @project_root = Dir.pwd 27 | @show_all_stack_events = false 28 | @ssh_config = SSHConfig.new 29 | @extra_tags = [] 30 | 31 | @dev_build_name_proc = lambda do |c| 32 | ['dev', c.app_name, c.environment_name, Time.now.to_i].join('/') 33 | end 34 | 35 | user = ENV.fetch('USER', 'default-user').gsub(/\W/, '') 36 | @environment_name = "dev-#{user}" 37 | end 38 | 39 | def in_account(name, &blk) 40 | # Store account specific configs as lambdas, to be evaluated 41 | # if the account name matches during controller execution. 42 | @per_account_config[name] = blk 43 | end 44 | 45 | def update_for_account! 46 | # Evaluated any account-specific configuration. 47 | @account_alias = Moonshot::AccountContext.get 48 | return unless @account_alias 49 | return unless @per_account_config.key?(@account_alias) 50 | 51 | @per_account_config[@account_alias].call(self) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'simplecov' 3 | 4 | SimpleCov.start do 5 | add_filter '/spec/' 6 | end 7 | end 8 | 9 | # RSpec brings this in ad-hoc, but if it comes in after fakefs we get 10 | # superclass mismatch errors. 11 | require 'pp' 12 | require 'moonshot' 13 | require 'fakefs/spec_helpers' 14 | 15 | puts "rspec pid: #{Process.pid}" 16 | trap 'USR1' do 17 | threads = Thread.list 18 | 19 | puts 20 | puts '=' * 80 21 | puts "Received USR1 signal; printing all #{threads.count} thread backtraces." 22 | 23 | threads.each do |thr| 24 | description = thr == Thread.main ? 'Main thread' : thr.inspect 25 | puts 26 | puts "#{description} backtrace: " 27 | puts thr.backtrace.join("\n") 28 | end 29 | 30 | puts '=' * 80 31 | end 32 | 33 | class MockInteractiveLogger 34 | attr_reader :final_logs 35 | 36 | def initialize 37 | @final_logs = [] 38 | end 39 | 40 | def start(msg = nil) 41 | @msg = msg 42 | yield self 43 | end 44 | alias start_threaded start 45 | 46 | def continue(msg = nil) 47 | @msg = msg if msg 48 | end 49 | 50 | def success(msg = nil) 51 | @final_logs << [:success, msg || @msg] 52 | end 53 | 54 | def failure(msg = nil) 55 | @final_logs << [:failure, msg || @msg] 56 | end 57 | 58 | def debug(msg) 59 | @final_logs << [:debug, msg] 60 | end 61 | 62 | def info(msg) 63 | @final_logs << [:info, msg] 64 | end 65 | end 66 | 67 | shared_examples 'with a working moonshot application' do 68 | include FakeFS::SpecHelpers 69 | 70 | # Force aws-sdk to load metadata before FakeFS takes over. 71 | before(:all) do 72 | Aws::CloudFormation::Client.new(stub_responses: true) 73 | end 74 | 75 | before(:each) do 76 | FakeFS::FileSystem.clone(File.join(File.dirname(__FILE__), 'fs_fixtures'), '/') 77 | end 78 | end 79 | 80 | shared_examples 'with a CloudFormation stubbed client' do 81 | let(:cf_client_stubs) { {} } 82 | let(:cf_client) { Aws::CloudFormation::Client.new(stub_responses: cf_client_stubs) } 83 | 84 | before(:each) do 85 | allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf_client) 86 | end 87 | end 88 | 89 | def fixture_path(path) 90 | File.join(File.dirname(__FILE__), 'fixtures', path) 91 | end 92 | 93 | def fixture(path) 94 | File.read(fixture_path(path)) 95 | end 96 | -------------------------------------------------------------------------------- /lib/moonshot/command_line_dispatcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class CommandLineDispatcher 5 | def initialize(command, klass, args) 6 | @command = command 7 | @klass = klass 8 | @args = args 9 | end 10 | 11 | def dispatch! 12 | # Look to see if we're allowed only to run in certain accounts, or 13 | # not allowed to run in specific accounts. 14 | check_account_restrictions 15 | 16 | # Allow any mechanisms or plugins to hook into this CLI command. 17 | handler = @klass.new 18 | parser = build_parser(handler) 19 | parser.parse! 20 | 21 | req_arguments = handler.method(:execute).parameters.select { |arg| arg[0] == :req } 22 | if ARGV.size < req_arguments.size 23 | warn handler.parser.help 24 | raise "Invalid command line for '#{@command}'." 25 | end 26 | 27 | handler.execute(*@args) 28 | end 29 | 30 | private 31 | 32 | def check_account_restrictions 33 | this_account = Moonshot::AccountContext.get 34 | 35 | return if @klass.only_in_account.nil? || 36 | Array(@klass.only_in_account).any? { |a| a == this_account } 37 | 38 | warn "'#{@command}' can only be run in the following accounts:" 39 | Array(@klass.only_in_account).each do |account_name| 40 | warn "- #{account_name}" 41 | end 42 | 43 | raise 'Command account restriction violation.' 44 | end 45 | 46 | def build_parser(handler) 47 | parser = handler.parser 48 | 49 | # Each mechanism / plugin may manipulate the OptionParser object 50 | # associated with this command. 51 | %i[build_mechanism deployment_mechanism artifact_repository].each do |prov| 52 | provider = Moonshot.config.send(prov) 53 | 54 | parser = provider.send(hook_func_name(@command), parser) if provider.respond_to?(hook_func_name(@command)) 55 | end 56 | 57 | Moonshot.config.plugins.each do |plugin| 58 | parser = plugin.send(hook_func_name(@command), parser) if plugin.respond_to?(hook_func_name(@command)) 59 | end 60 | 61 | parser 62 | end 63 | 64 | # Name of the function a plugin or mechanism could define to manipulate 65 | # the parser for a command. 66 | def hook_func_name(command) 67 | command.tr('-', '_') << '_cli_hook' 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/moonshot/artifact_repository/s3_bucket_via_github_releases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fakefs/spec_helpers' 2 | 3 | module Moonshot 4 | describe ArtifactRepository::S3BucketViaGithubReleases do 5 | let(:version) { '0.0.0'} 6 | let(:bucket_name) { 'moonshot-test-bucket' } 7 | let(:artifact_name) { "moonshot-#{version}-beta.tar.gz" } 8 | let(:hub_command) { "hub release download #{version}" } 9 | 10 | def fake_artifact 11 | FakeFS { FileUtils.touch(artifact_name) } 12 | end 13 | 14 | def fake_checksum_file 15 | FakeFS do 16 | base = File.basename(artifact_name, '.tar.gz') 17 | FileUtils.touch("#{base}.md5") 18 | end 19 | end 20 | 21 | subject { described_class.new(bucket_name) } 22 | 23 | describe '#download_from_github' do 24 | include FakeFS::SpecHelpers 25 | 26 | it 'should use hub to download the latest version' do 27 | obj = subject 28 | expect(obj).to receive(:sh_out) { fake_artifact }.with(hub_command) 29 | 30 | subject.send(:download_from_github, version) 31 | end 32 | 33 | it 'should raise RuntimeError when download has failed' do 34 | obj = subject 35 | expect(obj).to receive(:sh_out).at_least(:once) 36 | 37 | expect do 38 | obj.send(:download_from_github, version) 39 | end.to raise_error(RuntimeError) 40 | end 41 | 42 | it 'should retry if the download has failed' do 43 | obj = subject 44 | expect(obj).to receive(:sh_out).with(hub_command).exactly(3).times 45 | 46 | expect { obj.send(:download_from_github, version) }.to raise_error(RuntimeError) 47 | end 48 | 49 | it 'should verify the checksum of the downloaded artifact' do 50 | obj = subject 51 | 52 | expect(obj).to receive(:sh_out) do 53 | fake_artifact 54 | fake_checksum_file 55 | end.with(hub_command) 56 | 57 | expect(obj).to receive(:verify_download_checksum) 58 | obj.send(:download_from_github, version) 59 | end 60 | 61 | it 'should return the name of the downloaded artifact' do 62 | obj = subject 63 | 64 | allow(obj).to receive(:sh_out) { fake_artifact }.with(hub_command) 65 | 66 | expect(obj.send(:download_from_github, version))\ 67 | .to eql("/#{artifact_name}") 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/moonshot/build_mechanism/script.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | 5 | # Compile a release artifact using a shell script. 6 | # 7 | # The output file will be deleted before the script is run, and is expected to 8 | # exist after the script exits. Any non-zero exit status will be consider a 9 | # build failure, and any output will be displayed to the user. 10 | # 11 | # Creating a new Script BuildMechanism looks like this: 12 | # 13 | # class MyReleaseTool < Moonshot::CLI 14 | # include Moonshot::BuildMechanism 15 | # self.build_mechanism = Script.new('script/build.sh') 16 | # end 17 | # 18 | class Moonshot::BuildMechanism::Script 19 | include Moonshot::ResourcesHelper 20 | include Moonshot::DoctorHelper 21 | 22 | include Open3 23 | 24 | attr_reader :output_file 25 | 26 | def initialize(script, output_file: 'output.tar.gz') 27 | @script = script 28 | @output_file = output_file 29 | end 30 | 31 | def pre_build_hook(_version) 32 | File.delete(@output_file) if File.exist?(@output_file) 33 | end 34 | 35 | def build_hook(version) 36 | env = { 37 | 'VERSION' => version, 38 | 'OUTPUT_FILE' => @output_file 39 | } 40 | ilog.start_threaded "Running Script: #{@script}" do |s| 41 | run_script(s, env:) 42 | end 43 | end 44 | 45 | def post_build_hook(_version) 46 | unless File.exist?(@output_file) # rubocop:disable Style/GuardClause 47 | raise 'Build command did not produce output file!' 48 | end 49 | end 50 | 51 | private 52 | 53 | def run_script(step, env: {}) 54 | popen2e(env, @script) do |_, out, wait| 55 | output = [] 56 | 57 | loop do 58 | str = out.gets 59 | unless str.nil? 60 | output << str.chomp 61 | ilog.debug(str.chomp) 62 | end 63 | break if out.eof? 64 | end 65 | 66 | result = wait.value 67 | step.success "Build script #{@script} exited successfully!" if result.exitstatus.zero? 68 | unless result.exitstatus.zero? 69 | ilog.error "Build script failed with exit status #{result.exitstatus}!" 70 | ilog.error output.join("\n") 71 | step.failure "Build script #{@script} failed with exit status #{result.exitstatus}!" 72 | end 73 | end 74 | end 75 | 76 | def doctor_check_script_exists 77 | if File.exist?(@script) 78 | success "Script '#{@script}' exists." 79 | else 80 | critical "Could not find build script '#{@script}'!" 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/moonshot/changelog_parser.rb: -------------------------------------------------------------------------------- 1 | # Makes all string literals in this file frozen/immutable for performance 2 | # frozen_string_literal: true 3 | 4 | module Moonshot 5 | # Custom changelog parser to replace the unmaintained Vandamme gem (previously used in Moonshot until v3.0.4) 6 | class ChangelogParser 7 | # Parses a changelog and extracts content for a specific version 8 | # 9 | # @param changelog_content [String] The full changelog content 10 | # @param version [String] The version to extract content for 11 | # @return [String] The changelog content for the specified version 12 | # @raise [RuntimeError] if the version is not found in the changelog 13 | def self.parse(changelog_content, version) 14 | new(changelog_content).parse(version) 15 | end 16 | 17 | def initialize(changelog_content) 18 | @content = changelog_content 19 | end 20 | 21 | def parse(version) 22 | version = normalize_version(version) 23 | version_section = find_version_section(version) 24 | 25 | raise "#{version} not found in CHANGELOG.md" unless version_section 26 | 27 | extract_version_content(version_section) 28 | end 29 | 30 | private 31 | 32 | def normalize_version(version) 33 | version.to_s.strip 34 | end 35 | 36 | def find_version_section(version) 37 | sections = split_into_sections 38 | 39 | sections.find do |section| 40 | section_matches_version?(section, version) 41 | end 42 | end 43 | 44 | def split_into_sections 45 | @content.split(/^#+\s+/).reject(&:empty?) 46 | end 47 | 48 | def section_matches_version?(section, version) 49 | # Match version patterns like: 50 | # - "1.0.0" 51 | # - "[1.0.0]" 52 | # - "v1.0.0" 53 | # - "1.0.0 - 2023-01-01" 54 | # - "1.0.0 / 2023-01-01" 55 | # - "1.0.0 (2023-01-01)" 56 | version_pattern = %r{^(\[?v?#{Regexp.escape(version)}\]?)(\s+[-–]\s+|\s+/\s+|\s+\(|\s*$)}i 57 | section.match(version_pattern) 58 | end 59 | 60 | def extract_version_content(version_section) 61 | lines = version_section.lines 62 | content_lines = [] 63 | 64 | # Skip the version header line 65 | lines[1..]&.each do |line| 66 | # Stop at next version header 67 | break if next_version_header?(line) 68 | 69 | content_lines << line 70 | end 71 | 72 | # Clean up and return the content 73 | content_lines.join.strip 74 | end 75 | 76 | def next_version_header?(line) 77 | # Check if this line is the start of another version section 78 | line.match(/^#+\s+(\[?v?\d+\.\d+\.\d+)/i) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/moonshot/commands/new.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | module Commands 5 | class New < Moonshot::Command 6 | self.usage = 'new [options]' 7 | self.description = 'Creates a new Moonshot project.' 8 | 9 | DEFAULT_DIRECTORY = File.join(__dir__, '..', '..', 'default').freeze 10 | 11 | def execute 12 | warn 'Looks like your project is already set up!' 13 | end 14 | 15 | class << self 16 | def run!(application_name) 17 | @application_name = application_name 18 | 19 | create_project_dir 20 | copy_defaults 21 | fill_moonfile 22 | print_success_message 23 | end 24 | 25 | private 26 | 27 | def create_project_dir 28 | raise "Directory '#{@application_name}' already exists!" \ 29 | if Dir.exist?(project_path) 30 | 31 | Dir.mkdir(project_path) 32 | end 33 | 34 | def project_path 35 | @project_path ||= File.join(Dir.pwd, @application_name) 36 | end 37 | 38 | def copy_defaults 39 | target_path = File.join(DEFAULT_DIRECTORY.dup, '.') 40 | FileUtils.cp_r(target_path, project_path) 41 | end 42 | 43 | def fill_moonfile 44 | File.open(File.join(project_path, 'Moonfile.rb'), 'w') { |f| f.write generate_moonfile } 45 | end 46 | 47 | def generate_moonfile 48 | <<-EOF 49 | Moonshot.config do |m| 50 | m.app_name = '#{@application_name}' 51 | m.artifact_repository = S3Bucket.new('') 52 | m.build_mechanism = Script.new('bin/build.sh') 53 | m.deployment_mechanism = CodeDeploy.new(asg: 'AutoScalingGroup') 54 | end 55 | EOF 56 | end 57 | 58 | def print_success_message 59 | warn <<-EOF 60 | Your application is configured, the following changes have been made 61 | to your project directory: 62 | 63 | * Created Moonfile.rb, where you can configure your project. 64 | * Created moonshot/plugins, where you can place custom Ruby code 65 | to add hooks to core Moonshot actions (create, update, delete, etc.) 66 | * Created moonshot/cli_extensions, where you can place custom Ruby 67 | code to add your own project-specific commands to Moonshot. 68 | * Created moonshot/template.yml, where you can build your 69 | CloudFormation template. 70 | 71 | You will also need to ensure your Amazon account is configured for 72 | CodeDeploy by creating a role that allows deployments. 73 | 74 | See: http://moonshot.readthedocs.io/en/latest/mechanisms/deployment/ 75 | EOF 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/moonshot/command_restrictions.rb: -------------------------------------------------------------------------------- 1 | class RestrictedCommand1 < Moonshot::Command 2 | self.only_in_account = 'jeff' 3 | 4 | def execute 5 | puts "Yep, I ran!" 6 | end 7 | end 8 | 9 | class RestrictedCommand2 < Moonshot::Command 10 | self.only_in_account = [ 'jeff', 'panda' ] 11 | 12 | def execute 13 | puts "Yep, I ran!" 14 | end 15 | end 16 | 17 | class UnrestrictedCommand < Moonshot::Command 18 | def execute 19 | puts "Yep, I ran!" 20 | end 21 | end 22 | 23 | describe 'command account restrictions' do 24 | def try(klass) 25 | Moonshot::CommandLineDispatcher.new('stuff', klass, []) 26 | .dispatch! 27 | end 28 | 29 | before(:each) do 30 | Moonshot::AccountContext.set(account_name) 31 | ARGV.clear 32 | end 33 | 34 | context 'in the "jeff" account' do 35 | let(:account_name) { 'jeff' } 36 | 37 | it 'should let us run RestrictedCommand1' do 38 | expect { try(RestrictedCommand1) } 39 | .to output(/Yep, I ran!/).to_stdout 40 | end 41 | 42 | it 'should let us run RestrictedCommand2' do 43 | expect { try(RestrictedCommand2) } 44 | .to output(/Yep, I ran!/).to_stdout 45 | end 46 | 47 | it 'should let us run UnrestrictedCommand' do 48 | expect { try(UnrestrictedCommand) } 49 | .to output(/Yep, I ran!/).to_stdout 50 | end 51 | end 52 | 53 | context 'in the "panda" account' do 54 | let(:account_name) { 'panda' } 55 | 56 | it 'should not let us run RestrictedCommand1' do 57 | expect { try(RestrictedCommand1) } 58 | .to raise_error(/Command account restriction/) 59 | .and output(/can only be run/).to_stderr 60 | end 61 | 62 | it 'should let us run RestrictedCommand2' do 63 | expect { try(RestrictedCommand2) } 64 | .to output(/Yep, I ran!/).to_stdout 65 | end 66 | 67 | it 'should let us run UnrestrictedCommand' do 68 | expect { try(UnrestrictedCommand) } 69 | .to output(/Yep, I ran!/).to_stdout 70 | end 71 | end 72 | 73 | context 'in the "carrot" account' do 74 | let(:account_name) { 'carrot' } 75 | 76 | it 'should not let us run RestrictedCommand1' do 77 | expect { try(RestrictedCommand1) } 78 | .to raise_error(/Command account restriction/) 79 | .and output(/can only be run/).to_stderr 80 | end 81 | 82 | it 'should not let us run RestrictedCommand2' do 83 | expect { try(RestrictedCommand2) } 84 | .to raise_error(/Command account restriction/) 85 | .and output(/can only be run/).to_stderr 86 | end 87 | 88 | it 'should let us run UnrestrictedCommand' do 89 | expect { try(UnrestrictedCommand) } 90 | .to output(/Yep, I ran!/).to_stdout 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/moonshot/shell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | require 'retriable' 5 | 6 | # Mixin providing the Thor::Shell methods and other shell execution helpers. 7 | module Moonshot::Shell 8 | CommandError = Class.new(RuntimeError) 9 | 10 | # Retry every 10 seconds for a maximum of 2 minutes. 11 | DEFAULT_RETRY_OPTIONS = { 12 | on: RuntimeError, 13 | tries: 50, 14 | multiplier: 1, 15 | base_interval: 10, 16 | max_elapsed_time: 120, 17 | rand_factor: 0 18 | }.freeze 19 | 20 | DEFAULT_TERMINAL_WIDTH = 80 21 | 22 | # Run a command, returning stdout. Stderr is suppressed unless the command 23 | # returns non-zero. 24 | def sh_out(cmd, fail = true, stdin = '') 25 | r_in, w_in = IO.pipe 26 | r_out, w_out = IO.pipe 27 | r_err, w_err = IO.pipe 28 | w_in.write(stdin) 29 | w_in.close 30 | pid = Process.spawn(cmd, in: r_in, out: w_out, err: w_err) 31 | Process.wait(pid) 32 | 33 | r_in.close 34 | w_out.close 35 | w_err.close 36 | stdout = r_out.read 37 | r_out.close 38 | stderr = r_err.read 39 | r_err.close 40 | 41 | if fail && $CHILD_STATUS.exitstatus != 0 42 | raise CommandError, "`#{cmd}` exited #{$CHILD_STATUS.exitstatus}\n" \ 43 | "stdout:\n" \ 44 | "#{stdout}\n" \ 45 | "stderr:\n" \ 46 | "#{stderr}\n" 47 | end 48 | stdout 49 | end 50 | module_function :sh_out 51 | 52 | def shell 53 | @thor_shell ||= Thor::Base.shell.new 54 | end 55 | 56 | def terminal_width 57 | Thor::Shell::Terminal.terminal_width || DEFAULT_TERMINAL_WIDTH 58 | end 59 | 60 | Thor::Shell::Basic.public_instance_methods(false).each do |meth| 61 | define_method(meth) { |*args| shell.public_send(meth, *args) } 62 | end 63 | 64 | def sh_step(cmd, **args) 65 | msg = args.delete(:msg) || cmd 66 | msg = "#{msg[0..(terminal_width - 22)]}..." if msg.length > (terminal_width - 18) 67 | ilog.start_threaded(msg) do |step| 68 | out = sh_out(cmd, **args) 69 | yield step, out if block_given? 70 | step.success 71 | end 72 | end 73 | 74 | # Retries every second upto maximum of 2 minutes with the default options. 75 | # 76 | # @param cmd [String] command to execute. 77 | # @param fail [Boolean] Raise error when the command exits with non-zero. 78 | # @param stdin [String] Input to the command. 79 | # @param opts [Hash] Options for retriable. 80 | # 81 | # @return [String] Stdout form the command. 82 | def sh_retry(cmd, fail = true, stdin = '', opts: {}) 83 | Retriable.retriable(DEFAULT_RETRY_OPTIONS.merge(opts)) do 84 | out = sh_out(cmd, stdin:) 85 | yield out if block_given? 86 | out 87 | end 88 | rescue CommandError => e 89 | raise e if fail 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/moonshot/artifact_repository/s3_bucket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../resources_helper' 4 | require_relative '../creds_helper' 5 | require_relative '../doctor_helper' 6 | 7 | # The S3Bucket stores builds in an S3 Bucket. 8 | # 9 | # For example: 10 | # 11 | # def MyApplication < Moonshot::CLI 12 | # self.artifact_repository = S3Bucket.new('my-application-builds') 13 | # end 14 | class Moonshot::ArtifactRepository::S3Bucket 15 | include Moonshot::ResourcesHelper 16 | include Moonshot::CredsHelper 17 | include Moonshot::DoctorHelper 18 | 19 | attr_reader :bucket_name 20 | 21 | def initialize(bucket_name, prefix: '') 22 | @bucket_name = bucket_name 23 | @prefix = prefix 24 | end 25 | 26 | def store_hook(build_mechanism, version_name) 27 | unless build_mechanism.respond_to?(:output_file) 28 | raise "S3Bucket does not know how to store artifacts from #{build_mechanism.class}, no method '#output_file'." 29 | end 30 | 31 | file = build_mechanism.output_file 32 | bucket_name = @bucket_name 33 | key = filename_for_version(version_name) 34 | 35 | ilog.start_threaded "Uploading #{file} to s3://#{bucket_name}/#{key}" do |s| 36 | upload_to_s3(file, key) 37 | s.success "Uploaded s3://#{bucket_name}/#{key} successfully." 38 | end 39 | end 40 | 41 | def filename_for_version(version_name) 42 | "#{@prefix}#{version_name}.tar.gz" 43 | end 44 | 45 | private 46 | 47 | def upload_to_s3(file, key) 48 | s3_client.put_object( 49 | acl: 'bucket-owner-full-control', 50 | key:, 51 | body: File.open(file), 52 | bucket: @bucket_name, 53 | storage_class: 'STANDARD_IA' 54 | ) 55 | end 56 | 57 | def doctor_check_bucket_exists 58 | s3_client.get_bucket_location(bucket: @bucket_name) 59 | success "Bucket '#{@bucket_name}' exists." 60 | rescue StandardError => e 61 | # This is warning because the role you use for deployment may not actually 62 | # be able to read builds, however the instance role assigned to the nodes 63 | # might. 64 | str = "Could not get information about bucket '#{@bucket_name}'." 65 | warning(str, e.message) 66 | end 67 | 68 | def doctor_check_bucket_writable 69 | s3_client.put_object( 70 | key: 'test-object', 71 | body: '', 72 | bucket: @bucket_name, 73 | storage_class: 'REDUCED_REDUNDANCY' 74 | ) 75 | s3_client.delete_object(key: 'test-object', bucket: @bucket_name) 76 | success 'Bucket is writable, new builds can be uploaded.' 77 | rescue StandardError => e 78 | # This is a warning because you may deploy to an environment where you have 79 | # read access to builds, but could not publish a new build. 80 | warning('Could not write to bucket, you may still be able to deploy existing builds.', e.message) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/README.md: -------------------------------------------------------------------------------- 1 | # ELB and ASG lifecycle event scripts 2 | 3 | Often when running a web service, you'll have your instances behind a load balancer. But when 4 | deploying new code to these instances, you don't want the load balancer to continue sending customer 5 | traffic to an instance while the deployment is in progress. Lifecycle event scripts give you the 6 | ability to integrate your AWS CodeDeploy deployments with instances that are behind an Elastic Load 7 | Balancer or in an Auto Scaling group. Simply set the name (or names) of the Elastic Load Balancer 8 | your instances are a part of, set the scripts in the appropriate lifecycle events, and the scripts 9 | will take care of deregistering the instance, waiting for connection draining, and re-registering 10 | after the deployment finishes. 11 | 12 | ## Requirements 13 | 14 | The register and deregister scripts have a couple of dependencies in order to properly interact with 15 | Elastic Load Balancing and AutoScaling: 16 | 17 | 1. The [AWS CLI](http://aws.amazon.com/cli/). In order to take advantage of 18 | AutoScaling's Standby feature, the CLI must be at least version 1.3.25. If you 19 | have Python and PIP already installed, the CLI can simply be installed with `pip 20 | install awscli`. Otherwise, follow the [installation instructions](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) 21 | in the CLI's user guide. 22 | 1. An instance profile with a policy that allows, at minimum, the following actions: 23 | 24 | ``` 25 | elasticloadbalancing:Describe* 26 | elasticloadbalancing:DeregisterInstancesFromLoadBalancer 27 | elasticloadbalancing:RegisterInstancesWithLoadBalancer 28 | autoscaling:Describe* 29 | autoscaling:EnterStandby 30 | autoscaling:ExitStandby 31 | autoscaling:UpdateAutoScalingGroup 32 | ``` 33 | 34 | Note: the AWS CodeDeploy Agent requires that an instance profile be attached to all instances that 35 | are to participate in AWS CodeDeploy deployments. For more information on creating an instance 36 | profile for AWS CodeDeploy, see the [Create an IAM Instance Profile for Your Amazon EC2 Instances]() 37 | topic in the documentation. 38 | 1. All instances are assumed to already have the AWS CodeDeploy Agent installed. 39 | 40 | ## Installing the Scripts 41 | 42 | To use these scripts in your own application: 43 | 44 | 1. Install the AWS CLI on all your instances. 45 | 1. Update the policies on the EC2 instance profile to allow the above actions. 46 | 1. Copy the `.sh` files in this directory into your application source. 47 | 1. Edit your application's `appspec.yml` to run `deregister_from_elb.sh` on the ApplicationStop event, 48 | and `register_with_elb.sh` on the ApplicationStart event. 49 | 1. Edit `common_functions.sh` to set `ELB_LIST` to contain the name(s) of the Elastic Load 50 | Balancer(s) your deployment group is a part of. Make sure the entries in ELB_LIST are separated by space. 51 | 1. Deploy! 52 | 53 | -------------------------------------------------------------------------------- /lib/moonshot/change_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Moonshot 4 | class ChangeSet 5 | attr_reader :name, :stack_name 6 | 7 | def initialize(name, stack_name) 8 | @name = name 9 | @stack_name = stack_name 10 | @change_set = nil 11 | @cf_client = Aws::CloudFormation::Client.new 12 | end 13 | 14 | def confirm? 15 | raise 'Cannot confirm ChangeSet when interactive mode is disabled!' unless Moonshot.config.interactive 16 | 17 | loop do 18 | print 'Apply changes? ' 19 | resp = gets.chomp.downcase 20 | 21 | return true if resp == 'yes' 22 | return false if resp == 'no' 23 | 24 | puts "Please enter 'yes' or 'no'!" 25 | end 26 | end 27 | 28 | def valid? 29 | @change_set.status == 'CREATE_COMPLETE' 30 | end 31 | 32 | def invalid_reason 33 | @change_set.status_reason 34 | end 35 | 36 | def display_changes 37 | wait_for_change_set unless @change_set 38 | 39 | @change_set.changes.map(&:resource_change).each do |c| 40 | puts "* #{c.action} #{c.logical_resource_id} (#{c.resource_type})" 41 | 42 | case c.replacement 43 | when 'True' 44 | puts ' - Will be replaced' 45 | when 'Conditional' 46 | puts ' - May be replaced (Conditional)' 47 | end 48 | 49 | c.details.each do |d| 50 | case d.change_source 51 | when 'ResourceReference', 'ParameterReference' 52 | puts " - Caused by #{d.causing_entity.blue} (#{d.change_source})" 53 | when 'DirectModification' 54 | puts " - Caused by template change (#{d.target.attribute}: #{d.target.name})" 55 | end 56 | end 57 | end 58 | end 59 | 60 | def execute 61 | wait_for_change_set unless @change_set 62 | @cf_client.execute_change_set( 63 | change_set_name: @name, 64 | stack_name: @stack_name 65 | ) 66 | end 67 | 68 | def delete 69 | wait_for_change_set unless @change_set 70 | @cf_client.delete_change_set( 71 | change_set_name: @name, 72 | stack_name: @stack_name 73 | ) 74 | rescue Aws::CloudFormation::Errors::InvalidChangeSetStatus 75 | sleep 1 76 | retry 77 | end 78 | 79 | def wait_for_change_set 80 | begin 81 | @cf_client.wait_until(:change_set_create_complete, 82 | stack_name: @stack_name, 83 | change_set_name: @name) 84 | rescue Aws::Waiters::Errors::FailureStateError => e 85 | if e.message != 'stopped waiting, encountered a failure state' 86 | throw e 87 | else 88 | puts "The change set didn't contain any new changes." 89 | end 90 | end 91 | 92 | @change_set = @cf_client.describe_change_set(stack_name: @stack_name, 93 | change_set_name: @name) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /docs/plugins/backup.md: -------------------------------------------------------------------------------- 1 | # Moonshot backup plugin 2 | 3 | Moonshot plugin for backing up config files. 4 | 5 | ## Functionality 6 | 7 | The plugin collects and deflates certain files to a single tarball, 8 | and uploads that to a a given S3 bucket. The whole process happens 9 | in memory, nothing is written to disk. The plugin currently supports single files only, 10 | including whole directories in your tarball is not possible yet. 11 | 12 | The plugin uses the Moonshot AWS config, meaning that the bucket must be 13 | present in the same account and region as your deployment. 14 | 15 | ## Basic usage 16 | 17 | When instantiating a class, you need to set the following options 18 | in a block, where the object is provided as a block argument: 19 | 20 | - `bucket`: the name of the S3 bucket you wish to upload the tarball (optional) 21 | - `buckets`: a hash map containing account aliases as keys, and target buckets as values (optional). 22 | - `files`: an array of relative path names as strings. 23 | - `backup_parameters`: boolean value for backing up all parameters into a YAML file (optional, defaults to `false`). 24 | - `backup_template`: boolean value for backing up the current CloudFormation template (optional, defaults to `false`). 25 | - `hooks`: which hooks to run the backup logic, works with all valid Moonshot hooks 26 | - `target_name`: tarball archive name (optional, defaults to `__.tar.gz`). 27 | 28 | You must provide either `bucket` or `buckets`, but **not both**. 29 | 30 | If you provide either `backup_parameters` or `backup_template` you may not provide `files` additionally. 31 | 32 | `pre_create` and `post_delete` hooks are **not** allowed to use due to certain implementation restrictions. 33 | 34 | ## Default method 35 | 36 | If you wish to back up only the current template and parameter files, you can simply 37 | use the factory method provided: 38 | 39 | ```ruby 40 | Moonshot.config do |c| 41 | # ... 42 | c.plugins << Moonshot::Plugins::Backup.to_bucket('your-bucket-name') 43 | ``` 44 | 45 | ## Placeholders 46 | 47 | You can use the following placeholders both in your filenames 48 | and tarball target names (meanings are pretty self explaining): 49 | 50 | - `%{app_name}` 51 | - `%{stack_name}` 52 | - `%{timestamp}` 53 | - `%{user}` 54 | 55 | ## Example 56 | 57 | A possible use-case is backing up a CF template and/or 58 | parameter file after create or update. 59 | 60 | ```ruby 61 | c.plugins << Backup.new do |b| 62 | b.bucket = 'your-bucket-name' 63 | 64 | b.files = [ 65 | 'cloud_formation/%{app_name}.json', 66 | 'cloud_formation/parameters/%{stack_name}.yml' 67 | ] 68 | 69 | b.hooks = [:post_create, :post_update] 70 | end 71 | ``` 72 | 73 | ```ruby 74 | c.plugins << Backup.new do |b| 75 | b.buckets = { 76 | 'dev_account' => 'dev_bucket', 77 | 'prod_account' => 'prod_bucket' 78 | } 79 | 80 | b.backup_template = true 81 | b.backup_parameters = true 82 | 83 | b.hooks = [:post_create, :post_update] 84 | end 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Moonshot 2 | _Because releasing services shouldn't be a moonshot._ 3 | 4 | ## Overview 5 | 6 | Moonshot is a command line tool and library for provisioning and 7 | managing application environments using CloudFormation. It has native 8 | support for integration with S3 and CodeDeploy, as well. Other systems 9 | may be added using our pluggable system. The core components are: 10 | 11 | - A DeploymentMechanism controls releasing code. For example, Amazon 12 | CodeDeploy. 13 | - A BuildMechanism creates a release artifact. For example, a local 14 | shell script. 15 | - A ArtifactRepository stores the release artifacts. For example, 16 | Amazon S3. 17 | 18 | ![General Flow](moonshot.png "General Flow") 19 | 20 | ## Design Goals 21 | 22 | The goal of Moonshot is to wrap CloudFormation in a toolchain that 23 | codifies the deployment and management of a service. Our goal is that 24 | within a given service the Moonshot configuration, CloudFormation 25 | template, and supporting AWS services should be easily understood. 26 | 27 | Some of our original design goals were: 28 | 29 | - Simplicity: It shouldn't take more than a few hours to understand what your 30 | release tooling does. 31 | - Choice: As much as possible, each component should be pluggable and omittable, 32 | so teams are free to use what works best for them. 33 | - Verbosity: The output of core Moonshot code should explain in detail what 34 | changes are being made, so knowledge is shared and not abstracted. 35 | 36 | ## Installation 37 | 38 | You can install Moonshot for your local user with: 39 | 40 | $ gem install moonshot 41 | 42 | If you would prefer to manage your projects dependencies with Bundler, 43 | add the following to your Gemfile: 44 | 45 | gem 'moonshot' 46 | 47 | And then execute: 48 | 49 | $ bundle install 50 | 51 | After installation, there is still some work required. Follow 52 | the [example documentation](example.md) as described below to dig in! 53 | 54 | ## Getting started 55 | 56 | The Moonshot tool has been designed to be an extensible library for 57 | your specific use-case. We aren't trying to solve every use case, but 58 | rather give you an extensible toolkit that your project can grow with, 59 | without leaving your trapped behind rigid design philosophy. 60 | Interested in how it can be used? See our [example documentation][2]. 61 | The example doc uses the files shown in the [sample directory][3] so 62 | you can figure out how to modify this for your own application. 63 | 64 | We also want to [help you contribute and answer all your questions][1] 65 | on how Moonshot is maintained. 66 | 67 | [1]: http://moonshot.readthedocs.org/en/latest/about/contribute 68 | [2]: example.md 69 | [3]: https://github.com/acquia/moonshot/tree/master/sample 70 | 71 | ## Requirements 72 | 73 | - Ruby 2.2 or higher 74 | 75 | ## Attributions 76 | 77 | Thanks to [Acquia Inc.](https://acquia.com) for sponsoring the time to work on this tool. 78 | Thanks to [Ted](https://github.com/tottey) for the funky logo. 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moonshot [![Documentation Status](https://readthedocs.org/projects/moonshot/badge/?version=latest)](http://moonshot.readthedocs.org/en/latest/?badge=latest)[![Test Coverage](https://codeclimate.com/github/acquia/moonshot/badges/coverage.svg)](https://codeclimate.com/github/acquia/moonshot/coverage)[![Code Climate](https://codeclimate.com/github/acquia/moonshot/badges/gpa.svg)](https://codeclimate.com/github/acquia/moonshot)[![Gem Version](https://badge.fury.io/rb/moonshot.svg)](https://badge.fury.io/rb/moonshot) 2 | _Because releasing services shouldn't be a moonshot._ 3 | 4 | ## Overview 5 | 6 | [We also have pretty docs, lots more to find there.](http://moonshot.readthedocs.org/en/latest/) 7 | 8 | Moonshot is a Ruby gem for provisioning environments in AWS using a CLI. 9 | The environments are centered around a single CloudFormation stack and supported 10 | by pluggable systems: 11 | 12 | - A DeploymentMechanism controls releasing code. 13 | - A BuildMechanism creates a release artifact. 14 | - A ArtifactRepository stores the release artifacts. 15 | 16 | ![General Flow](docs/moonshot.png "General Flow") 17 | 18 | ## Design Goals 19 | 20 | These are core ideas to the creation of this project. Not all are met to the 21 | level we'd like (e.g. CloudFormation isn't much of a Choice currently), but we 22 | should aspire to meet them with each iteration. 23 | 24 | - Simplicity: It shouldn't take more than a few hours to understand what your 25 | release tooling does. 26 | - Choice: As much as possible, each component should be pluggable and omittable, 27 | so teams are free to use what works best for them. 28 | - Verbosity: The output of core Moonshot code should explain in detail what 29 | changes are being made, so knowledge is shared and not abstracted. 30 | 31 | ## Existing limitations 32 | 33 | - Moonshot does not support detailed error logging from Cloudformation substacks. 34 | - Moonshot does not support a non-local cloudformation file. 35 | 36 | ## Installation 37 | 38 | Install the Moonshot gem: 39 | 40 | ```shell 41 | $ gem install moonshot 42 | ``` 43 | 44 | After installation, there is still some work required. Follow the [example documentation](docs/example.md) as described below to dig in! 45 | 46 | ## Getting started 47 | 48 | The Moonshot tool has been designed to be an extensible library for your 49 | specific use-case. Interested in how it can be used? See our [example 50 | documentation](http://moonshot.readthedocs.org/en/latest/example). The example 51 | doc uses the files shown in the [sample 52 | directory](https://github.com/acquia/moonshot/tree/master/sample) so you can 53 | figure out how to modify this for your own deployment strategy. 54 | 55 | We also want to [help you contribute and answer all your questions](http://moonshot.readthedocs.org/en/latest/about/contribute) on how Moonshot is maintained. 56 | 57 | ## Requirements 58 | 59 | - Ruby 2.2 or higher 60 | 61 | ## Attributions 62 | 63 | Thanks to [Acquia Inc.](https://acquia.com) for sponsoring the time to work on this tool. 64 | Thanks to [Ted](https://github.com/tottey) for the funky logo. 65 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/register_with_elb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). 6 | # You may not use this file except in compliance with the License. 7 | # A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed 12 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | # express or implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | 16 | . $(dirname $0)/common_functions.sh 17 | 18 | msg "Running AWS CLI with region: $(get_instance_region)" 19 | 20 | # get this instance's ID 21 | INSTANCE_ID=$(get_instance_id) 22 | if [ $? != 0 -o -z "$INSTANCE_ID" ]; then 23 | error_exit "Unable to get this instance's ID; cannot continue." 24 | fi 25 | 26 | # Get current time 27 | msg "Started $(basename $0) at $(/bin/date "+%F %T")" 28 | start_sec=$(/bin/date +%s.%N) 29 | 30 | msg "Checking if instance $INSTANCE_ID is part of an AutoScaling group" 31 | asg=$(autoscaling_group_name $INSTANCE_ID) 32 | if [ $? == 0 -a -n "$asg" ]; then 33 | msg "Found AutoScaling group for instance $INSTANCE_ID: $asg" 34 | 35 | msg "Checking that installed CLI version is at least at version required for AutoScaling Standby" 36 | check_cli_version 37 | if [ $? != 0 ]; then 38 | error_exit "CLI must be at least version ${MIN_CLI_X}.${MIN_CLI_Y}.${MIN_CLI_Z} to work with AutoScaling Standby" 39 | fi 40 | 41 | msg "Attempting to move instance out of Standby" 42 | autoscaling_exit_standby $INSTANCE_ID $asg 43 | if [ $? != 0 ]; then 44 | error_exit "Failed to move instance out of standby" 45 | else 46 | msg "Instance is no longer in Standby" 47 | exit 0 48 | fi 49 | fi 50 | 51 | msg "Instance is not part of an ASG, continuing..." 52 | 53 | msg "Checking that user set at least one load balancer" 54 | if test -z "$ELB_LIST"; then 55 | error_exit "Must have at least one load balancer to register to" 56 | fi 57 | 58 | # Loop through all LBs the user set, and attempt to register this instance to them. 59 | for elb in $ELB_LIST; do 60 | msg "Checking validity of load balancer named '$elb'" 61 | validate_elb $INSTANCE_ID $elb 62 | if [ $? != 0 ]; then 63 | msg "Error validating $elb; cannot continue with this LB" 64 | continue 65 | fi 66 | 67 | msg "Registering $INSTANCE_ID to $elb" 68 | register_instance $INSTANCE_ID $elb 69 | 70 | if [ $? != 0 ]; then 71 | error_exit "Failed to register instance $INSTANCE_ID from ELB $elb" 72 | fi 73 | done 74 | 75 | # Wait for all Registrations to finish 76 | msg "Waiting for instance to register to its load balancers" 77 | for elb in $ELB_LIST; do 78 | wait_for_state "elb" $INSTANCE_ID "InService" $elb 79 | if [ $? != 0 ]; then 80 | error_exit "Failed waiting for $INSTANCE_ID to return to $elb" 81 | fi 82 | done 83 | 84 | msg "Finished $(basename $0) at $(/bin/date "+%F %T")" 85 | 86 | end_sec=$(/bin/date +%s.%N) 87 | elapsed_seconds=$(echo "$end_sec - $start_sec" | /usr/bin/bc) 88 | 89 | msg "Elapsed time: $elapsed_seconds" 90 | -------------------------------------------------------------------------------- /sample/bin/aws-codedeploy-samples/load-balancing/elb/deregister_from_elb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). 6 | # You may not use this file except in compliance with the License. 7 | # A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed 12 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | # express or implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | 16 | . $(dirname $0)/common_functions.sh 17 | 18 | msg "Running AWS CLI with region: $(get_instance_region)" 19 | 20 | # get this instance's ID 21 | INSTANCE_ID=$(get_instance_id) 22 | if [ $? != 0 -o -z "$INSTANCE_ID" ]; then 23 | error_exit "Unable to get this instance's ID; cannot continue." 24 | fi 25 | 26 | # Get current time 27 | msg "Started $(basename $0) at $(/bin/date "+%F %T")" 28 | start_sec=$(/bin/date +%s.%N) 29 | 30 | msg "Checking if instance $INSTANCE_ID is part of an AutoScaling group" 31 | asg=$(autoscaling_group_name $INSTANCE_ID) 32 | if [ $? == 0 -a -n "$asg" ]; then 33 | msg "Found AutoScaling group for instance $INSTANCE_ID: $asg" 34 | 35 | msg "Checking that installed CLI version is at least at version required for AutoScaling Standby" 36 | check_cli_version 37 | if [ $? != 0 ]; then 38 | error_exit "CLI must be at least version ${MIN_CLI_X}.${MIN_CLI_Y}.${MIN_CLI_Z} to work with AutoScaling Standby" 39 | fi 40 | 41 | msg "Attempting to put instance into Standby" 42 | autoscaling_enter_standby $INSTANCE_ID $asg 43 | if [ $? != 0 ]; then 44 | error_exit "Failed to move instance into standby" 45 | else 46 | msg "Instance is in standby" 47 | exit 0 48 | fi 49 | fi 50 | 51 | msg "Instance is not part of an ASG, continuing..." 52 | 53 | msg "Checking that user set at least one load balancer" 54 | if test -z "$ELB_LIST"; then 55 | error_exit "Must have at least one load balancer to deregister from" 56 | fi 57 | 58 | # Loop through all LBs the user set, and attempt to deregister this instance from them. 59 | for elb in $ELB_LIST; do 60 | msg "Checking validity of load balancer named '$elb'" 61 | validate_elb $INSTANCE_ID $elb 62 | if [ $? != 0 ]; then 63 | msg "Error validating $elb; cannot continue with this LB" 64 | continue 65 | fi 66 | 67 | msg "Deregistering $INSTANCE_ID from $elb" 68 | deregister_instance $INSTANCE_ID $elb 69 | 70 | if [ $? != 0 ]; then 71 | error_exit "Failed to deregister instance $INSTANCE_ID from ELB $elb" 72 | fi 73 | done 74 | 75 | # Wait for all Deregistrations to finish 76 | msg "Waiting for instance to de-register from its load balancers" 77 | for elb in $ELB_LIST; do 78 | wait_for_state "elb" $INSTANCE_ID "OutOfService" $elb 79 | if [ $? != 0 ]; then 80 | error_exit "Failed waiting for $INSTANCE_ID to leave $elb" 81 | fi 82 | done 83 | 84 | msg "Finished $(basename $0) at $(/bin/date "+%F %T")" 85 | 86 | end_sec=$(/bin/date +%s.%N) 87 | elapsed_seconds=$(echo "$end_sec - $start_sec" | /usr/bin/bc) 88 | 89 | msg "Elapsed time: $elapsed_seconds" 90 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin Support 2 | 3 | **Warning, the plugin support in Moonshot is a work-in-progress. The interface 4 | to plugins may change dramatically in future versions.** 5 | 6 | 7 | Moonshot supports adding plugins (implemented as a Ruby class) to the 8 | controller that can perform actions before and after the `create`, 9 | `update`, `delete`, `deploy`, `status`, `doctor` and `ssh` actions. 10 | 11 | ## Writing a Moonshot Plugin 12 | 13 | A Moonshot Plugin is a Ruby class that responds to one or more of the following 14 | methods: 15 | 16 | - pre_create 17 | - post_create 18 | - pre_update 19 | - post_update 20 | - pre_delete 21 | - post_delete 22 | - pre_deploy 23 | - post_deploy 24 | - pre_status 25 | - post_status 26 | - pre_doctor 27 | - post_doctor 28 | - pre_ssh 29 | - post_ssh 30 | - setup_create 31 | - setup_update 32 | - setup_status 33 | - setup_build 34 | - setup_deploy 35 | - setup_delete 36 | 37 | 38 | The method will be handed a single argument, which is an instance of the 39 | `Moonshot::Resources` class. This instance gives the plugin access to three 40 | important resources: 41 | 42 | - `Moonshot::Resources#ilog` is an instance of `InteractiveLogger`, used to 43 | display status to the user of the CLI interface. 44 | - `Moonshot::Resources#stack` is an instance of `Moonshot::Stack` which can 45 | retreive the name of the stack, stack parameters and stack outputs. This support 46 | should be expanded in the future to provide Plugins with more control over the 47 | CloudFormation stack. 48 | 49 | `setup` hooks work a bit differently. They are invoked before any other business 50 | logic is executed. This means no resource objects are actually instantiated, 51 | therefore no parameters are passed in this case. 52 | 53 | ## Manipulating CLI options with Plugins 54 | 55 | If you wish to modify the options accepted by a core Moonshot command 56 | in order to affect the pre/post hooks defined in your plugin, 57 | implement a method called `_cli_hook`. This hook will be 58 | passed an instance of OptionParser, which you can manipulate and 59 | return. For example: 60 | 61 | ```ruby 62 | class MyPlugin 63 | def pre_build(_) 64 | puts "FULL SPEED AHEAD!!!!" if @hyperdrive 65 | end 66 | 67 | def build_cli_hook(parser) 68 | parser.on('--foo', '-F', TrueClass, 'ENABLE HYPERDRIVE') do |v| 69 | @hyperdrive = v 70 | end 71 | end 72 | end 73 | ``` 74 | 75 | With this plugin, the output of `moonshot build --help` reflects the 76 | new command line option: 77 | 78 | ``` 79 | Usage: moonshot build VERSION 80 | -v, --[no-]verbose Show debug logging 81 | -s, --skip-ci-status Skip checks on CI jobs 82 | -n, --environment=NAME Which environment to operate on. 83 | --[no-]interactive-logger Enable or disable fancy logging 84 | -F, --foo ENABLE HYPERDRIVE 85 | ``` 86 | 87 | ## Adding a plugin to Moonshot 88 | 89 | Once you have defined or included your plugin class, you can add a 90 | plugin by modifying your `Moonfile.rb` file, like so: 91 | 92 | ```ruby 93 | Moonshot.config do |c| 94 | c.app_name = 'my-app' 95 | # ... 96 | c.plugins << MyPlugin.new 97 | end 98 | ``` 99 | 100 | ## Auto-loading Plugin Source 101 | 102 | The Moonshot CLI tool will auto-load plugin source in the path 103 | `moonshot/plugins/**/*.rb` relative to the `Moonfile.rb` file for your 104 | project. This can be useful for plugins that define project-specific 105 | behaviors. 106 | --------------------------------------------------------------------------------