Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ember Deployment Automation with AWS and Terraform

Ember Deployment Automation with AWS and Terraform

This presentation was given first at Berlin Ember meetup on September 11, 2018. It's a step-by-step walkthrough and an overview of key parts of the code, as well as highlights of how Terraform works and helps manage and maintain AWS (and other) infrastructure.

Resources:
A companion blog post:
https://medium.com/@piotr.steininger/automating-ember-js-app-deployment-on-aws-feccc6d94828

Example Ember App:
https://github.com/psteininger/ember-deploy-app

Example Terraform project (can be used as a module)
https://github.com/psteininger/ember-deploy-aws

Piotr Steininger

September 11, 2018
Tweet

More Decks by Piotr Steininger

Other Decks in Technology

Transcript

  1. Why AWS? Why Terraform? ➡ Building new, complex internal tools

    ➡ AWS Activate credit via 500 Startups ➡ Backend developed with Elixir + Phoenix ➡ to be hosted on AWS ➡ Need for increased security (VPC, etc) ➡ AWS is incredibly complex ➡ Insatiable curiosity, monumental challenge 2
  2. End Goal ➡ Heroku-like setup ➡ push to master deploy

    to staging ➡ push to production deploy to production ➡ Host on AWS ➡ S3 + Cloudfront ➡ Cloudflare for DNS ➡ Cheap CI/CD with CodeBuild ➡ source on GitHub 3
  3. The Plan ➡ Use ember-cli-deploy ➡ ember-cli-deploy-s3-pack ➡ ember-cli-deploy-cloudfront ➡

    Host on AWS ➡ separate setup for staging and production ➡ don't make a glorious mess ➡ document infrastructure 4
  4. To the rescue... ➡Declarative infrastructure as code ➡Modular design -

    share and reuse ➡Multiple provider support ➡Open-source, decent community ➡Allows for collaboration ➡Prevents many stupid mistakes ➡Shows current state 5
  5. App Setup - The Easy Part ember install ember-cli-deploy ember

    install ember-cli-deploy-s3-pack ember install ember-cli-deploy-cloudfront Edit config/deploy.js Add buildspec.yml git push origin master Voila! 6
  6. Infrastructure Setup - The Part 1. Get AWS account and

    create API user 2. Install and configure AWS CLI 3. Install Terraform, get GitHub and Cloudflare Tokens 4. Create Terraform Project 5. Configure ENV vars, providers, remote state 6. FINALLY get to REAL work ! 7. Do even MORE work " 7
  7. Step 1 - AWS Account and User ➡ Sign up

    at aws.amazon.com ➡ In AWS Console: Create IAM user ➡ Assign Full Management Permissions ➡ Later restrict to what's acually needed ➡ Download credentials NEVER, EVER use root account 8
  8. Step 2 - Install AWS CLI > brew install awscli

    > aws configure --profile emberjs ... > cat ~/.aws/credentials ... [emberjs] aws_access_key_id = ***** aws_secret_access_key = ***** ... 9
  9. Step 3 - Install Terraform, Get API Tokens > brew

    install terraform ➡ Generate a Personal Access Token on GitHub ➡ Log into Cloudflare and copy down your API token 10
  10. Step 4 - Create Terraform Project > mkdir ember-deploy-aws >

    cd ember-deploy-aws > touch main.tf > touch variables.tf > touch state.tf > touch providers.tf 11
  11. Step 5 - Configure the Terraform Project 5.1 - Environment

    Variables 5.2 - Providers (AWS, GitHub, Cloudflare) 5.3 - Remote state (S3 + DynamoDB) 5.4 - Workspaces 5.5 - Initialize 12
  12. Step 5.1 - Environment Variables variables.tf variable "cloudflare_email" { type

    = "string" } variable "cloudflare_api_token" { type = "string" } variable "github_token" { type = "string" } 13
  13. Step 5.1 - Environment Variables > export TF_VAR_cloudflare_api_token=<your_cf_token> > export

    TF_VAR_cloudflare_email=<your_cf_email_address> > export TF_VAR_github_token=<your_github_token> Put the above in a file to source or in ~/.bash_profile or equivalent 14
  14. Step 5.2 - Providers providers.tf provider "aws" { region =

    "eu-central-1" profile = "emberjs" version = "~> 1.32" } provider "cloudflare" { email = "${var.cloudflare_email}" token = "${var.cloudflare_api_token}" version = "~> 1.2" } ... 15
  15. Step 5.2 - Providers providers.tf ... provider "cloudflare" { email

    = "${var.cloudflare_email}" token = "${var.cloudflare_api_token}" version = "~> 1.2" } provider "github" { organization = "psteininger" token = "${var.github_token}" version = "~> 1.2" } 16
  16. Step 5.3 - Remote state state.tf terraform { backend "s3"

    { bucket = "my-ember-tf-state" key = "state" region = "eu-central-1" profile = "emberjs" dynamodb_table = "my-ember-tf-state" } } 17
  17. Step 5.4 - Workspaces ➡ Isolate state per environment (i.e.staging,

    production) ➡ Logic derived from the name of the workspace > terraform workspace new production > terraform workspace new staging 18
  18. Step 6 - Hosting Module 6.0 - Variables 6.1 -

    S3 Bucket and Permissions 6.2 - Cloudfront Distribution 6.3 - Cloudflare DNS entry 6.4 - Define Outputs 6.5 - Instantiate hosting module 22
  19. Step 6 - Hosting Module > mkdir hosting > touch

    hosting/main.tf > touch hosting/variables.tf > touch hosting/data.tf > touch hosting/outputs.tf 23
  20. Step 6.0 - Module Variables hosting/variables.tf variable "name" { description

    = "name of the hosted app" type = "string" } variable "app_domain" { description = "The domain prefix for the bucket name" } ... 24
  21. Step 6.1 - S3 Bucket and Permissions hosting/main.tf resource "aws_s3_bucket"

    "frontend" { bucket = "${local.bucket_name}" acl = "public-read" website { index_document = "index.html" } # CORS setup see in blog post or repo for details ... } 26
  22. Step 6.1 - S3 Bucket and Permissions hosting/data.tf data "aws_iam_policy_document"

    "bucket_policy" { statement { effect = "Allow" principals { identifiers = ["*"] type = "AWS" } actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.frontend.arn}/*"] } } 27
  23. Step 6.1 - S3 Bucket and Permissions hosting/main.tf ... resource

    "aws_s3_bucket_policy" "frontend" { bucket = "${aws_s3_bucket.frontend.id}" policy = "${data.aws_iam_policy_document.bucket_policy.json}" } ... 28
  24. Step 6.2 - Cloudfront Distribution hosting/main.tf resource "aws_cloudfront_distribution" "frontend" {

    origin { domain_name = "${aws_s3_bucket.frontend.bucket_regional_domain_name}" origin_id = "${aws_s3_bucket.frontend.bucket}" } #cache behavior details # custom error response - next slide } 29
  25. Step 6.2 - Cloudfront Distribution hosting/main.tf ... custom_error_response { error_code

    = "404" response_page_path = "/index.html" response_code = "200" } enabled = true is_ipv6_enabled = true default_root_object = "index.html" price_class = "PriceClass_200" ... 30
  26. Step 6.3 - Cloudflare DNS entry hosting/main.tf ... resource "cloudflare_record"

    "frontend-dns" { domain = "${var.app_domain}" name = "${var.name}" type = "CNAME" value = "${aws_cloudfront_distribution.frontend.domain_name}" proxied = true } 31
  27. Step 6.4 - Define Module Outputs hosting/outputs.tf output "host_bucket" {

    value = "${aws_s3_bucket.frontend.bucket}" } output "distribution_id" { value = "${aws_cloudfront_distribution.frontend.id}" } 32
  28. Step 6.4 - Instantiate hosting module main.tf module "hosting" {

    source = "./hosting" app_domain = "${var.app_domain}" name = "${local.name}" } 33
  29. Step 6.4 - Instantiate hosting module variables.tf variable "name" {

    type = "string" default = "app" } locals { env_suffix = "${terraform.workspace == "production" ? "" : join("", list("-", terraform.workspace))}" name = "${var.name}${local.env_suffix}" } 34
  30. Step 7 - CodeBuild Project 7.0 - Module Variables 7.1

    - Service Role and Role Policy 7.2 - Access Policy 7.3 - CodeBuild Project 7.4 - Webhooks 7.5 - Instantiate CI module 35
  31. Step 7 - CodeBuild Project > mkdir ci > touch

    ci/main.tf > touch ci/variables.tf > touch ci/data.tf 36
  32. Step 7.0 - Module Variables ci/variables.tf variable "name" { description

    = "name of the hosted app" type = "string" } variable "host_bucket" { description = "name of the bucket, where the app is hosted" type = "string" } variable "cf_distribution_id" { type = "string" } ... 37
  33. Step 7.0 - Module Variables ci/variables.tf ... variable "app_repo" {

    description = "Repo of the Ember app" type = "string" default = "psteininger/ember-deploy-app" } variable "github_token" { type = "string" } variable "backend_api_base_url" { type = "string" } 38
  34. Step 7.1 - Service Role and Role Policy ci/data.tf data

    "aws_iam_policy_document" "assume-role" { statement { effect = "Allow" principals { identifiers = ["codebuild.amazonaws.com"] type = "Service" } actions = ["sts:AssumeRole"] } } 39
  35. Step 7.1 - Service Role and Role Policy ci/main.tf resource

    "aws_iam_role" "ci" { name = "${var.name}-ci" assume_role_policy = "${data.aws_iam_policy_document.assume-role.json}" } 40
  36. Step 7.2 - Access Policy ci/data.tf ... data "aws_iam_policy_document" "ci-access"

    { ... # really LOOONG - see repo / blog post ... } 41
  37. Step 7.2 - Access Policy ➡ CodeBuild to capture and

    store the logs ➡ Access parameters kept in Parameter Store (SSM) ➡ for CodeBuild and your custom path ➡ Access S3 - allow the deployment, and activation ➡ Create an Cloudfront Invalidation ➡ enable visitors to see the new version of code 42
  38. Step 7.2 - Assign Access Policy to Role ci/main.tf ...

    resource "aws_iam_role_policy" "ci" { policy = "${data.aws_iam_policy_document.ci-access.json}" role = "${aws_iam_role.ci.id}" } 43
  39. Step 7.3 - CodeBuild Project ci/main.tf resource "aws_codebuild_project" "ci" {

    name = "${var.name}" service_role = "${aws_iam_role.ci.arn}" # see repo for complete code ... } 45
  40. Step 7.3 - CodeBuild Project - Source ci/main.tf resource "aws_codebuild_project"

    "ci" { ... source { type = "GITHUB" location = "${data.github_repository.app-repo.http_clone_url}" git_clone_depth = 1 auth = { type = "OAUTH" resource = "${var.github_token}" } buildspec = "buildspec.yml" } ... } 46
  41. Step 7.3 - CodeBuild Project - Environment ci/main.tf resource "aws_codebuild_project"

    "ci" { ... artifacts { type = "NO_ARTIFACTS" } environment { compute_type = "BUILD_GENERAL1_SMALL" image = "aws/codebuild/nodejs:8.11.0" type = "LINUX_CONTAINER" ... } } 47
  42. Step 7.3 - CodeBuild Project - ENV Vars ci/main.tf resource

    "aws_codebuild_project" "ci" { ... environment { ... environment_variable { name = "API_URL" value = "${var.backend_api_base_url}" } ... } } 48
  43. Step 7.3 - CodeBuild Project - ENV Vars ci/main.tf resource

    "aws_codebuild_project" "ci" { ... environment { ... environment_variable { name = "INDEX_${upper(terraform.workspace)}_BUCKET" value = "${var.host_bucket}" } ... } } 49
  44. Step 7.4 - CodeBuild Webhook ci/main.tf ... resource "aws_codebuild_webhook" "ci"

    { project_name = "${aws_codebuild_project.ci.name}" branch_filter = "${terraform.workspace == "staging" ? "master" : terraform.workspace}" } ... 50
  45. Step 7.4 - GitHub Webhook ci/main.tf resource "github_repository_webhook" "ci" {

    active = true events = ["push"] name = "web" repository = "${data.github_repository.app-repo.full_name}" configuration { url = "${aws_codebuild_webhook.ci.payload_url}" secret = "${aws_codebuild_webhook.ci.secret}" content_type = "json" insecure_ssl = false } } 51
  46. Step 7.5 - Instantiate CI module main.tf module "ci" {

    source = "./ci" name = "${local.name}" host_bucket = "${module.hosting.host_bucket}" cf_distribution_id = "${module.hosting.distribution_id}" backend_api_base_url = "${var.backend_api_base_url}" github_token = "${var.github_token}" } 52
  47. Step 7.6 - Update Variables variables.tf variable "app_domain" { description

    = "The domain prefix for the bucket name" type = "string" default = "example.com" } variable "backend_api_base_url" { description = "The URL pointing to your backend API" type = "string" default = "api.example.com" } 53
  48. Step 8 - Init and Plan Run > terraform init

    ... > terraform plan ... Then > terraform apply 55
  49. Final Toughts ➡ Terraform is great, but far from perfect

    ➡ Reusable, comprehensible, self-documenting ➡ Helps separate environments ➡ Multi-provider = no vendor lock-in ➡ It doesn't hide AWS complexity ➡ Some resources need extra work 56
  50. Final Toughts ➡ Always review PLAN before APPLY ➡ Successful

    PLAN does not guarantee AWS will agree ➡ Relatively simple parts are complex 57
  51. Resources ➡ Terraform Docs ➡ https://www.terraform.io/docs/index.html ➡ AWS Docs ➡

    https://aws.amazon.com/documentation/ ➡ AWS Help in Berlin ➡ Renato Losio - http://arsenio.it 59