diff --git a/environment-templates/fargate-dotnetfx-env/README.md b/environment-templates/fargate-dotnetfx-env/README.md new file mode 100644 index 0000000..ebbdf90 --- /dev/null +++ b/environment-templates/fargate-dotnetfx-env/README.md @@ -0,0 +1,295 @@ +# AWS Proton Sample .NET Framework Microservices Using Amazon ECS and AWS Fargate + +This directory contains a sample AWS Proton Environment and Service templates for a set of .NET Framework 4.8 deployed to an Amazon ECS load-balanced service running on AWS Fargate, and a CI/CD pipeline used to deploy updates, as well as sample specs for creating Proton Environments and Services using the templates. All resources deployed are tagged. + +The environment template deploys: + +- a VPC +- an Internet Gateway +- 2 Nat Gateways across 2 Availability Zones +- 2 private subnets across 2 Availability Zones +- 2 public subnets across 2 Availability Zones +- a private namespace for service discovery + +Developers provisioning their services can configure the following properties through their service spec: + +- ECS Cluster OS family - Because Windows of the coupling in the User/Kernel boundary in Windows, customers must take care to [match the container host OS version with container image versions](https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-20H2%2Cwindows-10-20H2#windows-server-host-os-compatibility) +- ECS task size - Medium (cpu 1024, memory 2048), Large (cpu 2048, memory 4096), and X-Large (cpu 4096, memory 8192). See the [Task Size](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size) page for supported Windows options. +- Number of running containers +- Service name to register and use for service discovery + +Developers provisioning their pipeline can configure the following properties through their pipeline spec: + +- AMI to use for Build Server - Priviledged modes is not supported on Windows so a Windows EC2 instance is used to create the container image. +- Build Server instance type, volume size, and volume type +- Dockerfile path + +If you need application code to run in the services, you can find a sample application here: +* https://github.com/aws-quickstart/quickstart-dotnetfx-ecs-cicd + +# Registering and deploying these templates + +You can register and deploy these templates by using the AWS Proton console. To do this, you will need to compress the templates using the instructions below, upload them to an S3 bucket, and use the Proton console to register and test them. If you prefer to use the Command Line Interface, follow the instructions below: + +## Prerequisites + +First, make sure you have the AWS CLI installed, and configured. Run the following command to set the AWS region and an `account_id` environment variable: + +```bash +export AWS_DEFAULT_REGION=us-west-2 +account_id=`aws sts get-caller-identity --query Account --output text` +``` + +### Configure IAM Role, S3 Bucket, and CodeStar Connections Connection + +Before you register your templates and deploy your environments and services, you will need to create an Amazon IAM role so that AWS Proton can manage resources in your AWS account, an Amazon S3 bucket to store your templates, and a CodeStar Connections connection to pull and deploy your application code. + +Create the S3 bucket to store your templates: + +``` +aws s3api create-bucket \ + --bucket "proton-cli-templates-${account_id}" \ + --create-bucket-configuration LocationConstraint=us-west-2 +``` + +Create the IAM role that Proton will assume to provision resources and manage AWS CloudFormation stacks in your AWS account. + +``` +aws iam create-role \ + --role-name ProtonServiceRole \ + --assume-role-policy-document file://./policies/proton-service-assume-policy.json + +aws iam attach-role-policy \ + --role-name ProtonServiceRole \ + --policy-arn arn:aws:iam::aws:policy/AdministratorAccess +``` + +Then, allow Proton to use that role to provision resources for your services' continuous delivery pipelines: + +``` +aws proton update-account-settings \ + --pipeline-service-role-arn "arn:aws:iam::${account_id}:role/ProtonServiceRole" +``` + +Create an AWS CodeStar Connections connection to your application code stored in a GitHub or Bitbucket source code repository. This connection allows CodePipeline to pull your application source code before building and deploying the code to your Proton service. To use sample application code for the public service, first create a fork of the sample application repository here: +https://github.com/aws-quickstart/quickstart-dotnetfx-ecs-cicd + +Creating the source code connection must be completed in the CodeStar Connections console: +https://us-west-2.console.aws.amazon.com/codesuite/settings/connections?region=us-west-2 + +## Register an Environment Template + +Register the sample environment template, which contains an ECS Cluster and a VPC with two public subnets. + +First, create an environment template, which will contain all of the environment template's versions. + +``` +aws proton create-environment-template \ + --name "public-vpc" \ + --display-name "PublicVPC" \ + --description "VPC with Public Access and ECS Cluster" +``` + +Now create a version which contains the contents of the sample environment template. Compress the sample template files and register the version: + +``` +tar -zcvf env-template.tar.gz loadbalanced-fargate-dotnetfx-env/ + +aws s3 cp env-template.tar.gz s3://proton-cli-templates-${account_id}/env-template.tar.gz + +rm env-template.tar.gz + +aws proton create-environment-template-version \ + --template-name "public-vpc" \ + --description "Version 1" \ + --source s3="{bucket=proton-cli-templates-${account_id},key=env-template.tar.gz}" +``` + +Wait for the environment template version to be successfully registered. + +``` +aws proton wait environment-template-version-registered \ + --template-name "public-vpc" \ + --major-version "1" \ + --minor-version "0" + +aws proton get-environment-template-version \ + --template-name "public-vpc" \ + --major-version "1" \ + --minor-version "0" +``` + +You can now publish the environment template version, making it available for users in your AWS account to create Proton environments. + +``` +aws proton update-environment-template-version \ + --template-name "public-vpc" \ + --major-version "1" \ + --minor-version "0" \ + --status "PUBLISHED" +``` + +## Register the Service Templates + +Register the sample service template, which contains all the resources required to provision an ECS Fargate service behind a load balancer as well as a continuous delivery pipeline using AWS CodePipeline. + +First, create the service template. + +``` +aws proton create-service-template \ + --name "lb-fargate-service-dotnetfx" \ + --display-name "PublicLoadbalancedDotNetFxFargateService" \ + --description ".NET Framework Windows Fargate Service with an Application Load Balancer" +``` + +Now create a version which contains the contents of the sample service template. Compress the sample template files and register the version: + +``` +tar -zcvf lb-fargate-service-dotnetfx-template.tar.gz loadbalanced-fargate-dotnetfx-svc/ + +aws s3 cp lb-fargate-service-dotnetfx-template.tar.gz s3://proton-cli-templates-${account_id}/lb-fargate-service-dotnetfx-template.tar.gz + +rm lb-fargate-service-dotnetfx-template.tar.gz + +aws proton create-service-template-version \ + --template-name "lb-fargate-service-dotnetfx" \ + --description "Version 1" \ + --source s3="{bucket=proton-cli-templates-${account_id},key=lb-fargate-service-dotnetfx-template.tar.gz}" \ + --compatible-environment-templates '[{"templateName":"lb-fargate-service-dotnetfx","majorVersion":"1"}]' +``` + +Wait for the service template version to be successfully registered. + +``` +aws proton wait service-template-version-registered \ + --template-name "lb-fargate-service-dotnetfx" \ + --major-version "1" \ + --minor-version "0" + +aws proton get-service-template-version \ + --template-name "lb-fargate-service-dotnetfx" \ + --major-version "1" \ + --minor-version "0" +``` + +You can now publish the Public service template version, making it available for users in your AWS account to create Proton services. + +``` +aws proton update-service-template-version \ + --template-name "lb-fargate-service-dotnetfx" \ + --major-version "1" \ + --minor-version "0" \ + --status "PUBLISHED" +``` + +## Deploy An Environment + +With the registered and published environment template, you can now instantiate a Proton environment from the template. + +You can use two different environment provisioning methods when you create environments. + +* Create, manage and provision an environment in a single account. + +* In a single management account create and manage an environment that is provisioned in another account with environment account connections. For more information, see [Create an environment in one account and provision in another account](https://docs.aws.amazon.com/proton/latest/adminguide/ag-create-env.html#ag-create-env-deploy-other) and [Environment account connections](https://docs.aws.amazon.com/proton/latest/adminguide/ag-env-account-connections.html). + +### Create and Provision Environment in a single account + +First, deploy a Proton environment. This command reads your environment spec at `specs/env-spec.yaml`, merges it with the environment template created above, and deploys the resources in a CloudFormation stack in your AWS account using the Proton service role. + +``` +aws proton create-environment \ + --name "Beta" \ + --template-name public-vpc \ + --template-major-version 1 \ + --proton-service-role-arn arn:aws:iam::${account_id}:role/ProtonServiceRole \ + --spec file://specs/env-spec.yaml +``` + +Wait for the environment to successfully deploy. + +``` +aws proton wait environment-deployed --name Beta + +aws proton get-environment --name Beta +``` +### Create Environment in one account and Provision in another account + +First, log into the environment account where you want to provision the environment resources and create the IAM role that Proton will assume to provision resources and manage AWS CloudFormation stacks in your AWS account. +This can also be done from the console. https://docs.aws.amazon.com/proton/latest/adminguide/security_iam_service-role-policy-examples.html#proton-svc-role + +```bash +environment_account_id=`your_environment_account_id` + +aws iam create-role \ + --role-name ProtonServiceRole \ + --assume-role-policy-document file://./policies/proton-service-assume-policy.json + +aws iam attach-role-policy \ + --role-name ProtonServiceRole \ + --policy-arn arn:aws:iam::aws:policy/AdministratorAccess +``` + +Then, create and send an environment account connection request to your management account. When the request is accepted, AWS Proton can use the associated IAM role that permits environment resource provisioning in the associated environment account. +You need to specify the environment name that you will use for the environment. + +```bash +aws proton create-environment-account-connection \ + --management-account-id ${account_id} \ + --environment-name "Beta" \ + --role-arn arn:aws:iam::${environment_account_id}:role/ProtonServiceRole + +environment_account_connection_id=`replace_with_the_environment_account_connection_id_returned_above` +``` + +Log into the management account and accept the environment account connection request from your environment account. This can also be done from the console. + +```bash +aws proton accept-environment-account-connection --id ${environment_account_connection_id} +``` + +Then, create a Proton environment. This command reads your environment spec at `specs/env-spec.yaml`, merges it with the environment template created above, and deploys the resources in a CloudFormation stack in your environment AWS account using the Proton service role attached to the environment account connection. + +```bash +aws proton create-environment \ + --name "Beta" \ + --template-name public-vpc \ + --template-major-version 1 \ + --environment-account-connection-id ${environment_account_connection_id} \ + --spec file://specs/env-spec.yaml +``` + +Wait for the environment to successfully deploy. Use the `get` call to check for deployment status: + +```bash +aws proton wait environment-deployed --name Beta + +aws proton get-environment --name Beta +``` + +## Deploy A Service + +With the registered and published service template and deployed environment, you can now create a Proton service and deploy it into your Proton environment. + +This command reads your service spec at `specs/svc-public--spec.yaml`, merges it with the service template created above, and deploys the resources in CloudFormation stacks in the AWS account of the environment. +The service will provision a Lambda-based CRUD API endpoint and a CodePipeline pipeline to deploy your application code. + +Fill in your CodeStar Connections connection ID and your source code repository details in this command. + +``` +aws proton create-service \ + --name "front-end" \ + --repository-connection-arn arn:aws:codestar-connections:us-west-2:${account_id}:connection/ \ + --repository-id "/" \ + --branch "main" \ + --template-major-version 1 \ + --template-name lb-fargate-service-dotnetfx \ + --spec file://specs/svc-public-spec.yaml +``` + +Wait for the service to successfully deploy. + +``` +aws proton wait service-created --name front-end + +aws proton get-service --name front-end +``` diff --git a/environment-templates/fargate-dotnetfx-env/spec/spec.yaml b/environment-templates/fargate-dotnetfx-env/spec/spec.yaml new file mode 100644 index 0000000..59834af --- /dev/null +++ b/environment-templates/fargate-dotnetfx-env/spec/spec.yaml @@ -0,0 +1,3 @@ +proton: EnvironmentSpec +spec: + vpc_cidr: "10.0.0.0/16" diff --git a/environment-templates/fargate-dotnetfx-env/v1/infrastructure/cloudformation.yaml b/environment-templates/fargate-dotnetfx-env/v1/infrastructure/cloudformation.yaml new file mode 100644 index 0000000..f523191 --- /dev/null +++ b/environment-templates/fargate-dotnetfx-env/v1/infrastructure/cloudformation.yaml @@ -0,0 +1,284 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: AWS Fargate cluster running containers in public and private subnets. Supports + public facing microservices behind load balancer, private microservices, and private service discovery namespaces. Deployed resources are all tagged with defined environment name tag. +Mappings: + # The VPC and subnet configuration is passed in via the environment spec. + EnvironmentNameConfig: + Environment: + Name: '{{ environment.name}}' + SubnetConfig: + VPC: + CIDR: '{{ environment.inputs.vpc_cidr}}' + PublicOne: + CIDR: '{{ environment.inputs.public_subnet_one_cidr}}' + PublicTwo: + CIDR: '{{ environment.inputs.public_subnet_two_cidr}}' + PrivateOne: + CIDR: '{{ environment.inputs.private_subnet_one_cidr}}' + PrivateTwo: + CIDR: '{{ environment.inputs.private_subnet_two_cidr}}' + ServiceDiscoveryConfig: + PrivateNamespace: + Name: '{{ environment.inputs.service_discovery_namespace}}' + +Resources: + # Create the VPC with subnets across 2 Availability Zones, 2 Public subnets, 2 Private subnets, + # an Internet Gateway, 2 Nat Gateways and the required routetables and routes + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref InternetGateway + VpcId: !Ref VPC + + PublicSubnetOne: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: {Ref: 'AWS::Region'} + CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PublicSubnetTwo: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: {Ref: 'AWS::Region'} + CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PrivateSubnetOne: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: {Ref: 'AWS::Region'} + CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PrivateSubnetTwo: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: {Ref: 'AWS::Region'} + CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR'] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + NatGatewayOneEIP: + Type: AWS::EC2::EIP + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + NatGatewayTwoEIP: + Type: AWS::EC2::EIP + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + NatGatewayOne: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGatewayOneEIP.AllocationId + SubnetId: !Ref PublicSubnetOne + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + NatGatewayTwo: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGatewayTwoEIP.AllocationId + SubnetId: !Ref PublicSubnetTwo + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + DefaultPublicRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + PublicSubnetOneRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnetOne + + PublicSubnetTwoRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnetTwo + + PrivateRouteTableOne: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + DefaultPrivateRouteOne: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTableOne + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGatewayOne + + PrivateSubnetOneRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTableOne + SubnetId: !Ref PrivateSubnetOne + + PrivateRouteTableTwo: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + DefaultPrivateRouteTwo: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTableTwo + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGatewayTwo + + PrivateSubnetTwoRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTableTwo + SubnetId: !Ref PrivateSubnetTwo + + # Create a Private namespace for Service Discovery + PrivateNamespace: + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !FindInMap ['ServiceDiscoveryConfig', 'PrivateNamespace', 'Name'] + Vpc: !Ref 'VPC' + + # Create the ECS Cluster to schedule and orchestrate the Fargate containers + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + # A security group for the containers we will run in Fargate. + # Rules are added to this security group based on what ingress you + # add for the cluster. + ContainerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the Fargate containers + VpcId: !Ref 'VPC' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + # This is a role which is used by the ECS tasks themselves. + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [ecs-tasks.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + ManagedPolicyArns: + - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + +# These output values will be available to service templates to use. +Outputs: + ClusterName: + Description: The name of the ECS cluster + Value: !Ref 'ECSCluster' + ECSTaskExecutionRole: + Description: The ARN of the ECS role + Value: !GetAtt 'ECSTaskExecutionRole.Arn' + VpcId: + Description: The ID of the VPC that this stack is deployed in + Value: !Ref 'VPC' + PublicSubnetOne: + Description: A reference to the public subnet in the 1st Availability Zone + Value: !Ref 'PublicSubnetOne' + PublicSubnetTwo: + Description: A reference to the public subnet in the 2nd Availability Zone + Value: !Ref 'PublicSubnetTwo' + PrivateSubnetOne: + Description: A reference to the private subnet in the 1st Availability Zone + Value: !Ref 'PrivateSubnetOne' + PrivateSubnetTwo: + Description: A reference to the private subnet in the 2nd Availability Zone + Value: !Ref 'PrivateSubnetTwo' + ContainerSecurityGroup: + Description: A security group used to allow Fargate containers to receive traffic + Value: !Ref 'ContainerSecurityGroup' + PrivateNamespace: + Description: The NamespaceId registered for Service Discovery + Value: !Ref 'PrivateNamespace' diff --git a/environment-templates/fargate-dotnetfx-env/v1/infrastructure/manifest.yaml b/environment-templates/fargate-dotnetfx-env/v1/infrastructure/manifest.yaml new file mode 100644 index 0000000..eed587b --- /dev/null +++ b/environment-templates/fargate-dotnetfx-env/v1/infrastructure/manifest.yaml @@ -0,0 +1,5 @@ +infrastructure: + templates: + - file: "cloudformation.yaml" + rendering_engine: jinja + template_language: cloudformation diff --git a/environment-templates/fargate-dotnetfx-env/v1/schema/schema.yaml b/environment-templates/fargate-dotnetfx-env/v1/schema/schema.yaml new file mode 100644 index 0000000..48d7516 --- /dev/null +++ b/environment-templates/fargate-dotnetfx-env/v1/schema/schema.yaml @@ -0,0 +1,39 @@ +schema: + format: + openapi: "3.0.0" + environment_input_type: "PublicEnvironmentInput" + types: + PublicEnvironmentInput: + type: object + description: "Input properties for my environment" + properties: + vpc_cidr: + type: string + description: "The CIDR range for your VPC" + default: 10.0.0.0/16 + pattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) + public_subnet_one_cidr: + type: string + description: "The CIDR range for public subnet one" + default: 10.0.1.0/24 + pattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) + public_subnet_two_cidr: + type: string + description: "The CIDR range for public subnet one" + default: 10.0.2.0/24 + pattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) + private_subnet_one_cidr: + type: string + description: "The CIDR range for private subnet one" + default: 10.0.3.0/24 + pattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) + private_subnet_two_cidr: + type: string + description: "The CIDR range for private subnet two" + default: 10.0.4.0/24 + pattern: ([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24)) + service_discovery_namespace: + type: string + description: "The name of the private namespace for service discovery" + default: example.local + diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/spec/spec.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/spec/spec.yaml new file mode 100644 index 0000000..499689a --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/spec/spec.yaml @@ -0,0 +1,13 @@ +proton: ServiceSpec + +pipeline: + build_server_instance_type: "t3.medium" + +instances: + - name: "frontend-dev" + environment: "Beta" + spec: + desired_count: 1 + port: 80 + task_size: "medium" + service_discovery_name: "frontend-dev" diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/.compatible-envs b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/.compatible-envs new file mode 100644 index 0000000..164e93a --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/.compatible-envs @@ -0,0 +1 @@ +fargate-dotnetfx-env:1 diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/cloudformation.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/cloudformation.yaml new file mode 100644 index 0000000..3622368 --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/cloudformation.yaml @@ -0,0 +1,283 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Deploy a service on AWS Fargate, hosted in a public subnet, and accessible via a public load balancer. +Mappings: + EnvironmentNameConfig: + Environment: + Name: '{{environment.name}}' + TaskSize: + medium: + cpu: 1024 + memory: 2048 + large: + cpu: 2048 + memory: 4096 + x-large: + cpu: 4096 + memory: 8192 +Resources: + # A log group for storing the stdout logs from this service's containers + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: '{{service.name}}/{{service_instance.name}}' + + # The task definition. This is a simple metadata description of what + # container to run, and what resource requirements it has. + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: '{{service.name}}_{{service_instance.name}}' + Cpu: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, cpu] + Memory: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, memory] + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + RuntimePlatform: + CpuArchitecture: 'X86_64' + OperatingSystemFamily: '{{service_instance.inputs.cluster_operating_system_family}}' + ExecutionRoleArn: '{{environment.outputs.ECSTaskExecutionRole}}' + TaskRoleArn: !Ref 'AWS::NoValue' + ContainerDefinitions: + - Name: '{{service_instance.name}}' + Cpu: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, cpu] + Memory: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, memory] + Image: '{{service_instance.inputs.image}}' + PortMappings: + - ContainerPort: '{{service_instance.inputs.port}}' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: '{{service.name}}/{{service_instance.name}}' + awslogs-region: !Ref 'AWS::Region' + awslogs-stream-prefix: '{{service.name}}/{{service_instance.name}}' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + # The service_instance.inputs. The service is a resource which allows you to run multiple + # copies of a type of task, and gather up their logs and metrics, as well + # as monitor the number of running tasks and replace any that have crashed + Service: + Type: AWS::ECS::Service + DependsOn: LoadBalancerRule + Properties: + ServiceName: '{{service.name}}_{{service_instance.name}}' + Cluster: '{{environment.outputs.ClusterName}}' + LaunchType: FARGATE + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 75 + DesiredCount: {{service_instance.inputs.desired_count}} + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - '{{environment.outputs.ContainerSecurityGroup}}' + Subnets: + - '{{environment.outputs.PublicSubnetOne}}' + - '{{environment.outputs.PublicSubnetTwo}}' + TaskDefinition: !Ref 'TaskDefinition' + LoadBalancers: + - ContainerName: '{{service_instance.name}}' + ContainerPort: {{service_instance.inputs.port}} + TargetGroupArn: !Ref 'TargetGroup' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + # A target group. This is used for keeping track of all the tasks, and + # what IP addresses / port numbers they have. You can query it yourself, + # to use the addresses yourself, but most often this target group is just + # connected to an application load balancer, or network load balancer, so + # it can automatically distribute traffic across all the targets. + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 6 + HealthCheckPath: / + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + TargetType: ip + # Note that the Name property has a 32 character limit, which could be + # reached by using either {{service.name}}, {{service_instance.name}} + # or a combination of both as we're doing here, so we truncate the name to 29 characters + # plus an ellipsis different from '...' or '---' to avoid running into errors. + Name: '{{(service.name~"--"~service_instance.name)|truncate(29, true, 'zzz', 0)}}' + Port: '{{service_instance.inputs.port}}' + Protocol: HTTP + UnhealthyThresholdCount: 2 + VpcId: '{{environment.outputs.VpcId}}' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + # Create a rule on the load balancer for routing traffic to the target group + LoadBalancerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref 'TargetGroup' + Type: 'forward' + Conditions: + - Field: path-pattern + Values: + - '*' + ListenerArn: !Ref PublicLoadBalancerListener + Priority: 1 + + # Enable autoscaling for this service + ScalableTarget: + Type: AWS::ApplicationAutoScaling::ScalableTarget + DependsOn: Service + Properties: + ServiceNamespace: 'ecs' + ScalableDimension: 'ecs:service:DesiredCount' + ResourceId: !Sub 'service/{{environment.outputs.ClusterName}}/{{service.name}}_{{service_instance.name}}' + MinCapacity: 1 + MaxCapacity: 10 + RoleARN: !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService' + + # Create scaling policies for the service + ScaleDownPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: !Sub 'scale/{{service.name}}_{{service_instance.name}}/down' + PolicyType: StepScaling + ResourceId: !Sub 'service/{{environment.outputs.ClusterName}}/{{service.name}}_{{service_instance.name}}' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalUpperBound: 0 + ScalingAdjustment: -1 + MetricAggregationType: 'Average' + Cooldown: 60 + + ScaleUpPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: !Sub 'scale/{{service.name}}_{{service_instance.name}}/up' + PolicyType: StepScaling + ResourceId: !Sub 'service/{{environment.outputs.ClusterName}}/{{service.name}}_{{service_instance.name}}' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalLowerBound: 0 + MetricIntervalUpperBound: 15 + ScalingAdjustment: 1 + - MetricIntervalLowerBound: 15 + MetricIntervalUpperBound: 25 + ScalingAdjustment: 2 + - MetricIntervalLowerBound: 25 + ScalingAdjustment: 3 + MetricAggregationType: 'Average' + Cooldown: 60 + + # Create alarms to trigger these policies + LowCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub 'low-cpu-{{service.name}}_{{service_instance.name}}' + AlarmDescription: !Sub 'Low CPU utilization for service {{service.name}}_{{service_instance.name}}' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: '{{service.name}}_{{service_instance.name}}' + - Name: ClusterName + Value: + '{{environment.outputs.ClusterName}}' + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 20 + ComparisonOperator: LessThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleDownPolicy + + HighCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub 'high-cpu-{{service.name}}_{{service_instance.name}}' + AlarmDescription: !Sub 'High CPU utilization for service {{service.name}}_{{service_instance.name}}' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: '{{service.name}}_{{service_instance.name}}' + - Name: ClusterName + Value: + '{{environment.outputs.ClusterName}}' + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 70 + ComparisonOperator: GreaterThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleUpPolicy + + EcsSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from the public ALB + GroupId: '{{environment.outputs.ContainerSecurityGroup}}' + IpProtocol: '-1' + SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' + + # Public load balancer, hosted in public subnets that is accessible + # to the public, and is intended to route traffic to one or more public + # facing services. This is used for accepting traffic from the public + # internet and directing it to public facing microservices + PublicLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the public facing load balancer + VpcId: '{{environment.outputs.VpcId}}' + SecurityGroupIngress: + # Allow access to ALB from anywhere on the internet + - CidrIp: 0.0.0.0/0 + IpProtocol: '-1' + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PublicLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '30' + Subnets: + # The load balancer is placed into the public subnets, so that traffic + # from the internet can reach the load balancer directly via the internet gateway + - '{{environment.outputs.PublicSubnetOne}}' + - '{{environment.outputs.PublicSubnetTwo}}' + SecurityGroups: [!Ref 'PublicLoadBalancerSG'] + Tags: + - Key: Name + Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] + + PublicLoadBalancerListener: + Type: AWS::ElasticLoadBalancingV2::Listener + DependsOn: + - PublicLoadBalancer + Properties: + DefaultActions: + - TargetGroupArn: !Ref 'TargetGroup' + Type: 'forward' + LoadBalancerArn: !Ref 'PublicLoadBalancer' + Port: 80 + Protocol: HTTP + +Outputs: + ServiceEndpoint: + Description: The URL to access the service + Value: !Sub 'http://${PublicLoadBalancer.DNSName}' + diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/manifest.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/manifest.yaml new file mode 100644 index 0000000..eed587b --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/instance_infrastructure/manifest.yaml @@ -0,0 +1,5 @@ +infrastructure: + templates: + - file: "cloudformation.yaml" + rendering_engine: jinja + template_language: cloudformation diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/cloudformation.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/cloudformation.yaml new file mode 100644 index 0000000..dd62d21 --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/cloudformation.yaml @@ -0,0 +1,832 @@ +Mappings: + BuildConfig: + BuildServer: + InstanceType: '{{pipeline.inputs.build_server_instance_type}}' + VolumeType: '{{pipeline.inputs.build_server_volume_type}}' + VolumeSize: {{pipeline.inputs.build_server_volume_size}} + ImageBuild: + Dockerfile: '{{pipeline.inputs.dockerfile}}' +Resources: + ECRRepo: + Type: AWS::ECR::Repository + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + # This policy allows environment accounts to get the image from ECRRepo in the management account + RepositoryPolicyText: + Version: '2012-10-17' + Statement: + - Sid: AllowPull + Effect: Allow + Principal: + AWS: + {% for service_instance in service_instances %} + - "arn:aws:iam::{{service_instance.environment.account_id}}:root" + {% endfor %} + Action: + - ecr:GetAuthorizationToken + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + ContainerImageBuildServerRole: + Type : AWS::IAM::Role + Properties: + Path: / + ManagedPolicyArns: + - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' + - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "ec2.amazonaws.com" + - "ssm.amazonaws.com" + Action: "sts:AssumeRole" + ContainerImageBuildServerRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: ContainerImageBuildServerRoleDefaultPolicy + Roles: [!Ref ContainerImageBuildServerRole ] + PolicyDocument: + Version: '2012-10-17' + Statement: + - Action: s3:GetObject + Resource: !Sub arn:${AWS::Partition}:s3:::${PipelineArtifactsBucket}/* + Effect: Allow + - Action: + - kms:Decrypt + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + - Action: + - "ecr:GetAuthorizationToken" + Resource: "*" + Effect: Allow + - Action: + - "ecr:BatchCheckLayerAvailability" + - "ecr:BatchGetImage" + - "ecr:CompleteLayerUpload" + - "ecr:DescribeImages" + - "ecr:DescribeImageScanFindings" + - "ecr:DescribeRepositories" + - "ecr:GetDownloadUrlForLayer" + - "ecr:GetLifecyclePolicy" + - "ecr:GetLifecyclePolicyPreview" + - "ecr:GetRepositoryPolicy" + - "ecr:InitiateLayerUpload" + - "ecr:ListImages" + - "ecr:ListTagsForResource" + - "ecr:PutImage" + - "ecr:UploadLayerPart" + Resource: !GetAtt ECRRepo.Arn + Effect: Allow + ContainerImageBuildServerInstanceProfile: + Type: "AWS::IAM::InstanceProfile" + Properties: + Roles: + - !Ref ContainerImageBuildServerRole + BuildDotNetContainerDocument: + Type: AWS::SSM::Document + Properties: + DocumentType: Automation + Content: + schemaVersion: "0.3" + description: "Create a .NET Framework Container for CodeBuild" + parameters: + AmiId: + type: String + default: "{{'{{'}}ssm:{{pipeline.inputs.build_server_ami_id}}{{'}}'}}" + description: The Windows Server AMI to use for the build server must match version used by ECS cluster. + DockerFile: + type: String + default: !FindInMap ['BuildConfig', 'ImageBuild', 'Dockerfile'] + description: Location of the Dockerfile relative to repository's root folder + InstanceType: + type: String + description: (Optional) The instance type of the EC2 instance to be launched. + default: !FindInMap ['BuildConfig', 'BuildServer', 'InstanceType'] + IamInstanceProfileName: + type: String + description: (Required) The IAM instance profile to attach to the build instance. + default: !Ref ContainerImageBuildServerInstanceProfile + InstanceVolumeSize: + type: String + description: (Required) Desired volume size (GiB) of the build instance. + default: !FindInMap ['BuildConfig', 'BuildServer', 'VolumeSize'] + InstanceVolumeType: + type: String + description: (Required) Desired volume type of the build instance. + default: !FindInMap ['BuildConfig', 'BuildServer', 'VolumeType'] + PipelineBucketName: + type: String + description: (Required) Bucket that build artifact is stored in. + default: !Ref PipelineArtifactsBucket + SourceArtifactS3Path: + type: String + description: (Required) Build artifact key. + EcrRepoName: + default: !Ref ECRRepo + description: "ECR Repo Name" + type: "String" + ImageTag: + type: String + description: (Optional) Tag for container image. + default: latest + BuildLogGroupName: + type: String + description: (Optional) CloudWatch Log Group Name for build. + mainSteps: + {% raw %} + - name: createNewInstance + action: 'aws:runInstances' + maxAttempts: 3 + timeoutSeconds: 1200 + onFailure: Abort + inputs: + ImageId: '{{ AmiId }}' + InstanceType: '{{ InstanceType }}' + MinInstanceCount: 1 + MaxInstanceCount: 1 + IamInstanceProfileName: '{{ IamInstanceProfileName }}' + BlockDeviceMappings: + - DeviceName: /dev/xvdb + Ebs: + VolumeSize: '{{ InstanceVolumeSize }}' + VolumeType: '{{ InstanceVolumeType }}' + TagSpecifications: + - ResourceType: instance + Tags: + - Key: tag:aws:cloudformation:stack-id + Value: !Ref AWS::StackId + - Key: tag:aws:cloudformation:stack-name + Value: !Ref AWS::StackName + - Key: Name + Value: dotnetfx-container-build + nextStep: getInstance + - name: getInstance + action: 'aws:executeAwsApi' + maxAttempts: 2 + onFailure: Abort + inputs: + Service: ec2 + Api: DescribeInstances + InstanceIds: + - '{{ createNewInstance.InstanceIds }}' + outputs: + - Name: InstanceId + Selector: '$.Reservations[0].Instances[0].InstanceId' + Type: String + isCritical: 'true' + nextStep: waitForInstanceToBeReady + - name: waitForInstanceToBeReady + action: 'aws:waitForAwsResourceProperty' + onFailure: 'step:terminateInstance' + timeoutSeconds: 600 + maxAttempts: 2 + inputs: + Service: ec2 + Api: DescribeInstanceStatus + InstanceIds: + - '{{ getInstance.InstanceId }}' + PropertySelector: '$.InstanceStatuses[0].InstanceStatus.Details[0].Status' + DesiredValues: + - passed + isCritical: 'false' + nextStep: waitForSSMAgentOnline + - name: waitForSSMAgentOnline + action: 'aws:waitForAwsResourceProperty' + onFailure: 'step:terminateInstance' + timeoutSeconds: 600 + inputs: + Service: ssm + Api: DescribeInstanceInformation + InstanceInformationFilterList: + - key: InstanceIds + valueSet: + - '{{ getInstance.InstanceId }}' + PropertySelector: '$.InstanceInformationList[0].PingStatus' + DesiredValues: + - Online + isCritical: 'true' + nextStep: CreateDockerImage + - name: CreateDockerImage + action: 'aws:runCommand' + onFailure: 'step:terminateInstance' + timeoutSeconds: 7200 + inputs: + DocumentName: AWS-RunPowerShellScript + InstanceIds: + - '{{ getInstance.InstanceId }}' + Parameters: + commands: | + $ErrorActionPreference = "Stop" + + Write-Host "*****Downloading from S3...*****" + + New-Item -Name tmp -ItemType directory -Force | out-null + New-Item -Name tmp\src -ItemType directory -Force | out-null + + cd .\tmp + + $tmpFolder = $(Get-Location).Path + $srcFolder = "$tmpFolder\src" + $key = "{{SourceArtifactS3Path}}" + $srcFileName = "$(Split-Path $key -Leaf)" + + # Expand-Archive requires '.zip' file extension so add it if necessary + if ([System.IO.Path]::GetExtension($srcFileName) -ne ".zip") { + $srcFileName += ".zip" + } + + Read-S3Object -BucketName "{{PipelineBucketName}}" -Key $key -File $srcFileName + Expand-Archive -Path "$tmpFolder\$srcFileName" -DestinationPath "$srcFolder" -Force + + Write-Host "*****Building Docker image...*****" + + $instanceInfo = (Invoke-RestMethod -Method Get -Uri http://169.254.169.254/latest/dynamic/instance-identity/document) + $repoUri = $instanceInfo.accountId + '.dkr.ecr.' + $instanceInfo.region + '.amazonaws.com/{{EcrRepoName}}' + $latestUri = "$($repoUri):latest" + $hasCustomTag = $False + $buildArgs = @("-t", $latestUri) + $customTag = "{{ImageTag}}" + + if ($customTag -and $($customTag -ne "latest")) { + $hasCustomTag = $True + $uriWithTag = "$($repoUri):$customTag" + $buildArgs += @("-t", $uriWithTag) + } + + Write-Host "Docker Build Args are '$buildArgs'" + + cd $srcFolder + docker build $buildArgs $srcFolder\ + + if ($LASTEXITCODE -ne 0) { + throw ("'docker build $buildArgs $srcFolder\' execution failed with exit code $LASTEXITCODE.") + } + + Write-Host "*****Pushing Docker image to ECR...*****" + + Invoke-Expression –Command (Get-ECRLoginCommand –Region $region).Command + docker push $repoUri --all-tags + + if ($LASTEXITCODE -ne 0) { + throw "'docker push $repoUri --all-tags' execution failed with exit code $LASTEXITCODE." + } + executionTimeout: '7200' + CloudWatchOutputConfig: + CloudWatchLogGroupName: '{{ BuildLogGroupName }}' + CloudWatchOutputEnabled: true + isCritical: 'true' + nextStep: terminateInstance + - name: terminateInstance + action: 'aws:changeInstanceState' + maxAttempts: 3 + onFailure: Continue + inputs: + InstanceIds: + - '{{getInstance.InstanceId}}' + DesiredState: terminated + isCritical: 'true' + isEnd: 'true' + {% endraw %} + BuildProject: + Type: AWS::CodeBuild::Project + Properties: + Artifacts: + Type: CODEPIPELINE + Packaging: NONE + Environment: + ComputeType: BUILD_GENERAL1_SMALL + Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 + PrivilegedMode: false + Type: LINUX_CONTAINER + EnvironmentVariables: + - Name: repo_name + Type: PLAINTEXT + Value: !Ref ECRRepo + - Name: service_name + Type: PLAINTEXT + Value: '{{service.name}}' + ServiceRole: !GetAtt PublishRole.Arn + LogsConfig: + CloudWatchLogs: + GroupName: '{{service.name}}/pipeline' + Status: 'ENABLED' + StreamName: 'build' + Source: + InsecureSsl: false + BuildSpec: !Sub | + version: "0.2" + env: + variables: + DOCUMENT_NAME: ${BuildDotNetContainerDocument} + ECR_REPO_URI: ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepo} + LOG_GROUP_NAME: {{service.name}}/pipeline + phases: + install: + runtime-versions: + docker: 18 + commands: + - pip3 install --upgrade --user awscli + - echo 'f6bd1536a743ab170b35c94ed4c7c4479763356bd543af5d391122f4af852460 yq_linux_amd64' > yq_linux_amd64.sha + - wget https://github.com/mikefarah/yq/releases/download/3.4.0/yq_linux_amd64 + - sha256sum -c yq_linux_amd64.sha + - mv yq_linux_amd64 /usr/bin/yq + - chmod +x /usr/bin/yq + pre_build: + commands: + - BUILD_ID=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8) + - IMAGE_TAG=build-$BUILD_ID + - SRC_ARTIFACT_FULL_S3_PATH=${!CODEBUILD_SOURCE_VERSION#*"s3:::"} + - SRC_ARTIFACT_S3_PATH=${!SRC_ARTIFACT_FULL_S3_PATH#*"/"} + - SRC_ARTIFACT_BUCKET_NAME=$(echo $SRC_ARTIFACT_FULL_S3_PATH | cut -c 1-$((${!#SRC_ARTIFACT_FULL_S3_PATH} - ${!#SRC_ARTIFACT_S3_PATH} - 1))) + - echo SRC_ARTIFACT_FULL_S3_PATH=$SRC_ARTIFACT_FULL_S3_PATH + - echo SRC_ARTIFACT_S3_PATH=$SRC_ARTIFACT_S3_PATH + - echo SRC_ARTIFACT_BUCKET_NAME=$SRC_ARTIFACT_BUCKET_NAME + build: + commands: + - echo Starting automation execution using document $DOCUMENT_NAME... + - echo Logs are stored under CloudWatch Log Group $LOG_GROUP_NAME... + - EXECUTION_ID=$(aws ssm start-automation-execution --document-name $DOCUMENT_NAME --parameters BuildLogGroupName=$LOG_GROUP_NAME,DockerFile={{ pipeline.inputs.dockerfile }},ImageTag=$IMAGE_TAG,PipelineBucketName=$SRC_ARTIFACT_BUCKET_NAME,SourceArtifactS3Path=$SRC_ARTIFACT_S3_PATH --output text) + - echo Running execution $EXECUTION_ID... + - COMMAND="aws ssm describe-automation-executions --filters Key=ExecutionId,Values=$EXECUTION_ID" + - STATUS=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].AutomationExecutionStatus") + - | + while [ $STATUS = "InProgress" ]; + do sleep 3; + STATUS=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].AutomationExecutionStatus"); + done + - | + if [ $STATUS = "Success" ]; + then echo Automation execution succeeded.; + else echo Automation execution failed. Please check CloudWatch log for details.; + ERROR_MSG=$($COMMAND | jq -r ".AutomationExecutionMetadataList[0].FailureMessage"); + echo SSM Failure Message Follows.; + echo $ERROR_MSG; + exit 1; + fi + post_build: + commands: + - | + if [ $CODEBUILD_BUILD_SUCCEEDING -eq 1 ]; + then echo Writing updated service specification file...; + aws proton --region $AWS_DEFAULT_REGION get-service --name $service_name | jq -r .service.spec > service.yaml; + yq w service.yaml 'instances[*].spec.image' "$ECR_REPO_URI:$IMAGE_TAG" > rendered_service.yaml; + echo === Updated service spec follows === + cat rendered_service.yaml + fi + artifacts: + files: + - rendered_service.yaml + Type: CODEPIPELINE + EncryptionKey: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + TimeoutInMinutes: 60 + QueuedTimeoutInMinutes: 480 + + {% for service_instance in service_instances %} + Deploy{{loop.index}}Project: + Type: AWS::CodeBuild::Project + Properties: + Artifacts: + Type: CODEPIPELINE + Environment: + ComputeType: BUILD_GENERAL1_SMALL + Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 + PrivilegedMode: false + Type: LINUX_CONTAINER + EnvironmentVariables: + - Name: service_name + Type: PLAINTEXT + Value: '{{service.name}}' + - Name: service_instance_name + Type: PLAINTEXT + Value: '{{service_instance.name}}' + ServiceRole: !GetAtt DeploymentRole.Arn + LogsConfig: + CloudWatchLogs: + GroupName: '{{service.name}}/pipeline' + Status: 'ENABLED' + StreamName: 'deploy' + Source: + BuildSpec: | + version: "0.2" + phases: + build: + commands: + - pip3 install --upgrade --user awscli + - aws proton --region $AWS_DEFAULT_REGION update-service-instance --deployment-type CURRENT_VERSION --name $service_instance_name --service-name $service_name --spec file://rendered_service.yaml + - aws proton --region $AWS_DEFAULT_REGION wait service-instance-deployed --name $service_instance_name --service-name $service_name + Type: CODEPIPELINE + EncryptionKey: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + {% endfor %} + # This role is used to build container image + PublishRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Version: '2012-10-17' + PublishRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: PublishRoleDefaultPolicy + PolicyDocument: + Statement: + - Effect: Allow + Resource: + - !Sub 'arn:${AWS::Partition}:ec2:*::image/*' + - !Sub 'arn:${AWS::Partition}:ec2:*:*:network-interface/*' + - !Sub 'arn:${AWS::Partition}:ec2:*:*:security-group/*' + - !Sub 'arn:${AWS::Partition}:ec2:*:*:subnet/*' + - !Sub 'arn:${AWS::Partition}:ec2:*:*:volume/*' + Action: + - 'ec2:RunInstances' + - Effect: Allow + Resource: !Sub 'arn:${AWS::Partition}:ec2:*:*:instance/*' + Action: + - 'ec2:RunInstances' + Condition: + StringEquals: + "aws:RequestTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId + - Effect: Allow + Resource: !Sub 'arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' + Action: + - 'ec2:TerminateInstances' + Condition: + StringEquals: + "aws:ResourceTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId + - Effect: Allow + Resource: '*' + Action: + - 'ec2:DescribeInstances' + - 'ec2:DescribeInstanceStatus' + - 'ec2:CreateTags' + - Effect: Allow + Resource: + - !Sub 'arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' + Action: + - 'ssm:SendCommand' + Condition: + StringEquals: + "aws:ResourceTag/tag:aws:cloudformation:stack-id": !Ref AWS::StackId + - Effect: Allow + Resource: + - !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunPowerShellScript' + Action: + - 'ssm:SendCommand' + - Effect: Allow + Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ContainerImageBuildServerRole} + Action: + - 'iam:PassRole' + - Effect: Allow + Resource: '*' + Action: + - 'ssm:CancelCommand' + - 'ssm:DescribeAutomationExecutions' + - 'ssm:DescribeInstanceInformation' + - 'ssm:ListCommands' + - 'ssm:ListCommandInvocations' + - Effect: Allow + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-execution/ + Action: + - 'ssm:StopAutomationExecution' + - 'ssm:GetAutomationExecution' + - Effect: Allow + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${BuildDotNetContainerDocument}:$DEFAULT + Action: + - 'ssm:StartAutomationExecution' + - Effect: Allow + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::parameter/aws/service/ami-windows-latest/* + Action: + - 'ssm:GetParameters' + - Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Effect: Allow + Resource: + - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:{{service.name}}/pipeline' + - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:{{service.name}}/pipeline:*' + - Action: + - codebuild:CreateReportGroup + - codebuild:CreateReport + - codebuild:UpdateReport + - codebuild:BatchPutTestCases + Effect: Allow + Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${BuildProject}-*' + - Action: + - proton:GetService + Effect: Allow + Resource: "*" + - Action: + - 's3:PutObject' + - 's3:GetObject' + - 's3:GetObjectVersion' + - 's3:GetBucketAcl' + - 's3:GetBucketLocation' + Effect: Allow + Resource: + - !GetAtt PipelineArtifactsBucket.Arn + - !Sub '${PipelineArtifactsBucket.Arn}/*' + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + Version: '2012-10-17' + Roles: [ !Ref PublishRole ] + + DeploymentRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Version: '2012-10-17' + DeploymentRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: DeploymentRoleDefaultPolicy + PolicyDocument: + Statement: + - Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Effect: Allow + Resource: + - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:{{service.name}}/pipeline' + - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:{{service.name}}/pipeline:*' + - Action: + - codebuild:CreateReportGroup + - codebuild:CreateReport + - codebuild:UpdateReport + - codebuild:BatchPutTestCases + Effect: Allow + Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/Deploy*Project-*' + - Action: + - proton:UpdateServiceInstance + - proton:GetServiceInstance + Effect: Allow + Resource: '*' + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Effect: Allow + Resource: + - !GetAtt PipelineArtifactsBucket.Arn + - !Sub '${PipelineArtifactsBucket.Arn}/*' + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + Version: '2012-10-17' + Roles: [ !Ref DeploymentRole ] + PipelineArtifactsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + - kms:TagResource + - kms:UntagResource + Effect: Allow + Principal: + AWS: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:root' + Resource: '*' + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + - !GetAtt PipelineRole.Arn + - !GetAtt PublishRole.Arn + - !GetAtt DeploymentRole.Arn + - !GetAtt ContainerImageBuildServerRole.Arn + Resource: '*' + Version: '2012-10-17' + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + PipelineArtifactsBucket: + Type: AWS::S3::Bucket + Properties: + VersioningConfiguration: + Status: Enabled + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + KMSMasterKeyID: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + SSEAlgorithm: aws:kms + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + PipelineArtifactsBucketEncryptionKeyAlias: + Type: AWS::KMS::Alias + Properties: + AliasName: 'alias/codepipeline-encryption-key-{{ service.name }}' + TargetKeyId: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + PipelineRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: codepipeline.amazonaws.com + Version: '2012-10-17' + PipelineRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: PipelineRoleDefaultPolicy + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Effect: Allow + Resource: + - !GetAtt PipelineArtifactsBucket.Arn + - !Sub '${PipelineArtifactsBucket.Arn}/*' + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + - Action: codestar-connections:* + Effect: Allow + Resource: '*' + - Action: sts:AssumeRole + Effect: Allow + Resource: !GetAtt PipelineBuildCodePipelineActionRole.Arn + - Action: sts:AssumeRole + Effect: Allow + Resource: !GetAtt PipelineDeployCodePipelineActionRole.Arn + Version: '2012-10-17' + Roles: [ !Ref PipelineRole ] + Pipeline: + Type: AWS::CodePipeline::Pipeline + Properties: + RoleArn: !GetAtt PipelineRole.Arn + Stages: + - Actions: + - ActionTypeId: + Category: Source + Owner: AWS + Provider: CodeStarSourceConnection + Version: '1' + Configuration: + ConnectionArn: '{{ service.repository_connection_arn }}' + FullRepositoryId: '{{ service.repository_id }}' + BranchName: '{{ service.branch_name }}' + Name: Checkout + OutputArtifacts: + - Name: Artifact_Source_Checkout + RunOrder: 1 + Name: Source + - Actions: + - ActionTypeId: + Category: Build + Owner: AWS + Provider: CodeBuild + Version: '1' + Configuration: + ProjectName: !Ref BuildProject + InputArtifacts: + - Name: Artifact_Source_Checkout + Name: Build + OutputArtifacts: + - Name: BuildOutput + RoleArn: !GetAtt PipelineBuildCodePipelineActionRole.Arn + RunOrder: 1 + Name: Build + {%- for service_instance in service_instances %} + - Actions: + - ActionTypeId: + Category: Build + Owner: AWS + Provider: CodeBuild + Version: '1' + Configuration: + ProjectName: !Ref Deploy{{loop.index}}Project + InputArtifacts: + - Name: BuildOutput + Name: Deploy + RoleArn: !GetAtt PipelineDeployCodePipelineActionRole.Arn + RunOrder: 1 + Name: 'Deploy-{{service_instance.name}}' + {%- endfor %} + ArtifactStore: + EncryptionKey: + Id: !GetAtt PipelineArtifactsBucketEncryptionKey.Arn + Type: KMS + Location: !Ref PipelineArtifactsBucket + Type: S3 + DependsOn: + - PipelineRoleDefaultPolicy + - PublishRoleDefaultPolicy + - PipelineBuildCodePipelineActionRoleDefaultPolicy + - PipelineDeployCodePipelineActionRoleDefaultPolicy + - ContainerImageBuildServerRoleDefaultPolicy + PipelineBuildCodePipelineActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:root' + Version: '2012-10-17' + PipelineBuildCodePipelineActionRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - codebuild:BatchGetBuilds + - codebuild:StartBuild + - codebuild:StopBuild + Effect: Allow + Resource: !GetAtt BuildProject.Arn + Version: '2012-10-17' + PolicyName: PipelineBuildCodePipelineActionRoleDefaultPolicy + Roles: [ !Ref PipelineBuildCodePipelineActionRole ] + PipelineDeployCodePipelineActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:root' + Version: '2012-10-17' + PipelineDeployCodePipelineActionRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - codebuild:BatchGetBuilds + - codebuild:StartBuild + - codebuild:StopBuild + Effect: Allow + Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/Deploy*' + Version: '2012-10-17' + PolicyName: PipelineDeployCodePipelineActionRoleDefaultPolicy + Roles: [ !Ref PipelineDeployCodePipelineActionRole ] +Outputs: + PipelineEndpoint: + Description: The URL to access the pipeline + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${Pipeline}/view?region=${AWS::Region}' diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/manifest.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/manifest.yaml new file mode 100644 index 0000000..eed587b --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/pipeline_infrastructure/manifest.yaml @@ -0,0 +1,5 @@ +infrastructure: + templates: + - file: "cloudformation.yaml" + rendering_engine: jinja + template_language: cloudformation diff --git a/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/schema/schema.yaml b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/schema/schema.yaml new file mode 100644 index 0000000..526fa8d --- /dev/null +++ b/service-templates/loadbalanced-fargate-dotnetfx-svc/v1/schema/schema.yaml @@ -0,0 +1,77 @@ +schema: + format: + openapi: "3.0.0" + service_input_type: "LoadBalancedServiceInput" + pipeline_input_type: "PipelineInputs" + + types: + LoadBalancedServiceInput: + type: object + description: "Input properties for a loadbalanced Fargate service" + properties: + cluster_operating_system_family: + type: string + description: "The operating system used for the ECS Task Definition" + default: WINDOWS_SERVER_2019_CORE + enum: ['WINDOWS_SERVER_2019_CORE', 'WINDOWS_SERVER_2019_FULL', 'WINDOWS_SERVER_2022_CORE', 'WINDOWS_SERVER_2022_FULL'] + port: + type: number + description: "The port to route traffic to" + default: 80 + minimum: 0 + maximum: 65535 + desired_count: + type: number + description: "The default number of Fargate tasks you want running" + default: 1 + minimum: 1 + task_size: + type: string + description: "The size of the task you want to run" + enum: [ "medium", "large", "x-large"] + default: "medium" + image: + type: string + description: "The name/url of the container image" + default: "mcr.microsoft.com/dotnet/framework/samples:aspnetapp" + minLength: 1 + maxLength: 200 + service_discovery_name: + type: string + description: "The name of the service to register in service discovery" + minLength: 3 + maxLength: 24 + required: + - service_discovery_name + + PipelineInputs: + type: object + description: "Pipeline input properties" + properties: + build_server_ami_id: + type: string + description: SSM Parameter expression to use to retrieve the AMI ID for the build server. Must be compatible with operating system version used by ECS cluster. + default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Core-ECS_Optimized/image_id + build_server_instance_type: + type: string + description: "Instance type used for the EC2 Instance that serves as the build server." + default: t2.medium + enum: [t2.small, t2.medium, t2.large, + t3.small, t3.medium, t3.large, + t3a.small, t3a.medium, t3a.large ] + build_server_volume_size: + type: number + description: "Volume size in GiBs used when provisioning the build server." + default: 30 + minimum: 4 + maximum: 1024 + build_server_volume_type: + type: string + description: "Volume type used when provisioning the build server." + default: gp3 + enum: [ gp3, gp2 ] + dockerfile: + type: string + description: "The location of the Dockerfile to build" + default: "Dockerfile" + minLength: 1