# # KMS template used to create SSM keys for all environments. # From: https://typicalrunt.me/2017/04/07/storing-secrets-with-aws-parameterstore/ # # # AWSTemplateFormatVersion: "2010-09-09" Description: "KMS key for secrets management (see Parameters for more info)" Parameters: ENV: Type: String Default: dev AllowedValues: - dev - staging - prod Resources: KmsKeyAlias: Type: "AWS::KMS::Alias" Properties: AliasName: !Sub "alias/${ENV}" TargetKeyId: !Ref KmsKey KmsKey: Type: "AWS::KMS::Key" Properties: Description: !Sub "Manages secrets in the ${ENV} namespace" Enabled: true EnableKeyRotation: true KeyPolicy: Version: "2012-10-17" Id: "KeyPolicyForKMS" Statement: - Sid: "Enable IAM User Permissions" Effect: "Allow" Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: "kms:*" Resource: "*" Outputs: KmsKeyId: Description: "ID of the KMS key" Value: !Ref KmsKey Export: Name: !Sub "kms:key:${ENV}:id" KmsKeyArn: Description: "ARN of the KMS key" Value: !GetAtt KmsKey.Arn Export: Name: !Sub "kms:key:${ENV}:arn" # # CloudFormation template for Node.js service on AWS Fargate. # # # Example: # aws cloudformation deploy # --stack-name dev-service # --template-file cloudformation/service.yaml # --parameter-overrides ENV=dev Vpc=vpc-XXX Subnets=subnet-XXX,subnet-XXX,subnet-XXX # --capabilities CAPABILITY_IAM # Description: > My Service Parameters: ENV: Type: String Default: dev AllowedValues: - dev - staging - prod Vpc: Type: AWS::EC2::VPC::Id Subnets: Type: List Repository: Type: String Default: my-service InstanceCount: Type: Number Default: 1 HealthCheckPath: Type: String Default: /api/v1/liveness # your service will need a healthcheck endpoint Mappings: CertMap: Certificate: "dev": arn:aws:acm:us-east-1:XXX:certificate/XXX-XXX-XXX-XXX-XXXX "staging": arn:aws:acm:us-east-1:XXX:certificate/XXX-XXX-XXX-XXX-XXXX "prod": arn:aws:acm:us-east-1:XXX:certificate/XXX-XXX-XXX-XXX-XXXX Environment: NodeEnv: "dev": "development" "staging": "test" "prod": "production" Resources: Cluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Sub ${ENV}-service TaskIamRole: Type: AWS::IAM::Role Properties: Path: !Sub /service/${ENV}/ AssumeRolePolicyDocument: | { "Statement": [{ "Effect": "Allow", "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, "Action": [ "sts:AssumeRole" ] }] } ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy Policies: - PolicyName: "secrets-management" PolicyDocument: Version: "2012-10-17" Id: "AllowAccessToParameters" Statement: - Sid: "AllowAccessToGetParameters" Effect: "Allow" Action: - "ssm:GetParameters" - "ssm:GetParametersByPath" Resource: - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/service/${ENV}/*" - Sid: "AllowAccessToDecryptParameters" Effect: "Allow" Action: "kms:Decrypt" Resource: - Fn::ImportValue: !Sub "kms:key:${ENV}:arn" LoadBalancerSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: !Sub ${ENV}-service SecurityGroupIngress: - CidrIp: "0.0.0.0/0" IpProtocol: "TCP" FromPort: 443 ToPort: 443 VpcId: !Ref Vpc ServiceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${ENV}-service SecurityGroupIngress: - IpProtocol: tcp FromPort: 3000 # node.js running on port 3000 in this example ToPort: 3000 SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub ${ENV}-service LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: !Sub ${ENV}-service Subnets: !Ref Subnets SecurityGroups: - !Ref LoadBalancerSecurityGroup TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: !Sub ${ENV}-service VpcId: !Ref Vpc TargetType: ip Port: 3000 Protocol: HTTP Matcher: HttpCode: 200-299 HealthCheckIntervalSeconds: 60 HealthCheckPath: !Ref HealthCheckPath HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 30 Tags: - Key: Name Value: !Sub ${ENV}-service LoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: LoadBalancerArn: !Ref LoadBalancer Certificates: - CertificateArn: !FindInMap [CertMap, "Certificate", !Ref ENV] Port: 443 Protocol: HTTPS DefaultActions: - Type: forward TargetGroupArn: Ref: TargetGroup ListenerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - Type: forward TargetGroupArn: !Ref TargetGroup Conditions: - Field: path-pattern Values: - "*" ListenerArn: !Ref LoadBalancerListener Priority: 1 Service: Type: AWS::ECS::Service DependsOn: - LoadBalancerListener Properties: ServiceName: !Sub ${ENV}-service Cluster: !Ref Cluster DesiredCount: !Ref InstanceCount LaunchType: FARGATE TaskDefinition: !Ref TaskDefinition DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 50 LoadBalancers: - ContainerName: !Sub ${ENV}-service ContainerPort: 3000 TargetGroupArn: !Ref TargetGroup NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED ## fixme Required for non-Nat'd deployments? SecurityGroups: - !GetAtt ServiceSecurityGroup.GroupId Subnets: !Ref Subnets TaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Cpu: 256 Memory: 512 NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: !GetAtt TaskIamRole.Arn TaskRoleArn: !GetAtt TaskIamRole.Arn ContainerDefinitions: - Name: !Sub ${ENV}-service Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}:${ENV}-latest Essential: true Memory: 512 PortMappings: - ContainerPort: 3000 Environment: - Name: ENV Value: !Sub ${ENV} - Name: NODE_ENV Value: !FindInMap [Environment, "NodeEnv", !Ref ENV] LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref LogGroup awslogs-region: us-east-1 awslogs-stream-prefix: !Sub ${ENV}-service LogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub ${ENV}-service RetentionInDays: 30 Outputs: Cluster: Value: !Ref Cluster #!/bin/sh # # Get all system parameters from AWS SSM and store into the current environment # # Run node process # echo "*************************" echo " Env: ${ENV:-dev}" echo "*************************" json=$(aws ssm get-parameters-by-path --path "/${SERVICE:-service}/${ENV:-dev}/" --recursive --with-decryption) params=$(echo $json | jq -r '.Parameters[] | [.Name, .Value] | join("=")') for param in $params do export ${param##/${SERVICE:-service}/${ENV:-dev}/} done export -p node server.js # # Dockerfile # FROM node:9 WORKDIR /app COPY package.json /app RUN npm install RUN apt-get update --yes RUN apt-get install jq --yes --quiet RUN apt-get install python-pip python-dev build-essential --yes --quiet RUN pip install awscli --upgrade COPY . /app CMD /app/boot.sh EXPOSE 3000