From 90936c450fd83869cc22feeb47e401d8a4d9fa87 Mon Sep 17 00:00:00 2001 From: Sam Alba Date: Fri, 9 Apr 2021 16:00:38 -0700 Subject: [PATCH] examples/jamstack: implemented backend deployment running on ecs Signed-off-by: Sam Alba --- examples/jamstack/backend.cue | 125 ++++++++++++++++++++++++ examples/jamstack/ecr_image.cue | 55 +++++++++++ examples/jamstack/ecs.cue | 162 ++++++++++++++++++++++++++++++++ examples/jamstack/main.cue | 4 + 4 files changed, 346 insertions(+) create mode 100644 examples/jamstack/backend.cue create mode 100644 examples/jamstack/ecr_image.cue create mode 100644 examples/jamstack/ecs.cue create mode 100644 examples/jamstack/main.cue diff --git a/examples/jamstack/backend.cue b/examples/jamstack/backend.cue new file mode 100644 index 00000000..1a116307 --- /dev/null +++ b/examples/jamstack/backend.cue @@ -0,0 +1,125 @@ +package main + +import ( + "dagger.io/dagger" + "dagger.io/aws" + "dagger.io/aws/ecs" +) + +infra: { + // AWS auth & default region + awsConfig: aws.#Config + + // VPC Id + vpcId: string + + // ECR Image repository + ecrRepository: string + + // ECS cluster name + ecsClusterName: string + + // Execution Role ARN used for all tasks running on the cluster + ecsTaskRoleArn?: string + + // ELB listener ARN + elbListenerArn: string +} + +// Backend configuration +backend: { + // Source code to build this container + source: dagger.#Artifact + + // Container environment variables + environment: [string]: string + + // Public hostname (need to match the master domain configures on the loadbalancer) + hostname: string + + // Container configuration + container: { + // Desired number of running containers + desiredCount: *1 | int + // Time to wait for the HTTP timeout to complete + healthCheckTimeout: *10 | int + // HTTP Path to perform the healthcheck request (HTTP Get) + healthCheckPath: *"/" | string + // Number of times the health check needs to fail before recycling the container + healthCheckUnhealthyThreshold: *2 | int + // Port used by the process inside the container + port: *80 | int + // Memory to allocate + memory: *1024 | int + // Override the default container command + command: [...string] + // Custom dockerfile path + dockerfilePath: *"" | string + // docker build args + dockerBuildArgs: [string]: string + } + + // Init container runs only once when the main container starts + initContainer: { + command: [...string] + environment: [string]: string + } +} + +// Backend deployment logic +backend: { + let slug = name + + // Docker image built from source, pushed to ECR + image: #ECRImage & { + source: source + repository: infra.ecrRepository + tag: slug + awsConfig: infra.awsConfig + if backend.container.dockerfilePath != "" { + dockerfilePath: backend.container.dockerfilePath + } + buildArgs: backend.container.dockerBuildArgs + } + + // Creates an ECS Task + Service + deploy via Cloudformation + app: #ECSApp & { + awsConfig: infra.awsConfig + "slug": slug + clusterName: infra.ecsClusterName + vpcId: infra.vpcId + elbListenerArn: infra.elbListenerArn + if infra.ecsTaskRoleArn != _|_ { + taskRoleArn: infra.ecsTaskRoleArn + } + hostname: hostname + healthCheck: { + timeout: backend.container.healthCheckTimeout + path: backend.container.healthCheckPath + unhealthyThresholdCount: backend.container.healthCheckUnhealthyThreshold + } + desiredCount: backend.container.desiredCount + container: { + command: backend.container.command + environment: environment + port: backend.container.port + memory: backend.container.memory + "image": image.ref + } + } + + // Optional container to run one-time during the deploy (eg. db migration) + if len(backend.initContainer.command) > 0 { + initContainer: ecs.#RunTask & { + config: infra.awsConfig + containerName: slug + cluster: infra.ecsClusterName + if infra.ecsTaskRoleArn != _|_ { + roleArn: infra.ecsTaskRoleArn + } + containerEnvironment: backend.initContainer.environment + containerCommand: backend.initContainer.command + taskArn: app.taskArn + } + } +} diff --git a/examples/jamstack/ecr_image.cue b/examples/jamstack/ecr_image.cue new file mode 100644 index 00000000..06dccd8f --- /dev/null +++ b/examples/jamstack/ecr_image.cue @@ -0,0 +1,55 @@ +package main + +import ( + "dagger.io/dagger" + "dagger.io/dagger/op" + "dagger.io/aws" + "dagger.io/aws/ecr" +) + +// Build an image and push it to ECR +#ECRImage: { + source: dagger.#Artifact + // Path of the Dockerfile + dockerfilePath?: string + repository: string + tag: string + awsConfig: aws.#Config + buildArgs: [string]: string + + pushTarget: "\(repository):\(tag)" + + // Build the image + buildImage: op.#DockerBuild & { + context: source + if dockerfilePath != _|_ { + "dockerfilePath": dockerfilePath + } + buildArg: buildArgs + } + + // Use these credentials to push + ecrCreds: ecr.#Credentials & { + config: awsConfig + target: pushTarget + } + + push: #up: [ + op.#DockerBuild & { + context: source + if dockerfilePath != _|_ { + "dockerfilePath": dockerfilePath + } + buildArg: buildArgs + }, + op.Export & { + format: "string" + source: op.#PushContainer & { + ref: pushTarget + } + }, + ] + + // FIXME: ref does not include the sha256: https://github.com/dagger/dagger/issues/303 + ref: pushTarget +} diff --git a/examples/jamstack/ecs.cue b/examples/jamstack/ecs.cue new file mode 100644 index 00000000..6cf21ffa --- /dev/null +++ b/examples/jamstack/ecs.cue @@ -0,0 +1,162 @@ +package main + +import ( + "encoding/json" + + "dagger.io/aws" + "dagger.io/aws/elb" + "dagger.io/aws/cloudformation" +) + +#ECSApp: { + awsConfig: aws.#Config + slug: string + clusterName: string + vpcId: string + elbListenerArn: string + tlsCertificateArn: string + taskRoleArn: *"" | string + hostname: string + healthCheck: { + timeout: *10 | int + path: *"/" | string + unhealthyThresholdCount: *2 | int + } + desiredCount: int + container: { + command: [...string] + environment: [string]: string + port: *80 | int + cpu: *256 | int + memory: *1024 | int + image: string + } + + taskArn: cfnStack.outputs.TaskArn + + elbRulePriority: elb.#RandomRulePriority & { + config: awsConfig + listenerArn: elbListenerArn + vhost: hostname + } + + cfnStack: cloudformation.#Stack & { + config: awsConfig + stackName: slug + onFailure: "DO_NOTHING" + parameters: { + ELBRulePriority: elbRulePriority.out + ImageRef: container.image + ELBListenerArn: elbListenerArn + TLSCertificateArn: tlsCertificateArn + } + source: json.Marshal(template) + } + + template: { + AWSTemplateFormatVersion: "2010-09-09" + Description: "Blocklayer deployed app" + Parameters: { + ELBRulePriority: Type: "Number" + ImageRef: Type: "String" + ELBListenerArn: Type: "String" + TLSCertificateArn: Type: "String" + } + Resources: { + TLSCertificate: { + Type: "AWS::ElasticLoadBalancingV2::ListenerCertificate" + Properties: { + Certificates: [ {CertificateArn: Ref: "TLSCertificateArn"}] + ListenerArn: Ref: "ELBListenerArn" + } + } + ECSTaskDefinition: { + Type: "AWS::ECS::TaskDefinition" + Properties: { + Cpu: "\(container.cpu)" + Memory: "\(container.memory)" + if taskRoleArn != "" { + TaskRoleArn: taskRoleArn + } + NetworkMode: "bridge" + ContainerDefinitions: [{ + if len(container.command) > 0 { + Command: container.command + } + Name: slug + Image: Ref: "ImageRef" + Essential: true + Environment: [ for k, v in container.environment { + Name: k + Value: v + }] + PortMappings: [{ + ContainerPort: container.port + }] + StopTimeout: 5 + LogConfiguration: { + LogDriver: "awslogs" + Options: { + "awslogs-group": "bl/provider/ecs/\(clusterName)" + "awslogs-region": Ref: "AWS::Region" + "awslogs-create-group": "true" + "awslogs-stream-prefix": slug + } + } + }] + } + } + ECSListenerRule: { + Type: "AWS::ElasticLoadBalancingV2::ListenerRule" + Properties: { + ListenerArn: Ref: "ELBListenerArn" + Priority: Ref: "ELBRulePriority" + Conditions: [{ + Field: "host-header" + Values: [hostname]}] + Actions: [{ + Type: "forward" + TargetGroupArn: Ref: "ECSTargetGroup" + }]}} + ECSTargetGroup: { + Type: "AWS::ElasticLoadBalancingV2::TargetGroup" + Properties: { + Protocol: "HTTP" + VpcId: vpcId + Port: 80 + HealthCheckPath: healthCheck.path + UnhealthyThresholdCount: healthCheck.unhealthyThresholdCount + HealthCheckTimeoutSeconds: healthCheck.timeout + HealthCheckIntervalSeconds: healthCheck.timeout + 1 + HealthyThresholdCount: 3 + TargetGroupAttributes: [{ + Value: "10" + Key: "deregistration_delay.timeout_seconds" + }]}} + ECSService: { + Type: "AWS::ECS::Service" + Properties: { + Cluster: clusterName + DesiredCount: desiredCount + LaunchType: "EC2" + LoadBalancers: [{ + ContainerPort: container.port + TargetGroupArn: Ref: "ECSTargetGroup" + ContainerName: slug + }] + ServiceName: slug + TaskDefinition: Ref: "ECSTaskDefinition" + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true + Rollback: true + } + MaximumPercent: 200 + MinimumHealthyPercent: 100 + }} + DependsOn: "ECSListenerRule" + } + } + Outputs: TaskArn: Value: Ref: "ECSTaskDefinition" + } +} diff --git a/examples/jamstack/main.cue b/examples/jamstack/main.cue new file mode 100644 index 00000000..aecb750f --- /dev/null +++ b/examples/jamstack/main.cue @@ -0,0 +1,4 @@ +package main + +// Name of the application +name: string & =~"[a-z0-9-]+"