1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-06 22:32:51 +02:00

checkpoint

This commit is contained in:
Lee Brown
2019-07-11 14:15:29 -08:00
parent 0a6837a450
commit 4d78385103
9 changed files with 249 additions and 76 deletions

View File

@ -2,10 +2,10 @@ package main
import (
"context"
"crypto/tls"
"encoding/json"
"expvar"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/devops"
"log"
"net/http"
_ "net/http/pprof"
@ -16,6 +16,8 @@ import (
"syscall"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/devops"
"golang.org/x/crypto/acme/autocert"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/docs"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
@ -342,23 +344,70 @@ func main() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.API(shutdown, log, masterDb, redisClient, authenticator),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
// Make a channel to listen for errors coming from the listener. Use a
// buffered channel so the goroutine can exit if we don't collect this error.
serverErrors := make(chan error, 1)
// Start the service listening for requests.
go func() {
log.Printf("main : API Listening %s", cfg.HTTP.Host)
serverErrors <- api.ListenAndServe()
}()
// Make an list of HTTP servers for both HTTP and HTTPS requests.
var httpServers []http.Server
// Start the HTTP service listening for requests.
if cfg.HTTP.Host != "" {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.API(shutdown, log, masterDb, redisClient, authenticator),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
httpServers = append(httpServers, api)
go func() {
log.Printf("main : API Listening %s", cfg.HTTP.Host)
serverErrors <- api.ListenAndServe()
}()
}
// Start the HTTPS service listening for requests.
if cfg.HTTPS.Host != "" {
api := http.Server{
Addr: cfg.HTTPS.Host,
Handler: handlers.API(shutdown, log, masterDb, redisClient, authenticator),
ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
// Note: use a sensible value for data directory
// this is where cached certificates are stored
dataDir := "."
hostPolicy := func(ctx context.Context, host string) error {
// Note: change to your real domain
allowedHost := "www.mydomain.com"
if host == allowedHost {
return nil
}
return fmt.Errorf("acme/autocert: only %s host is allowed", allowedHost)
}
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: hostPolicy,
Cache: autocert.DirCache(dataDir),
}
api.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate}
httpServers = append(httpServers, api)
go func() {
log.Printf("main : API Listening %s", cfg.HTTPS.Host)
serverErrors <- api.ListenAndServeTLS("", "")
}()
}
// =========================================================================
// Shutdown
@ -381,11 +430,16 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), cfg.App.ShutdownTimeout)
defer cancel()
// Asking listener to shutdown and load shed.
err := api.Shutdown(ctx)
if err != nil {
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.App.ShutdownTimeout, err)
err = api.Close()
// Handle closing connections for both possible HTTP servers.
for _, api := range httpServers {
// Asking listener to shutdown and load shed.
err := api.Shutdown(ctx)
if err != nil {
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.App.ShutdownTimeout, err)
err = api.Close()
}
}
// Log the status of this shutdown.

View File

@ -20,7 +20,7 @@ import (
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-app/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/deploy"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
img_resize "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/img-resize"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
@ -277,7 +277,7 @@ func main() {
case "sync-static":
// sync static files to S3
if cfg.App.StaticS3.S3Enabled || cfg.App.StaticS3.CloudFrontEnabled {
err = deploy.SyncS3StaticFiles(awsSession, cfg.App.StaticS3.S3Bucket, cfg.App.StaticS3.S3KeyPrefix, cfg.App.StaticDir)
err = devops.SyncS3StaticFiles(awsSession, cfg.App.StaticS3.S3Bucket, cfg.App.StaticS3.S3KeyPrefix, cfg.App.StaticDir)
if err != nil {
log.Fatalf("main : deploy : %v", err)
}
@ -291,7 +291,7 @@ func main() {
// a publicly available image URL.
var staticS3UrlFormatter func(string) string
if cfg.App.StaticS3.S3Enabled || cfg.App.StaticS3.CloudFrontEnabled || cfg.App.StaticS3.ImgResizeEnabled {
s3UrlFormatter, err := deploy.S3UrlFormatter(awsSession, cfg.App.StaticS3.S3Bucket, cfg.App.StaticS3.S3KeyPrefix, cfg.App.StaticS3.CloudFrontEnabled)
s3UrlFormatter, err := devops.S3UrlFormatter(awsSession, cfg.App.StaticS3.S3Bucket, cfg.App.StaticS3.S3KeyPrefix, cfg.App.StaticS3.CloudFrontEnabled)
if err != nil {
log.Fatalf("main : S3UrlFormatter failed : %v", err)
}

View File

@ -215,6 +215,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=

View File

@ -1,4 +1,4 @@
package deploy
package devops
import (
"github.com/aws/aws-sdk-go/aws"

View File

@ -8,7 +8,9 @@
"Sid": "ServiceDeployPermissions",
"Effect": "Allow",
"Action": [
"ec2:DescribeSubnets",
"acm:ListCertificates",
"acm:RequestCertificate",
"acm:DescribeCertificate",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:CreateSecurityGroup",

View File

@ -1155,25 +1155,14 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
// When we are not using an Elastic Load Balancer, services need to support direct access via HTTPS.
// HTTPS is terminated via the web server and not on the Load Balancer.
if req.EnableHTTPS {
if req.EnableEcsElb {
// Enable services to be publicly available via HTTPS port 443 and forwarded to port 80.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
CidrIp: aws.String("0.0.0.0/0"),
FromPort: aws.Int64(443),
ToPort: aws.Int64(80),
GroupId: aws.String(securityGroupId),
})
} else {
// Enable services to be publicly available via HTTPS port 443.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
CidrIp: aws.String("0.0.0.0/0"),
FromPort: aws.Int64(443),
ToPort: aws.Int64(443),
GroupId: aws.String(securityGroupId),
})
}
// Enable services to be publicly available via HTTPS port 443.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
CidrIp: aws.String("0.0.0.0/0"),
FromPort: aws.Int64(443),
ToPort: aws.Int64(443),
GroupId: aws.String(securityGroupId),
})
}
// Add all the default ingress to the security group.
@ -2101,13 +2090,90 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
ValidationMethod: aws.String("DNS"),
})
if err != nil {
return errors.Wrapf(err, "failed to create certiciate '%s'", req.ServiceDomainName)
return errors.Wrapf(err, "failed to create certificate '%s'", req.ServiceDomainName)
}
certificateArn = *createRes.CertificateArn
log.Printf("\t\tCreated certiciate '%s'", req.ServiceDomainName)
log.Printf("\t\tCreated certificate '%s'", req.ServiceDomainName)
} else {
log.Printf("\t\tFound certiciate '%s'", req.ServiceDomainName)
log.Printf("\t\tFound certificate '%s'", req.ServiceDomainName)
}
descRes, err := svc.DescribeCertificate(&acm.DescribeCertificateInput{
CertificateArn: aws.String(certificateArn),
})
if err != nil {
return errors.Wrapf(err, "failed to describe certificate '%s'", certificateArn)
}
cert := descRes.Certificate
log.Printf("\t\t\tStatus: %s", *cert.Status)
if *cert.Status == "PENDING_VALIDATION" {
svc := route53.New(req.awsSession())
log.Println("\tList all hosted zones.")
var zoneValOpts = map[string][]*acm.DomainValidation{}
for _, opt := range cert.DomainValidationOptions {
var found bool
for zoneId, aNames := range zoneArecNames {
for _, aName := range aNames {
fmt.Println(*opt.DomainName, " ==== ", aName)
if *opt.DomainName == aName {
if _, ok := zoneValOpts[zoneId]; !ok {
zoneValOpts[zoneId] = []*acm.DomainValidation{}
}
zoneValOpts[zoneId] = append(zoneValOpts[zoneId], opt)
found = true
break
}
}
if found {
break
}
}
if !found {
return errors.Errorf("Failed to find zone ID for '%s'", *opt.DomainName)
}
}
for zoneId, opts := range zoneValOpts {
for _, opt := range opts {
if *opt.ValidationStatus == "SUCCESS" {
continue
}
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
&route53.Change{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: opt.ResourceRecord.Name,
ResourceRecords: []*route53.ResourceRecord{
&route53.ResourceRecord{Value: opt.ResourceRecord.Value},
},
Type: opt.ResourceRecord.Type,
TTL: aws.Int64(60),
},
},
},
},
HostedZoneId: aws.String(zoneId),
}
log.Printf("\tAdded verification record for '%s'.\n", *opt.ResourceRecord.Name)
_, err := svc.ChangeResourceRecordSets(input)
if err != nil {
return errors.Wrapf(err, "failed to update A records for zone '%s'", zoneId)
}
}
}
}
log.Printf("\t%s\tUsing ACM Certicate '%s'.\n", tests.Success, certificateArn)
@ -2261,7 +2327,7 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
// characters, must contain only alphanumeric characters or hyphens, and must
// not begin or end with a hyphen.
// Name is a required field
Name: aws.String(fmt.Sprintf("%s-http", *elb.LoadBalancerName)),
Name: aws.String(fmt.Sprintf("%s-http", req.EcsServiceName)),
// The port on which the targets receive traffic. This port is used unless you
// specify a port override when registering the target. If the target is a Lambda
@ -2348,17 +2414,18 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
},
}
/*
// If HTTPS is enabled, then add the associated target group.
if req.EnableHTTPS {
// Default target group for HTTPS via port 443.
targetGroupInputs = append(targetGroupInputs, &elbv2.CreateTargetGroupInput{
Name: aws.String(fmt.Sprintf("%s-https", *elb.LoadBalancerName)),
Name: aws.String(fmt.Sprintf("%s-https", req.EcsServiceName)),
Port: aws.Int64(443),
Protocol: aws.String("HTTPS"),
Protocol: aws.String("HTTP"),
HealthCheckEnabled: aws.Bool(true),
HealthCheckIntervalSeconds: aws.Int64(30),
HealthCheckPath: aws.String("/ping"),
HealthCheckProtocol: aws.String("HTTPS"),
HealthCheckProtocol: aws.String("HTTP"),
HealthCheckTimeoutSeconds: aws.Int64(5),
HealthyThresholdCount: aws.Int64(3),
UnhealthyThresholdCount: aws.Int64(3),
@ -2369,6 +2436,7 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
VpcId: aws.String(projectVpcId),
})
}
*/
for _, targetGroupInput := range targetGroupInputs {
var targetGroup *elbv2.TargetGroup
@ -2402,20 +2470,6 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
log.Printf("\t\tHas target group: %s.", *targetGroup.TargetGroupArn)
}
ecsELBs = append(ecsELBs, &ecs.LoadBalancer{
// The name of the container (as it appears in a container definition) to associate
// with the load balancer.
ContainerName: aws.String(req.EcsServiceName),
// The port on the container to associate with the load balancer. This port
// must correspond to a containerPort in the service's task definition. Your
// container instances must allow ingress traffic on the hostPort of the port
// mapping.
ContainerPort: targetGroup.Port,
// The full Amazon Resource Name (ARN) of the Elastic Load Balancing target
// group or groups associated with a service or task set.
TargetGroupArn: targetGroup.TargetGroupArn,
})
if req.ElbDeregistrationDelay != nil {
// If no target group was found, create one.
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
@ -2491,7 +2545,6 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
if *listenerInput.Protocol == "HTTPS" {
listenerInput.Certificates = append(listenerInput.Certificates, &elbv2.Certificate{
CertificateArn: aws.String(certificateArn),
IsDefault: aws.Bool(true),
})
}
@ -2503,11 +2556,75 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
log.Printf("\t\t\tAdded Listener: %s.", *createRes.Listeners[0].ListenerArn)
}
// HTTPS is terminated at the load balance by the listener and should be forwarded to the container
// via port 80. Port 80 is included already, so don't add a second ELB (not supported).
if *targetGroup.Port == 443 {
continue
}
ecsELBs = append(ecsELBs, &ecs.LoadBalancer{
// The name of the container (as it appears in a container definition) to associate
// with the load balancer.
ContainerName: aws.String(req.EcsServiceName),
// The port on the container to associate with the load balancer. This port
// must correspond to a containerPort in the service's task definition. Your
// container instances must allow ingress traffic on the hostPort of the port
// mapping.
ContainerPort: targetGroup.Port,
// The full Amazon Resource Name (ARN) of the Elastic Load Balancing target
// group or groups associated with a service or task set.
TargetGroupArn: targetGroup.TargetGroupArn,
})
}
{
log.Println("Ensure Load Balancer DNS name exists for hosted zones.")
log.Printf("\t\tDNSName: '%s'.\n", *elb.DNSName)
svc := route53.New(req.awsSession())
for zoneId, aNames := range zoneArecNames {
log.Printf("\tChange zone '%s'.\n", zoneId)
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{},
},
HostedZoneId: aws.String(zoneId),
}
// Add all the A record names with the same set of public IPs.
for _, aName := range aNames {
log.Printf("\t\tAdd A record for '%s'.\n", aName)
input.ChangeBatch.Changes = append(input.ChangeBatch.Changes, &route53.Change{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: aws.String(aName),
Type: aws.String("A"),
AliasTarget: &route53.AliasTarget{
HostedZoneId: elb.CanonicalHostedZoneId,
DNSName: elb.DNSName,
EvaluateTargetHealth: aws.Bool(true),
},
},
})
}
log.Printf("\tUpdated '%s'.\n", zoneId)
_, err := svc.ChangeResourceRecordSets(input)
if err != nil {
return errors.Wrapf(err, "failed to update A records for zone '%s'", zoneId)
}
}
}
log.Printf("\t%s\tUsing ELB '%s'.\n", tests.Success, *elb.LoadBalancerName)
}
}
return nil
// Try to find AWS ECS Cluster by name or create new one.
var ecsCluster *ecs.Cluster
@ -3288,6 +3405,14 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
healthCheckGracePeriodSeconds = req.EscServiceHealthCheckGracePeriodSeconds
}
// When ELB is enabled get the following error when using the default VPC.
// Status reason CannotPullContainerError:
// Error response from daemon:
// Get https://888955683113.dkr.ecr.us-west-2.amazonaws.com/v2/:
// net/http: request canceled while waiting for connection
// (Client.Timeout exceeded while awaiting headers)
assignPublicIp = aws.String("ENABLED")
serviceInput := &ecs.CreateServiceInput{
// The short name or full Amazon Resource Name (ARN) of the cluster that your
// service is running on. If you do not specify a cluster, the default cluster
@ -3654,14 +3779,5 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
log.Printf("\t%s\tService running.\n", tests.Success)
}
if req.EnableEcsElb {
// TODO: Need to connect ELB to route53
} else {
// This happens on the task Level when the service starts.
//if err := devops.RegisterEcsServiceTasksRoute53(log, req.awsSession(), req.EcsClusterName, req.EcsServiceName, zoneArecNames); err != nil {
// return err
//}
}
return nil
}