diff --git a/.eslintrc.json b/.eslintrc.json
index 593b33b..f7df2a6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -17,7 +17,7 @@
"react/require-default-props": "off",
"react/prefer-stateless-function": "off",
"react/forbid-prop-types": "off",
- "jsx-a11y/no-access-key": 2,
+ "jsx-a11y/no-access-key": "off",
"jsx-a11y/href-no-hash": 2,
"jsx-a11y/img-has-alt": 2
},
diff --git a/Dockerfile b/Dockerfile
index db7b691..d3d6ef4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,5 +29,7 @@ ADD . .
RUN brunch build --production && \
mix do compile, phoenix.digest
RUN mix phoenix.swagger.generate priv/swagger/swagger.json
+RUN mkdir -p _build/prod/lib/adpq/priv/swagger/
+RUN cp priv/swagger/swagger.json _build/prod/lib/adpq/priv/swagger/swagger.json
CMD ["mix", "do", "ecto.migrate,", "phoenix.server"]
\ No newline at end of file
diff --git a/README.md b/README.md
index e1a7ef5..902c3b5 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-# Prototype A URL
-https://adpq.labzero.com/
+# Prototype A: CDT Procurement demo
+The prototype is running at this url: https://adpq.labzero.com/
**Administration Login**
* User: admin
@@ -15,10 +15,10 @@ https://adpq.labzero.com/
2. [Quick-access walkthrough](https://github.com/labzero/adpq/blob/develop/RFI-Walkthrough.md) to confirm how Lab Zero's prototype meets the functional requirements stated in Prototype A RFI.
# Table of Contents
-* Setup Instructions
-* Technical Approach
-* Playbook Adherence
-* Requirements List
+* [Setup Instructions](#setup-instructions)
+* [Technical Approach](#technical-approach)
+* [Playbook Adherence](#playbook-adherence)
+* [Requirements List](#requirements-list)
# Setup Instructions
@@ -54,11 +54,25 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
# Technical Approach
## Introduction
-The Lab Zero team’s approach to product development and software delivery is very close to the steps outlined in the U.S. Digital Services Playbook and fully illustrated within the [Docs folder](https://github.com/labzero/adpq/tree/develop/docs) and the Playbook Adherence section below. In the abbreviated timeline for this assessment, our team engaged with target users to gain a deeper understanding of their needs and to test solution ideas. User feedback is quickly incorporated into our work through prioritized user stories, which are addressed by members of the team during a sprint. Collaboration between roles let the team choose best-of-breed designs that could be feasibly delivered within the timeline. Our engineers chose modern tools that supported our need to bring features together quickly and deliver them continually through the timeline with a high degree of quality. The team’s high level of rigor in engineering—gleaned from years of experience delivering mission-critical applications—results in code that’s easy to adapt to meet evolving business needs.
+The Lab Zero team’s approach to product development and agile software delivery mirrors the U.S. Digital Services Playbook as shown in the [Playbook Adherence section below](#playbook-adherence) and fully illustrated within the [Docs folder in this repo](https://github.com/labzero/adpq/tree/develop/docs). Our team kicked off [design](https://github.com/labzero/adpq/blob/develop/Design-Process.pdf) by interviewing target users to understand their needs and to test solution ideas. User feedback informed design iterations, user stories in the backlog, and prioritization during the sprint cycles. Collaboration enabled the team to optimize design iterations that could be feasibly delivered within the timeline. Our engineers chose modern tools that supported our need to bring features together quickly and deliver them continually with a high degree of quality. The team’s high level of rigor in engineering—gleaned from years of experience delivering mission-critical applications—results in code that is easy to adapt to meet evolving business needs for the State of California.
## Architectural Approach
This web application consists of a modern React.js app (Single Page Application) that consumes a JSON API backend written in Elixir using the Phoenix framework backed by a Postgres database. We considered using Shopify or Spree but ultimately decided to build the prototype from scratch. This decision enabled us to demonstrate our ability to develop an easy-to-use application designed in light of careful and deliberate conversations with real users.
+1. React Components
+ * [https://github.com/labzero/adpq/tree/master/web/static/js/components/Category](https://github.com/labzero/adpq/tree/master/web/static/js/components/Category)
+ * [https://github.com/labzero/adpq/tree/master/web/static/js/components/CatalogItem](https://github.com/labzero/adpq/tree/master/web/static/js/components/CatalogItem)
+1. JS REST access
+ * [https://github.com/labzero/adpq/blob/master/web/static/js/actions/index.js](https://github.com/labzero/adpq/blob/master/web/static/js/actions/index.js)
+1. JS routes (defining client side URLs)
+ * [https://github.com/labzero/adpq/blob/master/web/static/js/routes/index.js](https://github.com/labzero/adpq/blob/master/web/static/js/routes/index.js)
+1. JSON serialization
+ * [https://github.com/labzero/adpq/blob/master/web/views/catalog_item_view.ex](https://github.com/labzero/adpq/blob/master/web/views/catalog_item_view.ex)
+1. Controller
+ * [https://github.com/labzero/adpq/blob/master/web/controllers/catalog_item_controller.ex](https://github.com/labzero/adpq/blob/master/web/controllers/catalog_item_controller.ex)
+1. Model
+ * [https://github.com/labzero/adpq/blob/master/web/models/catalog_item.ex](https://github.com/labzero/adpq/blob/master/web/models/catalog_item.ex)
+
## Development Process
We use the GitFlow branching model and create feature branches off of the develop branch for all new changes. All
commits should adhere to the guidelines described in our [commit guide](https://github.com/labzero/guides/blob/master/process/commit_guide.md).
@@ -84,8 +98,11 @@ Using GitFlow tooling, we create a release branch and tag. The tag is then used
## Infrastructure Approach
We built the application in a cloud-first manner on AWS, but deployed it in a Docker container in order to allow cloud portability. However, if AWS offers a managed service for something we need, we prefer the managed service to rolling our own infrastructure, i.e. Postgres via RDS instead of running our own Postgres servers in EC2.
-We maintain our VPC and security blueprints as CloudFormation templates checked into Git.
-
+We maintain our VPC and security blueprints as [CloudFormation templates checked into Git](https://github.com/labzero/adpq/tree/develop/docs/12-CloudFormation).
+
+Database table definition/migrations
+https://github.com/labzero/adpq/blob/master/priv/repo/migrations/20170217185137_create_catalog_item.exs
+
![Cloud Architecture](docs/11-ADPQ-PrototypeA-Architecture.png)
@@ -123,32 +140,32 @@ The list below associates key activities and artifacts with the Digital Service
* Drafted a prioritized features backlog and review with team [Link](https://github.com/labzero/adpq/projects/1)
## 6: Assign on leader and hold that person accountable
-* See Requirements List, Section A
+* See Requirements List, [Section A](#a-assigned-one-1-leader-and-gave-that-person-authority-and-responsibility-and-held-that-person-accountable-for-the-quality-of-the-prototype-submitted)
## 7: Bring in experienced teams
-* See Requirements List, Section B
+* See Requirements List, [Section B](#b-assembled-a-multidisciplinary-and-collaborative-team-that-includes-at-a-minimum-five-5-of-the-labor-categories-as-identified-in-attachment-b-pqvp-ds-ad-labor-category-descriptions)
## 8: Choose a modern technology stack
-* See Requirements List, Section L
+* See Requirements List, [Section L](#l-used-at-least-five-5-modern-and-open-source-technologies-regardless-of-architectural-layer-frontend-backend-etc)
## 9: Deploy in a flexible hosting environment
-* See Requirements List, Section M
+* See Requirements List, [Section M](#m-deployed-the-prototype-on-an-infrastructure-as-a-service-iaas-or-platform-as-service-paas-provider-and-indicated-which-provider-they-used)
## 10: Automate testing and deployments
-* See Requirements List, Section O
+* See Requirements List, [Section O](#o-setup-or-used-a-continuous-integration-system-to-automate-the-running-of-tests-and-continuously-deployed-their-code-to-their-iaas-or-paas-provider)
## 12: User data to drive
-* See Requirements List, Section Q
+* See Requirements List, [Section Q](#q-setup-or-used-continuous-monitoring)
## 13: Default to open
-* Utilized open source
+* Utilized open source [as documented in the Open Source Technology Audit](https://github.com/labzero/adpq/blob/develop/docs/Open%20Source%20Technology%20Audit.xlsx)
# Requirements List
-**a. Assigned one (1) leader and gave that person authority and responsibility and held that person accountable for the quality of the prototype submitted**
+####A. Assigned one (1) leader and gave that person authority and responsibility and held that person accountable for the quality of the prototype submitted
> Aaron Cripps, Product Owner
-**b. Assembled a multidisciplinary and collaborative team that includes, at a minimum, five (5) of the labor categories as identified in Attachment B: PQVP DS-AD Labor Category Descriptions**
+####B. Assembled a multidisciplinary and collaborative team that includes, at a minimum, five (5) of the labor categories as identified in Attachment B: PQVP DS-AD Labor Category Descriptions
> The majority of the team is based in the San Francisco Bay Area. One member is in Tucson AZ, one member in Little Rock AR. Our team collaborates using tools like Slack, Google Hangouts, Screen Hero, GoToMeeting, and Google Docs.
* Product Manager - Aaron Cripps
* Technical Architect - Sasha Voynow, Matt Wilson
@@ -158,13 +175,13 @@ The list below associates key activities and artifacts with the Digital Service
* Backend Web Developer - Sasha Voynow
* DevOps Engineer - Brien Wankel, Dave O’Dell
-**c. Understood what people needed, by including people in the prototype development and design process**
+####C. Understood what people needed, by including people in the prototype development and design process
> Informed by our initial persona attributes, we found three individuals whose job activities aligned with or related to the Lead Purchasing Organization Administration and State Agency IT Requester roles.
* [Dennis Baker](https://github.com/labzero/adpq/blob/develop/docs/03-UserInterviews/01-Interview1.1DennisBaker-StateAssemblyReprographicsManager.pdf), State of California Assembly Reprographics Manager
* [Robert Lee](https://github.com/labzero/adpq/blob/develop/docs/03-UserInterviews/02-Interview2.1RobertLee-StartupOfficeManager.pdf), Startup Office Manager
* [Ned Holets](https://github.com/labzero/adpq/blob/develop/docs/03-UserInterviews/04-Interview3.1NedHolets-CMSDeveloper.pdf), Lead Software Engineer who has worked on CMS projects
-**d. Used at least a minimum of three (3) “user-centric design” techniques and/or tools**
+####D. Used at least a minimum of three (3) “user-centric design” techniques and/or tools
> Human-centered design is a core aspect of our process. We consider each idea to be a hypothesis which should be tested and proven. You can find a richer explanation of our findings [here](https://github.com/labzero/adpq/blob/develop/Design-Process.pdf). Key activity examples below:
* Customer Development
* Stating and prioritizing learning goals (hypotheses)
@@ -176,25 +193,25 @@ The list below associates key activities and artifacts with the Digital Service
* Leveraging existing usability research
* Baymard Institute, an ecommerce usability research firm who uses qualitative and quantitative research methods.
-**e. Used GitHub to document code commits**
+####E. Used GitHub to document code commits
> Yes, we’ve used Github fully for peer-review and as our sole code repository.
-**f. Used Swagger to document the RESTful API, and provided a link to the Swagger API**
+####F. Used Swagger to document the RESTful API, and provided a link to the Swagger API
> Yes, we've implemented Swagger, you can view it [here](http://adpq.labzero.com:88/swagger-ui)
-**g. Complied with Section 508 of the Americans with Disabilities Act and WCAG 2.0**
+####G. Complied with Section 508 of the Americans with Disabilities Act and WCAG 2.0
> Yes, we have used HTML and CSS in a manner that complies with the ADA and WCAG 2.0
-**h. Created or used a design style guide and/or a pattern library**
+####H. Created or used a design style guide and/or a pattern library
* Utilized the US Web Design Standards for user experience, visual design and responsive guidelines and patterns.
* Leveraged the Baymard Institute’s research-based user interaction guidelines for eCommerce product lists, homepages and checkout.
-**i. Performed usability tests with people**
+####I. Performed usability tests with people
> We showed functional prototypes to the following individuals facilitated by a “Think Aloud” qualitative user test.
* [Robert Lee](https://github.com/labzero/adpq/blob/develop/docs/03-UserInterviews/03-Interview2.2RobertLeeConceptTest.pdf)
* [Tracey Thompson](https://github.com/labzero/adpq/blob/develop/docs/09-UserTesting/03-Interview4.1TraceyThompsonUsabilityTest.pdf)
-**j. Used an iterative approach, where feedback informed subsequent work or versions of the prototype**
+####J. Used an iterative approach, where feedback informed subsequent work or versions of the prototype
> We began by clarifying the business case and target outcomes without proposing solutions. This sets the stage for each activity to be oriented around learning and empowers each team member to bring their expertise and creativity into the solutions which are iteratively built and tested. Learnings from each activity are fed back into subsequent iterations, cross-functionally.
* Product Owner led goal-oriented kickoff and drafted a first version of the “Speclet” to align and hold the team accountable to high-level key outcomes and measurements.
* [Explorations](https://github.com/labzero/adpq/tree/develop/docs) improve in fidelity based on our learning needs
@@ -208,10 +225,10 @@ The list below associates key activities and artifacts with the Digital Service
* Daily standup
* [Sprints](https://github.com/labzero/adpq/tree/develop/docs/10-RetrospectiveNotes): team performed demos and retrospectives
-**k. Created a prototype that works on multiple devices, and presents a responsive design**
+####K. Created a prototype that works on multiple devices, and presents a responsive design
> Our prototype has been designed, developed and tested to work on desktop browsers, iOS and Android phones.
-**l. Used at least five (5) modern and open-source technologies, regardless of architectural layer (frontend, backend, etc.)**
+####L. Used at least five (5) modern and open-source technologies, regardless of architectural layer (frontend, backend, etc.)
> We utilized many modern open-source technologies:
* Elixir
* Phoenix Framework
@@ -220,30 +237,29 @@ The list below associates key activities and artifacts with the Digital Service
* Docker
* SASS
* Javascript/ES6
-* REST
-**m. Deployed the prototype on an Infrastructure as a Service (IaaS) or Platform as Service (PaaS) provider, and indicated which provider they used**
+####M. Deployed the prototype on an Infrastructure as a Service (IaaS) or Platform as Service (PaaS) provider, and indicated which provider they used
> Our prototype has been deployed to AWS as a Docker container running in ECS using RDS for it’s datastore.
-**n. Developed automated unit tests for their code**
+####N. Developed automated unit tests for their code
> The Engineering Team delivered stories with working code and some level of automated testing. All tests are run in the continuous integration loop with each.
-* Javascript we wrote Jest tests (link)
-* Elixir we wrote ExUnit tests (link)
+* Javascript we wrote [Jest tests](https://github.com/labzero/adpq/tree/master/test/javascript)
+* Elixir we wrote [ExUnit tests](https://github.com/labzero/adpq/tree/master/test/)
-**o. Setup or used a continuous integration system to automate the running of tests and continuously deployed their code to their IaaS or PaaS provider**
+####O. Setup or used a continuous integration system to automate the running of tests and continuously deployed their code to their IaaS or PaaS provider
> Our use of a CI server drives automated tests and our deployment pipeline. All new pull requests are tested. We used CircleCI to automate our CI and CD automation.
-**p. Setup or used configuration management**
-> We generate CloudFormation templates and build Docker containers, adhering to a https://12factor.net/ approach.
+####P. Setup or used configuration management
+> We generate CloudFormation templates and build Docker containers, adhering to a https://12factor.net/ approach. CloudFormation templates for staging and production environments can be found in the [docs/12-CloudFormation](docs/12-CloudFormation) directory.
-**q. Setup or used continuous monitoring**
+####Q. Setup or used continuous monitoring
> We setup Honeybadger.io for error reporting and Pingdom for uptime monitoring.
-**r. Deployed their software in an open source container, such as Docker (i.e., utilized operating-system-level virtualization)**
+####R. Deployed their software in an open source container, such as Docker (i.e., utilized operating-system-level virtualization)
> We build Docker containers in our CI/CD process and deploy them to ECR/ECS in AWS.
-**s. Provided sufficient documentation to install and run their prototype on another machine**
-> Please see the Setup section in this document or the SETUP.md file in root directory of this repository. All engineers used these steps to set up their development environments.
+####S. Provided sufficient documentation to install and run their prototype on another machine
+> Please see the [Setup Instructions](#setup-instructions) section in this document or the SETUP.md file in root directory of this repository. All engineers used these steps to set up their development environments.
-**t. Prototype and underlying platforms used to create and run the prototype are openly licensed and free of charge**
+####T. Prototype and underlying platforms used to create and run the prototype are openly licensed and free of charge
> All systems used to create and run the prototype are [open source and free of charge for use](docs/Open%20Source%20Technology%20Audit.xlsx). Our prototype carries an [MIT license](LICENSE.md) as well.
diff --git a/brunch-config.js b/brunch-config.js
index 6588b69..47e6849 100644
--- a/brunch-config.js
+++ b/brunch-config.js
@@ -22,7 +22,7 @@ exports.config = {
stylesheets: {
joinTo: "css/app.css",
order: {
- before: ["node_modules/uswds/dist/css/uswds.css", "node_modules/c3/c3.css", "web/static/css/uswds-bugfixes.css"],
+ before: ["node_modules/uswds/dist/css/uswds.css", "node_modules/c3/c3.css", "web/static/css/uswds-bugfixes.scss"],
after: ["web/static/css/app.scss"] // concat app.css last
}
},
diff --git a/docs/11-ADPQ-PrototypeA-Architecture.graffle/data.plist b/docs/11-ADPQ-PrototypeA-Architecture.graffle/data.plist
index 5419cf8..9798a86 100644
--- a/docs/11-ADPQ-PrototypeA-Architecture.graffle/data.plist
+++ b/docs/11-ADPQ-PrototypeA-Architecture.graffle/data.plist
@@ -51,6 +51,55 @@
8
GraphicsList
+
+ Bounds
+ {{10.499687194824208, 682.66666666666663}, {470, 14}}
+ Class
+ ShapedGraphic
+ FitText
+ YES
+ Flow
+ Resize
+ ID
+ 687
+ Shape
+ Rectangle
+ Style
+
+ fill
+
+ Draws
+ NO
+
+ shadow
+
+ Draws
+ NO
+
+ stroke
+
+ Draws
+ NO
+
+
+ Text
+
+ Pad
+ 0
+ Text
+ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf760
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+{\*\expandedcolortbl;;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 Shaded objects indicate services that we would implement beyond the prototype period }
+ VerticalPad
+ 0
+
+ Wrap
+ NO
+
Class
LineGraphic
@@ -63,8 +112,8 @@
359
Points
- {256.92779419161917, 285.40788418694325}
- {257.36488544174904, 328.1885403218734}
+ {256.92794149050195, 285.40788418588272}
+ {257.3653248582649, 328.18854023007123}
Style
@@ -98,8 +147,8 @@
183
Points
- {136.24332717636472, 201.85345585831868}
- {215.29321611894019, 242.5354302641353}
+ {136.24333942642699, 201.85050278084256}
+ {215.30923899197762, 242.53535968312505}
Style
@@ -133,8 +182,8 @@
382
Points
- {211.25080745343411, 352.04796750305752}
- {82.829850625773716, 352.69747662077049}
+ {211.25080745296964, 352.04760191395513}
+ {82.829850506016541, 352.69609823643526}
Style
@@ -570,8 +619,15 @@ API (Phoenix )}
fill
- Draws
- NO
+ FillType
+ 2
+ GradientAngle
+ 90
+ GradientColor
+
+ w
+ 0.666667
+
shadow
@@ -594,6 +650,21 @@ API (Phoenix )}
686
Shape
Rectangle
+ Style
+
+ fill
+
+ FillType
+ 2
+ GradientAngle
+ 90
+ GradientColor
+
+ w
+ 0.666667
+
+
+
Text
Text
@@ -1901,7 +1972,20 @@ Product Images, Logs}
Shape
Cylinder
Style
-
+
+ fill
+
+ FillType
+ 2
+ GradientAngle
+ 90
+ GradientColor
+
+ w
+ 0.666667
+
+
+
Text
Text
@@ -2036,7 +2120,7 @@ Architecture\
MasterSheets
ModificationDate
- 2017-03-02 18:54:01 +0000
+ 2017-03-03 18:25:38 +0000
Modifier
Matthew Wilson
NotesVisible
@@ -2117,7 +2201,7 @@ Architecture\
Frame
- {{752, 178}, {1168, 906}}
+ {{144, 121}, {1168, 906}}
ListView
OutlineWidth
@@ -2131,7 +2215,7 @@ Architecture\
SidebarWidth
120
VisibleRegion
- {{-56, 0}, {688.66666666666663, 509.33333333333331}}
+ {{-56, 223.33333333333334}, {688.66666666666663, 509.33333333333326}}
Zoom
1.5
ZoomValues
diff --git a/docs/11-ADPQ-PrototypeA-Architecture.png b/docs/11-ADPQ-PrototypeA-Architecture.png
index ed8e5e1..88832ef 100644
Binary files a/docs/11-ADPQ-PrototypeA-Architecture.png and b/docs/11-ADPQ-PrototypeA-Architecture.png differ
diff --git a/docs/12-CloudFormation/cloudformation_adpq_production.template b/docs/12-CloudFormation/cloudformation_adpq_production.template
new file mode 100644
index 0000000..bea8fac
--- /dev/null
+++ b/docs/12-CloudFormation/cloudformation_adpq_production.template
@@ -0,0 +1,1116 @@
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "AWS CloudFormation template to create a new VPC or use an existing VPC for ECS deployment",
+ "Mappings": {
+ "VpcCidrs": {
+ "ap-northeast-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ap-southeast-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ap-southeast-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ca-central-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-central-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-west-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-west-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-east-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-east-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-west-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-west-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ }
+ }
+ },
+ "Parameters": {
+ "EcsAmiId": {
+ "Type": "String",
+ "Description": "ECS AMI Id"
+ },
+ "EcsInstanceType": {
+ "Type": "String",
+ "Description": "ECS EC2 instance type",
+ "Default": "t2.micro",
+ "ConstraintDescription": "must be a valid EC2 instance type."
+ },
+ "KeyName": {
+ "Type": "String",
+ "Description": "Optional - Name of an existing EC2 KeyPair to enable SSH access to the ECS instances",
+ "Default": ""
+ },
+ "VpcId": {
+ "Type": "String",
+ "Description": "Optional - VPC Id of existing VPC. Leave blank to have a new VPC created",
+ "Default": "",
+ "AllowedPattern": "^(?:vpc-[0-9a-f]{8}|)$",
+ "ConstraintDescription": "VPC Id must begin with 'vpc-' or leave blank to have a new VPC created"
+ },
+ "SubnetIds": {
+ "Type": "CommaDelimitedList",
+ "Description": "Optional - Comma separated list of existing VPC Subnet Ids where ECS instances will run",
+ "Default": ""
+ },
+ "AsgMaxSize": {
+ "Type": "Number",
+ "Description": "Maximum size and initial Desired Capacity of ECS Auto Scaling Group",
+ "Default": "1"
+ },
+ "IamRoleInstanceProfile": {
+ "Type": "String",
+ "Description": "Name or the Amazon Resource Name (ARN) of the instance profile associated with the IAM role for the instance"
+ },
+ "EcsClusterName": {
+ "Type": "String",
+ "Description": "ECS Cluster Name",
+ "Default": "default"
+ },
+ "EcsPort": {
+ "Type": "String",
+ "Description": "Optional - Security Group port to open on ECS instances - defaults to port 80",
+ "Default": "80"
+ },
+ "ElbPort": {
+ "Type": "String",
+ "Description": "Optional - Security Group port to open on ELB - port 80 will be open by default",
+ "Default": "80"
+ },
+ "ElbProtocol": {
+ "Type": "String",
+ "Description": "Optional - ELB Protocol - defaults to HTTP",
+ "Default": "HTTP"
+ },
+ "ElbHealthCheckTarget": {
+ "Type": "String",
+ "Description": "Optional - Health Check Target for ELB - defaults to HTTP:80/",
+ "Default": "HTTP:80/"
+ },
+ "SourceCidr": {
+ "Type": "String",
+ "Description": "Optional - CIDR/IP range for EcsPort and ElbPort - defaults to 0.0.0.0/0",
+ "Default": "0.0.0.0/0"
+ },
+ "EcsEndpoint": {
+ "Type": "String",
+ "Description": "Optional : ECS Endpoint for the ECS Agent to connect to",
+ "Default": ""
+ },
+ "CreateElasticLoadBalancer": {
+ "Type": "String",
+ "Description": "Optional : When set to true, creates a ELB for ECS Service",
+ "Default": "false"
+ },
+ "VpcAvailabilityZones": {
+ "Type": "CommaDelimitedList",
+ "Description": "Optional : Comma-delimited list of two VPC availability zones in which to create subnets",
+ "Default": ""
+ }
+ },
+ "Conditions": {
+ "CreateVpcResources": {
+ "Fn::Equals": [
+ {
+ "Ref": "VpcId"
+ },
+ ""
+ ]
+ },
+ "ExistingVpcResources": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "VpcId"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "SetEndpointToECSAgent": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "EcsEndpoint"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "CreateELBForExistingVpc": {
+ "Fn::And": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "CreateElasticLoadBalancer"
+ },
+ "true"
+ ]
+ },
+ {
+ "Condition": "ExistingVpcResources"
+ }
+ ]
+ },
+ "CreateELBForNewVpc": {
+ "Fn::And": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "CreateElasticLoadBalancer"
+ },
+ "true"
+ ]
+ },
+ {
+ "Condition": "CreateVpcResources"
+ }
+ ]
+ },
+ "CreateELB": {
+ "Fn::Or": [
+ {
+ "Condition": "CreateELBForExistingVpc"
+ },
+ {
+ "Condition": "CreateELBForNewVpc"
+ }
+ ]
+ },
+ "CreateEC2LCWithKeyPair": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "KeyName"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "CreateEC2LCWithoutKeyPair": {
+ "Fn::Equals": [
+ {
+ "Ref": "KeyName"
+ },
+ ""
+ ]
+ },
+ "UseSpecifiedVpcAvailabilityZones": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Fn::Join": [
+ "",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ ""
+ ]
+ }
+ ]
+ }
+ },
+ "Resources": {
+ "Vpc": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::VPC",
+ "Properties": {
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "vpc"
+ ]
+ },
+ "EnableDnsSupport": "true",
+ "EnableDnsHostnames": "true"
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "7baa7d45-f7fc-4051-8842-9b077fb482c3"
+ }
+ }
+ },
+ "PubSubnetAz1": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Subnet",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "pubsubnet1"
+ ]
+ },
+ "AvailabilityZone": {
+ "Fn::If": [
+ "UseSpecifiedVpcAvailabilityZones",
+ {
+ "Fn::Select": [
+ "0",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ "0",
+ {
+ "Fn::GetAZs": {
+ "Ref": "AWS::Region"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "cfdd2b8a-43a8-42d2-9398-f5d704fb212c"
+ }
+ }
+ },
+ "PubSubnetAz2": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Subnet",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "pubsubnet2"
+ ]
+ },
+ "AvailabilityZone": {
+ "Fn::If": [
+ "UseSpecifiedVpcAvailabilityZones",
+ {
+ "Fn::Select": [
+ "1",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ "1",
+ {
+ "Fn::GetAZs": {
+ "Ref": "AWS::Region"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac"
+ }
+ }
+ },
+ "InternetGateway": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::InternetGateway",
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "9f8719d0-0ae0-40ae-8fd1-eac39f5ea1f0"
+ }
+ }
+ },
+ "AttachGateway": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::VPCGatewayAttachment",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "InternetGatewayId": {
+ "Ref": "InternetGateway"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "de3a41b1-06dd-4f9c-b068-017df284ae07"
+ }
+ }
+ },
+ "RouteViaIgw": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::RouteTable",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7"
+ }
+ }
+ },
+ "PublicRouteViaIgw": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Route",
+ "DependsOn": "AttachGateway",
+ "Properties": {
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ },
+ "DestinationCidrBlock": "0.0.0.0/0",
+ "GatewayId": {
+ "Ref": "InternetGateway"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "7c4bd58c-9eec-4035-a8b7-a2de5f2fd7a6"
+ }
+ }
+ },
+ "PubSubnet1RouteTableAssociation": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::SubnetRouteTableAssociation",
+ "Properties": {
+ "SubnetId": {
+ "Ref": "PubSubnetAz1"
+ },
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "4ac039de-17bc-42ca-a3e4-5161a283958f"
+ }
+ }
+ },
+ "PubSubnet2RouteTableAssociation": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::SubnetRouteTableAssociation",
+ "Properties": {
+ "SubnetId": {
+ "Ref": "PubSubnetAz2"
+ },
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "b07014ab-c4cb-4a7c-9d3c-4856c4e85934"
+ }
+ }
+ },
+ "ElbSecurityGroup": {
+ "Type": "AWS::EC2::SecurityGroup",
+ "Properties": {
+ "GroupDescription": "ELB Allowed Ports",
+ "VpcId": {
+ "Fn::If": [
+ "CreateVpcResources",
+ {
+ "Ref": "Vpc"
+ },
+ {
+ "Ref": "VpcId"
+ }
+ ]
+ },
+ "SecurityGroupIngress": [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "ElbPort"
+ },
+ "ToPort": {
+ "Ref": "ElbPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ }
+ ]
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "1ebb0901-0c9d-47b7-864d-ba9352b7d38d"
+ }
+ }
+ },
+ "EcsSecurityGroup": {
+ "Type": "AWS::EC2::SecurityGroup",
+ "Properties": {
+ "GroupDescription": "ECS Allowed Ports",
+ "VpcId": {
+ "Fn::If": [
+ "CreateVpcResources",
+ {
+ "Ref": "Vpc"
+ },
+ {
+ "Ref": "VpcId"
+ }
+ ]
+ },
+ "SecurityGroupIngress": {
+ "Fn::If": [
+ "CreateELB",
+ [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "EcsPort"
+ },
+ "ToPort": {
+ "Ref": "EcsPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ },
+ {
+ "IpProtocol": "tcp",
+ "FromPort": "1",
+ "ToPort": "65535",
+ "SourceSecurityGroupId": {
+ "Ref": "ElbSecurityGroup"
+ }
+ }
+ ],
+ [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "EcsPort"
+ },
+ "ToPort": {
+ "Ref": "EcsPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ }
+ ]
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "0657b9b8-92bf-4a21-b2a6-84333178cb13"
+ }
+ }
+ },
+ "EcsElasticLoadBalancer": {
+ "Condition": "CreateELBForNewVpc",
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "SecurityGroups": [
+ {
+ "Ref": "ElbSecurityGroup"
+ }
+ ],
+ "Subnets": [
+ {
+ "Ref": "PubSubnetAz1"
+ },
+ {
+ "Ref": "PubSubnetAz2"
+ }
+ ],
+ "CrossZone": "true",
+ "Listeners": [
+ {
+ "LoadBalancerPort": {
+ "Ref": "ElbPort"
+ },
+ "InstancePort": {
+ "Ref": "EcsPort"
+ },
+ "Protocol": {
+ "Ref": "ElbProtocol"
+ }
+ }
+ ],
+ "HealthCheck": {
+ "Target": {
+ "Ref": "ElbHealthCheckTarget"
+ },
+ "HealthyThreshold": "2",
+ "UnhealthyThreshold": "10",
+ "Interval": "30",
+ "Timeout": "5"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "82f3104a-af43-4618-9cb7-563b066f5f4a"
+ }
+ }
+ },
+ "EcsElasticLoadBalancerExistingVpc": {
+ "Condition": "CreateELBForExistingVpc",
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "SecurityGroups": [
+ {
+ "Ref": "ElbSecurityGroup"
+ }
+ ],
+ "Subnets": {
+ "Ref": "SubnetIds"
+ },
+ "CrossZone": "true",
+ "Listeners": [
+ {
+ "LoadBalancerPort": {
+ "Ref": "ElbPort"
+ },
+ "InstancePort": {
+ "Ref": "EcsPort"
+ },
+ "Protocol": {
+ "Ref": "ElbProtocol"
+ }
+ }
+ ],
+ "HealthCheck": {
+ "Target": {
+ "Ref": "ElbHealthCheckTarget"
+ },
+ "HealthyThreshold": "2",
+ "UnhealthyThreshold": "10",
+ "Interval": "30",
+ "Timeout": "5"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "d748a8dc-4ad4-4a4b-948a-016b0521c5f5"
+ }
+ }
+ },
+ "EcsInstanceLc": {
+ "Condition": "CreateEC2LCWithKeyPair",
+ "Type": "AWS::AutoScaling::LaunchConfiguration",
+ "Properties": {
+ "ImageId": {
+ "Ref": "EcsAmiId"
+ },
+ "InstanceType": {
+ "Ref": "EcsInstanceType"
+ },
+ "AssociatePublicIpAddress": true,
+ "IamInstanceProfile": {
+ "Ref": "IamRoleInstanceProfile"
+ },
+ "KeyName": {
+ "Ref": "KeyName"
+ },
+ "SecurityGroups": [
+ {
+ "Ref": "EcsSecurityGroup"
+ }
+ ],
+ "UserData": {
+ "Fn::If": [
+ "SetEndpointToECSAgent",
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config",
+ "\necho ECS_BACKEND_HOST=",
+ {
+ "Ref": "EcsEndpoint"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ },
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "437dd5ef-6a7d-4714-b75e-17b287228740"
+ }
+ }
+ },
+ "EcsInstanceLcWithoutKeyPair": {
+ "Condition": "CreateEC2LCWithoutKeyPair",
+ "Type": "AWS::AutoScaling::LaunchConfiguration",
+ "Properties": {
+ "ImageId": {
+ "Ref": "EcsAmiId"
+ },
+ "InstanceType": {
+ "Ref": "EcsInstanceType"
+ },
+ "AssociatePublicIpAddress": true,
+ "IamInstanceProfile": {
+ "Ref": "IamRoleInstanceProfile"
+ },
+ "SecurityGroups": [
+ {
+ "Ref": "EcsSecurityGroup"
+ }
+ ],
+ "UserData": {
+ "Fn::If": [
+ "SetEndpointToECSAgent",
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config",
+ "\necho ECS_BACKEND_HOST=",
+ {
+ "Ref": "EcsEndpoint"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ },
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "51bfbee7-b0dc-4c86-998b-370a48473210"
+ }
+ }
+ },
+ "EcsInstanceAsg": {
+ "Type": "AWS::AutoScaling::AutoScalingGroup",
+ "Properties": {
+ "VPCZoneIdentifier": {
+ "Fn::If": [
+ "CreateVpcResources",
+ [
+ {
+ "Fn::Join": [
+ ",",
+ [
+ {
+ "Ref": "PubSubnetAz1"
+ },
+ {
+ "Ref": "PubSubnetAz2"
+ }
+ ]
+ ]
+ }
+ ],
+ {
+ "Ref": "SubnetIds"
+ }
+ ]
+ },
+ "LaunchConfigurationName": {
+ "Fn::If": [
+ "CreateEC2LCWithKeyPair",
+ {
+ "Ref": "EcsInstanceLc"
+ },
+ {
+ "Ref": "EcsInstanceLcWithoutKeyPair"
+ }
+ ]
+ },
+ "MinSize": "1",
+ "MaxSize": {
+ "Ref": "AsgMaxSize"
+ },
+ "DesiredCapacity": {
+ "Ref": "AsgMaxSize"
+ },
+ "Tags": [
+ {
+ "Key": "Name",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "ECS Instance - ",
+ {
+ "Ref": "AWS::StackName"
+ }
+ ]
+ ]
+ },
+ "PropagateAtLaunch": "true"
+ }
+ ]
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "e04c560a-c446-4b2c-9242-3dec6b0cf20d"
+ }
+ }
+ }
+ },
+ "Outputs": {
+ "EcsInstanceAsgName": {
+ "Description": "Auto Scaling Group Name for ECS Instances",
+ "Value": {
+ "Ref": "EcsInstanceAsg"
+ }
+ },
+ "EcsElbName": {
+ "Description": "Load Balancer for ECS Service",
+ "Value": {
+ "Fn::If": [
+ "CreateELB",
+ {
+ "Fn::If": [
+ "CreateELBForNewVpc",
+ {
+ "Ref": "EcsElasticLoadBalancer"
+ },
+ {
+ "Ref": "EcsElasticLoadBalancerExistingVpc"
+ }
+ ]
+ },
+ ""
+ ]
+ }
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "9f8719d0-0ae0-40ae-8fd1-eac39f5ea1f0": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 60,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": []
+ },
+ "7baa7d45-f7fc-4051-8842-9b077fb482c3": {
+ "size": {
+ "width": 600,
+ "height": 510
+ },
+ "position": {
+ "x": 60,
+ "y": 90
+ },
+ "z": 1,
+ "embeds": [
+ "1ebb0901-0c9d-47b7-864d-ba9352b7d38d",
+ "0657b9b8-92bf-4a21-b2a6-84333178cb13",
+ "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7",
+ "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac",
+ "cfdd2b8a-43a8-42d2-9398-f5d704fb212c"
+ ]
+ },
+ "1ebb0901-0c9d-47b7-864d-ba9352b7d38d": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 90,
+ "y": 450
+ },
+ "z": 2,
+ "parent": "7baa7d45-f7fc-4051-8842-9b077fb482c3",
+ "embeds": []
+ },
+ "d748a8dc-4ad4-4a4b-948a-016b0521c5f5": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 180,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "1ebb0901-0c9d-47b7-864d-ba9352b7d38d"
+ ]
+ },
+ "0657b9b8-92bf-4a21-b2a6-84333178cb13": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 210,
+ "y": 450
+ },
+ "z": 2,
+ "parent": "7baa7d45-f7fc-4051-8842-9b077fb482c3",
+ "embeds": [],
+ "isrelatedto": [
+ "1ebb0901-0c9d-47b7-864d-ba9352b7d38d"
+ ]
+ },
+ "51bfbee7-b0dc-4c86-998b-370a48473210": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 300,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "0657b9b8-92bf-4a21-b2a6-84333178cb13"
+ ]
+ },
+ "437dd5ef-6a7d-4714-b75e-17b287228740": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 420,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "0657b9b8-92bf-4a21-b2a6-84333178cb13"
+ ]
+ },
+ "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7": {
+ "size": {
+ "width": 240,
+ "height": 240
+ },
+ "position": {
+ "x": 90,
+ "y": 150
+ },
+ "z": 2,
+ "parent": "7baa7d45-f7fc-4051-8842-9b077fb482c3",
+ "embeds": [
+ "7c4bd58c-9eec-4035-a8b7-a2de5f2fd7a6"
+ ]
+ },
+ "de3a41b1-06dd-4f9c-b068-017df284ae07": {
+ "source": {
+ "id": "9f8719d0-0ae0-40ae-8fd1-eac39f5ea1f0"
+ },
+ "target": {
+ "id": "7baa7d45-f7fc-4051-8842-9b077fb482c3"
+ }
+ },
+ "7c4bd58c-9eec-4035-a8b7-a2de5f2fd7a6": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 120,
+ "y": 210
+ },
+ "z": 3,
+ "parent": "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7",
+ "embeds": [],
+ "references": [
+ "9f8719d0-0ae0-40ae-8fd1-eac39f5ea1f0"
+ ],
+ "dependson": [
+ "de3a41b1-06dd-4f9c-b068-017df284ae07"
+ ]
+ },
+ "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac": {
+ "size": {
+ "width": 150,
+ "height": 150
+ },
+ "position": {
+ "x": 390,
+ "y": 360
+ },
+ "z": 2,
+ "parent": "7baa7d45-f7fc-4051-8842-9b077fb482c3",
+ "embeds": []
+ },
+ "b07014ab-c4cb-4a7c-9d3c-4856c4e85934": {
+ "source": {
+ "id": "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7"
+ },
+ "target": {
+ "id": "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac"
+ }
+ },
+ "cfdd2b8a-43a8-42d2-9398-f5d704fb212c": {
+ "size": {
+ "width": 150,
+ "height": 150
+ },
+ "position": {
+ "x": 390,
+ "y": 150
+ },
+ "z": 2,
+ "parent": "7baa7d45-f7fc-4051-8842-9b077fb482c3",
+ "embeds": []
+ },
+ "e04c560a-c446-4b2c-9242-3dec6b0cf20d": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 540,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "isassociatedwith": [
+ "437dd5ef-6a7d-4714-b75e-17b287228740"
+ ],
+ "isrelatedto": [
+ "cfdd2b8a-43a8-42d2-9398-f5d704fb212c",
+ "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac",
+ "51bfbee7-b0dc-4c86-998b-370a48473210"
+ ]
+ },
+ "82f3104a-af43-4618-9cb7-563b066f5f4a": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 660,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "isconnectedto": [
+ "cfdd2b8a-43a8-42d2-9398-f5d704fb212c",
+ "14e8a9a7-a33a-4cc6-bb27-78ffa31346ac"
+ ],
+ "ismemberof": [
+ "1ebb0901-0c9d-47b7-864d-ba9352b7d38d"
+ ]
+ },
+ "4ac039de-17bc-42ca-a3e4-5161a283958f": {
+ "source": {
+ "id": "4cb0b374-d06d-4fd4-aee2-b774cab0e5a7"
+ },
+ "target": {
+ "id": "cfdd2b8a-43a8-42d2-9398-f5d704fb212c"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/12-CloudFormation/cloudformation_adpq_staging.template b/docs/12-CloudFormation/cloudformation_adpq_staging.template
new file mode 100644
index 0000000..466d608
--- /dev/null
+++ b/docs/12-CloudFormation/cloudformation_adpq_staging.template
@@ -0,0 +1,1116 @@
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "AWS CloudFormation template to create a new VPC or use an existing VPC for ECS deployment",
+ "Mappings": {
+ "VpcCidrs": {
+ "ap-northeast-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ap-southeast-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ap-southeast-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "ca-central-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-central-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-west-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "eu-west-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-east-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-east-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-west-1": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ },
+ "us-west-2": {
+ "vpc": "10.0.0.0/16",
+ "pubsubnet1": "10.0.0.0/24",
+ "pubsubnet2": "10.0.1.0/24"
+ }
+ }
+ },
+ "Parameters": {
+ "EcsAmiId": {
+ "Type": "String",
+ "Description": "ECS AMI Id"
+ },
+ "EcsInstanceType": {
+ "Type": "String",
+ "Description": "ECS EC2 instance type",
+ "Default": "t2.micro",
+ "ConstraintDescription": "must be a valid EC2 instance type."
+ },
+ "KeyName": {
+ "Type": "String",
+ "Description": "Optional - Name of an existing EC2 KeyPair to enable SSH access to the ECS instances",
+ "Default": ""
+ },
+ "VpcId": {
+ "Type": "String",
+ "Description": "Optional - VPC Id of existing VPC. Leave blank to have a new VPC created",
+ "Default": "",
+ "AllowedPattern": "^(?:vpc-[0-9a-f]{8}|)$",
+ "ConstraintDescription": "VPC Id must begin with 'vpc-' or leave blank to have a new VPC created"
+ },
+ "SubnetIds": {
+ "Type": "CommaDelimitedList",
+ "Description": "Optional - Comma separated list of existing VPC Subnet Ids where ECS instances will run",
+ "Default": ""
+ },
+ "AsgMaxSize": {
+ "Type": "Number",
+ "Description": "Maximum size and initial Desired Capacity of ECS Auto Scaling Group",
+ "Default": "1"
+ },
+ "IamRoleInstanceProfile": {
+ "Type": "String",
+ "Description": "Name or the Amazon Resource Name (ARN) of the instance profile associated with the IAM role for the instance"
+ },
+ "EcsClusterName": {
+ "Type": "String",
+ "Description": "ECS Cluster Name",
+ "Default": "default"
+ },
+ "EcsPort": {
+ "Type": "String",
+ "Description": "Optional - Security Group port to open on ECS instances - defaults to port 80",
+ "Default": "80"
+ },
+ "ElbPort": {
+ "Type": "String",
+ "Description": "Optional - Security Group port to open on ELB - port 80 will be open by default",
+ "Default": "80"
+ },
+ "ElbProtocol": {
+ "Type": "String",
+ "Description": "Optional - ELB Protocol - defaults to HTTP",
+ "Default": "HTTP"
+ },
+ "ElbHealthCheckTarget": {
+ "Type": "String",
+ "Description": "Optional - Health Check Target for ELB - defaults to HTTP:80/",
+ "Default": "HTTP:80/"
+ },
+ "SourceCidr": {
+ "Type": "String",
+ "Description": "Optional - CIDR/IP range for EcsPort and ElbPort - defaults to 0.0.0.0/0",
+ "Default": "0.0.0.0/0"
+ },
+ "EcsEndpoint": {
+ "Type": "String",
+ "Description": "Optional : ECS Endpoint for the ECS Agent to connect to",
+ "Default": ""
+ },
+ "CreateElasticLoadBalancer": {
+ "Type": "String",
+ "Description": "Optional : When set to true, creates a ELB for ECS Service",
+ "Default": "false"
+ },
+ "VpcAvailabilityZones": {
+ "Type": "CommaDelimitedList",
+ "Description": "Optional : Comma-delimited list of two VPC availability zones in which to create subnets",
+ "Default": ""
+ }
+ },
+ "Conditions": {
+ "CreateVpcResources": {
+ "Fn::Equals": [
+ {
+ "Ref": "VpcId"
+ },
+ ""
+ ]
+ },
+ "ExistingVpcResources": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "VpcId"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "SetEndpointToECSAgent": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "EcsEndpoint"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "CreateELBForExistingVpc": {
+ "Fn::And": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "CreateElasticLoadBalancer"
+ },
+ "true"
+ ]
+ },
+ {
+ "Condition": "ExistingVpcResources"
+ }
+ ]
+ },
+ "CreateELBForNewVpc": {
+ "Fn::And": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "CreateElasticLoadBalancer"
+ },
+ "true"
+ ]
+ },
+ {
+ "Condition": "CreateVpcResources"
+ }
+ ]
+ },
+ "CreateELB": {
+ "Fn::Or": [
+ {
+ "Condition": "CreateELBForExistingVpc"
+ },
+ {
+ "Condition": "CreateELBForNewVpc"
+ }
+ ]
+ },
+ "CreateEC2LCWithKeyPair": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Ref": "KeyName"
+ },
+ ""
+ ]
+ }
+ ]
+ },
+ "CreateEC2LCWithoutKeyPair": {
+ "Fn::Equals": [
+ {
+ "Ref": "KeyName"
+ },
+ ""
+ ]
+ },
+ "UseSpecifiedVpcAvailabilityZones": {
+ "Fn::Not": [
+ {
+ "Fn::Equals": [
+ {
+ "Fn::Join": [
+ "",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ ""
+ ]
+ }
+ ]
+ }
+ },
+ "Resources": {
+ "Vpc": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::VPC",
+ "Properties": {
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "vpc"
+ ]
+ },
+ "EnableDnsSupport": "true",
+ "EnableDnsHostnames": "true"
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "d38414ea-1374-43c6-93b4-466c2f56ac94"
+ }
+ }
+ },
+ "PubSubnetAz1": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Subnet",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "pubsubnet1"
+ ]
+ },
+ "AvailabilityZone": {
+ "Fn::If": [
+ "UseSpecifiedVpcAvailabilityZones",
+ {
+ "Fn::Select": [
+ "0",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ "0",
+ {
+ "Fn::GetAZs": {
+ "Ref": "AWS::Region"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "4530042c-8a1b-45cc-90f9-5eef13ae138f"
+ }
+ }
+ },
+ "PubSubnetAz2": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Subnet",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "CidrBlock": {
+ "Fn::FindInMap": [
+ "VpcCidrs",
+ {
+ "Ref": "AWS::Region"
+ },
+ "pubsubnet2"
+ ]
+ },
+ "AvailabilityZone": {
+ "Fn::If": [
+ "UseSpecifiedVpcAvailabilityZones",
+ {
+ "Fn::Select": [
+ "1",
+ {
+ "Ref": "VpcAvailabilityZones"
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ "1",
+ {
+ "Fn::GetAZs": {
+ "Ref": "AWS::Region"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "fb54e06e-fa39-45c3-9460-11b58472370b"
+ }
+ }
+ },
+ "InternetGateway": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::InternetGateway",
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "510a8ef3-dd79-49d8-b60a-3498054e501f"
+ }
+ }
+ },
+ "AttachGateway": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::VPCGatewayAttachment",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ },
+ "InternetGatewayId": {
+ "Ref": "InternetGateway"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "f03c1336-06da-41cf-a1db-4d3d02e74b8e"
+ }
+ }
+ },
+ "RouteViaIgw": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::RouteTable",
+ "Properties": {
+ "VpcId": {
+ "Ref": "Vpc"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69"
+ }
+ }
+ },
+ "PublicRouteViaIgw": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::Route",
+ "DependsOn": "AttachGateway",
+ "Properties": {
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ },
+ "DestinationCidrBlock": "0.0.0.0/0",
+ "GatewayId": {
+ "Ref": "InternetGateway"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "409548d0-5840-4f30-9a67-c917a7f64e49"
+ }
+ }
+ },
+ "PubSubnet1RouteTableAssociation": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::SubnetRouteTableAssociation",
+ "Properties": {
+ "SubnetId": {
+ "Ref": "PubSubnetAz1"
+ },
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "de70c8b9-a3ef-4f3b-aa72-f86929ee4bed"
+ }
+ }
+ },
+ "PubSubnet2RouteTableAssociation": {
+ "Condition": "CreateVpcResources",
+ "Type": "AWS::EC2::SubnetRouteTableAssociation",
+ "Properties": {
+ "SubnetId": {
+ "Ref": "PubSubnetAz2"
+ },
+ "RouteTableId": {
+ "Ref": "RouteViaIgw"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "507439a3-ad05-451b-a320-81cbb673743b"
+ }
+ }
+ },
+ "ElbSecurityGroup": {
+ "Type": "AWS::EC2::SecurityGroup",
+ "Properties": {
+ "GroupDescription": "ELB Allowed Ports",
+ "VpcId": {
+ "Fn::If": [
+ "CreateVpcResources",
+ {
+ "Ref": "Vpc"
+ },
+ {
+ "Ref": "VpcId"
+ }
+ ]
+ },
+ "SecurityGroupIngress": [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "ElbPort"
+ },
+ "ToPort": {
+ "Ref": "ElbPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ }
+ ]
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "a54b3814-1acf-4bd3-a95e-84989f7c09e8"
+ }
+ }
+ },
+ "EcsSecurityGroup": {
+ "Type": "AWS::EC2::SecurityGroup",
+ "Properties": {
+ "GroupDescription": "ECS Allowed Ports",
+ "VpcId": {
+ "Fn::If": [
+ "CreateVpcResources",
+ {
+ "Ref": "Vpc"
+ },
+ {
+ "Ref": "VpcId"
+ }
+ ]
+ },
+ "SecurityGroupIngress": {
+ "Fn::If": [
+ "CreateELB",
+ [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "EcsPort"
+ },
+ "ToPort": {
+ "Ref": "EcsPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ },
+ {
+ "IpProtocol": "tcp",
+ "FromPort": "1",
+ "ToPort": "65535",
+ "SourceSecurityGroupId": {
+ "Ref": "ElbSecurityGroup"
+ }
+ }
+ ],
+ [
+ {
+ "IpProtocol": "tcp",
+ "FromPort": {
+ "Ref": "EcsPort"
+ },
+ "ToPort": {
+ "Ref": "EcsPort"
+ },
+ "CidrIp": {
+ "Ref": "SourceCidr"
+ }
+ }
+ ]
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "da33979a-ede9-49ed-927d-dd754b21fd61"
+ }
+ }
+ },
+ "EcsElasticLoadBalancer": {
+ "Condition": "CreateELBForNewVpc",
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "SecurityGroups": [
+ {
+ "Ref": "ElbSecurityGroup"
+ }
+ ],
+ "Subnets": [
+ {
+ "Ref": "PubSubnetAz1"
+ },
+ {
+ "Ref": "PubSubnetAz2"
+ }
+ ],
+ "CrossZone": "true",
+ "Listeners": [
+ {
+ "LoadBalancerPort": {
+ "Ref": "ElbPort"
+ },
+ "InstancePort": {
+ "Ref": "EcsPort"
+ },
+ "Protocol": {
+ "Ref": "ElbProtocol"
+ }
+ }
+ ],
+ "HealthCheck": {
+ "Target": {
+ "Ref": "ElbHealthCheckTarget"
+ },
+ "HealthyThreshold": "2",
+ "UnhealthyThreshold": "10",
+ "Interval": "30",
+ "Timeout": "5"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "59a181ba-5055-412a-b849-302db4673ec9"
+ }
+ }
+ },
+ "EcsElasticLoadBalancerExistingVpc": {
+ "Condition": "CreateELBForExistingVpc",
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "SecurityGroups": [
+ {
+ "Ref": "ElbSecurityGroup"
+ }
+ ],
+ "Subnets": {
+ "Ref": "SubnetIds"
+ },
+ "CrossZone": "true",
+ "Listeners": [
+ {
+ "LoadBalancerPort": {
+ "Ref": "ElbPort"
+ },
+ "InstancePort": {
+ "Ref": "EcsPort"
+ },
+ "Protocol": {
+ "Ref": "ElbProtocol"
+ }
+ }
+ ],
+ "HealthCheck": {
+ "Target": {
+ "Ref": "ElbHealthCheckTarget"
+ },
+ "HealthyThreshold": "2",
+ "UnhealthyThreshold": "10",
+ "Interval": "30",
+ "Timeout": "5"
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "73bb2c17-09a2-44ed-800b-1741adea793f"
+ }
+ }
+ },
+ "EcsInstanceLc": {
+ "Condition": "CreateEC2LCWithKeyPair",
+ "Type": "AWS::AutoScaling::LaunchConfiguration",
+ "Properties": {
+ "ImageId": {
+ "Ref": "EcsAmiId"
+ },
+ "InstanceType": {
+ "Ref": "EcsInstanceType"
+ },
+ "AssociatePublicIpAddress": true,
+ "IamInstanceProfile": {
+ "Ref": "IamRoleInstanceProfile"
+ },
+ "KeyName": {
+ "Ref": "KeyName"
+ },
+ "SecurityGroups": [
+ {
+ "Ref": "EcsSecurityGroup"
+ }
+ ],
+ "UserData": {
+ "Fn::If": [
+ "SetEndpointToECSAgent",
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config",
+ "\necho ECS_BACKEND_HOST=",
+ {
+ "Ref": "EcsEndpoint"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ },
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "c88e8822-f824-4602-8bd8-8576f3c31cd4"
+ }
+ }
+ },
+ "EcsInstanceLcWithoutKeyPair": {
+ "Condition": "CreateEC2LCWithoutKeyPair",
+ "Type": "AWS::AutoScaling::LaunchConfiguration",
+ "Properties": {
+ "ImageId": {
+ "Ref": "EcsAmiId"
+ },
+ "InstanceType": {
+ "Ref": "EcsInstanceType"
+ },
+ "AssociatePublicIpAddress": true,
+ "IamInstanceProfile": {
+ "Ref": "IamRoleInstanceProfile"
+ },
+ "SecurityGroups": [
+ {
+ "Ref": "EcsSecurityGroup"
+ }
+ ],
+ "UserData": {
+ "Fn::If": [
+ "SetEndpointToECSAgent",
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config",
+ "\necho ECS_BACKEND_HOST=",
+ {
+ "Ref": "EcsEndpoint"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ },
+ {
+ "Fn::Base64": {
+ "Fn::Join": [
+ "",
+ [
+ "#!/bin/bash\n",
+ "echo ECS_CLUSTER=",
+ {
+ "Ref": "EcsClusterName"
+ },
+ " >> /etc/ecs/ecs.config"
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "1021973c-c768-45ed-9ebc-73d846933de0"
+ }
+ }
+ },
+ "EcsInstanceAsg": {
+ "Type": "AWS::AutoScaling::AutoScalingGroup",
+ "Properties": {
+ "VPCZoneIdentifier": {
+ "Fn::If": [
+ "CreateVpcResources",
+ [
+ {
+ "Fn::Join": [
+ ",",
+ [
+ {
+ "Ref": "PubSubnetAz1"
+ },
+ {
+ "Ref": "PubSubnetAz2"
+ }
+ ]
+ ]
+ }
+ ],
+ {
+ "Ref": "SubnetIds"
+ }
+ ]
+ },
+ "LaunchConfigurationName": {
+ "Fn::If": [
+ "CreateEC2LCWithKeyPair",
+ {
+ "Ref": "EcsInstanceLc"
+ },
+ {
+ "Ref": "EcsInstanceLcWithoutKeyPair"
+ }
+ ]
+ },
+ "MinSize": "1",
+ "MaxSize": {
+ "Ref": "AsgMaxSize"
+ },
+ "DesiredCapacity": {
+ "Ref": "AsgMaxSize"
+ },
+ "Tags": [
+ {
+ "Key": "Name",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "ECS Instance - ",
+ {
+ "Ref": "AWS::StackName"
+ }
+ ]
+ ]
+ },
+ "PropagateAtLaunch": "true"
+ }
+ ]
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "id": "e1ecde09-a63e-4340-b61f-8fd355adab56"
+ }
+ }
+ }
+ },
+ "Outputs": {
+ "EcsInstanceAsgName": {
+ "Description": "Auto Scaling Group Name for ECS Instances",
+ "Value": {
+ "Ref": "EcsInstanceAsg"
+ }
+ },
+ "EcsElbName": {
+ "Description": "Load Balancer for ECS Service",
+ "Value": {
+ "Fn::If": [
+ "CreateELB",
+ {
+ "Fn::If": [
+ "CreateELBForNewVpc",
+ {
+ "Ref": "EcsElasticLoadBalancer"
+ },
+ {
+ "Ref": "EcsElasticLoadBalancerExistingVpc"
+ }
+ ]
+ },
+ ""
+ ]
+ }
+ }
+ },
+ "Metadata": {
+ "AWS::CloudFormation::Designer": {
+ "510a8ef3-dd79-49d8-b60a-3498054e501f": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 60,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": []
+ },
+ "d38414ea-1374-43c6-93b4-466c2f56ac94": {
+ "size": {
+ "width": 600,
+ "height": 510
+ },
+ "position": {
+ "x": 60,
+ "y": 90
+ },
+ "z": 1,
+ "embeds": [
+ "a54b3814-1acf-4bd3-a95e-84989f7c09e8",
+ "da33979a-ede9-49ed-927d-dd754b21fd61",
+ "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69",
+ "fb54e06e-fa39-45c3-9460-11b58472370b",
+ "4530042c-8a1b-45cc-90f9-5eef13ae138f"
+ ]
+ },
+ "a54b3814-1acf-4bd3-a95e-84989f7c09e8": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 90,
+ "y": 450
+ },
+ "z": 2,
+ "parent": "d38414ea-1374-43c6-93b4-466c2f56ac94",
+ "embeds": []
+ },
+ "73bb2c17-09a2-44ed-800b-1741adea793f": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 180,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "a54b3814-1acf-4bd3-a95e-84989f7c09e8"
+ ]
+ },
+ "da33979a-ede9-49ed-927d-dd754b21fd61": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 210,
+ "y": 450
+ },
+ "z": 2,
+ "parent": "d38414ea-1374-43c6-93b4-466c2f56ac94",
+ "embeds": [],
+ "isrelatedto": [
+ "a54b3814-1acf-4bd3-a95e-84989f7c09e8"
+ ]
+ },
+ "1021973c-c768-45ed-9ebc-73d846933de0": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 300,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "da33979a-ede9-49ed-927d-dd754b21fd61"
+ ]
+ },
+ "c88e8822-f824-4602-8bd8-8576f3c31cd4": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 420,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "ismemberof": [
+ "da33979a-ede9-49ed-927d-dd754b21fd61"
+ ]
+ },
+ "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69": {
+ "size": {
+ "width": 240,
+ "height": 240
+ },
+ "position": {
+ "x": 90,
+ "y": 150
+ },
+ "z": 2,
+ "parent": "d38414ea-1374-43c6-93b4-466c2f56ac94",
+ "embeds": [
+ "409548d0-5840-4f30-9a67-c917a7f64e49"
+ ]
+ },
+ "f03c1336-06da-41cf-a1db-4d3d02e74b8e": {
+ "source": {
+ "id": "510a8ef3-dd79-49d8-b60a-3498054e501f"
+ },
+ "target": {
+ "id": "d38414ea-1374-43c6-93b4-466c2f56ac94"
+ }
+ },
+ "409548d0-5840-4f30-9a67-c917a7f64e49": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 120,
+ "y": 210
+ },
+ "z": 3,
+ "parent": "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69",
+ "embeds": [],
+ "references": [
+ "510a8ef3-dd79-49d8-b60a-3498054e501f"
+ ],
+ "dependson": [
+ "f03c1336-06da-41cf-a1db-4d3d02e74b8e"
+ ]
+ },
+ "fb54e06e-fa39-45c3-9460-11b58472370b": {
+ "size": {
+ "width": 150,
+ "height": 150
+ },
+ "position": {
+ "x": 390,
+ "y": 360
+ },
+ "z": 2,
+ "parent": "d38414ea-1374-43c6-93b4-466c2f56ac94",
+ "embeds": []
+ },
+ "507439a3-ad05-451b-a320-81cbb673743b": {
+ "source": {
+ "id": "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69"
+ },
+ "target": {
+ "id": "fb54e06e-fa39-45c3-9460-11b58472370b"
+ }
+ },
+ "4530042c-8a1b-45cc-90f9-5eef13ae138f": {
+ "size": {
+ "width": 150,
+ "height": 150
+ },
+ "position": {
+ "x": 390,
+ "y": 150
+ },
+ "z": 2,
+ "parent": "d38414ea-1374-43c6-93b4-466c2f56ac94",
+ "embeds": []
+ },
+ "e1ecde09-a63e-4340-b61f-8fd355adab56": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 540,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "isassociatedwith": [
+ "c88e8822-f824-4602-8bd8-8576f3c31cd4"
+ ],
+ "isrelatedto": [
+ "4530042c-8a1b-45cc-90f9-5eef13ae138f",
+ "fb54e06e-fa39-45c3-9460-11b58472370b",
+ "1021973c-c768-45ed-9ebc-73d846933de0"
+ ]
+ },
+ "59a181ba-5055-412a-b849-302db4673ec9": {
+ "size": {
+ "width": 60,
+ "height": 60
+ },
+ "position": {
+ "x": 660,
+ "y": 660
+ },
+ "z": 1,
+ "embeds": [],
+ "isconnectedto": [
+ "4530042c-8a1b-45cc-90f9-5eef13ae138f",
+ "fb54e06e-fa39-45c3-9460-11b58472370b"
+ ],
+ "ismemberof": [
+ "a54b3814-1acf-4bd3-a95e-84989f7c09e8"
+ ]
+ },
+ "de70c8b9-a3ef-4f3b-aa72-f86929ee4bed": {
+ "source": {
+ "id": "286bc9f7-03c6-46b5-9e1a-8bc22cc1fa69"
+ },
+ "target": {
+ "id": "4530042c-8a1b-45cc-90f9-5eef13ae138f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/javascript/components/Header.spec.js b/test/javascript/components/Header.spec.js
index 631fc48..b5552c1 100644
--- a/test/javascript/components/Header.spec.js
+++ b/test/javascript/components/Header.spec.js
@@ -8,13 +8,6 @@ it('knows to render the default menu', () => {
expect(rendered.text()).toContain('')
})
-it('renders the default menu', () => {
- const rendered = shallow()
- expect(rendered.text()).toContain('Tech Shop')
- expect(rendered.text()).toContain('Cart')
- expect(rendered.text()).toContain('Account')
-})
-
it('renders no menu when set to login', () => {
const rendered = render()
expect(rendered.text()).toContain('Tech Shop')
@@ -26,11 +19,4 @@ it('renders admin menu when set to admin', () => {
expect(rendered.text()).toContain('Tech Shop Admin')
expect(rendered.text()).toContain('Tech Shop Site')
expect(rendered.text()).toContain('Logout')
- expect(rendered.text()).toContain('Orders')
- expect(rendered.text()).toContain('Catalog')
})
-
-it('renders a cart count when passed in', () => {
- const rendered = shallow()
- expect(rendered.text()).toContain('Cart 133')
-})
\ No newline at end of file
diff --git a/test/javascript/components/Order.spec.js b/test/javascript/components/Order.spec.js
index 73ca342..3ceb89a 100644
--- a/test/javascript/components/Order.spec.js
+++ b/test/javascript/components/Order.spec.js
@@ -27,19 +27,7 @@ describe('', () => {
it('renders an order detail view', () => {
const rendered = render();
- expect(rendered.html()).toContain('
Order #1324
');
- })
-
- it('should let admins sees sku links', () => {
- const rendered = render ();
- expect(rendered.html()).toContain('39103');
- expect(rendered.html()).toContain('439103');
- })
-
- it('should not let requestor see sku link', () => {
- props.isAdmin = false;
- const rendered = render();
- expect(rendered.html()).not.toContain('39103');
+ expect(rendered.html()).toContain('Order #1324');
})
});
diff --git a/web/static/css/_utilities.scss b/web/static/css/_utilities.scss
index a365dc7..19e5b09 100644
--- a/web/static/css/_utilities.scss
+++ b/web/static/css/_utilities.scss
@@ -1 +1,13 @@
+@import 'node_modules/uswds/src/stylesheets/lib/bourbon';
+@import 'node_modules/uswds/src/stylesheets/core/variables';
@import 'node_modules/uswds/src/stylesheets/core/utilities';
+
+@mixin link {
+ color: $color-primary;
+ text-decoration: underline;
+
+ &:hover,
+ &:active {
+ color: $color-primary-darker;
+ }
+}
diff --git a/web/static/css/_variables.scss b/web/static/css/_variables.scss
index 6c08993..a13d420 100644
--- a/web/static/css/_variables.scss
+++ b/web/static/css/_variables.scss
@@ -1,3 +1,4 @@
@import 'node_modules/uswds/src/stylesheets/lib/bourbon';
@import 'node_modules/uswds/src/stylesheets/core/variables';
+
$button-stroke: inset 0 0 0 2px;
\ No newline at end of file
diff --git a/web/static/css/app.scss b/web/static/css/app.scss
index 7e22ef6..cc9b9be 100644
--- a/web/static/css/app.scss
+++ b/web/static/css/app.scss
@@ -18,13 +18,12 @@ h1, h2, h3, h4 {
font-family: "Source Sans Pro", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
}
-h2 {
+h1 {
font-weight: 300;
margin: 0 0 10px 0;
- font-size: 40px;
}
-h4 {
+h3, h4 {
font-size: $lead-font-size;
margin: 0;
}
diff --git a/web/static/css/category.scss b/web/static/css/category.scss
index 5f455ae..98ef844 100644
--- a/web/static/css/category.scss
+++ b/web/static/css/category.scss
@@ -23,6 +23,13 @@
padding: 1.5em 0;
}
+.category-toggle-details,
+.category-toggle-filters {
+ button {
+ @include link;
+ }
+}
+
.category-filter-sections-hidden,
.category-item-details-hidden ul {
display: none;
@@ -54,4 +61,8 @@
.category-sort-section {
line-height: 4.4rem;
}
+
+ .category-sort-label {
+ margin: 0;
+ }
}
diff --git a/web/static/css/homepage.scss b/web/static/css/homepage.scss
index e571740..9f8650c 100644
--- a/web/static/css/homepage.scss
+++ b/web/static/css/homepage.scss
@@ -4,8 +4,8 @@
padding: 1rem 0;
border-top: 2px solid $color-gray-lighter;
@include clearfix;
- h4 {
- padding-top: 10px;
+ h3 {
+ margin: 0;
}
p {
padding: 0 0 20px 0;
diff --git a/web/static/css/item-form.scss b/web/static/css/item-form.scss
index ffbcfc4..2968729 100644
--- a/web/static/css/item-form.scss
+++ b/web/static/css/item-form.scss
@@ -7,7 +7,7 @@
}
.item-form-subheading {
- line-height: .7;
+ line-height: .2;
margin: .5em 0;
padding: 1rem 0;
}
diff --git a/web/static/css/login.scss b/web/static/css/login.scss
index d382f88..4cbbaf1 100644
--- a/web/static/css/login.scss
+++ b/web/static/css/login.scss
@@ -1,7 +1,14 @@
+@import 'utilities';
+
.login {
margin-bottom: 10rem;
}
+
.login-show-password {
text-align: right;
max-width: 46rem;
+
+ button {
+ @include link;
+ }
}
\ No newline at end of file
diff --git a/web/static/css/order.scss b/web/static/css/order.scss
index e43a985..ea74ca9 100644
--- a/web/static/css/order.scss
+++ b/web/static/css/order.scss
@@ -38,7 +38,7 @@
}
.order-total {
- h3 {
+ h4 {
margin: 0 0 2rem 0;
}
button {
diff --git a/web/static/css/uswds-bugfixes.css b/web/static/css/uswds-bugfixes.scss
similarity index 63%
rename from web/static/css/uswds-bugfixes.css
rename to web/static/css/uswds-bugfixes.scss
index 267a6aa..bdecf43 100644
--- a/web/static/css/uswds-bugfixes.css
+++ b/web/static/css/uswds-bugfixes.scss
@@ -3,3 +3,11 @@
.usa-width-one-third:nth-child(3n) {
margin-right: 0;
}
+
+.usa-button-unstyled {
+ color: inherit;
+
+ &:hover {
+ color: inherit;
+ }
+}
diff --git a/web/static/js/actions/index.js b/web/static/js/actions/index.js
index 07e24c3..6301769 100644
--- a/web/static/js/actions/index.js
+++ b/web/static/js/actions/index.js
@@ -380,6 +380,7 @@ export function createItem(item) {
.then(() => dispatch(createItemSuccess()))
.then(() => dispatch(alert(createItemSuccess())))
.then(() => dispatch(fetchAdminCatalog()))
+ .then(() => dispatch(fetchCatalog()))
.catch(error => dispatch(createItemError(error))); // TODO flash message
}
@@ -411,6 +412,7 @@ export function updateItem(item) {
.then(() => dispatch(updateItemSuccess()))
.then(() => dispatch(alert(updateItemSuccess())))
.then(() => dispatch(fetchAdminCatalog()))
+ .then(() => dispatch(fetchCatalog()))
.catch(error => dispatch(updateItemError(error))); // TODO flash message
}
@@ -433,12 +435,13 @@ export function cancelOrder(order, admin = false) {
body: JSON.stringify({ status: 'CANCELLED' })
};
const url = admin ? `/api/admin/orders/${order.id}` : `/api/user/${getUserData().id}/orders/${order.id}`;
- const refresh = admin ? fetchAdminOrders : fetchOrders;
+ const maybeFetchAdminOrders = dispatch => () => admin && dispatch(fetchAdminOrders());
return dispatch => fetch(url, requestWithAuth(request))
.then(checkHttpStatus)
.then(() => dispatch(cancelOrderSuccess(admin)))
.then(() => dispatch(alert(cancelOrderSuccess(admin))))
- .then(() => dispatch(refresh()))
+ .then(maybeFetchAdminOrders(dispatch))
+ .then(() => dispatch(fetchOrders()))
.catch(error => dispatch(cancelOrderError(admin, error))); // TODO flash message
}
diff --git a/web/static/js/components/Cart/Cart.js b/web/static/js/components/Cart/Cart.js
index 0014482..b177bec 100644
--- a/web/static/js/components/Cart/Cart.js
+++ b/web/static/js/components/Cart/Cart.js
@@ -32,7 +32,7 @@ export default class Cart extends Component {
return (
-
Your Cart
+ Your Cart
diff --git a/web/static/js/components/CartItem/CartItem.js b/web/static/js/components/CartItem/CartItem.js
index df90aa7..4b9efa3 100644
--- a/web/static/js/components/CartItem/CartItem.js
+++ b/web/static/js/components/CartItem/CartItem.js
@@ -41,11 +41,11 @@ class CartItem extends Component {
-
+
-
{item.name}
+
{item.name}
{item.manufacturer} SKU: {item.sku}
diff --git a/web/static/js/components/CatalogItem/CatalogItem.js b/web/static/js/components/CatalogItem/CatalogItem.js
index e1ff41f..43afe53 100644
--- a/web/static/js/components/CatalogItem/CatalogItem.js
+++ b/web/static/js/components/CatalogItem/CatalogItem.js
@@ -41,9 +41,9 @@ class CatalogItem extends Component {
const { item } = this.props;
return (
-
{this.maybeLink(
)}
+
{this.maybeLink(
)}
-
{this.maybeLink(item.name)}
+
{this.maybeLink(item.name)}
{this.maybeLink(`${item.manufacturer} SKU: ${item.sku}`)}
{item.description.split(',').length ?
diff --git a/web/static/js/components/Category/Category.js b/web/static/js/components/Category/Category.js
index 4344fa4..39139f5 100644
--- a/web/static/js/components/Category/Category.js
+++ b/web/static/js/components/Category/Category.js
@@ -117,7 +117,7 @@ export default class Category extends Component {
return (
-
{title}
+
{title}
{category.fields[field].map(value => (-
@@ -137,14 +137,14 @@ export default class Category extends Component {
return (
-
{category.name}
+ {category.name}
@@ -29,14 +29,14 @@ class HeaderAdmin extends Component {
diff --git a/web/static/js/components/Header/HeaderDefault.js b/web/static/js/components/Header/HeaderDefault.js
index 44d3089..7fa0d48 100644
--- a/web/static/js/components/Header/HeaderDefault.js
+++ b/web/static/js/components/Header/HeaderDefault.js
@@ -19,7 +19,7 @@ export default class HeaderDefault extends Component {
@@ -100,7 +100,7 @@ export default class HeaderDefault extends Component {
-
-
+
Cart
{cartCount !== 0 ? (
{cartCount}
) : ''}
-
+
-
- Account
+ Account
-
diff --git a/web/static/js/components/Header/HeaderLogin.js b/web/static/js/components/Header/HeaderLogin.js
index 20d1224..44bf44b 100644
--- a/web/static/js/components/Header/HeaderLogin.js
+++ b/web/static/js/components/Header/HeaderLogin.js
@@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react';
+import { Link } from 'react-router';
class HeaderLogin extends Component {
static propTypes = {
@@ -15,7 +16,7 @@ class HeaderLogin extends Component {
diff --git a/web/static/js/components/Homepage/Homepage.js b/web/static/js/components/Homepage/Homepage.js
index 0dae363..dc4fe96 100644
--- a/web/static/js/components/Homepage/Homepage.js
+++ b/web/static/js/components/Homepage/Homepage.js
@@ -23,12 +23,12 @@ export default class Homepage extends React.Component {
-
Desktop or laptop?
+
Desktop or laptop?
Options and pricing for every hardware need.
-
Popular Configurations
+
Popular Configurations
{this.props.recommendations && this.props.recommendations.length ?
this.props.recommendations.map(recommendation => (
diff --git a/web/static/js/components/ItemDetail/ItemDetail.js b/web/static/js/components/ItemDetail/ItemDetail.js
index e741ca1..9bfa0b4 100644
--- a/web/static/js/components/ItemDetail/ItemDetail.js
+++ b/web/static/js/components/ItemDetail/ItemDetail.js
@@ -25,7 +25,7 @@ export default class ItemDetail extends React.Component {
return (
-
Product Detail
+ Product Detail
diff --git a/web/static/js/components/LoginForm/LoginForm.js b/web/static/js/components/LoginForm/LoginForm.js
index e68bc36..7a2d07e 100644
--- a/web/static/js/components/LoginForm/LoginForm.js
+++ b/web/static/js/components/LoginForm/LoginForm.js
@@ -26,16 +26,20 @@ class LoginForm extends React.Component {
const { handleSubmit } = this.props;
return (