---
Mappings:
  RegionMap:
    us-west-2:
      AMI: ami-7d11c51d
      VPC: 10.1.0.0/16
      a: 10.1.1.0/24
      b: 10.1.2.0/24
      c: 10.1.3.0/24
Parameters:
  InstanceType:
    Description: EC2 HVM instance type (t2.small, etc).
    Type: String
    Default: t2.micro
    ConstraintDescription: Must be a valid EC2 HVM instance type.
  DiscoveryURL:
    Description: An unique etcd cluster discovery URL. Grab a new token from https://discovery.etcd.io/new?size=<your cluster size>
    Type: String
  AllowSSHFrom:
    Description: The net block (CIDR) that SSH is available to.
    Default: 0.0.0.0/0
    Type: String
  KeyPair:
    Description: The name of an EC2 Key Pair to allow SSH access to the instance.
    Type: AWS::EC2::KeyPair::KeyName
    MinLength: 1
  DeployKeyPath:
    Description: Long URI component used as a passphrase (in a URI) for deployment. (e.g. pwgen -A 64 1)
    Type: String
    MinLength: 64
    MaxLength: 128
    NoEcho: true
    AllowedPattern: "[a-z0-9]*"
    ConstraintDescription: Must be 64-128 characters
Outputs:
  ELB:
    Description: ELB Hostname
    Value: !GetAtt [ LoadBalancer, DNSName ]
  Backup:
    Description: S3 Bucket for Backups
    Value: !Ref S3Backup
  DeployHook:
    Description: URL to put in Docker Hub web hook
    Value: !Sub "https://${DeployApi}.execute-api.${AWS::Region}.amazonaws.com/prod/${DeployKeyPath}"
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [ RegionMap, !Ref "AWS::Region", VPC ]
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties: {}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties: { VpcId: !Ref VPC }
  InternetEgressRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  InternetNetworkAcl:
    Type: AWS::EC2::NetworkAcl
    Properties: { VpcId: !Ref VPC }
  SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !FindInMap [ RegionMap, !Ref "AWS::Region", a ]
      AvailabilityZone: !Sub ${AWS::Region}a
      VpcId: !Ref VPC
  SubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !FindInMap [ RegionMap, !Ref "AWS::Region", b ]
      AvailabilityZone: !Sub ${AWS::Region}b
      VpcId: !Ref VPC
  SubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !FindInMap [ RegionMap, !Ref "AWS::Region", c ]
      AvailabilityZone: !Sub ${AWS::Region}c
      VpcId: !Ref VPC
  AssociationSubnetA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetA
      RouteTableId: !Ref RouteTable
  AssociationSubnetB:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetB
      RouteTableId: !Ref RouteTable
  AssociationSubnetC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetC
      RouteTableId: !Ref RouteTable
  CoreOSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: { Service: [ ec2.amazonaws.com ] }
            Action: [ "sts:AssumeRole" ]
      Path: /
      Policies:
        - PolicyName: coreos
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "ec2:CreateRoute"
                  - "ec2:DeleteRoute"
                  - "ec2:ReplaceRoute"
                  - "ec2:ModifyNetworkInterfaceAttribute"
                  - "ec2:ModifyInstanceAttribute"
                Resource: "*"
              - Effect: Allow
                Action: [ "ec2:DescribeRouteTables", "ec2:DescribeInstances" ]
                Resource: "*"
  CoreOSInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    DependsOn: [ CoreOSRole ]
    Properties: { Path: /, Roles: [ !Ref CoreOSRole ] }
  ELBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: LoadBalancer SecurityGroup
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - { IpProtocol: tcp, FromPort: 80, ToPort: 80, CidrIp: 0.0.0.0/0 }
        - { IpProtocol: tcp, FromPort: 8091, ToPort: 8091, CidrIp: 0.0.0.0/0 }
  CoreOSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn: [ ELBSecurityGroup ]
    Properties:
      GroupDescription: CoreOS SecurityGroup
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - { IpProtocol: tcp, FromPort: 22, ToPort: 22, CidrIp: !Ref AllowSSHFrom }
        - { IpProtocol: -1, CidrIp: 10.10.0.0/16 }
        - { IpProtocol: -1, SourceSecurityGroupId: !Ref ELBSecurityGroup }
  Ingress4001:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: [ CoreOSSecurityGroup ]
    Properties:
      GroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
      IpProtocol: tcp
      FromPort: 4001
      ToPort: 4001
      SourceSecurityGroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
  Ingress2379:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: [ CoreOSSecurityGroup ]
    Properties:
      GroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
      IpProtocol: tcp
      FromPort: 2379
      ToPort: 2379
      SourceSecurityGroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
  Ingress2380:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: [ CoreOSSecurityGroup ]
    Properties:
      GroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
      IpProtocol: tcp
      FromPort: 2380
      ToPort: 2380
      SourceSecurityGroupId: !GetAtt [ CoreOSSecurityGroup, GroupId ]
  CoreOSServerAutoScale:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn: [ VPC, WebTargetGroup, CouchbaseTargetGroup ]
    Properties:
      VPCZoneIdentifier: [ !Ref SubnetA, !Ref SubnetB, !Ref SubnetC ]
      LaunchConfigurationName: !Ref CoreOSServerLaunchConfig
      MinSize: 3
      MaxSize: 6
      DesiredCapacity: 3
      LoadBalancerNames: [ !Ref InternalEtcdLB ]
      TargetGroupARNs: [ !Ref WebTargetGroup, !Ref CouchbaseTargetGroup ]
  CoreOSServerLaunchConfig:
    DependsOn: [ VPC, CoreOSSecurityGroup, CoreOSInstanceProfile ]
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      ImageId: !FindInMap [ RegionMap, !Ref "AWS::Region", AMI ]
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyPair
      SecurityGroups: [ !Ref CoreOSSecurityGroup ]
      IamInstanceProfile: !Ref CoreOSInstanceProfile
      AssociatePublicIpAddress: true
      BlockDeviceMappings:
        - DeviceName: /dev/xvdb
          Ebs: { VolumeSize: 10, VolumeType: gp2, DeleteOnTermination: true }
      UserData:
        "Fn::Base64": !Sub |
          #cloud-config
          coreos:
            etcd2:
              discovery: ${DiscoveryURL}
              advertise-client-urls: http://$private_ipv4:2379
              initial-advertise-peer-urls: http://$private_ipv4:2380
              listen-client-urls: http://0.0.0.0:2379,http://0.0.0.0:4001
              listen-peer-urls: http://$private_ipv4:2380
            units:
              - name: etcd2.service
                command: start
              - name: fleet.service
                command: start
              - name: set-metadata.service
                runtime: true
                command: start
                content: |
                  [Unit]
                  Description=Puts metadata in /etc/instance
                  Requires=etcd2.service
                  After=etcd2.service
                  [Service]
                  Type=oneshot
                  RemainAfterExit=yes
                  ExecStart=/usr/bin/sh -c 'echo INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id) > /etc/instance'
                  ExecStart=/usr/bin/sh -c 'echo AZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone) >> /etc/instance'
                  ExecStart=/usr/bin/sh -c 'echo REGION=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev) >> /etc/instance'
              - name: format-docker.service
                runtime: true
                command: start
                content: |
                  [Unit]
                  Description=Wipe Ephemeral
                  Before=docker.service
                  Before=docker-early.service
                  [Service]
                  Type=oneshot
                  RemainAfterExit=yes
                  ExecStart=/usr/sbin/wipefs -f /dev/xvdb
                  ExecStart=/usr/sbin/mkfs.ext4 -m0 -L docker -b 4096 -i 4096 -I 128 /dev/xvdb
              - name: set-sdcheck-off.service
                runtime: true
                command: start
                content: |
                  [Unit]
                  Requires=var-lib-docker.mount set-metadata.service flanneld.service
                  After=var-lib-docker.mount set-metadata.service flanneld.service
                  Description=Sets source-dest check to off
                  [Service]
                  EnvironmentFile=/etc/instance
                  Type=oneshot
                  RemainAfterExit=yes
                  ExecStart=/usr/bin/docker run cgswong/aws:aws --region $REGION ec2 modify-instance-attribute --no-source-dest-check --instance-id $INSTANCEID
              - name: var-lib-docker.mount
                command: start
                content: |
                  [Unit]
                  Description=Mount storage to /var/lib/docker
                  Requires=format-docker.service
                  After=format-docker.service
                  Before=docker.service
                  Before=docker-early.service
                  [Mount]
                  What=/dev/xvdb
                  Where=/var/lib/docker
                  Type=ext4
              - name: flanneld.service
                command: start
                drop-ins:
                  - name: 50-network-config.conf
                    content: |
                      [Service]
                      ExecStartPre=/usr/bin/etcdctl set /coreos.com/network/config '{ "Network": "10.10.0.0/16", "Backend": {"Type": "aws-vpc"} }'
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    DependsOn: [ ELBSecurityGroup ]
    Properties:
      SecurityGroups: [ !Ref ELBSecurityGroup ]
      Subnets: [ !Ref SubnetA, !Ref SubnetB, !Ref SubnetC ]
  CouchbaseTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 8091
      VpcId: !Ref VPC
      Protocol: HTTP
      TargetGroupAttributes:
        - { Key: stickiness.enabled, Value: true }
        - { Key: stickiness.type, Value: lb_cookie }
  CouchbaseListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn: [ LoadBalancer, CouchbaseTargetGroup ]
    Properties:
      DefaultActions: [ { Type: forward, TargetGroupArn: !Ref CouchbaseTargetGroup } ]
      Port: 8091
      LoadBalancerArn: !Ref LoadBalancer
      Protocol: HTTP
  WebTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 3000
      VpcId: !Ref VPC
      Protocol: HTTP
      TargetGroupAttributes:
        - { Key: stickiness.enabled, Value: true }
        - { Key: stickiness.type, Value: lb_cookie }
  WebListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn: [ LoadBalancer, WebTargetGroup ]
    Properties:
      DefaultActions: [ { Type: forward, TargetGroupArn: !Ref WebTargetGroup } ]
      Port: 80
      LoadBalancerArn: !Ref LoadBalancer
      Protocol: HTTP
  InternalEtcdLB:
    Type: AWS::ElasticLoadBalancing::LoadBalancer
    DependsOn: [ CoreOSSecurityGroup ]
    Properties:
      Scheme: internal
      Listeners: [ { LoadBalancerPort: 2379, InstancePort: 2379, Protocol: TCP } ]
      HealthCheck:
        Target: TCP:2379
        HealthyThreshold: 3
        UnhealthyThreshold: 5
        Interval: 10
        Timeout: 5
      SecurityGroups: [ !Ref CoreOSSecurityGroup ]
      Subnets: [ !Ref SubnetA, !Ref SubnetB, !Ref SubnetC ]
  S3Backup:
    Type: AWS::S3::Bucket
    Properties:
      LifecycleConfiguration:
        Rules: [ { ExpirationInDays: 10, Status: Enabled } ]
  BackupPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: [ CoreOSRole ]
    Properties:
      Bucket: !Ref S3Backup
      PolicyDocument:
        Id: backup
        Version: 2012-10-17
        Statement:
          - Sid: backup
            Action: "s3:*"
            Effect: Allow
            Principal: { AWS: !GetAtt [ CoreOSRole, Arn ] }
            Resource: [ !Sub "arn:aws:s3:::${S3Backup}", !Sub "arn:aws:s3:::${S3Backup}/*" ]
  LambdaPermission:
    Type: AWS::Lambda::Permission
    DependsOn: [ DeployLambda ]
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt [ DeployLambda, Arn ]
      Principal: apigateway.amazonaws.com
  LambdaDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: "sts:AssumeRole"
            Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
  DeployLambda:
    Type: AWS::Lambda::Function
    DependsOn: [ CoreOSSecurityGroup, LambdaDeployRole ]
    Properties:
      VpcConfig:
        SubnetIds: [ !Ref SubnetA, !Ref SubnetB, !Ref SubnetC ]
        SecurityGroupIds: [ !Ref CoreOSSecurityGroup ]
      Role: !GetAtt [ LambdaDeployRole, Arn ]
      Timeout: 2
      Handler: index.handler
      Runtime: nodejs4.3
      Code:
        ZipFile: !Sub
          - |
            const options = {
              host: '${host}',
              port: 2379,
              path: '/v2/keys/coreos.com/deploy',
              method: 'PUT',
              headers: {'Content-Type': 'application/x-www-form-urlencoded'}
            };
            exports.handler = (event, context, callback) => {
              if (event['push_data'].tag === 'production') {
                const payload = event.repository.name;
                const req = require('http').request(options);
                req.write(`value=${!payload}`);
                req.end(() => callback(null, `deploy:${!payload}`));
              } else {
                callback(null, 'non-production push');
              }
            };
          - host: !GetAtt [ InternalEtcdLB, DNSName ]
  DeployApi:
    Type: AWS::ApiGateway::RestApi
    Properties: { Name: deploy-coreosinaction }
  DeployResource:
    Type: AWS::ApiGateway::Resource
    DependsOn: [ DeployApi ]
    Properties:
      ParentId: !GetAtt [ DeployApi, RootResourceId ]
      PathPart: !Ref DeployKeyPath
      RestApiId: !Ref DeployApi
  DeployPOST:
    Type: AWS::ApiGateway::Method
    DependsOn: [ DeployLambda ]
    Properties:
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        PassthroughBehavior: WHEN_NO_MATCH
        Type: AWS
        IntegrationHttpMethod: POST
        IntegrationResponses: [ StatusCode: 200 ]
        Uri: !Sub
          - "arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations"
          - { arn: !GetAtt [ DeployLambda, Arn ], region: !Ref "AWS::Region" }
      MethodResponses: [ StatusCode: 200 ]
      ResourceId: !Ref DeployResource
      RestApiId: !Ref DeployApi
  DeployDeployment:
    DependsOn: DeployPOST
    Type: AWS::ApiGateway::Deployment
    Properties: { RestApiId: !Ref DeployApi, StageName: DummyStage }
  DeployProdStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref DeployDeployment
      MethodSettings: [ { ResourcePath: !Sub "/${DeployKeyPath}", HttpMethod: POST } ]
      RestApiId: !Ref DeployApi
      StageName: prod
