feat: configure environment security group from env manifest#3810
feat: configure environment security group from env manifest#3810mergify[bot] merged 21 commits intoaws:mainlinefrom
Conversation
|
Should this be classified as |
75f2f23 to
5f382ae
Compare
| } | ||
|
|
||
| // IsEmpty returns true if there is no security group rule defined. | ||
| func (cfg securityGroupConfig) IsEmpty() bool { |
There was a problem hiding this comment.
Does this method need to be exported?
There was a problem hiding this comment.
Actually it was required before when we had not implemented the Demeter law suggested by Efe but now I think we don't need to export this I will change it to lower case.
| } | ||
|
|
||
| type securityGroupRule struct { | ||
| CidrIP string `yaml:"cidr"` |
There was a problem hiding this comment.
This is sort of related to my other comment in templates/environment/cf.yml. I keep debating whether in general we will need to standardize these basic fields to have a pointer, so that we can distinguish an input of zero value from an absent input in env manifest, that is for example, to tell
- to_port: 0
ip_protocol: "tcp"
from
- ip_protocol: "tcp"
If we use *int, then we would be able to; if we use int (like what you are doing now), then we won't be able to tell.
For service manifest, we need to be able to tell them apart so that environment overrides are applied correctly; but this is not a thing in environment manifest.
Up until now, it seems like for env manifest, it's not entirely necessary for us to distinguish zero-value vs. absent except for bool fields. But I don wonder though whether we should force every basic field to be a pointer field just to standardize it 💭
There was a problem hiding this comment.
That is a good call out actually. In the env manifest we do not have a consistent way of implementing this particularly thing. Currently, I have changed ToPort and FromPort to be pointers so that we can figure if customer has actually set this parameters in the manifest or not? And if not then set them as 0 or 65535 whatever we decide in our other conversation here
efekarakus
left a comment
There was a problem hiding this comment.
Looks great! just some small feedback
|
|
||
| func convertEnvSecurityGroupCfg(mft *manifest.Environment) template.SecurityGroupConfig { | ||
| securityGroupConfig, isSecurityConfigSet := mft.EnvSecurityGroup() | ||
| if isSecurityConfigSet { |
There was a problem hiding this comment.
nit: we can flip the conditional to reduce nesting
| if isSecurityConfigSet { | |
| if !isSecurityConfigSet { | |
| return template.SecurityGroupConfig{} | |
| } | |
| ingress := make(// remaining code...) |
| Observability environmentObservability `yaml:"observability,omitempty,flow"` | ||
| HTTPConfig EnvironmentHTTPConfig `yaml:"http,omitempty,flow"` | ||
| CDNConfig environmentCDNConfig `yaml:"cdn,omitempty,flow"` | ||
| SecurityGroupConfig securityGroupConfig `yaml:"security_group,omitempty"` |
There was a problem hiding this comment.
Sorry Parag this is my bad! would you mind moving back this field to under what you had originally network.vpc.?
The main reasoning behind it is that:
- Consistency between the existing
network.vpc.security_groupsfield as this sg affects the connectivity of all services in the VPC. - We will be adding also
http.public.security_groupso there is a nice symmetry by being two levels deep
There was a problem hiding this comment.
I had a conversation with @Lou1415926 about this and we concluded that security group of the environment is not a part of VPC but it's a part of environment itself so it shouldn't go under VPC config and be on it's own in the env manifest, hence I made changes accordingly.
What are your thoughts on this?
There was a problem hiding this comment.
As per internal discussion with the team we are concluding this as -
security_group should be inside network.vpc as EnvironmentSecurityGroup will get applied to all the services in the environment in the particular vpc. And it also aligns well with the http.public.security_groups that we have in the env manifest. So network.vpc's security_group is for all the services in the environment and http.public's security_groups is applied to the load balancer.
efekarakus
left a comment
There was a problem hiding this comment.
Yay! the port range changes look awesome!
| func getPortValues(cfg manifest.SecurityGroupRule) (fromPort int, toPort int, err error) { | ||
| if cfg.PortsConfig.Ports.IsEmpty() { | ||
| toPort = *cfg.PortsConfig.Port | ||
| fromPort = *cfg.PortsConfig.Port | ||
| } else { | ||
| fromPort, toPort, err = cfg.PortsConfig.Ports.Parse() | ||
| if err != nil { | ||
| return 0, 0, err | ||
| } | ||
| } | ||
| return fromPort, toPort, nil | ||
| } |
There was a problem hiding this comment.
What do you think of moving this to be a method on *manifest.SecurityGroupRule instead? This way IsEmpty and Parse can become private methods.
// Ports returns the from and to ports of a security group rule.
func (r manifest.SecurityGroupRule) Ports() (from, to int, err error) {
if r.PortsConfig.Ports.isEmpty() {
return *cfg.PortsConfig.Port, *cfg.PortsConfig.Port, nil // a single value is provided for ports.
}
return cfg.PortsConfig.Ports.parse()
}There was a problem hiding this comment.
That's such a great idea. I like this one. I will make the change. :)
But I can only make Parse method private as IsEmpty is used in the validate_env.
|
|
||
| // PortsRangeBand represents a range of from and to port values. | ||
| // For example, "0-65536". | ||
| type PortsRangeBand string |
There was a problem hiding this comment.
qq: Do these structs have to be exported? SecurityGroupRule, PortsConfig and PortsRangeBand, etc.
There was a problem hiding this comment.
Also, what do you think of reusing IntRangeBand instead of creating PortsRangeBand?
There was a problem hiding this comment.
Very good question, even I was wondering if I need these structs to be exported but then I could see almost all the structs are kept public in env manifest file. But you are right I should change them if they are not exported.
Also I did not use IntRangeBand purposefully because IntRangeBand has different definition of validate than what we needed for the port configs.
There was a problem hiding this comment.
I'm not sure which part of IntRangeBand's validate logic can't be reused here 🤔 I might be missing something obvious, but it seems to me that both struct's validate are just checking if they are formatted as min-max...
There was a problem hiding this comment.
So the error message that validate function returns is different than what we need. Customer might get confused by the min and max wordings in the error message. Other than that I guess everything is fine. It's just that we return human readable errors for strconv.Atoi. Let me know if you still want me to use IntRangeBand
| // struct, allowing it to perform more complex unmarshaling behavior. | ||
| // This method implements the yaml.Unmarshaler (v3) interface. | ||
| func (cfg *PortsConfig) UnmarshalYAML(value *yaml.Node) error { | ||
| _, err := strconv.Atoi(value.Value) // detmines if input is integer? if err then we decode range values |
There was a problem hiding this comment.
qq: is this check necessary? If the input isn't an integer, value.Decode(&cfg.Port) would have returned an error anyway, right?
There was a problem hiding this comment.
if customer somehow defines ports: "10" then unmarshaling fails. Also when I write an integration test, ports are considered as string and then it unmarshals ports: 10 into PortsRangeBand and the integ test fails. Hence converting it into int before unmarshal it.
There was a problem hiding this comment.
What about swap the order, trying to unmarshal into int first then string?
There was a problem hiding this comment.
I tried doing that as well. But the problem is that if I unmarshal into int first then it sets the value of the for example 0-65335 as 0 in the int variable and then int variable becomes non empty and wrong unmarshaling happens. So that is why I am trying to convert input into int 1st and if it is not convertible then we know for sure that it's a range.
Lou1415926
left a comment
There was a problem hiding this comment.
Just some nits and thoughts!
| return *r.PortsConfig.Port, *r.PortsConfig.Port, nil // a single value is provided for ports. | ||
| // GetPorts returns the from and to ports of a security group rule. | ||
| func (r securityGroupRule) GetPorts() (from, to int, err error) { | ||
| if r.Ports.Ports == nil { |
There was a problem hiding this comment.
nit: r.Ports.Ports reads a little awkward 😂 :homer-disappearing what do you think of r.Ports.Range?
There was a problem hiding this comment.
Hahah, I know it looks little weird.
I will change it Range.
| if cfg.Ports != nil { | ||
| if err := cfg.Ports.validate(); err != nil { | ||
| if strings.Contains(err.Error(), "invalid range value") { | ||
| return fmt.Errorf("invalid ports value %s: valid port format is ${from_port}-${to_port}", *cfg.Ports) | ||
| } | ||
| return err | ||
| } |
There was a problem hiding this comment.
nit: what do you think of flipping the conditional
| if cfg.Ports != nil { | |
| if err := cfg.Ports.validate(); err != nil { | |
| if strings.Contains(err.Error(), "invalid range value") { | |
| return fmt.Errorf("invalid ports value %s: valid port format is ${from_port}-${to_port}", *cfg.Ports) | |
| } | |
| return err | |
| } | |
| if cfg.Ports == nil { | |
| retur nil | |
| } | |
| // check `cfg.Ports.validate()` | |
| } |
There was a problem hiding this comment.
Another nit that I'd have is that we could think of a more robust way to match the invalid range value error. Consider if one day we decide to change the error message for IntRangeBand's validate, from "invalid range value..." to something like"range %s is invalid", it's likely that we'd forget to update it here 😰 .
It is not entirely necessary for us to address the nit here. Just sharing the ⬇️ code snippets for your thoughts 💭
An alternative that can deal with this & does not involve too much modification on top of what we have here is:
// In validate.go
type errInvalidRange struct {
value string
validFormat string
}
func (e *errInvalidRange) Error() string {
return fmt.Sprintf("invalid range value %s: valid format is %s", e.value, e.validFormat)
}
// validate returns nil if IntRangeBand is configured correctly.
func (r IntRangeBand) validate() error {
....
// Valid minMax example: ["1-2", "1", "2"]
if len(minMax) != 3 {
return &errInvalidRange{
value: str,
validFormat: "${min}-${max}",
}
}
...
}then here:
// In validate_env.go
func (cfg portsConfig) validate() error {
...
if err := cfg.Ports.validate(); err != nil {
var targetErr *errInvalidRange
if errors.As(err, &targetErr) {
return &errInvalidRange{
value: aws.StringValue((*string)(cfg.Ports)),
validFormat: "${from_port}-${to_port}",
}
}
return err
}
return nil
}There was a problem hiding this comment.
Hmm totally make sense to me. We need robust way to handle such kinds of errors and your solution looks good to me. I will make changes in this PR itself to add this robust change now only in copilot.
This PR fix the issue #3719
Related to #3749
Now customers can define security group rules for EnvironmentSecurityGroup (e.g. ingress/egress) in their env manifest
Sample manifest template with security group rules is given below