1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-10 23:57:45 +02:00

completed autocert implimentation for web-api

This commit is contained in:
Lee Brown 2019-07-13 03:03:30 -08:00
parent c757463a17
commit c5ea09d8e1
10 changed files with 164 additions and 129 deletions

View File

@ -36,8 +36,8 @@
{"name": "WEB_API_HTTPS_HOST", "value": "{HTTPS_HOST}"}, {"name": "WEB_API_HTTPS_HOST", "value": "{HTTPS_HOST}"},
{"name": "WEB_API_APP_PROJECT", "value": "{APP_PROJECT}"}, {"name": "WEB_API_APP_PROJECT", "value": "{APP_PROJECT}"},
{"name": "WEB_API_APP_BASE_URL", "value": "{APP_BASE_URL}"}, {"name": "WEB_API_APP_BASE_URL", "value": "{APP_BASE_URL}"},
{"name": "WEB_API_HOST_PRIMARY", "value": "{HOST_PRIMARY}"}, {"name": "WEB_API_APP_HOST_PRIMARY", "value": "{HOST_PRIMARY}"},
{"name": "WEB_API_HOST_NAMES", "value": "{HOST_NAMES}"}, {"name": "WEB_API_APP_HOST_NAMES", "value": "{HOST_NAMES}"},
{"name": "WEB_API_REDIS_HOST", "value": "{CACHE_HOST}"}, {"name": "WEB_API_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"}, {"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_API_DB_USER", "value": "{DB_USER}"}, {"name": "WEB_API_DB_USER", "value": "{DB_USER}"},

View File

@ -30,6 +30,7 @@ import (
"github.com/go-redis/redis" "github.com/go-redis/redis"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/lib/pq" "github.com/lib/pq"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws" awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
@ -84,6 +85,7 @@ func main() {
Host string `default:"" envconfig:"HOST"` Host string `default:"" envconfig:"HOST"`
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"` ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"` WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
} }
App struct { App struct {
Name string `default:"web-api" envconfig:"NAME"` Name string `default:"web-api" envconfig:"NAME"`
@ -179,7 +181,6 @@ func main() {
cfg.Aws.SecretsManagerConfigPrefix = filepath.Join(pts...) cfg.Aws.SecretsManagerConfigPrefix = filepath.Join(pts...)
} }
// If base URL is empty, set the default value from the HTTP Host // If base URL is empty, set the default value from the HTTP Host
if cfg.App.BaseUrl == "" { if cfg.App.BaseUrl == "" {
baseUrl := cfg.HTTP.Host baseUrl := cfg.HTTP.Host
@ -196,7 +197,6 @@ func main() {
cfg.App.BaseUrl = baseUrl cfg.App.BaseUrl = baseUrl
} }
// ========================================================================= // =========================================================================
// Log App Info // Log App Info
@ -239,7 +239,6 @@ func main() {
awsSession = awstrace.WrapSession(awsSession) awsSession = awstrace.WrapSession(awsSession)
} }
// ========================================================================= // =========================================================================
// Start Redis // Start Redis
// Ensure the eviction policy on the redis cluster is set correctly. // Ensure the eviction policy on the redis cluster is set correctly.
@ -310,7 +309,6 @@ func main() {
} }
defer masterDb.Close() defer masterDb.Close()
// ========================================================================= // =========================================================================
// Init new Authenticator // Init new Authenticator
var authenticator *auth.Authenticator var authenticator *auth.Authenticator
@ -324,15 +322,17 @@ func main() {
log.Fatalf("main : Constructing authenticator : %+v", err) log.Fatalf("main : Constructing authenticator : %+v", err)
} }
// ========================================================================= // =========================================================================
// Init redirect middleware to ensure all requests go to the primary domain. // Init redirect middleware to ensure all requests go to the primary domain.
primaryDomain := cfg.App.HostPrimary
// When primary host is not set, we can parse host from the base app URL.
if primaryDomain == "" {
baseSiteUrl, err := url.Parse(cfg.App.BaseUrl) baseSiteUrl, err := url.Parse(cfg.App.BaseUrl)
if err != nil { if err != nil {
log.Fatalf("main : Parse App Base URL : %s : %+v", cfg.App.BaseUrl, err) log.Fatalf("main : Parse App Base URL : %s : %+v", cfg.App.BaseUrl, err)
} }
var primaryDomain string
if strings.Contains(baseSiteUrl.Host, ":") { if strings.Contains(baseSiteUrl.Host, ":") {
primaryDomain, _, err = net.SplitHostPort(baseSiteUrl.Host) primaryDomain, _, err = net.SplitHostPort(baseSiteUrl.Host)
if err != nil { if err != nil {
@ -341,13 +341,22 @@ func main() {
} else { } else {
primaryDomain = baseSiteUrl.Host primaryDomain = baseSiteUrl.Host
} }
}
redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{ redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{
RedirectConfig: mid.RedirectConfig{
Code: http.StatusMovedPermanently,
Skipper: func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) bool {
if r.URL.Path == "/ping" {
return true
}
return false
},
},
DomainName: primaryDomain, DomainName: primaryDomain,
HTTPSEnabled: (cfg.HTTPS.Host != ""), HTTPSEnabled: (cfg.HTTPS.Host != ""),
}) })
// ========================================================================= // =========================================================================
// Start Tracing Support // Start Tracing Support
th := fmt.Sprintf("%s:%d", cfg.Trace.Host, cfg.Trace.Port) th := fmt.Sprintf("%s:%d", cfg.Trace.Host, cfg.Trace.Port)
@ -369,7 +378,6 @@ func main() {
}() }()
} }
// ========================================================================= // =========================================================================
// ECS Task registration for services that don't use an AWS Elastic Load Balancer. // ECS Task registration for services that don't use an AWS Elastic Load Balancer.
err = devops.EcsServiceTaskInit(log, awsSession) err = devops.EcsServiceTaskInit(log, awsSession)
@ -377,7 +385,6 @@ func main() {
log.Fatalf("main : Ecs Service Task init : %+v", err) log.Fatalf("main : Ecs Service Task init : %+v", err)
} }
// ========================================================================= // =========================================================================
// Start API Service // Start API Service
@ -448,7 +455,10 @@ func main() {
// Enable autocert to store certs via Secret Manager. // Enable autocert to store certs via Secret Manager.
secretPrefix := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "autocert") secretPrefix := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "autocert")
cache, err := devops.NewSecretManagerAutocertCache(log, awsSession, secretPrefix) // Local file cache to reduce requests hitting Secret Manager.
localCache := autocert.DirCache(os.TempDir())
cache, err := devops.NewSecretManagerAutocertCache(log, awsSession, secretPrefix, localCache)
if err != nil { if err != nil {
log.Fatalf("main : HTTPS : %+v", err) log.Fatalf("main : HTTPS : %+v", err)
} }
@ -459,11 +469,15 @@ func main() {
Cache: cache, Cache: cache,
} }
api.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate} api.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate}
api.TLSConfig.NextProtos = append(api.TLSConfig.NextProtos, acme.ALPNProto)
if !cfg.HTTPS.DisableHTTP2 {
api.TLSConfig.NextProtos = append(api.TLSConfig.NextProtos, "h2")
}
httpServers = append(httpServers, api) httpServers = append(httpServers, api)
go func() { go func() {
log.Printf("main : API Listening %s", cfg.HTTPS.Host) log.Printf("main : API Listening %s with SSL cert for hosts %s", cfg.HTTPS.Host, strings.Join(hosts, ", "))
serverErrors <- api.ListenAndServeTLS("", "") serverErrors <- api.ListenAndServeTLS("", "")
}() }()
} }

View File

@ -2,9 +2,10 @@ package mid
import ( import (
"context" "context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"net/http"
) )
type ( type (
@ -56,8 +57,6 @@ func DomainNameRedirect(config DomainNameRedirectConfig) web.Middleware {
// Redirects http requests to https. // Redirects http requests to https.
if config.HTTPSEnabled { if config.HTTPSEnabled {
if ok = scheme != "https"; ok { if ok = scheme != "https"; ok {
url = "https://" + host + uri
scheme = "https" scheme = "https"
} }
} }
@ -65,16 +64,15 @@ func DomainNameRedirect(config DomainNameRedirectConfig) web.Middleware {
// Redirects all domain name alternatives to the primary hostname. // Redirects all domain name alternatives to the primary hostname.
if host != config.DomainName { if host != config.DomainName {
host = config.DomainName host = config.DomainName
ok = true
} }
url = scheme + "://" + host + uri url = scheme + "://" + host + uri
return return
}) })
} }
// HTTPSRedirect redirects http requests to https. // HTTPSRedirect redirects http requests to https.
// For example, http://geeksinthewoods.com will be redirect to https://geeksinthewoods.com. // For example, http://geeksinthewoods.com will be redirect to https://geeksinthewoods.com.
func HTTPSRedirect() web.Middleware { func HTTPSRedirect() web.Middleware {
@ -83,7 +81,7 @@ func HTTPSRedirect() web.Middleware {
// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSRedirect()`. // See `HTTPSRedirect()`.
func HTTPSRedirectWithConfig(config RedirectConfig)web.Middleware { func HTTPSRedirectWithConfig(config RedirectConfig) web.Middleware {
return redirect(config, func(scheme, host, uri string) (ok bool, url string) { return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if ok = scheme != "https"; ok { if ok = scheme != "https"; ok {
url = "https://" + host + uri url = "https://" + host + uri

View File

@ -7,10 +7,10 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"github.com/aws/aws-sdk-go/aws/session"
) )
// SecretManagerAutocertCache implements the autocert.Cache interface for AWS Secrets Manager that is used by Manager // SecretManagerAutocertCache implements the autocert.Cache interface for AWS Secrets Manager that is used by Manager
@ -19,14 +19,16 @@ type SecretManagerAutocertCache struct {
awsSession *session.Session awsSession *session.Session
log *log.Logger log *log.Logger
secretPrefix string secretPrefix string
cache autocert.Cache
} }
// NewSecretManagerAutocertCache provides the functionality to keep config files sync'd between running tasks and across deployments. // NewSecretManagerAutocertCache provides the functionality to keep config files sync'd between running tasks and across deployments.
func NewSecretManagerAutocertCache(log *log.Logger, awsSession *session.Session, secretPrefix string ) (*SecretManagerAutocertCache, error) { func NewSecretManagerAutocertCache(log *log.Logger, awsSession *session.Session, secretPrefix string, cache autocert.Cache) (*SecretManagerAutocertCache, error) {
return &SecretManagerAutocertCache{ return &SecretManagerAutocertCache{
awsSession, awsSession,
log, log,
secretPrefix, secretPrefix,
cache,
}, nil }, nil
} }
@ -34,6 +36,16 @@ func NewSecretManagerAutocertCache(log *log.Logger, awsSession *session.Session,
// If there's no such key, Get returns ErrCacheMiss. // If there's no such key, Get returns ErrCacheMiss.
func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byte, error) { func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byte, error) {
// Check short term cache.
if c.cache != nil {
v, err := c.cache.Get(ctx, key)
if err != nil && err != autocert.ErrCacheMiss {
return nil, errors.WithStack(err)
} else if len(v) > 0 {
return v, nil
}
}
svc := secretsmanager.New(c.awsSession) svc := secretsmanager.New(c.awsSession)
secretID := filepath.Join(c.secretPrefix, key) secretID := filepath.Join(c.secretPrefix, key)
@ -43,7 +55,7 @@ func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byt
SecretId: aws.String(secretID), SecretId: aws.String(secretID),
}) })
if err != nil { if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException { if aerr, ok := err.(awserr.Error); ok && (aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException || aerr.Code() == secretsmanager.ErrCodeInvalidRequestException) {
return nil, autocert.ErrCacheMiss return nil, autocert.ErrCacheMiss
} }
@ -52,7 +64,7 @@ func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byt
log.Printf("AWS Secrets Manager : Secret %s found", secretID) log.Printf("AWS Secrets Manager : Secret %s found", secretID)
return res.SecretBinary, nil return []byte(*res.SecretString), nil
} }
// Put stores the data in the cache under the specified key. // Put stores the data in the cache under the specified key.
@ -70,9 +82,9 @@ func (c *SecretManagerAutocertCache) Put(ctx context.Context, key string, data [
SecretString: aws.String(string(data)), SecretString: aws.String(string(data)),
}) })
if err != nil { if err != nil {
if aerr, ok := err.(awserr.Error); !ok { aerr, ok := err.(awserr.Error)
if aerr.Code() == secretsmanager.ErrCodeInvalidRequestException { if ok && aerr.Code() == secretsmanager.ErrCodeInvalidRequestException {
// InvalidRequestException: You can't create this secret because a secret with this // InvalidRequestException: You can't create this secret because a secret with this
// name is already scheduled for deletion. // name is already scheduled for deletion.
@ -84,10 +96,9 @@ func (c *SecretManagerAutocertCache) Put(ctx context.Context, key string, data [
return errors.Wrapf(err, "autocert failed to restore secret %s", secretID) return errors.Wrapf(err, "autocert failed to restore secret %s", secretID)
} }
} else if aerr.Code() != secretsmanager.ErrCodeResourceExistsException { } else if !ok || aerr.Code() != secretsmanager.ErrCodeResourceExistsException {
return errors.Wrapf(err, "autocert failed to create secret %s", secretID) return errors.Wrapf(err, "autocert failed to create secret %s", secretID)
} }
}
// If where was a resource exists error for create, then need to update the secret instead. // If where was a resource exists error for create, then need to update the secret instead.
_, err = svc.UpdateSecret(&secretsmanager.UpdateSecretInput{ _, err = svc.UpdateSecret(&secretsmanager.UpdateSecretInput{
@ -103,6 +114,13 @@ func (c *SecretManagerAutocertCache) Put(ctx context.Context, key string, data [
log.Printf("AWS Secrets Manager : Secret %s created", secretID) log.Printf("AWS Secrets Manager : Secret %s created", secretID)
} }
if c.cache != nil {
err = c.cache.Put(ctx, key, data)
if err != nil {
return errors.WithStack(err)
}
}
return nil return nil
} }
@ -145,7 +163,14 @@ func (c *SecretManagerAutocertCache) Delete(ctx context.Context, key string) err
return errors.Wrapf(err, "autocert failed to delete secret %s", secretID) return errors.Wrapf(err, "autocert failed to delete secret %s", secretID)
} }
log.Printf("AWS Secrets Manager : Secret %s deleted for %s", secretID) log.Printf("AWS Secrets Manager : Secret %s deleted", secretID)
if c.cache != nil {
err = c.cache.Delete(ctx, key)
if err != nil {
return errors.WithStack(err)
}
}
return nil return nil
} }

View File

@ -41,7 +41,6 @@ func SyncCfgInit(log *log.Logger, awsSession *session.Session, secretPrefix, wat
// Init the watch to wait for sync local files to Secret Manager. // Init the watch to wait for sync local files to Secret Manager.
WatchCfgDir(log, awsSession, secretPrefix, watchDir, watcher, localfiles) WatchCfgDir(log, awsSession, secretPrefix, watchDir, watcher, localfiles)
// Init ticker to sync remote files from Secret Manager locally at the defined interval. // Init ticker to sync remote files from Secret Manager locally at the defined interval.
if syncInterval.Seconds() > 0 { if syncInterval.Seconds() > 0 {
ticker := time.NewTicker(syncInterval) ticker := time.NewTicker(syncInterval)

View File

@ -3,9 +3,6 @@ package devops
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"github.com/sethgrid/pester"
"io/ioutil"
"log" "log"
"os" "os"
"strconv" "strconv"
@ -33,15 +30,6 @@ func EcsServiceTaskInit(log *log.Logger, awsSession *session.Session) error {
return nil return nil
} }
res, err := pester.Get("http://169.254.170.2/v2/metadata")
if err != nil {
fmt.Println("http://169.254.170.2/v2/metadata failed", err.Error())
} else {
dat, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
fmt.Println("http://169.254.170.2/v2/metadata, OK", string(dat))
}
var zoneArecNames = map[string][]string{} var zoneArecNames = map[string][]string{}
if v := os.Getenv("ROUTE53_ZONES"); v != "" { if v := os.Getenv("ROUTE53_ZONES"); v != "" {
dat, err := base64.RawURLEncoding.DecodeString(v) dat, err := base64.RawURLEncoding.DecodeString(v)
@ -241,6 +229,15 @@ func RegisterEcsServiceTasksRoute53(log *log.Logger, awsSession *session.Session
} }
/* /*
res, err := pester.Get("http://169.254.170.2/v2/metadata")
if err != nil {
fmt.Println("http://169.254.170.2/v2/metadata failed", err.Error())
} else {
dat, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
fmt.Println("http://169.254.170.2/v2/metadata, OK", string(dat))
}
http://169.254.170.2/v2/metadata, http://169.254.170.2/v2/metadata,
{ {

View File

@ -403,6 +403,8 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
"secretsmanager:GetSecretValue", "secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret", "secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret", "secretsmanager:UpdateSecret",
"secretsmanager:RestoreSecret",
"secretsmanager:DeleteSecret",
}, },
Resource: "*", Resource: "*",
}, },
@ -2560,8 +2562,8 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
"{ECS_SERVICE}": req.EcsServiceName, "{ECS_SERVICE}": req.EcsServiceName,
"{AWS_REGION}": req.AwsCreds.Region, "{AWS_REGION}": req.AwsCreds.Region,
"{AWS_LOGS_GROUP}": req.CloudWatchLogGroupName, "{AWS_LOGS_GROUP}": req.CloudWatchLogGroupName,
"{AWS_AWS_S3_BUCKET_PRIVATE}": req.S3BucketPrivateName, "{AWS_S3_BUCKET_PRIVATE}": req.S3BucketPrivateName,
"{S3_BUCKET_PUBLIC}": req.S3BucketPublicName, "{AWS_S3_BUCKET_PUBLIC}": req.S3BucketPublicName,
"{ENV}": req.Env, "{ENV}": req.Env,
"{DATADOG_APIKEY}": datadogApiKey, "{DATADOG_APIKEY}": datadogApiKey,
"{DATADOG_ESSENTIAL}": "true", "{DATADOG_ESSENTIAL}": "true",