Setting up Elastic Beanstalk

Setting up Elastic Beanstalk

Enhance your project Makefile in the project directory with the following content:

.PHONY: app
app: guard-STAGE
	aws cloudformation deploy \
		--no-fail-on-empty-changeset \
		--template-file cloudformation/app.yml \
		--stack-name $(SERVICE)-$(STAGE)-app \
		--parameter-overrides Stage=$(STAGE) \
		--capabilities CAPABILITY_IAM \
		--region $(AWS_REGION)

.PHONY: guard-%
guard-%:
	$(if $(value ${*}),,$(error "Variable ${*} not set!"))

Create a new cloudformation template in cloudformation/app.yml and define your application stack in there:

---
AWSTemplateFormatVersion: 2010-09-09
Description: Webapp for '...'

Parameters:
  Stage:
    Type: String

Mappings:
  ELBHostedZoneId:
    eu-central-1:
      HostedZoneId: Z215JYRZR1TBD5

Resources:
  Application:
    Type: AWS::ElasticBeanstalk::Application
    Properties:
      ApplicationName: !Ref AWS::StackName
  Environment:
    Type: AWS::ElasticBeanstalk::Environment
    Properties:
      ApplicationName: !Ref Application
      EnvironmentName: !Sub '${AWS::StackName}-active'
      SolutionStackName: "64bit Amazon Linux 2018.03 v2.12.14 running Docker 18.06.1-ce"
      OptionSettings:
        - Namespace: 'aws:autoscaling:launchconfiguration'
          OptionName: IamInstanceProfile
          Value: !Ref InstanceProfile
        - Namespace: 'aws:autoscaling:launchconfiguration'
          OptionName: InstanceType
          Value: 't3.nano'
        - Namespace: 'aws:autoscaling:asg'
          OptionName: MinSize
          Value: '2'
        - Namespace: 'aws:autoscaling:asg'
          OptionName: MaxSize
          Value: '10'
        - Namespace: 'aws:elasticbeanstalk:environment'
          OptionName: ServiceRole
          Value: !Ref ServiceRole
        - Namespace: 'aws:elasticbeanstalk:command'
          OptionName: DeploymentPolicy
          Value: 'Rolling'
        - Namespace: 'aws:elasticbeanstalk:environment'
          OptionName: LoadBalancerType
          Value: 'application'
        - Namespace: 'aws:elbv2:listener:443'
          OptionName: ListenerEnabled
          Value: 'true'
        - Namespace: 'aws:elbv2:listener:443'
          OptionName: Protocol
          Value: 'HTTPS'
        - Namespace: 'aws:elbv2:listener:443'
          OptionName: SSLCertificateArns
          Value:
            'Fn::ImportValue': !Sub 'certificates-${Stage}-CertificateArn'
        - Namespace: 'aws:elasticbeanstalk:environment:process:default'
          OptionName: HealthCheckPath
          Value: '/counter/health'
        - Namespace: 'aws:elb:healthcheck'
          OptionName: Interval
          Value: '5'
        - Namespace: 'aws:elasticbeanstalk:healthreporting:system'
          OptionName: SystemType
          Value: 'enhanced'
        - Namespace: 'aws:elasticbeanstalk:cloudwatch:logs:health'
          OptionName: HealthStreamingEnabled
          Value: 'true'
        - Namespace: 'aws:ec2:vpc'
          OptionName: VPCId
          Value:
            'Fn::ImportValue': 'vpc-VPC'
        - Namespace: 'aws:ec2:vpc'
          OptionName: Subnets
          Value:
            'Fn::ImportValue': 'vpc-SubnetsPublic'
        - Namespace: 'aws:ec2:vpc'
          OptionName: ELBSubnets
          Value:
            'Fn::ImportValue': 'vpc-SubnetsPublic'
        - Namespace: 'aws:elasticbeanstalk:environment:proxy'
          OptionName: ProxyServer
          Value: 'none'
        - Namespace: 'aws:ec2:vpc'
          OptionName: AssociatePublicIpAddress
          Value: 'true'
        - Namespace: 'aws:elasticbeanstalk:application:environment'
          OptionName: MYSQL_USER
          Value: "{{resolve:secretsmanager:REPLACE_ME-db:SecretString:username}}"
        - Namespace: 'aws:elasticbeanstalk:application:environment'
          OptionName: MYSQL_PASSWORD
          Value: "{{resolve:secretsmanager:REPLACE_ME-db:SecretString:password}}"
        - Namespace: 'aws:elasticbeanstalk:application:environment'
          OptionName: MYSQL_HOST
          Value: "{{resolve:secretsmanager:REPLACE_ME-db:SecretString:host}}"
        - Namespace: 'aws:elasticbeanstalk:application:environment'
          OptionName: MYSQL_DATABASE
          Value: "{{resolve:secretsmanager:REPLACE_ME-db:SecretString:dbname}}"
  LoadBalancerRecordSet:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId:
        'Fn::ImportValue': !Sub 'dns-${Stage}-HostedZoneId'
      Name:
        'Fn::ImportValue': !Sub 'dns-${Stage}-HostedZoneName'
      Type: A
      AliasTarget:
        HostedZoneId:
          'Fn::FindInMap': [ "ELBHostedZoneId", !Ref "AWS::Region", "HostedZoneId" ]
        DNSName: !GetAtt Environment.EndpointURL
  ServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: "elasticbeanstalk.amazonaws.com"
            Action: 'sts:AssumeRole'
            Condition:
              StringEquals:
                'sts:ExternalId': elasticbeanstalk
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth'
      Path: /
  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref InstanceProfileRole
  InstanceProfileRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "ec2.amazonaws.com"
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: AllowEbAuth
                Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                Resource:
                  - "*"
              - Sid: AllowPull
                Effect: Allow
                Resource:
                  - "*"
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetRepositoryPolicy
                  - ecr:DescribeRepositories
                  - ecr:ListImages
                  - ecr:BatchGetImage
              - Sid: BucketAccess
                Action:
                  - 's3:Get*'
                  - 's3:List*'
                  - 's3:PutObject'
                Effect: Allow
                Resource:
                  - !Join
                    - ''
                    - - 'arn:'
                      - "aws"
                      - ':s3:::elasticbeanstalk-*-'
                      - !Ref 'AWS::AccountId'
                  - !Join
                    - ''
                    - - 'arn:'
                      - "aws"
                      - ':s3:::elasticbeanstalk-*-'
                      - !Ref 'AWS::AccountId'
                      - /*
                  - !Join
                    - ''
                    - - 'arn:'
                      - "aws"
                      - ':s3:::elasticbeanstalk-*-'
                      - !Ref 'AWS::AccountId'
                      - '-*'
                  - !Join
                    - ''
                    - - 'arn:'
                      - "aws"
                      - ':s3:::elasticbeanstalk-*-'
                      - !Ref 'AWS::AccountId'
                      - '-*/*'
              - Sid: ECSAccess
                Effect: Allow
                Action:
                  - 'ecs:StartTask'
                  - 'ecs:StopTask'
                  - 'ecs:RegisterContainerInstance'
                  - 'ecs:DeregisterContainerInstance'
                  - 'ecs:DescribeContainerInstances'
                  - 'ecs:DiscoverPollEndpoint'
                  - 'ecs:Submit*'
                  - 'ecs:Poll'
                Resource: '*'
              - Sid: QueueAccess
                Action:
                  - 'sqs:ChangeMessageVisibility'
                  - 'sqs:DeleteMessage'
                  - 'sqs:ReceiveMessage'
                  - 'sqs:SendMessage'
                Effect: Allow
                Resource: '*'
              - Sid: DynamoPeriodicTasks
                Action:
                  - 'dynamodb:BatchGetItem'
                  - 'dynamodb:BatchWriteItem'
                  - 'dynamodb:DeleteItem'
                  - 'dynamodb:GetItem'
                  - 'dynamodb:PutItem'
                  - 'dynamodb:Query'
                  - 'dynamodb:Scan'
                  - 'dynamodb:UpdateItem'
                Effect: Allow
                Resource:
                  - !Join
                    - ''
                    - - 'arn:'
                      - "aws"
                      - ':dynamodb:*:'
                      - !Ref 'AWS::AccountId'
                      - ':table/*-stack-AWSEBWorkerCronLeaderRegistry*'
              - Sid: MetricsAccess
                Action:
                  - 'cloudwatch:PutMetricData'
                Effect: Allow
                Resource: '*'

Create the Cloudformation stack with the following command in the app-staging AWS account

AWS_PROFILE=myorg-app-staging make app STAGE=dev